# 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 (
<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>
);
}; ```
## 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 nav = 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={() => nav.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={() => nav.navigate('/tasks/new')}
className="rounded-full w-14 h-14"
>
<FiPlus size={24} />
</Button>
</div>
</Page>
);
}; ```
## 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 nav = 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.del(`tasks/${id}`);
nav.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={() => nav.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>
);
}; ```
## 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';
export const TaskFormPage = () ⇒ {
const { id } = useParams();
const isEdit = !!id;
const api = useApi(); const nav = useNavigation();
const st = useStates({
initialStates: {
loading: isEdit,
submitting: false,
defaultValues: null
}
});
useEffect(() => {
if (isEdit) {
loadTask();
}
}, [id]);
const loadTask = async () => {
try {
const data = await api.private.get(`tasks/${id}`).json();
st.set('defaultValues', {
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);
nav.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 });
}
nav.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 onSubmit={handleSubmit} defaultValues={st.get('defaultValues')}>
<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={() => nav.navigate(-1)}
>
Annuler
</Button>
<Button
type="submit"
loading={st.get('submitting')}
>
{isEdit ? 'Enregistrer' : 'Créer'}
</Button>
</div>
</Form>
</Block>
</Page>
);
}; ```
## 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"
}
}; ```
← Chapitre précédent | Retour au module | Chapitre suivant : Déploiement →