Projet Todo App
Application de gestion de tâches
10-15 min
Projet complet : Todo App
Dans ce projet pratique, nous allons créer une application de gestion de tâches (Todo App) complète en utilisant React et toutes les connaissances acquises dans les tutoriels précédents.
Structure du projet
todo-app/
├── src/
│ ├── components/
│ │ ├── TodoList.jsx
│ │ ├── TodoItem.jsx
│ │ ├── TodoForm.jsx
│ │ ├── TodoFilter.jsx
│ │ └── TodoStats.jsx
│ ├── hooks/
│ │ └── useTodos.js
│ ├── context/
│ │ └── TodoContext.jsx
│ ├── utils/
│ │ └── localStorage.js
│ ├── styles/
│ │ └── TodoApp.css
│ └── App.jsx
├── public/
│ └── index.html
└── package.json
Configuration initiale
npx create-react-app todo-app
cd todo-app
npm install @mui/material @emotion/react @emotion/styled
Création des composants
TodoItem.jsx
import React from 'react';
import { ListItem, ListItemText, IconButton, Checkbox } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
function TodoItem({ todo, onToggle, onDelete }) {
return (
<ListItem>
<Checkbox
checked={todo.completed}
onChange={() => onToggle(todo.id)}
color="primary"
/>
<ListItemText
primary={todo.text}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
/>
<IconButton onClick={() => onDelete(todo.id)} color="error">
<DeleteIcon />
</IconButton>
</ListItem>
);
}
export default TodoItem;
TodoForm.jsx
import React, { useState } from 'react';
import { TextField, Button, Box } from '@mui/material';
function TodoForm({ onSubmit }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onSubmit(text);
setText('');
}
};
return (
<Box component="form" onSubmit={handleSubmit} sx={{ mb: 3 }}>
<TextField
fullWidth
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Ajouter une nouvelle tâche"
variant="outlined"
sx={{ mr: 2 }}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!text.trim()}
>
Ajouter
</Button>
</Box>
);
}
export default TodoForm;
TodoFilter.jsx
import React from 'react';
import { ButtonGroup, Button } from '@mui/material';
function TodoFilter({ filter, onFilterChange }) {
return (
<ButtonGroup variant="contained" sx={{ mb: 2 }}>
<Button
onClick={() => onFilterChange('all')}
color={filter === 'all' ? 'primary' : 'default'}
>
Toutes
</Button>
<Button
onClick={() => onFilterChange('active')}
color={filter === 'active' ? 'primary' : 'default'}
>
Actives
</Button>
<Button
onClick={() => onFilterChange('completed')}
color={filter === 'completed' ? 'primary' : 'default'}
>
Terminées
</Button>
</ButtonGroup>
);
}
export default TodoFilter;
TodoStats.jsx
import React from 'react';
import { Typography, Box } from '@mui/material';
function TodoStats({ total, completed, active }) {
return (
<Box sx={{ mt: 2 }}>
<Typography variant="body2">
Total: {total} | Terminées: {completed} | Actives: {active}
</Typography>
</Box>
);
}
export default TodoStats;
Gestion de l’état avec Context API
TodoContext.jsx
import React, { createContext, useContext, useReducer } from 'react';
const TodoContext = createContext();
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
};
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
};
return (
<TodoContext.Provider value={{ todos, addTodo, toggleTodo, deleteTodo }}>
{children}
</TodoContext.Provider>
);
}
export function useTodos() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodos doit être utilisé dans un TodoProvider');
}
return context;
}
Persistance des données
localStorage.js
export const saveTodos = (todos) => {
localStorage.setItem('todos', JSON.stringify(todos));
};
export const loadTodos = () => {
const todos = localStorage.getItem('todos');
return todos ? JSON.parse(todos) : [];
};
Composant principal App.jsx
import React, { useState, useEffect } from 'react';
import { Container, Paper, Typography } from '@mui/material';
import { TodoProvider, useTodos } from './context/TodoContext';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import TodoFilter from './components/TodoFilter';
import TodoStats from './components/TodoStats';
import { saveTodos, loadTodos } from './utils/localStorage';
function TodoApp() {
return (
<TodoProvider>
<TodoAppContent />
</TodoProvider>
);
}
function TodoAppContent() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
const [filter, setFilter] = useState('all');
useEffect(() => {
const savedTodos = loadTodos();
if (savedTodos.length > 0) {
savedTodos.forEach(todo => addTodo(todo.text));
}
}, []);
useEffect(() => {
saveTodos(todos);
}, [todos]);
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const stats = {
total: todos.length,
completed: todos.filter(todo => todo.completed).length,
active: todos.filter(todo => !todo.completed).length
};
return (
<Container maxWidth="md">
<Paper elevation={3} sx={{ p: 3, mt: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Todo App
</Typography>
<TodoForm onSubmit={addTodo} />
<TodoFilter filter={filter} onFilterChange={setFilter} />
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<TodoStats {...stats} />
</Paper>
</Container>
);
}
export default TodoApp;
Styles CSS
/* TodoApp.css */
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
margin-right: 10px;
}
.todo-list {
margin-top: 20px;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-item.completed {
text-decoration: line-through;
color: #888;
}
.todo-stats {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
Fonctionnalités supplémentaires
-
Catégories de tâches :
- Ajoutez des catégories aux tâches
- Filtrez par catégorie
- Affichez des statistiques par catégorie
-
Dates d’échéance :
- Ajoutez des dates d’échéance aux tâches
- Triez par date
- Affichez des notifications
-
Sous-tâches :
- Créez des sous-tâches
- Affichez la progression
- Gérez les dépendances
-
Thèmes et personnalisation :
- Ajoutez des thèmes clair/sombre
- Personnalisez les couleurs
- Ajoutez des animations
Déploiement
-
Préparation :
npm run build
-
Vérification :
npm run test
-
Déploiement :
- Netlify
- Vercel
- GitHub Pages
Conclusion
Ce projet Todo App met en pratique tous les concepts appris dans les tutoriels précédents. Dans le prochain chapitre, nous allons explorer les tests en React pour garantir la qualité de notre code.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion