# 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 => ( ))}
{/* 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 */}
); }; ``` ## 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 */}
{/* Popup suppression */} st.set('showDeletePopup', false)} title="Confirmer la suppression" >

Supprimer cette tâche ?

); }; ``` ## 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 (