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

# 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 →

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

  • SmartAuth
  • SmartMaker - Back (PHP)
    • Mapping Dolibarr - React
  • SmartMaker - Front (React)
    • Animations de pages
    • Architecture
    • Astuces
    • Calendar
    • Composants avancés (high-level)
    • Composants et pages
    • Configuration du Provider
    • Debug et Logs
    • Hooks SmartCommon
    • PWA (Progressive Web App)
    • Requêtes API
    • Routage
    • SmartCommon
    • Stockage de données
    • Synchronisation offline
    • 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 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
      • Chapitre 4 : Synchronisation Offline
    • 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