# Chapitre 2 : Frontend ## 1. Configuration des routes React ```javascript // components/app/Router/index.jsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { PublicRoutes, PrivateRoutes } from './Guards'; import { LoginPage } from '../../pages/public/LoginPage'; import { TasksPage } from '../../pages/private/TasksPage'; import { TaskDetailPage } from '../../pages/private/TaskDetailPage'; import { TaskFormPage } from '../../pages/private/TaskFormPage'; import { Error404Page } from '../../pages/errors/Error404Page'; export const Router = () => { return ( }> } /> }> } /> } /> } /> } /> } /> ); }; ``` ## 2. Page de liste ```javascript // components/pages/private/TasksPage/index.jsx import { useEffect } from 'react'; import { Page, Block, List, ListItem, Button, Spinner, Tag } from '@cap-rel/smartcommon'; import { useApi, useStates, useNavigation } from '@cap-rel/smartcommon'; import { FiPlus, FiCheck, FiClock } from 'react-icons/fi'; export const TasksPage = () => { const api = useApi(); const navigate = useNavigation(); const st = useStates({ initialStates: { tasks: [], loading: true, error: null, filter: 'all' // all, todo, done } }); useEffect(() => { loadTasks(); }, [st.get('filter')]); const loadTasks = async () => { st.set('loading', true); try { const params = {}; if (st.get('filter') === 'todo') params.status = 1; if (st.get('filter') === 'done') params.status = 2; const data = await api.private.get('tasks', { searchParams: params }).json(); st.set('tasks', data.tasks); } catch (err) { st.set('error', err.message); } finally { st.set('loading', false); } }; const toggleStatus = async (task) => { const newStatus = task.status === 'done' ? 1 : 2; try { await api.private.put(`tasks/${task.id}`, { json: { status: newStatus } }); loadTasks(); } catch (err) { alert('Erreur: ' + err.message); } }; const getStatusColor = (status) => { switch (status) { case 'done': return 'green'; case 'todo': return 'blue'; default: return 'gray'; } }; if (st.get('loading') && st.get('tasks').length === 0) { return ; } return ( {/* Filtres */} {['all', 'todo', 'done'].map(f => ( st.set('filter', f)} > {f === 'all' ? 'Toutes' : f === 'todo' ? 'À faire' : 'Terminées'} ))} {/* Liste */} {st.get('tasks').length === 0 ? ( Aucune tâche ) : ( {st.get('tasks').map(task => ( navigate(`/tasks/${task.id}`)} icon={task.status === 'done' ? FiCheck : FiClock} chevron actions={ {task.status} } /> ))} )} {/* Bouton ajout */} navigate('/tasks/new')} className="rounded-full w-14 h-14" > ); }; ``` ## 3. Page de détail ```javascript // components/pages/private/TaskDetailPage/index.jsx import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Page, Block, Button, Spinner, Tag, Popup } from '@cap-rel/smartcommon'; import { useApi, useStates, useNavigation } from '@cap-rel/smartcommon'; import { FiEdit, FiTrash2, FiCheck } from 'react-icons/fi'; export const TaskDetailPage = () => { const { id } = useParams(); const api = useApi(); const navigate = useNavigation(); const st = useStates({ initialStates: { task: null, loading: true, error: null, showDeletePopup: false } }); useEffect(() => { loadTask(); }, [id]); const loadTask = async () => { try { const data = await api.private.get(`tasks/${id}`).json(); st.set('task', data); } catch (err) { st.set('error', err.message); } finally { st.set('loading', false); } }; const toggleComplete = async () => { const task = st.get('task'); const newStatus = task.status === 'done' ? 1 : 2; try { await api.private.put(`tasks/${id}`, { json: { status: newStatus } }); loadTask(); } catch (err) { alert('Erreur: ' + err.message); } }; const handleDelete = async () => { try { await api.private.delete(`tasks/${id}`); navigate('/'); } catch (err) { alert('Erreur: ' + err.message); } }; if (st.get('loading')) { return ; } if (st.get('error')) { return ( Erreur: {st.get('error')} ); } const task = st.get('task'); return ( {/* Statut */} {task.status === 'done' ? 'Terminée' : 'À faire'} Ref: {task.ref} {/* Description */} {task.description && ( {task.description} )} {/* Dates */} {task.date_start && ( Début: {task.date_start} )} {task.date_end && ( Fin: {task.date_end} )} {/* Actions */} {task.status === 'done' ? 'Réouvrir' : 'Terminer'} navigate(`/tasks/${id}/edit`)} > Modifier st.set('showDeletePopup', true)} > {/* Popup suppression */} st.set('showDeletePopup', false)} title="Confirmer la suppression" > Supprimer cette tâche ? st.set('showDeletePopup', false)}> Annuler Supprimer ); }; ``` ## 4. Formulaire ```javascript // components/pages/private/TaskFormPage/index.jsx import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Page, Block, Form, Input, Textarea, Select, Calendar, Button, Spinner } from '@cap-rel/smartcommon'; import { useApi, useStates, useNavigation, useForm } from '@cap-rel/smartcommon'; import { z } from 'zod'; const schema = z.object({ label: z.string().min(1, 'Titre requis'), description: z.string().optional(), priority: z.number().optional(), date_start: z.string().optional(), date_end: z.string().optional() }); export const TaskFormPage = () => { const { id } = useParams(); const isEdit = !!id; const api = useApi(); const navigate = useNavigation(); const form = useForm({ schema }); const st = useStates({ initialStates: { loading: isEdit, submitting: false } }); useEffect(() => { if (isEdit) { loadTask(); } }, [id]); const loadTask = async () => { try { const data = await api.private.get(`tasks/${id}`).json(); form.reset({ label: data.label, description: data.description || '', priority: data.priority || 0, date_start: data.date_start || '', date_end: data.date_end || '' }); } catch (err) { alert('Erreur: ' + err.message); navigate('/'); } finally { st.set('loading', false); } }; const handleSubmit = async (values) => { st.set('submitting', true); try { if (isEdit) { await api.private.put(`tasks/${id}`, { json: values }); } else { await api.private.post('tasks', { json: values }); } navigate('/'); } catch (err) { alert('Erreur: ' + err.message); } finally { st.set('submitting', false); } }; if (st.get('loading')) { return ; } return ( navigate(-1)} > Annuler {isEdit ? 'Enregistrer' : 'Créer'} ); }; ``` ## 5. Configuration globale ```javascript // src/appConfig.js export const config = { debug: import.meta.env.DEV, api: { prefixUrl: import.meta.env.VITE_API_URL, timeout: 30000, paths: { login: "login", logout: "logout", refresh: "refresh" } }, storage: { local: ["session"] }, globalState: { reducers: { session: null } }, pages: { "*": "fade" } }; ``` [[:15_training:module9-integration:backend|← Chapitre précédent]] | [[:15_training:module9-integration:start|Retour au module]] | [[:15_training:module9-integration:deploiement|Chapitre suivant : Déploiement →]]
Aucune tâche
{task.description}
Début: {task.date_start}
Fin: {task.date_end}
Supprimer cette tâche ?