// 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 ( <BrowserRouter> <Routes> <Route element={<PublicRoutes />}> <Route path="/login" element={<LoginPage />} /> </Route> <Route element={<PrivateRoutes />}> <Route path="/" element={<TasksPage />} /> <Route path="/tasks/:id" element={<TaskDetailPage />} /> <Route path="/tasks/:id/edit" element={<TaskFormPage />} /> <Route path="/tasks/new" element={<TaskFormPage />} /> </Route> <Route path="*" element={<Error404Page />} /> </Routes> </BrowserRouter> ); };
// 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 <Page title="Tâches"><Spinner /></Page>; } return ( <Page title="Mes tâches" onRefresh={loadTasks}> {/* Filtres */} <Block> <div className="flex gap-2"> {['all', 'todo', 'done'].map(f => ( <Button key={f} variant={st.get('filter') === f ? 'primary' : 'outline'} size="sm" onClick={() => st.set('filter', f)} > {f === 'all' ? 'Toutes' : f === 'todo' ? 'À faire' : 'Terminées'} </Button> ))} </div> </Block> {/* Liste */} <Block> {st.get('tasks').length === 0 ? ( <p className="text-center text-gray-500 py-8"> Aucune tâche </p> ) : ( <List> {st.get('tasks').map(task => ( <ListItem key={task.id} title={task.label} subtitle={task.date_start || 'Pas de date'} onClick={() => navigate(`/tasks/${task.id}`)} icon={task.status === 'done' ? FiCheck : FiClock} chevron actions={ <Tag color={getStatusColor(task.status)}> {task.status} </Tag> } /> ))} </List> )} </Block> {/* Bouton ajout */} <div className="fixed bottom-20 right-4"> <Button variant="primary" onClick={() => navigate('/tasks/new')} className="rounded-full w-14 h-14" > <FiPlus size={24} /> </Button> </div> </Page> ); };
// 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 <Page><Spinner /></Page>; } if (st.get('error')) { return ( <Page> <Block>Erreur: {st.get('error')}</Block> </Page> ); } const task = st.get('task'); return ( <Page title={task.label}> {/* Statut */} <Block> <div className="flex justify-between items-center"> <Tag color={task.status === 'done' ? 'green' : 'blue'}> {task.status === 'done' ? 'Terminée' : 'À faire'} </Tag> <span className="text-gray-500">Ref: {task.ref}</span> </div> </Block> {/* Description */} {task.description && ( <Block title="Description"> <p>{task.description}</p> </Block> )} {/* Dates */} <Block title="Dates"> <div className="space-y-2"> {task.date_start && ( <p>Début: {task.date_start}</p> )} {task.date_end && ( <p>Fin: {task.date_end}</p> )} </div> </Block> {/* Actions */} <Block> <div className="flex gap-2"> <Button variant={task.status === 'done' ? 'outline' : 'primary'} onClick={toggleComplete} > <FiCheck className="mr-2" /> {task.status === 'done' ? 'Réouvrir' : 'Terminer'} </Button> <Button variant="outline" onClick={() => navigate(`/tasks/${id}/edit`)} > <FiEdit className="mr-2" /> Modifier </Button> <Button variant="danger" onClick={() => st.set('showDeletePopup', true)} > <FiTrash2 /> </Button> </div> </Block> {/* Popup suppression */} <Popup isOpen={st.get('showDeletePopup')} onClose={() => st.set('showDeletePopup', false)} title="Confirmer la suppression" > <p>Supprimer cette tâche ?</p> <div className="flex gap-2 mt-4"> <Button onClick={() => st.set('showDeletePopup', false)}> Annuler </Button> <Button variant="danger" onClick={handleDelete}> Supprimer </Button> </div> </Popup> </Page> ); };
// 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 <Page><Spinner /></Page>; } return ( <Page title={isEdit ? 'Modifier la tâche' : 'Nouvelle tâche'}> <Block> <Form form={form} onSubmit={handleSubmit}> <Input name="label" label="Titre" required /> <Textarea name="description" label="Description" rows={4} /> <Select name="priority" label="Priorité" options={[ { value: 0, label: 'Basse' }, { value: 1, label: 'Normale' }, { value: 2, label: 'Haute' }, { value: 3, label: 'Urgente' } ]} /> <Calendar name="date_start" label="Date de début" /> <Calendar name="date_end" label="Date de fin" /> <div className="flex gap-2 mt-4"> <Button type="button" variant="outline" onClick={() => navigate(-1)} > Annuler </Button> <Button type="submit" loading={st.get('submitting')} > {isEdit ? 'Enregistrer' : 'Créer'} </Button> </div> </Form> </Block> </Page> ); };
// 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" } };
← Chapitre précédent | Retour au module | Chapitre suivant : Déploiement →