SmartBoot: A ready-to-use skeleton

SmartBoot generates the complete structure of a Dolibarr module augmented with SmartMaker (React front + PHP back).

Sources: https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot.git

Installation

Linux

git clone https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot.git && ./smartboot/setup.sh

Windows

Note

The PowerShell script is being finalised.

git clone https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot.git && powershell ./smartboot/setup.ps1

Then follow the wizard steps:

Is your project name Coucou ?
[y/n] y
ok on continue

please wait during npm install depends ... it could take time :)

Generated structure

After installation, SmartBoot adds to your module:

monmodule/
├── mobile/                          # React application
│   ├── index.html                   # Entry point (dynamic PWA manifest)
│   ├── vite.config.js               # Vite + PWA + dev proxy configuration
│   ├── .env.example                 # .env template (created by setup.sh)
│   ├── src/
│   │   ├── main.jsx                 # React entry point
│   │   ├── App.jsx                  # Root component
│   │   ├── appConfig.js             # Global configuration
│   │   ├── sw.js                    # Service Worker (injectManifest mode)
│   │   ├── api/
│   │   │   └── mapping/             # Backend <-> front mapping, one per feature
│   │   ├── db/
│   │   │   ├── index.js             # Db instantiation
│   │   │   └── stores/users/        # indexes.js + useDbUsers.jsx
│   │   ├── components/
│   │   │   ├── app/                 # Router, Provider, Head, Toaster
│   │   │   ├── layouts/
│   │   │   │   ├── AnimationLayout/ # Page transitions
│   │   │   │   └── PagesLayout/     # Global layout (theme, scale)
│   │   │   └── pages/
│   │   │       ├── public/          # Pages without auth (Login, Welcome)
│   │   │       ├── private/         # Pages with auth (Home, DeviceIdentification)
│   │   │       └── errors/          # Error404Page
│   │   ├── global-state/slices/     # Interface state only
│   │   ├── hooks/
│   │   │   └── useSmartcommonLabels/  # smartcommon labels in the active language
│   │   ├── i18n/
│   │   └── utils/
│   │       ├── constants/
│   │       ├── functions/
│   │       └── maps/                # form.jsx and list.jsx
│   └── public/
│       ├── images/                  # PWA icons
│       └── locales/<lang>/<ns>.json # One file per language AND per feature
├── pwa/                             # Production build
│   ├── api.php                      # API router
│   └── .htaccess                    # Apache redirection
├── smartmaker-api/
│   ├── Controllers/                 # Your PHP controllers
│   ├── dmGenericObject.php          # Dolibarr mapping example
│   └── HomeController.php           # Example controller
└── smartmaker-api-prepend.php       # SmartAuth initialization

This tree is not decorative: it applies the organisation rules described in Architecture, in particular splitting the data layer into indexes.js and a useDb<Feature> hook, and splitting the translations per namespace.

Translations per namespace

The skeleton ships five namespaces, in French and in English: common, welcomePage, loginPage, deviceIdentificationPage and homePage.

const { t } = useTranslation('loginPage');

Warning

A Makefile deriving VITE_LOCALES must list the subdirectories of public/locales/, not the *.json files. Older recipes looked for locales/*.json: since the move to multiple namespaces they no longer find anything, the language list is empty and the application stays stuck on the fallback language.

Development proxy

If your Dolibarr is not served by the Vite development server, set the target in mobile/.env:

VITE_DEV_PROXY_TARGET=https://dolibarr.local/custom/monmodule/pwa

This is the directory that contains api.php, with no trailing slash. When the variable is missing or empty, no proxy is registered. The reasoning is explained in PWA, section "Front origin and API origin".

Two-pass linting

npm run lint runs oxlint then eslint .. The first pass is native and fast, the second covers what the first one does not handle. eslint-plugin-oxlint disables on the ESLint side the rules already checked by oxlint.

Dynamic PWA manifest

SmartBoot automatically configures the PWA manifest dynamically through SmartAuth. The index.html file points at api.php/manifest.webmanifest instead of a static file:

<link rel="manifest" href="api.php/manifest.webmanifest">
<link rel="icon" type="image/png" href="api.php/icon/64">
<link rel="apple-touch-icon" href="api.php/icon/192">

And in vite.config.js, the static manifest is disabled:

VitePWA({
  // ...
  manifest: false, // Served dynamically by SmartAuth
})

Important

This link is relative: it assumes the front and the API are served from the same origin, which is the case once the PWA is deployed in pwa/. In development, with vite dev on port 5173 and Dolibarr on another host, api.php has to be proxied. See PWA, section "Front origin and API origin".

See PWA for the Dolibarr constants, the two Service Worker modes and offline startup.

Layouts and route guards

SmartBoot only provides two visual layouts:

Layout Role
PagesLayout Global layout: applies the theme, the dark mode and the scale
AnimationLayout Handles the animated transitions between pages (Framer Motion)

Authentication and device identification are handled by the <RouteGuard> component from smartcommon (4 modes: requireGuest, requireAuth, requireDeviceIdentification, requireDeviceIdentified). See Advanced components -> RouteGuard for the detailed documentation.

Router example (SmartBoot skeleton)

import { Routes, Route, RouteGuard } from '@cap-rel/smartcommon';

import {
  LoginPage, HomePage, Error404Page, PagesLayout,
  WelcomePage, DeviceIdentificationPage, AnimationLayout,
} from 'src/components';

export const Router = () => (
  <Routes>
    <Route element={<PagesLayout />}>
      {/* Public pages: an authenticated user is sent back to / */}
      <Route element={<RouteGuard requireGuest />}>
        <Route path="/welcome" element={<WelcomePage />} />
        <Route path="/login" element={<LoginPage />} />
      </Route>

      {/* Authenticated, device identification still to be done */}
      <Route element={<RouteGuard requireDeviceIdentification />}>
        <Route path="/device-identification" element={<DeviceIdentificationPage />} />
      </Route>

      {/* Authenticated + device identified: every private page */}
      <Route element={<RouteGuard requireDeviceIdentified />}>
        {/* AnimationLayout is mounted ONCE here, never page by page */}
        <Route element={<AnimationLayout />}>
          <Route path="/" element={<HomePage />} />
          {/* Add your private routes here */}
        </Route>
      </Route>

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

Warning

No <BrowserRouter> here. The SmartCommon <Provider> already mounts one. Adding a second one produces the error You cannot render a <Router> inside another <Router>. A PWA served under a sub-path, or using hash-based deep links, passes config.router: "hash" and config.basename to the Provider rather than mounting its own router.

Note

Routes and Route are re-exported by SmartCommon: a page must never import react-router-dom directly. To navigate, use useNavigation().

Tip

Before the migration to smartcommon, SmartBoot provided four custom layouts (PrivatePagesLayout, PublicPagesLayout, PreDeviceIdentificationLayout, PostDeviceIdentificationLayout). They were replaced by <RouteGuard>, which centralises the logic in smartcommon and makes it possible to evolve without touching the skeleton.

Generated pages (LoginPage, DeviceIdentificationPage)

The public login and identification pages use the high-level components <LoginComponent> and <DeviceIdentificationComponent> from smartcommon. The skeleton only dresses them up (visual wrapper, background waves, register/forgot-password links) and wires onSuccess/onError to the local store.

  • <LoginComponent> includes the smartAuth QR pair flow for free (scan -> claim -> poll). See details.
  • <DeviceIdentificationComponent> reads useApi().user.deviceOptions to display either a plain input (first device) or a radio + input (pairing). See details.

AboutModal

SmartBoot integrates the smartcommon AboutModal (automatic PWA update check through usePWAUpdate):

import { AboutModal } from '@cap-rel/smartcommon';
import { APP_VERSION } from 'src/utils';

<AboutModal
  open={showAbout}
  onClose={() => setShowAbout(false)}
  appName="Mon Application"
  version={APP_VERSION}
/>

This component displays:

  • The application name and the version (APP_VERSION coming from VITE_APP_VERSION)
  • Optional free fields through the fields prop
  • A "Check for updates" button that restarts the Service Worker

See Advanced components -> AboutModal for the labels and slots.

Example controller

SmartBoot generates an example HomeController.php with a dmGenericObject.php mapping:

// smartmaker-api/HomeController.php
class HomeController
{
    public function index($arr = null)
    {
        global $db, $langs;

        $ret = [
            'statusCode' => 200,
            'generic_message' => "",
            'lastupdate'  => "",
            'home'  => "",
        ];

        return ([$ret, 200]);
    }
}

Components mounted by default

The skeleton already mounts, in App.jsx, three cross-cutting SmartCommon components. You have nothing to add:

Component Role
<UpdatePrompt> offers to reload when a new version is ready
<InstallPrompt> offers to install the PWA on the device
<ViewportProvider> exposes the display breakpoint (mobile, tablet, desktop)

Note

InstallPrompt has no translation bundle: its default labels are in English. The skeleton passes it its own labels, to be translated in your namespaces. See PWA, section "Offering the installation".

Next steps

You can now move on to development: