SmarMaker - Documentation
Docs» 15_training:module9-integration:frontend

Chapitre 2 : Frontend

1. Configuration des routes React

snippet.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

snippet.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 <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>
    );
};

3. Page de détail

snippet.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 <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>
    );
};

4. Formulaire

snippet.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 <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>
    );
};

5. Configuration globale

snippet.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 →

Previous Next

Made with ❤ by CAP-REL · SmartMaker · GNU AGPL v3+
Code source · Faire un don
SmarMaker - Documentation
Traductions de cette page:
  • Français
  • Deutsch
  • English
  • Español
  • Italiano
  • Nederlands

Table of Contents

Table des matières

  • Chapitre 2 : Frontend
    • 1. Configuration des routes React
    • 2. Page de liste
    • 3. Page de détail
    • 4. Formulaire
    • 5. Configuration globale
  • SmartAuth
  • SmartMaker - Back (PHP)
    • Mapping Dolibarr - React
  • SmartMaker - Front (React)
    • Animations de pages
    • Architecture
    • Astuces
    • Calendar
    • Composants et pages
    • Configuration du Provider
    • Debug et Logs
    • Hooks SmartCommon
    • PWA (Progressive Web App)
    • Requêtes API
    • Routage
    • SmartCommon
    • Stockage de données
    • Thèmes
    • Traductions
  • HowTo - Pas à pas - Votre première application
    • Développement PHP (back)
    • Développement React (front)
    • Première étape : Module Builder Dolibarr
    • SmartAuth
    • SmartBoot : Un "squelette" quasiment prêt à l'emploi
  • Formation SmartMaker
    • Module 1 : Fondamentaux JavaScript ES6+
      • Chapitre 1 : Variables et Scope
      • Chapitre 2 : Fonctions
      • Chapitre 3 : Programmation Asynchrone
      • Chapitre 4 : Modules ES6
    • Module 2 : Introduction à React
      • Chapitre 1 : Philosophie React
      • Chapitre 2 : JSX
      • Chapitre 3 : Composants
    • Module 3 : Hooks React Fondamentaux
      • Chapitre 1 : useState
      • Chapitre 2 : useEffect
      • Chapitre 3 : useRef
      • Chapitre 4 : useContext
    • Module 4 : React Avancé
      • Chapitre 1 : useCallback et useMemo
      • Chapitre 2 : Custom Hooks
      • Chapitre 3 : Redux et Redux Toolkit
    • Module 5 : Architecture SmartMaker
      • Chapitre 1 : Structure du projet
      • Chapitre 2 : Configuration
      • Chapitre 3 : Flux de données
    • Module 6 : SmartCommon - Composants
      • Chapitre 1 : Mise en page
      • Chapitre 2 : Navigation
      • Chapitre 3 : Formulaires
      • Chapitre 4 : Affichage
    • Module 7 : SmartCommon - Hooks
      • Chapitre 1 : useApi
      • Chapitre 2 : Gestion d'état
      • Chapitre 3 : Hooks utilitaires
    • Module 8 : Backend API (PHP)
      • Chapitre 1 : Routage
      • Chapitre 2 : Controllers
      • Chapitre 3 : Mappers
      • Extrafields et formulaires dynamiques
    • Module 9 : Intégration complète
      • Chapitre 1 : Backend
      • Chapitre 2 : Frontend
      • Chapitre 3 : Déploiement
    • Module 10 : Fonctionnalités avancées
      • Chapitre 1 : Mode offline
      • Chapitre 2 : Internationalisation (i18n)
      • Chapitre 3 : Autres fonctionnalités
    • Module 11 : Bonnes pratiques
  • Démonstration
  • Start
  • Composants et pages
  • Afficher le texte source
  • Anciennes révisions
  • Liens de retour
  • Haut de page