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
- Hooks - Complete hook documentation
- SmartCommon - All available components
- Configuration - Provider options
- API Requests - useApi usage
- PHP Development - Backend API