0%
Projet complet : Todo App

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

  1. Catégories de tâches :

    • Ajoutez des catégories aux tâches
    • Filtrez par catégorie
    • Affichez des statistiques par catégorie
  2. Dates d’échéance :

    • Ajoutez des dates d’échéance aux tâches
    • Triez par date
    • Affichez des notifications
  3. Sous-tâches :

    • Créez des sous-tâches
    • Affichez la progression
    • Gérez les dépendances
  4. Thèmes et personnalisation :

    • Ajoutez des thèmes clair/sombre
    • Personnalisez les couleurs
    • Ajoutez des animations

Déploiement

  1. Préparation :

    npm run build
  2. Vérification :

    npm run test
  3. 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

Lien copié !