This page has not been translated into Spanish yet. The French version is shown.

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 /howto/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

Astuce

Le skel SmartBoot utilise désormais <LoginComponent> de smartcommon, qui ship le formulaire complet et le scan QR pair smartAuth. Voir Composants avancés -> LoginComponent. L'exemple ci-dessous est un montage manuel utile si vous voulez comprendre le flux ou écrire une page très spécifique.

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';

export const LoginPage = () => {
  const api = useApi();
  const nav = useNavigation();
  const gst = useGlobalStates();

  const form = useForm({ defaultValues: { login: '', password: '' } });

  const handleSubmit = async (data) => {
    try {
      const user = await api.login(data);
      gst.local.set('session', user);
      nav.navigate('/');
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  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.isFormSubmitting}>
          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 nav = useNavigation();
  const gst = useGlobalStates();
  const items = gst.get('items') ?? [];

  // Charger les items au montage
  useEffect(() => {
    const fetchItems = async () => {
      try {
        const data = await api.get('items');
        gst.set('items', data);
      } catch (error) {
        console.error('Fetch failed:', error);
      }
    };
    fetchItems();
  }, []);

  // Déconnexion
  const handleLogout = async () => {
    await api.logout();
    nav.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={() => nav.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 nav = useNavigation();
  const [item, setItem] = useState(null);

  useEffect(() => {
    const fetchItem = async () => {
      try {
        const data = await api.get(`items/${id}`);
        setItem(data);
      } catch (error) {
        console.error('Fetch failed:', error);
      }
    };
    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={() => nav.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

Astuce

Pour les apps issues du skel SmartBoot, préférez <RouteGuard> qui couvre 4 modes (auth + identification d'appareil) et est maintenu dans smartcommon. Voir RouteGuard. La version manuelle ci-dessous illustre le pattern sous-jacent.

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

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

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

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

Utiliser les états globaux

Stocker des données

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

const gst = useGlobalStates();

// Lire une valeur
const items = gst.get('items');
const lng = gst.get('settings.lng');

// Écrire en mémoire (non persistant)
gst.set('items', newItems);

// Écrire dans localStorage (persistant)
gst.local.set('session', userData);

// Écrire dans sessionStorage (session uniquement)
gst.session.set('tempData', data);

// Supprimer
gst.unset('session');

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

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

const CreateItemForm = ({ onSuccess }) => {
  const api = useApi();
  const form = useForm({ defaultValues: { label: '', type: '', description: '' } });

  const handleSubmit = async (data) => {
    try {
      const created = await api.post('items', { json: data });
      onSuccess(created);
    } catch (error) {
      console.error('Create failed:', error);
    }
  };

  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" loading={form.isFormSubmitting}>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