Documentation React
Les composants sont les éléments de base d'un projet React. Chacun représente une partie de l'interface utilisateur, réutilisable et maintenable.
src/components/
├── app/ # Composants d'infrastructure
│ ├── Provider/
│ │ └── index.jsx
│ └── Router/
│ ├── index.jsx
│ └── Guards/
│ └── index.jsx
├── pages/ # Pages de l'application
│ ├── public/ # Pages sans authentification
│ │ ├── LoginPage/
│ │ └── WelcomePage/
│ ├── private/ # Pages authentifiées
│ │ ├── HomePage/
│ │ └── SettingsPage/
│ └── errors/ # Pages d'erreur
│ └── Error404Page/
├── forms/ # Composants de formulaire
│ ├── LoginForm/
│ └── ItemForm/
├── ui/ # Composants UI réutilisables
│ ├── Card/
│ ├── Modal/
│ └── Header/
└── layouts/ # Layouts de pages
├── MainLayout/
└── AuthLayout/
src/components/ui/Card/ ├── index.jsx # Composant principal ├── Card.module.css # Styles (optionnel) └── Card.test.jsx # Tests (optionnel)
// src/components/ui/Card/index.jsx
export const Card = ({ children, className = '' }) => {
return (
<div className={`bg-white rounded-lg shadow-md p-4 ${className}`}>
{children}
</div>
);
};
// src/components/ui/Button/index.jsx
export const Button = ({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
onClick,
type = 'button',
className = '',
}) => {
const variants = {
primary: 'bg-primary text-white hover:bg-primary-dark',
secondary: 'bg-secondary text-white hover:bg-secondary-dark',
outline: 'border-2 border-primary text-primary hover:bg-primary/10',
ghost: 'text-primary hover:bg-primary/10',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={`
inline-flex items-center justify-center
rounded-md font-medium transition-all
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]}
${sizes[size]}
${className}
`}
>
{loading && (
<span className="mr-2 animate-spin">⟳</span>
)}
{children}
</button>
);
};
// src/components/form/Input/index.jsx
export const Input = (props) => {
const { label, id, error, ...inputProps } = props;
return (
<div className="flex flex-col gap-2">
{label && (
<label
htmlFor={id}
className="text-sm font-medium text-gray-700"
>
{label}
</label>
)}
<input
id={id}
className={`
bg-gray-100 p-4 rounded-lg
outline-none focus:ring-2 focus:ring-primary
${error ? 'ring-2 ring-red-500' : ''}
`}
{...inputProps}
/>
{error && (
<span className="text-sm text-red-500">{error}</span>
)}
</div>
);
};
// src/components/pages/private/HomePage/index.jsx
import { useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
export const HomePage = () => {
const navigate = useNavigation();
const [session] = useGlobalStates('session');
return (
<div className="min-h-screen bg-gray-100">
{/* Header */}
<header className="bg-white shadow p-4">
<h1 className="text-xl font-bold">Accueil</h1>
</header>
{/* Content */}
<main className="p-4">
<p>Bienvenue {session?.user?.name}</p>
</main>
{/* Navigation */}
<nav className="fixed bottom-0 left-0 right-0 bg-white shadow-lg">
<div className="flex justify-around p-2">
<button onClick={() => navigate('/')}>Accueil</button>
<button onClick={() => navigate('/settings')}>Paramètres</button>
</div>
</nav>
</div>
);
};
// src/components/pages/private/ItemsPage/index.jsx
import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect, useState } from 'react';
import { Card } from '../../ui/Card';
export const ItemsPage = () => {
const api = useApi();
const navigate = useNavigation();
const [items, setItems] = useGlobalStates('items');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchItems = async () => {
setLoading(true);
setError(null);
const response = await api.private.get('items');
if (response.success) {
setItems(response.data);
} else {
setError(response.error);
}
setLoading(false);
};
fetchItems();
}, []);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<span className="animate-spin text-4xl">⟳</span>
</div>
);
}
if (error) {
return (
<div className="p-4 text-center text-red-500">
Erreur: {error}
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 p-4">
<h1 className="text-2xl font-bold mb-4">Mes items</h1>
<div className="space-y-4">
{items.map((item) => (
<Card
key={item.id}
onClick={() => navigate(`/items/${item.id}`)}
className="cursor-pointer hover:shadow-lg transition-shadow"
>
<h2 className="font-semibold">{item.label}</h2>
<p className="text-gray-600 text-sm">{item.description}</p>
</Card>
))}
</div>
</div>
);
};
// src/components/layouts/MainLayout/index.jsx
import { useNavigation } from '@cap-rel/smartcommon';
export const MainLayout = ({ children, title, showBack = true }) => {
const navigate = useNavigation();
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
{/* Header */}
<header className="bg-white shadow p-4 flex items-center gap-4">
{showBack && (
<button onClick={() => navigate(-1)} className="text-primary">
← Retour
</button>
)}
<h1 className="text-xl font-bold">{title}</h1>
</header>
{/* Content */}
<main className="flex-1 p-4">
{children}
</main>
{/* Bottom navigation */}
<nav className="bg-white shadow-lg p-2">
<div className="flex justify-around">
<button onClick={() => navigate('/')}>🏠</button>
<button onClick={() => navigate('/search')}>🔍</button>
<button onClick={() => navigate('/profile')}>👤</button>
</div>
</nav>
</div>
);
};
// src/components/pages/private/SettingsPage/index.jsx
import { MainLayout } from '../../layouts/MainLayout';
import { useGlobalStates, useIntl } from '@cap-rel/smartcommon';
export const SettingsPage = () => {
const { t, lng, setLng } = useIntl();
const [theme, setTheme] = useGlobalStates('settings.theme');
return (
<MainLayout title={t('settings.title')}>
<div className="space-y-6">
{/* Langue */}
<section className="bg-white rounded-lg p-4">
<h2 className="font-semibold mb-2">{t('settings.language')}</h2>
<select
value={lng}
onChange={(e) => setLng(e.target.value)}
className="w-full p-2 border rounded"
>
<option value="fr">Français</option>
<option value="en">English</option>
</select>
</section>
{/* Thème */}
<section className="bg-white rounded-lg p-4">
<h2 className="font-semibold mb-2">{t('settings.theme')}</h2>
<div className="flex gap-4">
<button
onClick={() => setTheme('light')}
className={`p-4 rounded ${theme === 'light' ? 'ring-2 ring-primary' : ''}`}
>
☀️ Clair
</button>
<button
onClick={() => setTheme('dark')}
className={`p-4 rounded ${theme === 'dark' ? 'ring-2 ring-primary' : ''}`}
>
🌙 Sombre
</button>
</div>
</section>
</div>
</MainLayout>
);
};
import { useForm, useApi, useNavigation } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, "Minimum 2 caractères"),
email: z.string().email("Email invalide"),
});
const ContactForm = () => {
const api = useApi();
const navigate = useNavigation();
const form = useForm({ schema });
const handleSubmit = async (data) => {
const response = await api.private.post('contacts', { json: data });
if (response.success) {
navigate('/contacts');
}
};
return (
<Form form={form} onSubmit={handleSubmit} className="space-y-4">
<Input name="name" label="Nom" />
<Input name="email" type="email" label="Email" />
<Button type="submit" loading={form.formState.isSubmitting}>
Envoyer
</Button>
</Form>
);
};
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const Modal = ({ isOpen, onClose, children }) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Content */}
<motion.div
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
className="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 z-50"
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
};
const ItemsList = ({ items }) => {
if (items.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p className="text-4xl mb-4">📭</p>
<p>Aucun élément pour le moment</p>
</div>
);
}
return (
<div className="space-y-4">
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
</div>
);
};
Préférez les composants SmartCommon aux composants customs quand c'est possible :
import {
Form,
Input,
Select,
Checkbox,
Button,
Card,
Modal,
} from '@cap-rel/smartcommon';
Voir SmartCommon pour la liste complète.