Vous venez de déployer SmartBoot dans votre module et vous vous demandez par où commencer ? Vous êtes au bon endroit !
cd mobile npm i
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
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",
},
};
cd mobile npm run dev
Ouvrez http://localhost:5173/ (mode mobile recommandé dans les DevTools).
Attention SmartAuth est nécessaire !
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.) |
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>
);
};
// 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>
);
};
// 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>
);
};
// 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>
);
};
// 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" />;
};
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');
Les clés listées dans config.storage.local sont automatiquement persistées en localStorage :
// appConfig.js
storage: {
local: ["session", "settings"], // Persisté automatiquement
}
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 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