Table of Contents

Développement React (front)

Vous venez de déployer SmartBoot dans votre module et vous vous demandez par où commencer ? Vous êtes au bon endroit !

Installation des dépendances

cd mobile
npm i

Configuration initiale

Fichier .env

Copiez ou renommez mobile/.env.example en mobile/.env :

VITE_API_URL=https://votre-dolibarr.com/custom/monmodule/pwa/api.php
VITE_APP_VERSION=dev
VITE_LOCALES=en,fr

Configuration de l'application

Le fichier appConfig.js centralise toute la configuration :

// src/appConfig.js

export const config = {
  // Mode debug (logs colorés dans la console)
  debug: import.meta.env.DEV,

  // Configuration API
  api: {
    prefixUrl: import.meta.env.VITE_API_URL,
    timeout: 30000,
    paths: {
      login: "login",
      logout: "logout",
      refresh: "refresh",
    },
  },

  // Stockage
  storage: {
    local: ["session", "settings"],  // Persisté en localStorage
  },

  // État global initial
  globalState: {
    reducers: {
      session: null,
      settings: { lng: "fr" },
      items: [],
    },
  },

  // Animations de pages
  pages: {
    "/": { "/settings": "slideLeft", "*": "fade" },
    "*": "fade",
  },
};

Lancement

cd mobile
npm run dev

Ouvrez http://localhost:5173/ (mode mobile recommandé dans les DevTools).

Attention SmartAuth est nécessaire !

Structure des pages

L'arborescence préinstallée par SmartBoot :

src/components/pages/
├── errors/
│   └── Error404Page/
│       └── index.jsx
├── private/
│   └── HomePage/
│       └── index.jsx
└── public/
    ├── LoginPage/
    │   └── index.jsx
    └── WelcomePage/
        └── index.jsx
Dossier Description
public/ Pages accessibles sans authentification
private/ Pages nécessitant une authentification
errors/ Pages d'erreur (404, etc.)

Créer une page de login

Avec SmartCommon, la page de login devient très simple :

// src/components/pages/public/LoginPage/index.jsx

import { useApi, useGlobalStates, useForm, useNavigation } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';

const schema = z.object({
  login: z.string().min(1, "Login requis"),
  password: z.string().min(1, "Mot de passe requis"),
});

export const LoginPage = () => {
  const api = useApi();
  const navigate = useNavigation();
  const [, setSession] = useGlobalStates('session');

  const form = useForm({ schema });

  const handleSubmit = async (data) => {
    const response = await api.public.post('login', { json: data });

    if (response.success) {
      setSession(response.data);
      navigate('/');
    }
  };

  return (
    <div className="fixed inset-0 bg-white flex justify-center items-center p-10">
      <Form form={form} onSubmit={handleSubmit} className="flex flex-col gap-6 w-full max-w-sm">
        <Input
          name="login"
          label="Identifiant"
          placeholder="Votre login..."
        />
        <Input
          name="password"
          type="password"
          label="Mot de passe"
          placeholder="●●●●●●●●"
        />
        <Button type="submit" loading={form.formState.isSubmitting}>
          Connexion
        </Button>
      </Form>
    </div>
  );
};

Créer une page privée

Page d'accueil avec liste

// src/components/pages/private/HomePage/index.jsx

import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect } from 'react';

export const HomePage = () => {
  const api = useApi();
  const navigate = useNavigation();
  const [items, setItems] = useGlobalStates('items');
  const [session, setSession] = useGlobalStates('session');

  // Charger les items au montage
  useEffect(() => {
    const fetchItems = async () => {
      const response = await api.private.get('items');
      if (response.success) {
        setItems(response.data);
      }
    };
    fetchItems();
  }, []);

  // Déconnexion
  const handleLogout = async () => {
    await api.private.post('logout');
    setSession(null);
    navigate('/login');
  };

  return (
    <div className="min-h-screen bg-gray-100 p-4">
      <header className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Mes items</h1>
        <button onClick={handleLogout} className="text-red-500">
          Déconnexion
        </button>
      </header>

      <div className="space-y-4">
        {items.map((item) => (
          <div
            key={item.id}
            onClick={() => navigate(`/items/${item.id}`)}
            className="bg-white p-4 rounded-lg shadow"
          >
            <h2 className="font-semibold">{item.label}</h2>
            <p className="text-gray-600">{item.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

Page de détail

// src/components/pages/private/ItemPage/index.jsx

import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

export const ItemPage = () => {
  const { id } = useParams();
  const api = useApi();
  const navigate = useNavigation();
  const [item, setItem] = useState(null);

  useEffect(() => {
    const fetchItem = async () => {
      const response = await api.private.get(`items/${id}`);
      if (response.success) {
        setItem(response.data);
      }
    };
    fetchItem();
  }, [id]);

  if (!item) {
    return <div className="p-4">Chargement...</div>;
  }

  return (
    <div className="min-h-screen bg-gray-100">
      <header className="bg-white p-4 shadow">
        <button onClick={() => navigate(-1)} className="text-blue-500">
          ← Retour
        </button>
      </header>

      <div className="p-4">
        <h1 className="text-2xl font-bold mb-4">{item.label}</h1>
        <p className="text-gray-600">{item.description}</p>
      </div>
    </div>
  );
};

Configurer le routeur

// src/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 { WelcomePage } from '../../pages/public/WelcomePage';
import { HomePage } from '../../pages/private/HomePage';
import { ItemPage } from '../../pages/private/ItemPage';
import { Error404Page } from '../../pages/errors/Error404Page';

export const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        {/* Routes publiques */}
        <Route element={<PublicRoutes />}>
          <Route path="/welcome" element={<WelcomePage />} />
          <Route path="/login" element={<LoginPage />} />
        </Route>

        {/* Routes privées */}
        <Route element={<PrivateRoutes />}>
          <Route path="/" element={<HomePage />} />
          <Route path="/items/:id" element={<ItemPage />} />
        </Route>

        {/* Erreur 404 */}
        <Route path="*" element={<Error404Page />} />
      </Routes>
    </BrowserRouter>
  );
};

Guards avec SmartCommon

// src/components/app/Router/Guards/index.jsx

import { Outlet, Navigate } from 'react-router-dom';
import { useGlobalStates } from '@cap-rel/smartcommon';

export const PublicRoutes = () => {
  const [session] = useGlobalStates('session');
  return session ? <Navigate to="/" /> : <Outlet />;
};

export const PrivateRoutes = () => {
  const [session] = useGlobalStates('session');
  return session ? <Outlet /> : <Navigate to="/login" />;
};

Utiliser les états globaux

Stocker des données

import { useGlobalStates } from '@cap-rel/smartcommon';

// Lecture + écriture
const [items, setItems] = useGlobalStates('items');

// Lecture seule
const [items] = useGlobalStates('items');

// Accès à une propriété imbriquée
const [settings, setSettings] = useGlobalStates('settings');
const [lng, setLng] = useGlobalStates('settings.lng');

Persistance automatique

Les clés listées dans config.storage.local sont automatiquement persistées en localStorage :

// appConfig.js
storage: {
  local: ["session", "settings"],  // Persisté automatiquement
}

Utiliser les formulaires

Formulaire complet avec validation

import { useForm, Form, Input, Select, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';

const schema = z.object({
  label: z.string().min(3, "Minimum 3 caractères"),
  type: z.enum(["A", "B", "C"]),
  description: z.string().optional(),
});

const CreateItemForm = ({ onSuccess }) => {
  const api = useApi();
  const form = useForm({ schema });

  const handleSubmit = async (data) => {
    const response = await api.private.post('items', { json: data });
    if (response.success) {
      onSuccess(response.data);
      form.reset();
    }
  };

  return (
    <Form form={form} onSubmit={handleSubmit}>
      <Input name="label" label="Libellé" />
      <Select
        name="type"
        label="Type"
        options={[
          { value: "A", label: "Type A" },
          { value: "B", label: "Type B" },
          { value: "C", label: "Type C" },
        ]}
      />
      <Input name="description" label="Description" multiline rows={4} />
      <Button type="submit">Créer</Button>
    </Form>
  );
};

Build et déploiement

# Build de production
npm run build

# Le build génère le dossier dist/
# Copiez-le dans le dossier pwa/ de votre module
cp -r dist/* ../pwa/

Ou utilisez le Makefile :

make pwa

Voir aussi