Un projet SmartMaker se compose de deux parties :
monmodule/ # Module Dolibarr ├── mobile/ # CODE SOURCE REACT │ ├── src/ │ │ ├── App.jsx # Composant racine │ │ ├── main.jsx # Point d'entrée │ │ ├── appConfig.js # Configuration │ │ ├── components/ │ │ │ ├── app/ │ │ │ │ └── Router/ # Configuration routes │ │ │ │ ├── index.jsx │ │ │ │ └── Guards/ # Protection routes │ │ │ └── pages/ │ │ │ ├── public/ # Pages sans auth │ │ │ │ ├── LoginPage/ │ │ │ │ └── WelcomePage/ │ │ │ ├── private/ # Pages avec auth │ │ │ │ ├── HomePage/ │ │ │ │ └── ItemPage/ │ │ │ └── errors/ │ │ │ └── Error404Page/ │ │ ├── redux/ # Store Redux (optionnel) │ │ │ └── reducers/ │ │ ├── i18n/ # Configuration i18next │ │ ├── utils/ # Helpers, constantes │ │ └── locales/ # Fichiers de traductions │ ├── public/ │ │ ├── images/ # Icônes PWA │ │ └── locales/ # Traductions │ ├── index.html # Template HTML │ ├── package.json # Dépendances npm │ ├── vite.config.js # Configuration Vite │ └── .env # Variables d'environnement │ ├── pwa/ # APPLICATION COMPILÉE + API │ ├── api.php # Point d'entrée API │ └── dist/ # Fichiers React compilés │ ├── smartmaker-api/ # BACKEND PHP │ ├── Controllers/ # Logique métier │ └── Mappers/ # Conversion Dolibarr → DTO │ └── smartmaker-api-prepend.php # Bootstrap PHP
// src/main.jsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; import './index.css'; createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode> );
C'est le fichier appelé par index.html. Il monte l'application React dans le DOM.
// src/App.jsx import { Provider } from '@cap-rel/smartcommon'; import { Router } from './components/app/Router'; import { config } from './appConfig'; export const App = () => ( <Provider config={config}> <Router /> </Provider> );
Le Provider de SmartCommon englobe toute l'application et fournit :
// 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", "settings"] }, globalState: { reducers: { session: null, settings: { lng: "fr" }, items: [] } }, pages: { "/": { "/settings": "slideLeft", "*": "fade" }, "*": "fade" } };
Détaillé dans le chapitre suivant.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mon Application</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Vite injecte automatiquement les scripts compilés en production.
components/pages/private/ItemPage/
├── index.jsx # Composant principal
├── ItemPage.module.css # Styles (optionnel)
└── components/ # Sous-composants locaux (optionnel)
├── ItemHeader.jsx
└── ItemActions.jsx
// components/pages/private/ItemPage/index.jsx import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Page, Block, Spinner } from '@cap-rel/smartcommon'; import { useApi, useStates } from '@cap-rel/smartcommon'; export const ItemPage = () => { const { id } = useParams(); const api = useApi(); const st = useStates({ initialStates: { item: null, loading: true, error: null } }); useEffect(() => { const fetchItem = async () => { try { const data = await api.private.get(`items/${id}`).json(); st.set('item', data); } catch (err) { st.set('error', err.message); } finally { st.set('loading', false); } }; fetchItem(); }, [id]); if (st.get('loading')) { return <Page><Spinner /></Page>; } if (st.get('error')) { return <Page><Block>Erreur : {st.get('error')}</Block></Page>; } const item = st.get('item'); return ( <Page title={item.label}> <Block> <p>{item.description}</p> </Block> </Page> ); };
// components/app/Router/index.jsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { PublicRoutes, PrivateRoutes } from './Guards'; // Pages import { LoginPage } from '../../pages/public/LoginPage'; 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="/login" element={<LoginPage />} /> </Route> {/* Routes protégées */} <Route element={<PrivateRoutes />}> <Route path="/" element={<HomePage />} /> <Route path="/items/:id" element={<ItemPage />} /> </Route> {/* Fallback */} <Route path="*" element={<Error404Page />} /> </Routes> </BrowserRouter> ); };
// components/app/Router/Guards/index.jsx import { Outlet, Navigate } from 'react-router-dom'; import { useGlobalStates } from '@cap-rel/smartcommon'; export const PrivateRoutes = () => { const [session] = useGlobalStates('session'); return session ? <Outlet /> : <Navigate to="/login" />; }; export const PublicRoutes = () => { const [session] = useGlobalStates('session'); return session ? <Navigate to="/" /> : <Outlet />; };
# .env VITE_API_URL=https://mondomaine.com/modules/monmodule/pwa/api.php
Accès dans le code :
const apiUrl = import.meta.env.VITE_API_URL; const isDev = import.meta.env.DEV; const isProd = import.meta.env.PROD;
Important : Les variables doivent commencer par VITE_ pour être exposées au client.
Après compilation (npm run build), les fichiers sont générés dans pwa/dist/ :
pwa/
├── api.php # Point d'entrée API (à créer)
└── dist/
├── index.html # HTML avec assets injectés
├── assets/
│ ├── index-abc123.js # Bundle JS
│ └── index-def456.css # Bundle CSS
└── images/