React Development (frontend)

You just deployed SmartBoot in your module and wonder where to start? You're in the right place!

Install Dependencies

cd mobile
npm i

Initial Configuration

.env File

Copy or rename mobile/.env.example to mobile/.env:

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

Application Configuration

The appConfig.js file centralizes all configuration:

// src/appConfig.js

export const config = {
  // Debug mode (colored logs in console)
  debug: import.meta.env.DEV,

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

  // Storage
  storage: {
    local: ["session", "settings"],  // Persisted in localStorage
  },

  // Initial global state
  globalState: {
    reducers: {
      session: null,
      settings: { lng: "fr" },
      items: [],
    },
  },

  // Page animations
  pages: {
    "/": { "/settings": "slideLeft", "*": "fade" },
    "*": "fade",
  },
};

Launch

cd mobile
npm run dev

Open http://localhost:5173/ (mobile mode recommended in DevTools).

Important

SmartAuth is required!

Page Structure

The tree structure pre-installed by SmartBoot:

src/components/pages/
├── errors/
│   └── Error404Page/
│       └── index.jsx
├── private/
│   └── HomePage/
│       └── index.jsx
└── public/
    ├── LoginPage/
    │   └── index.jsx
    └── WelcomePage/
        └── index.jsx
Directory Description
public/ Pages accessible without authentication
private/ Pages requiring authentication
errors/ Error pages (404, etc.)

Create a Login Page

Tip

The SmartBoot skeleton now uses <LoginComponent> from smartcommon, which provides the complete form and the QR scan pairing with smartAuth. See Advanced Components -> LoginComponent. The example below is a manual setup useful if you want to understand the flow or write a very specific page.

With SmartCommon, the login page becomes very 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="Username"
          placeholder="Your login..."
        />
        <Input
          name="password"
          type="password"
          label="Password"
          placeholder="●●●●●●●●"
        />
        <Button type="submit" loading={form.isFormSubmitting}>
          Login
        </Button>
      </Form>
    </div>
  );
};

Create a Private Page

Home Page with List

// 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') ?? [];

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

  // Logout
  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">My items</h1>
        <button onClick={handleLogout} className="text-red-500">
          Logout
        </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>
  );
};

Detail Page

// 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">Loading...</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">
          ← Back
        </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>
  );
};

Configure the Router

// 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>
        {/* Public routes */}
        <Route element={<PublicRoutes />}>
          <Route path="/welcome" element={<WelcomePage />} />
          <Route path="/login" element={<LoginPage />} />
        </Route>

        {/* Private routes */}
        <Route element={<PrivateRoutes />}>
          <Route path="/" element={<HomePage />} />
          <Route path="/items/:id" element={<ItemPage />} />
        </Route>

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

Guards with SmartCommon

Tip

For apps from the SmartBoot skeleton, prefer <RouteGuard> which covers 4 modes (auth + device identification) and is maintained in smartcommon. See RouteGuard. The manual version below illustrates the underlying pattern.

// 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" />;
};

Use Global States

Store Data

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

const gst = useGlobalStates();

// Read a value
const items = gst.get('items');
const lng = gst.get('settings.lng');

// Write in memory (non-persistent)
gst.set('items', newItems);

// Write to localStorage (persistent)
gst.local.set('session', userData);

// Write to sessionStorage (session only)
gst.session.set('tempData', data);

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

Automatic Persistence

Keys listed in config.storage.local are automatically persisted in localStorage:

// appConfig.js
storage: {
  local: ["session", "settings"],  // Automatically persisted
}

Use Forms

Complete Form

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="Label" />
      <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}>Create</Button>
    </Form>
  );
};

Build and Deployment

# Production build
npm run build

# The build generates the dist/ folder
# Copy it to the pwa/ folder of your module
cp -r dist/* ../pwa/

Or use the Makefile:

make pwa

See Also