chore: add documentation
ci-web / test (push) Successful in 14m44s

This commit is contained in:
Xor290
2026-08-03 15:11:40 +02:00
parent a9811fbb10
commit f0cb4d9045
20 changed files with 3026 additions and 1515 deletions
+1510
View File
File diff suppressed because one or more lines are too long
-1510
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
<script type="module" crossorigin src="/assets/index-CvCTqtVE.js"></script>
<script type="module" crossorigin src="/assets/index-Cfob2vGb.js"></script>
</head>
<body>
<div id="root"></div>
+2
View File
@@ -2,6 +2,7 @@ import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { Center, Spinner } from '@chakra-ui/react'
import { Landing } from './pages/Landing'
import { Pricing } from './pages/Pricing'
import { Documentation } from './pages/Documentation'
import { Login } from './pages/Login'
import { AdminLogin } from './pages/AdminLogin'
import { Register } from './pages/Register'
@@ -58,6 +59,7 @@ export function App() {
<Route element={<PublicLayout />}>
<Route path="/" element={<Landing />} />
<Route path="/tarifs" element={<Pricing />} />
<Route path="/documentation" element={<Documentation />} />
</Route>
<Route path="/login" element={<Login />} />
<Route path="/admin/login" element={<AdminLogin />} />
+1 -1
View File
@@ -22,7 +22,7 @@ export function Footer() {
</FooterCol>
<FooterCol title="Ressources">
<FooterExt href="#">Documentation</FooterExt>
<FooterLink to="/documentation">Documentation</FooterLink>
<FooterExt href="#">Statut</FooterExt>
<FooterExt href="#">Sécurité</FooterExt>
</FooterCol>
+1
View File
@@ -21,6 +21,7 @@ import { ColorModeToggle } from './ColorModeToggle'
const navLinks = [
{ to: '/', label: 'Accueil', end: true },
{ to: '/tarifs', label: 'Tarifs', end: false },
{ to: '/documentation', label: 'Documentation', end: false },
{ to: '/contact', label: 'Contact', end: false },
]
+329
View File
@@ -0,0 +1,329 @@
import type { ReactNode } from 'react'
import { Badge, Box, Flex, HStack, Progress, Stack, Text } from '@chakra-ui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
// Petits composants de maquette pour la page Documentation — reconstituent
// visuellement (et de façon interactive) l'espace admin de "gestion"
// (couleurs reprises de frontend-admin/src/theme/colors.ts) sans capture
// d'écran réelle. Icônes Font Awesome uniquement (pas d'emoji).
export const COLORS = {
bg: '#0a0a0a',
card: '#1e1e1e',
border: '#333333',
text: 'rgba(255,255,255,.87)',
text2: 'rgba(255,255,255,.6)',
text3: 'rgba(255,255,255,.4)',
accent: '#7c3aed',
accentLight: '#8b5cf6',
secondary: '#22d3ee',
success: '#4ade80',
danger: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6',
}
export function MockupDevice({ children }: { children: ReactNode }) {
return (
<Box
bg={COLORS.bg}
borderRadius="24px"
p="14px"
maxW="380px"
mx="auto"
boxShadow="0 30px 60px -20px rgba(60,20,110,.45)"
border="1px solid"
borderColor="whiteAlpha.100"
>
<Box bg={COLORS.bg} borderRadius="16px" p="16px 14px 20px" minH="220px">
{children}
</Box>
</Box>
)
}
export function ScreenTitle({
title,
subtitle,
action,
}: {
title: string
subtitle?: string
action?: ReactNode
}) {
return (
<Flex justify="space-between" align="flex-start" mb={4}>
<Box>
<Text color={COLORS.text} fontSize="1rem" fontWeight="800" mb={0}>
{title}
</Text>
{subtitle && (
<Text color={COLORS.text3} fontSize="0.72rem">
{subtitle}
</Text>
)}
</Box>
{action}
</Flex>
)
}
export function MockCard({
children,
mb = 2.5,
onClick,
active = false,
}: {
children: ReactNode
mb?: number
onClick?: () => void
active?: boolean
}) {
return (
<Box
bg={COLORS.card}
border="1px solid"
borderColor={active ? COLORS.accent : COLORS.border}
borderRadius="12px"
p={3}
mb={mb}
cursor={onClick ? 'pointer' : undefined}
transition="border-color .15s, transform .1s"
onClick={onClick}
_hover={onClick ? { borderColor: COLORS.accentLight } : undefined}
_active={onClick ? { transform: 'scale(0.99)' } : undefined}
>
{children}
</Box>
)
}
export function StatTile({
icon,
value,
label,
color = COLORS.accentLight,
}: {
icon: IconDefinition
value: string | number
label: string
color?: string
}) {
return (
<Box bg={COLORS.card} border="1px solid" borderColor={COLORS.border} borderRadius="12px" p={2.5}>
<Flex w="26px" h="26px" borderRadius="full" align="center" justify="center" bg={`${color}30`} mb={2}>
<FontAwesomeIcon icon={icon} style={{ color, fontSize: '0.7rem' }} />
</Flex>
<Text color={COLORS.text} fontSize="1.15rem" fontWeight="800" lineHeight="1">
{value}
</Text>
<Text color={COLORS.text3} fontSize="0.62rem" mt={1} textTransform="uppercase" letterSpacing="0.02em">
{label}
</Text>
</Box>
)
}
const BADGE_COLORS: Record<string, string> = {
success: COLORS.success,
danger: COLORS.danger,
warning: COLORS.warning,
info: COLORS.info,
accent: COLORS.accentLight,
muted: COLORS.text3,
}
export function MiniBadge({ children, tone = 'muted' }: { children: ReactNode; tone?: keyof typeof BADGE_COLORS }) {
const color = BADGE_COLORS[tone]
return (
<Badge
bg={`${color}26`}
color={color}
fontSize="0.62rem"
fontWeight="800"
px={2}
py={0.5}
borderRadius="full"
textTransform="none"
whiteSpace="nowrap"
>
{children}
</Badge>
)
}
export function MockText({
children,
variant = 'primary',
mono = false,
size,
}: {
children: ReactNode
variant?: 'primary' | 'secondary' | 'muted' | 'success' | 'danger'
mono?: boolean
size?: string
}) {
const colorMap = {
primary: COLORS.text,
secondary: COLORS.text2,
muted: COLORS.text3,
success: COLORS.success,
danger: COLORS.danger,
}
return (
<Text color={colorMap[variant]} fontSize={size ?? '0.78rem'} fontFamily={mono ? 'mono' : undefined} as="span">
{children}
</Text>
)
}
export function MockRow({ children }: { children: ReactNode }) {
return (
<HStack justify="space-between" align="center">
{children}
</HStack>
)
}
export function MockBar({ value, color = COLORS.accent }: { value: number; color?: string }) {
return (
<Progress
value={value}
size="xs"
borderRadius="full"
mt={1}
sx={{ '& > div': { background: color, transition: 'width .4s ease' }, background: '#2a2a2a' }}
/>
)
}
export function MockDot({ color, size = '9px' }: { color: string; size?: string }) {
return (
<Box as="span" display="inline-block" w={size} h={size} borderRadius="full" bg={color} mr={1.5} flexShrink={0} />
)
}
export function MockIcon({ icon, color, size = '0.75rem' }: { icon: IconDefinition; color?: string; size?: string }) {
return <FontAwesomeIcon icon={icon} style={{ color: color ?? COLORS.text2, fontSize: size, marginRight: 6 }} />
}
export function MockButton({
children,
tone = 'outline',
icon,
onClick,
isActive = false,
}: {
children: ReactNode
tone?: 'accent' | 'outline' | 'outlineDanger' | 'success'
icon?: IconDefinition
onClick?: () => void
isActive?: boolean
}) {
const styles = {
accent: { bg: COLORS.accent, color: 'white', border: 'none' },
outline: { bg: 'transparent', color: COLORS.text2, border: `1px solid ${COLORS.border}` },
outlineDanger: { bg: 'transparent', color: COLORS.danger, border: `1px solid ${COLORS.danger}` },
success: { bg: COLORS.success, color: '#0a0a0a', border: 'none' },
}[tone]
return (
<Box
as="button"
type="button"
display="inline-flex"
alignItems="center"
borderRadius="8px"
px={3}
py={1.5}
fontSize="0.68rem"
fontWeight="700"
cursor={onClick ? 'pointer' : 'default'}
transition="filter .15s, transform .1s"
opacity={isActive ? 1 : 0.92}
_hover={onClick ? { filter: 'brightness(1.15)' } : undefined}
_active={onClick ? { transform: 'scale(0.96)' } : undefined}
onClick={onClick}
{...styles}
>
{icon && <FontAwesomeIcon icon={icon} style={{ marginRight: 6, fontSize: '0.68rem' }} />}
{children}
</Box>
)
}
// Barre d'onglets cliquable (ex: filtres de statut) — vraie interactivité,
// pas une simple image de tabs.
export function MockTabs<T extends string>({
tabs,
active,
onChange,
}: {
tabs: { key: T; label: string }[]
active: T
onChange: (key: T) => void
}) {
return (
<HStack spacing={1.5} mb={3} flexWrap="wrap">
{tabs.map((t) => {
const isActive = t.key === active
return (
<Box
as="button"
key={t.key}
type="button"
onClick={() => onChange(t.key)}
px={2.5}
py={1}
borderRadius="999px"
fontSize="0.66rem"
fontWeight="700"
cursor="pointer"
transition="all .15s"
bg={isActive ? COLORS.accent : 'transparent'}
color={isActive ? 'white' : COLORS.text3}
border="1px solid"
borderColor={isActive ? COLORS.accent : COLORS.border}
_hover={{ borderColor: COLORS.accentLight, color: isActive ? 'white' : COLORS.text2 }}
>
{t.label}
</Box>
)
})}
</HStack>
)
}
export function MockStack({ children, spacing = 1.5 }: { children: ReactNode; spacing?: number }) {
return <Stack spacing={spacing}>{children}</Stack>
}
// Interrupteur cliquable (paramètres, liaisons de compte...) avec vrai état.
export function MockSwitch({ isOn, onToggle }: { isOn: boolean; onToggle: () => void }) {
return (
<Box
as="button"
type="button"
onClick={onToggle}
w="34px"
h="20px"
borderRadius="full"
bg={isOn ? COLORS.accent : COLORS.border}
position="relative"
cursor="pointer"
transition="background .2s"
flexShrink={0}
>
<Box
position="absolute"
top="2px"
left={isOn ? '16px' : '2px'}
w="16px"
h="16px"
borderRadius="full"
bg="white"
transition="left .2s"
/>
</Box>
)
}
@@ -0,0 +1,95 @@
import { useState } from 'react'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { COLORS, MockButton, MockCard, MockText, MockupDevice, ScreenTitle } from '../AdminMockup'
interface Correction {
id: string
wrong: string
right: string
}
const INITIAL: Correction[] = [{ id: '1', wrong: '10 rue de la paix', right: '10 Rue de la Paix, 75001 Paris' }]
// Le "+" révèle un vrai formulaire ; valider ajoute une carte à la liste.
export function AdressesSection() {
const [items, setItems] = useState(INITIAL)
const [showForm, setShowForm] = useState(false)
const [wrong, setWrong] = useState('')
const [right, setRight] = useState('')
const submit = () => {
if (!wrong.trim() || !right.trim()) return
setItems((prev) => [...prev, { id: String(prev.length + 1), wrong, right }])
setWrong('')
setRight('')
setShowForm(false)
}
return (
<MockupDevice>
<ScreenTitle
title="Corrections d'adresses"
action={
<MockButton tone="accent" icon={faPlus} onClick={() => setShowForm((v) => !v)}>
{showForm ? 'Fermer' : 'Ajouter'}
</MockButton>
}
/>
{showForm && (
<MockCard>
<input
placeholder="Adresse invalide (ex: 10 rue de la paix)"
value={wrong}
onChange={(e) => setWrong(e.target.value)}
style={{
width: '100%',
background: COLORS.bg,
border: `1px solid ${COLORS.border}`,
borderRadius: 8,
color: COLORS.text,
fontSize: '0.7rem',
padding: '6px 8px',
marginBottom: 6,
outline: 'none',
}}
/>
<input
placeholder="Adresse correcte (ex: 10 Rue de la Paix, 75001 Paris)"
value={right}
onChange={(e) => setRight(e.target.value)}
style={{
width: '100%',
background: COLORS.bg,
border: `1px solid ${COLORS.border}`,
borderRadius: 8,
color: COLORS.text,
fontSize: '0.7rem',
padding: '6px 8px',
marginBottom: 8,
outline: 'none',
}}
/>
<MockButton tone="accent" onClick={submit}>
Ajouter la correction
</MockButton>
</MockCard>
)}
{items.map((c) => (
<MockCard key={c.id}>
<MockText variant="danger" size="0.72rem">
{c.wrong}
</MockText>
<div style={{ margin: '3px 0' }}>
<FontAwesomeIcon icon={faArrowDown} style={{ color: COLORS.text3, fontSize: '0.62rem' }} />
</div>
<MockText variant="success" size="0.72rem">
{c.right}
</MockText>
</MockCard>
))}
</MockupDevice>
)
}
@@ -0,0 +1,73 @@
import { useState } from 'react'
import { faCheck, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { COLORS, MockButton, MockCard, MockRow, MockText, MockupDevice, ScreenTitle } from '../AdminMockup'
interface Alert {
id: string
driver: string
message: string
time: string
active: boolean
}
const INITIAL: Alert[] = [
{ id: '1', driver: 'karim_d', message: 'Accès bloqué, portail fermé', time: '03/08/2026, 14:32', active: true },
{ id: '2', driver: 'sofia_l', message: 'Client injoignable', time: '02/08/2026, 19:05', active: false },
]
// Le bouton "Résoudre" change réellement l'état de l'alerte (badge +
// couleur passent de rouge à vert), comme dans l'app.
export function AlertesSection() {
const [alerts, setAlerts] = useState(INITIAL)
const resolve = (id: string) => {
setAlerts((prev) => prev.map((a) => (a.id === id ? { ...a, active: false } : a)))
}
return (
<MockupDevice>
<ScreenTitle title="Alertes" subtitle="Cliquez « Résoudre » pour tester" />
{alerts.map((a) => (
<MockCard key={a.id}>
<MockRow>
<MockText variant={a.active ? 'danger' : 'secondary'}>
<FontAwesomeIcon
icon={faTriangleExclamation}
style={{ color: a.active ? COLORS.danger : COLORS.text3, marginRight: 6, fontSize: '0.72rem' }}
/>
{a.driver}
</MockText>
<span
style={{
background: a.active ? `${COLORS.danger}26` : `${COLORS.success}26`,
color: a.active ? COLORS.danger : COLORS.success,
fontSize: '0.6rem',
fontWeight: 800,
padding: '2px 8px',
borderRadius: 999,
}}
>
{a.active ? 'Active' : 'Terminée'}
</span>
</MockRow>
{a.active && (
<MockText variant="danger" size="0.7rem">
"{a.message}"
</MockText>
)}
<MockRow>
<MockText variant="muted" size="0.62rem">
{a.time}
</MockText>
{a.active && (
<MockButton tone="success" icon={faCheck} onClick={() => resolve(a.id)}>
Résoudre
</MockButton>
)}
</MockRow>
</MockCard>
))}
</MockupDevice>
)
}
@@ -0,0 +1,104 @@
import { useState } from 'react'
import { faChevronDown, faChevronUp, faPlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { COLORS, MockButton, MockCard, MockDot, MockRow, MockText, MockupDevice, ScreenTitle } from '../AdminMockup'
interface Category {
id: string
name: string
color: string
soon?: boolean
}
const INITIAL: Category[] = [
{ id: 'a', name: 'Fleurs', color: '#10b981' },
{ id: 'b', name: 'Résines', color: '#9333ea' },
{ id: 'c', name: 'Comestibles', color: '#3dc2f7', soon: true },
]
// Les flèches réordonnent réellement la liste (comme le fait l'app, en
// remplacement du drag-and-drop) — état local, persistance immédiate.
export function CategoriesSection() {
const [items, setItems] = useState(INITIAL)
const move = (index: number, dir: -1 | 1) => {
const target = index + dir
if (target < 0 || target >= items.length) return
setItems((prev) => {
const next = [...prev]
;[next[index], next[target]] = [next[target], next[index]]
return next
})
}
return (
<MockupDevice>
<ScreenTitle
title="Catégories"
action={
<MockButton tone="accent" icon={faPlus}>
Ajouter
</MockButton>
}
/>
{items.map((c, i) => (
<MockCard key={c.id}>
<MockRow>
<MockText>
<MockDot color={c.color} />
{c.name}
{c.soon && (
<span
style={{
marginLeft: 8,
background: `${COLORS.warning}26`,
color: COLORS.warning,
fontSize: '0.58rem',
fontWeight: 800,
padding: '2px 7px',
borderRadius: 999,
}}
>
Prochainement
</span>
)}
</MockText>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => move(i, -1)}
disabled={i === 0}
style={{
background: 'transparent',
border: 'none',
cursor: i === 0 ? 'default' : 'pointer',
color: i === 0 ? COLORS.border : COLORS.text2,
padding: 4,
}}
aria-label="Monter"
>
<FontAwesomeIcon icon={faChevronUp} style={{ fontSize: '0.7rem' }} />
</button>
<button
onClick={() => move(i, 1)}
disabled={i === items.length - 1}
style={{
background: 'transparent',
border: 'none',
cursor: i === items.length - 1 ? 'default' : 'pointer',
color: i === items.length - 1 ? COLORS.border : COLORS.text2,
padding: 4,
}}
aria-label="Descendre"
>
<FontAwesomeIcon icon={faChevronDown} style={{ fontSize: '0.7rem' }} />
</button>
</div>
</MockRow>
</MockCard>
))}
<MockText variant="muted" size="0.62rem">
Essayez les flèches / pour réordonner
</MockText>
</MockupDevice>
)
}
@@ -0,0 +1,109 @@
import { useState } from 'react'
import { faArrowRotateRight, faFileExport, faTruck, faUserCheck } from '@fortawesome/free-solid-svg-icons'
import {
COLORS,
MockButton,
MockCard,
MockRow,
MockTabs,
MockText,
MockupDevice,
} from '../AdminMockup'
type Tab = 'actives' | 'approuvees' | 'annulees'
const ORDERS: Record<Tab, { id: string; client: string; extra: string; tone: 'warning' | 'info' | 'accent'; label: string; amount: string }[]> = {
actives: [
{ id: '#1042', client: 'client_marie · 12 rue des Lilas', extra: 'il y a 4 min', tone: 'warning', label: 'En attente', amount: '46,00 €' },
{ id: '#1041', client: 'client_paul · Livreur: karim_d', extra: 'Net après parrainage', tone: 'info', label: 'En route', amount: '33,00 €' },
{ id: '#1040', client: 'client_lea · 8 avenue Foch', extra: 'en attente dassignation', tone: 'accent', label: 'Livreur arrivé', amount: '58,50 €' },
],
approuvees: [
{ id: '#1038', client: 'client_sam · 3 rue Victor Hugo', extra: 'Terminée il y a 1 h', tone: 'info', label: 'Livrée', amount: '27,00 €' },
{ id: '#1036', client: 'client_ana · 21 bd Voltaire', extra: 'Terminée hier', tone: 'info', label: 'Livrée', amount: '41,00 €' },
],
annulees: [
{ id: '#1029', client: 'client_theo · adresse introuvable', extra: 'Annulée par le livreur', tone: 'warning', label: 'Annulée', amount: '19,00 €' },
],
}
const TABS: { key: Tab; label: string }[] = [
{ key: 'actives', label: 'Actives' },
{ key: 'approuvees', label: 'Approuvées' },
{ key: 'annulees', label: 'Annulées' },
]
export function CommandesSection() {
const [tab, setTab] = useState<Tab>('actives')
const [expandedId, setExpandedId] = useState<string | null>('#1040')
const orders = ORDERS[tab]
return (
<MockupDevice>
<MockRow>
<MockText size="0.85rem" variant="primary">
Commandes
</MockText>
<MockButton tone="outline" icon={faFileExport}>
Export CSV
</MockButton>
</MockRow>
<MockText variant="muted" size="0.66rem">
Cliquez un onglet, puis une commande, pour explorer
</MockText>
<div style={{ marginTop: 10 }}>
<MockTabs tabs={TABS} active={tab} onChange={setTab} />
</div>
{orders.map((o) => {
const isOpen = expandedId === o.id
return (
<MockCard key={o.id} active={isOpen} onClick={() => setExpandedId(isOpen ? null : o.id)}>
<MockRow>
<MockText mono>{o.id}</MockText>
<MockText variant={o.tone === 'warning' ? 'danger' : 'primary'} size="0.62rem">
<span
style={{
background: `${{ warning: COLORS.warning, info: COLORS.info, accent: COLORS.accentLight }[o.tone]}26`,
color: { warning: COLORS.warning, info: COLORS.info, accent: COLORS.accentLight }[o.tone],
padding: '2px 9px',
borderRadius: 999,
fontWeight: 800,
}}
>
{o.label}
</span>
</MockText>
</MockRow>
<MockText variant="secondary" size="0.7rem">
{o.client}
</MockText>
<MockRow>
<MockText variant="muted" size="0.65rem">
{o.extra}
</MockText>
<MockText variant="primary" size="0.75rem">
{o.amount}
</MockText>
</MockRow>
{isOpen && (
<div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px solid ${COLORS.border}`, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<MockButton tone="accent" icon={faUserCheck}>
Assigner livreur
</MockButton>
<MockButton tone="outline" icon={faTruck}>
Passer en route
</MockButton>
<MockButton tone="outline" icon={faArrowRotateRight}>
Proposer adresse
</MockButton>
</div>
)}
</MockCard>
)
})}
</MockupDevice>
)
}
@@ -0,0 +1,53 @@
import { useState } from 'react'
import { faBell, faBox, faCheck, faClock, faLink, faLinkSlash, faLocationArrow, faUser, faUsers } from '@fortawesome/free-solid-svg-icons'
import { SimpleGrid } from '@chakra-ui/react'
import {
COLORS,
MockButton,
MockCard,
MockIcon,
MockRow,
MockText,
MockupDevice,
ScreenTitle,
StatTile,
} from '../AdminMockup'
// Tuiles de statistiques cliquées comme dans l'app (juste pour l'effet
// visuel) + interrupteur Telegram réellement fonctionnel (état local).
export function DashboardSection() {
const [telegramLinked, setTelegramLinked] = useState(true)
return (
<MockupDevice>
<ScreenTitle title="Bonjour, Admin" subtitle="Vue d'ensemble" />
<SimpleGrid columns={2} spacing={2} mb={2.5}>
<StatTile icon={faBox} value={312} label="Total commandes" color={COLORS.accentLight} />
<StatTile icon={faClock} value={8} label="En attente" color={COLORS.warning} />
<StatTile icon={faLocationArrow} value={5} label="En route" color={COLORS.info} />
<StatTile icon={faCheck} value={299} label="Terminées" color={COLORS.success} />
<StatTile icon={faUser} value={184} label="Clients" color={COLORS.accentLight} />
<StatTile icon={faUsers} value={6} label="Livreurs" color={COLORS.secondary} />
</SimpleGrid>
<MockCard mb={0}>
<MockRow>
<MockText>
<MockIcon icon={faBell} color={COLORS.secondary} />
Notifications Telegram
</MockText>
<MockButton
tone={telegramLinked ? 'outlineDanger' : 'accent'}
icon={telegramLinked ? faLinkSlash : faLink}
onClick={() => setTelegramLinked((v) => !v)}
>
{telegramLinked ? 'Délier' : 'Lier Telegram'}
</MockButton>
</MockRow>
<MockText variant="muted" size="0.68rem">
{telegramLinked ? 'Compte Telegram lié — alertes actives.' : 'Aucun compte lié — cliquez pour connecter.'}
</MockText>
</MockCard>
</MockupDevice>
)
}
@@ -0,0 +1,102 @@
import { useState } from 'react'
import { faClock, faLocationDot, faPowerOff, faRoute, faStar } from '@fortawesome/free-solid-svg-icons'
import { SimpleGrid } from '@chakra-ui/react'
import { COLORS, MockButton, MockCard, MockDot, MockRow, MockText, MockupDevice, StatTile } from '../AdminMockup'
const DRIVERS = [
{ name: 'karim_d', status: 'busy' as const, queue: 1, today: 14, total: 512, distance: '1,8 km', eta: '6 min' },
{ name: 'sofia_l', status: 'available' as const, queue: 0, today: 9, total: 340, distance: '0,6 km', eta: '2 min' },
{ name: 'yanis_b', status: 'offline' as const, queue: 0, today: 5, total: 128, distance: '—', eta: '—' },
]
const STATUS_TONE = { available: COLORS.success, busy: COLORS.warning, offline: COLORS.text3 }
const STATUS_LABEL = { available: 'Disponible', busy: 'Occupé', offline: 'Hors ligne' }
// Sélection d'un livreur = vraie mise à jour d'état (carte + fiche + bouton
// "suivre l'itinéraire"), pas une simple image de liste.
export function LivraisonSection() {
const [selected, setSelected] = useState('karim_d')
const driver = DRIVERS.find((d) => d.name === selected)!
return (
<MockupDevice>
<SimpleGrid columns={3} spacing={2} mb={2.5}>
<StatTile icon={faLocationDot} value={1} label="Dispo" color={COLORS.success} />
<StatTile icon={faClock} value={1} label="Occupés" color={COLORS.warning} />
<StatTile icon={faPowerOff} value={1} label="Hors ligne" color={COLORS.text3} />
</SimpleGrid>
<MockCard>
<div
style={{
height: 110,
borderRadius: 8,
position: 'relative',
backgroundImage:
'linear-gradient(135deg,#161022 25%,#1c1330 25%,#1c1330 50%,#161022 50%,#161022 75%,#1c1330 75%)',
backgroundSize: '18px 18px',
marginBottom: 6,
}}
>
<span
style={{
position: 'absolute',
top: 8,
left: 8,
background: `${STATUS_TONE[driver.status]}26`,
color: STATUS_TONE[driver.status],
fontSize: '0.62rem',
fontWeight: 800,
padding: '3px 9px',
borderRadius: 999,
}}
>
{driver.name} {driver.distance} · {driver.eta}
</span>
</div>
<MockText variant="muted" size="0.62rem">
Cliquez un livreur ci-dessous pour suivre son trajet en direct
</MockText>
</MockCard>
{DRIVERS.map((d) => {
const isSelected = d.name === selected
return (
<MockCard key={d.name} active={isSelected} onClick={() => setSelected(d.name)}>
<MockRow>
<MockText>
<MockDot color={STATUS_TONE[d.status]} />
{d.name}
</MockText>
<span
style={{
background: `${STATUS_TONE[d.status]}26`,
color: STATUS_TONE[d.status],
fontSize: '0.6rem',
fontWeight: 800,
padding: '2px 8px',
borderRadius: 999,
}}
>
{STATUS_LABEL[d.status]}
</span>
</MockRow>
<MockText variant="muted" size="0.62rem">
{d.queue} en attente · {d.today} aujourd'hui · {d.total} total
</MockText>
{isSelected && (
<div style={{ marginTop: 8, display: 'flex', gap: 6 }}>
<MockButton tone="outline" icon={faStar}>
Avis
</MockButton>
<MockButton tone={d.status !== 'offline' ? 'accent' : 'outline'} icon={faRoute}>
{d.status !== 'offline' ? "Suivre l'itinéraire" : 'Indisponible'}
</MockButton>
</div>
)}
</MockCard>
)
})}
</MockupDevice>
)
}
@@ -0,0 +1,80 @@
import { useState } from 'react'
import {
faChevronDown,
faChevronUp,
faCreditCard,
faPalette,
faPaperPlane,
faTruck,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { COLORS, MockCard, MockDot, MockRow, MockSwitch, MockText, MockupDevice, ScreenTitle } from '../AdminMockup'
const SECTIONS = [
{
id: 'delivery',
icon: faTruck,
title: 'Horaires de livraison',
body: 'Lundi → Vendredi : 11h00 22h30. Samedi : 12h00 23h00. Dimanche : fermé.',
},
{
id: 'crypto',
icon: faCreditCard,
title: 'Paiement crypto',
body: 'BTC, ETH, LTC, USDT acceptés via NowPayments. Clé API et secret IPN configurés séparément.',
},
{
id: 'telegram',
icon: faPaperPlane,
title: 'Notifications Telegram',
body: 'Bot configuré — @votre_bot. Authentification à deux facteurs activée pour les commandes sensibles.',
},
{
id: 'colors',
icon: faPalette,
title: "Couleurs de l'interface",
body: 'Personnalisez séparément les couleurs de votre espace admin et de lapp client.',
},
]
// Vrai accordéon : un seul panneau ouvert à la fois, clic pour
// déplier/replier — comme la page Paramètres réelle.
export function ParametresSection() {
const [openId, setOpenId] = useState<string | null>('crypto')
const [cryptoOn, setCryptoOn] = useState(true)
return (
<MockupDevice>
<ScreenTitle title="Paramètres" subtitle="Cliquez une section pour l'ouvrir" />
{SECTIONS.map((s) => {
const isOpen = openId === s.id
return (
<MockCard key={s.id} onClick={() => setOpenId(isOpen ? null : s.id)}>
<MockRow>
<MockText>
<FontAwesomeIcon icon={s.icon} style={{ color: COLORS.accentLight, marginRight: 8, fontSize: '0.72rem' }} />
{s.title}
</MockText>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{s.id === 'crypto' && (
<span onClick={(e) => e.stopPropagation()}>
<MockSwitch isOn={cryptoOn} onToggle={() => setCryptoOn((v) => !v)} />
</span>
)}
<FontAwesomeIcon icon={isOpen ? faChevronUp : faChevronDown} style={{ color: COLORS.text3, fontSize: '0.62rem' }} />
</div>
</MockRow>
{isOpen && (
<div style={{ marginTop: 8, paddingTop: 8, borderTop: `1px solid ${COLORS.border}` }}>
<MockText variant="muted" size="0.68rem">
<MockDot color={COLORS.text3} size="5px" />
{s.body}
</MockText>
</div>
)}
</MockCard>
)
})}
</MockupDevice>
)
}
@@ -0,0 +1,126 @@
import { useState } from 'react'
import { faCircleCheck, faCircleXmark, faImage, faPen, faPlus, faTrash, faVideo } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { COLORS, MockButton, MockCard, MockRow, MockText, MockupDevice, ScreenTitle } from '../AdminMockup'
interface Tier {
label: string
price: string
active: boolean
}
const INITIAL_TIERS: Tier[] = [
{ label: '1u', price: '12€', active: true },
{ label: '3u', price: '30€', active: true },
{ label: '5u', price: '45€', active: false },
]
// Les paliers de prix sont réellement cliquables : on active/désactive un
// tarif comme dans l'app, avec le style barré qui suit l'état.
export function ProduitsSection() {
const [tiers, setTiers] = useState(INITIAL_TIERS)
const toggleTier = (i: number) => {
setTiers((prev) => prev.map((t, idx) => (idx === i ? { ...t, active: !t.active } : t)))
}
return (
<MockupDevice>
<ScreenTitle
title="Produits (48)"
action={
<MockButton tone="accent" icon={faPlus}>
Créer
</MockButton>
}
/>
<MockCard>
<MockRow>
<MockText variant="primary" size="0.82rem">
Pack Découverte
</MockText>
<span
style={{
background: `${COLORS.accentLight}26`,
color: COLORS.accentLight,
fontSize: '0.6rem',
fontWeight: 800,
padding: '2px 8px',
borderRadius: 999,
}}
>
Premium
</span>
</MockRow>
<MockText variant="muted" size="0.65rem">
<FontAwesomeIcon icon={faImage} style={{ marginRight: 4 }} />3 images ·{' '}
<FontAwesomeIcon icon={faVideo} style={{ marginRight: 4 }} />1 vidéo · Stock: 24 u
</MockText>
<MockText variant="muted" size="0.66rem">
Cliquez un tarif pour l'activer / le désactiver
</MockText>
<div style={{ display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' }}>
{tiers.map((t, i) => (
<div
key={t.label}
onClick={() => toggleTier(i)}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
padding: '4px 9px',
borderRadius: 8,
border: `1px solid ${COLORS.border}`,
opacity: t.active ? 1 : 0.5,
textDecoration: t.active ? 'none' : 'line-through',
}}
>
<FontAwesomeIcon
icon={t.active ? faCircleCheck : faCircleXmark}
style={{ color: t.active ? COLORS.success : COLORS.danger, fontSize: '0.7rem' }}
/>
<span style={{ color: COLORS.text, fontSize: '0.72rem' }}>
{t.label} = {t.price}
</span>
</div>
))}
</div>
<div style={{ display: 'flex', gap: 6, marginTop: 10 }}>
<MockButton tone="outline" icon={faPen}>
Modifier
</MockButton>
<MockButton tone="outlineDanger" icon={faTrash}>
Supprimer
</MockButton>
</div>
</MockCard>
<MockCard mb={0}>
<MockRow>
<MockText variant="primary" size="0.82rem">
Édition Limitée
</MockText>
<span
style={{
background: `${COLORS.warning}26`,
color: COLORS.warning,
fontSize: '0.6rem',
fontWeight: 800,
padding: '2px 8px',
borderRadius: 999,
}}
>
À venir
</span>
</MockRow>
<MockText variant="muted" size="0.65rem">
Stock: 0 u masqué du catalogue tant que le stock est vide
</MockText>
</MockCard>
</MockupDevice>
)
}
@@ -0,0 +1,79 @@
import { useMemo, useState } from 'react'
import { faFire } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SimpleGrid } from '@chakra-ui/react'
import { COLORS, MockBar, MockCard, MockRow, MockTabs, MockText, MockupDevice, ScreenTitle, StatTile } from '../AdminMockup'
import { faCashRegister, faChartLine, faReceipt, faTrophy } from '@fortawesome/free-solid-svg-icons'
type SortKey = 'quantite' | 'commandes' | 'revenus'
const PRODUCTS = [
{ name: 'Pack Découverte', quantite: 210, commandes: 86, revenus: 1032 },
{ name: 'Édition Standard', quantite: 140, commandes: 54, revenus: 648 },
{ name: 'Format Duo', quantite: 96, commandes: 41, revenus: 492 },
]
const SORT_TABS: { key: SortKey; label: string }[] = [
{ key: 'quantite', label: 'Quantité' },
{ key: 'commandes', label: 'Commandes' },
{ key: 'revenus', label: 'Revenus' },
]
const BAR_COLOR: Record<SortKey, string> = {
quantite: COLORS.accent,
commandes: COLORS.info,
revenus: COLORS.success,
}
// Le tri "Top produits" est un vrai tri (state) qui recalcule et réanime
// les barres — pas 3 captures figées.
export function StatistiquesSection() {
const [sort, setSort] = useState<SortKey>('revenus')
const sorted = useMemo(() => [...PRODUCTS].sort((a, b) => b[sort] - a[sort]), [sort])
const max = sorted[0][sort]
return (
<MockupDevice>
<ScreenTitle title="Statistiques" subtitle="Activité globale & produits" />
<SimpleGrid columns={2} spacing={2} mb={2.5}>
<StatTile icon={faReceipt} value={312} label="Commandes totales" color={COLORS.accentLight} />
<StatTile icon={faCashRegister} value="6,4k€" label="Revenus" color={COLORS.success} />
</SimpleGrid>
<MockCard mb={0}>
<MockRow>
<MockText variant="secondary" size="0.72rem">
Top produits
</MockText>
<FontAwesomeIcon icon={faChartLine} style={{ color: COLORS.text3, fontSize: '0.7rem' }} />
</MockRow>
<div style={{ marginTop: 8 }}>
<MockTabs tabs={SORT_TABS} active={sort} onChange={setSort} />
</div>
{sorted.map((p, i) => {
const value = p[sort]
const display = sort === 'revenus' ? `${value}` : value
return (
<div key={p.name} style={{ marginBottom: 8 }}>
<MockRow>
<MockText variant="muted" size="0.68rem">
{i === 0 && <FontAwesomeIcon icon={faFire} style={{ color: COLORS.warning, marginRight: 4 }} />}
{p.name}
</MockText>
<MockText variant="muted" size="0.68rem">
{display}
</MockText>
</MockRow>
<MockBar value={(value / max) * 100} color={BAR_COLOR[sort]} />
</div>
)
})}
<MockText variant="muted" size="0.62rem">
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: 4 }} />
Classement recalculé automatiquement selon le tri choisi
</MockText>
</MockCard>
</MockupDevice>
)
}
@@ -0,0 +1,82 @@
import { useState } from 'react'
import { faBicycle, faShieldHalved, faUser, faUsers } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SimpleGrid } from '@chakra-ui/react'
import { COLORS, MockCard, MockRow, MockText, MockupDevice } from '../AdminMockup'
type Role = 'tous' | 'clients' | 'livreurs' | 'admins'
const USERS = [
{ name: 'client_marie', role: 'clients' as const, icon: faUser, tone: COLORS.info, detail: 'Cmd: 12 · Points: 340 · Parrain: +8€' },
{ name: 'karim_d', role: 'livreurs' as const, icon: faBicycle, tone: COLORS.success, detail: '512 livraisons · connecté aujourdhui' },
{ name: 'admin_yas', role: 'admins' as const, icon: faShieldHalved, tone: COLORS.accentLight, detail: 'Accès complet à la plateforme' },
]
const FILTERS: { key: Role; label: string; icon: typeof faUser; count: number }[] = [
{ key: 'tous', label: 'Tous', icon: faUsers, count: 3 },
{ key: 'clients', label: 'Clients', icon: faUser, count: 1 },
{ key: 'livreurs', label: 'Livreurs', icon: faBicycle, count: 1 },
{ key: 'admins', label: 'Admins', icon: faShieldHalved, count: 1 },
]
// Le filtre par rôle est un vrai filtre : cliquer une tuile réduit
// instantanément la liste affichée, comme dans l'application.
export function UtilisateursSection() {
const [role, setRole] = useState<Role>('tous')
const visible = role === 'tous' ? USERS : USERS.filter((u) => u.role === role)
return (
<MockupDevice>
<SimpleGrid columns={4} spacing={1.5} mb={2.5}>
{FILTERS.map((f) => {
const active = f.key === role
return (
<div
key={f.key}
onClick={() => setRole(f.key)}
style={{
cursor: 'pointer',
textAlign: 'center',
padding: '8px 2px',
borderRadius: 10,
border: `1px solid ${active ? COLORS.accent : COLORS.border}`,
background: active ? `${COLORS.accent}22` : COLORS.card,
}}
>
<FontAwesomeIcon icon={f.icon} style={{ color: active ? COLORS.accentLight : COLORS.text3, fontSize: '0.75rem' }} />
<div style={{ color: COLORS.text, fontSize: '0.72rem', fontWeight: 800, marginTop: 4 }}>{f.count}</div>
<div style={{ color: COLORS.text3, fontSize: '0.55rem' }}>{f.label}</div>
</div>
)
})}
</SimpleGrid>
{visible.map((u) => (
<MockCard key={u.name}>
<MockRow>
<MockText>
<FontAwesomeIcon icon={u.icon} style={{ color: u.tone, marginRight: 6, fontSize: '0.72rem' }} />
{u.name}
</MockText>
<span
style={{
background: `${u.tone}26`,
color: u.tone,
fontSize: '0.6rem',
fontWeight: 800,
padding: '2px 8px',
borderRadius: 999,
textTransform: 'capitalize',
}}
>
{u.role === 'clients' ? 'Client' : u.role === 'livreurs' ? 'Livreur' : 'Admin'}
</span>
</MockRow>
<MockText variant="muted" size="0.65rem">
{u.detail}
</MockText>
</MockCard>
))}
</MockupDevice>
)
}
+276
View File
@@ -0,0 +1,276 @@
import {
Box,
Button,
Container,
Heading,
HStack,
List,
ListIcon,
ListItem,
SimpleGrid,
Stack,
Text,
Wrap,
WrapItem,
} from '@chakra-ui/react'
import { Link as RouterLink } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import type { ReactNode } from 'react'
import { DashboardSection } from '../components/docs/sections/DashboardSection'
import { CommandesSection } from '../components/docs/sections/CommandesSection'
import { LivraisonSection } from '../components/docs/sections/LivraisonSection'
import { ProduitsSection } from '../components/docs/sections/ProduitsSection'
import { CategoriesSection } from '../components/docs/sections/CategoriesSection'
import { StatistiquesSection } from '../components/docs/sections/StatistiquesSection'
import { UtilisateursSection } from '../components/docs/sections/UtilisateursSection'
import { ParametresSection } from '../components/docs/sections/ParametresSection'
import { AlertesSection } from '../components/docs/sections/AlertesSection'
import { AdressesSection } from '../components/docs/sections/AdressesSection'
interface FeatureSpec {
id: string
kicker: string
title: string
lead: string
points: string[]
mockup: ReactNode
reverse?: boolean
}
const FEATURES: FeatureSpec[] = [
{
id: 'tableau-de-bord',
kicker: "Vue d'ensemble",
title: 'Toute votre activité en un coup d’œil',
lead: "Dès l'ouverture, vous voyez l'essentiel : commandes en attente ou en route, clients et livreurs actifs — sans avoir à chercher l'information.",
points: [
'Chiffres mis à jour en temps réel',
'Notifications Telegram directement liées à votre compte',
"Accès identique sur mobile et sur ordinateur",
],
mockup: <DashboardSection />,
},
{
id: 'commandes',
kicker: 'Suivi opérationnel',
title: 'Suivez chaque commande, de la validation à la livraison',
lead: "Chaque commande apparaît avec son statut, son montant et le livreur assigné. Un menu d'actions par commande permet d'assigner un livreur ou de la finaliser en un geste.",
points: [
'Archives des commandes approuvées et annulées, recherche instantanée',
'Export CSV pour votre comptabilité',
'Parrainage et fidélité pris en compte automatiquement dans le total',
],
mockup: <CommandesSection />,
reverse: true,
},
{
id: 'livraison',
kicker: 'Géolocalisation en direct',
title: 'Localisez vos livreurs en temps réel, sur la carte',
lead: "La position de chaque livreur s'affiche en direct, avec la distance et le temps restant estimé. Vue d'ensemble de toute la flotte : disponible, occupée ou hors ligne.",
points: [
'Distance et temps de trajet calculés automatiquement',
'Avis clients et historique de connexion par livreur',
'Notification client en un tap : "le livreur est arrivé"',
],
mockup: <LivraisonSection />,
},
{
id: 'produits',
kicker: 'Catalogue',
title: 'Un catalogue que vous gérez vous-même',
lead: "Ajoutez, modifiez ou retirez un produit en quelques secondes : photos, vidéos, stock, description, et plusieurs tarifs par produit.",
points: [
'Photos et vidéos multiples par produit',
'Plusieurs paliers de prix, activables indépendamment',
'Statut « à venir » pour annoncer un produit avant sa mise en vente',
],
mockup: <ProduitsSection />,
reverse: true,
},
{
id: 'categories',
kicker: 'Organisation',
title: 'Organisez votre catalogue comme vous le souhaitez',
lead: "Créez vos propres catégories, attribuez-leur une couleur, réordonnez-les en un clic. Une catégorie peut être marquée « prochainement » avant sa mise en ligne.",
points: [
'Couleur personnalisée par catégorie',
"Réorganisation manuelle de l'ordre d'affichage",
'Aperçu immédiat du rendu côté client',
],
mockup: <CategoriesSection />,
},
{
id: 'statistiques',
kicker: 'Pilotage',
title: 'Des chiffres clairs pour piloter votre activité',
lead: "Chiffre d'affaires, produits les plus vendus, heures et jours d'affluence : tout est visualisé simplement, sans éplucher vos commandes une par une.",
points: [
'Historique mensuel navigable, jour par jour',
'Classement des meilleurs produits (quantité, commandes ou revenus)',
"Repérage automatique du jour et de l'heure de pointe",
],
mockup: <StatistiquesSection />,
reverse: true,
},
{
id: 'utilisateurs',
kicker: 'Équipe & clients',
title: 'Clients, livreurs, équipe : tout au même endroit',
lead: "Un seul espace pour gérer tous les comptes. Ajustez les points de fidélité, appliquez une pénalité ou consultez l'historique d'un client.",
points: [
'Filtre par rôle et recherche instantanée',
'Gestion des points de fidélité et du parrainage',
"Suivi des annulations et de l'historique de connexion",
],
mockup: <UtilisateursSection />,
},
{
id: 'parametres',
kicker: 'Personnalisation',
title: "Une plateforme qui s'adapte à votre activité",
lead: 'Zones et horaires de livraison, moyens de paiement (dont le paiement crypto), Telegram, fidélité, couleurs : tout est configurable vous-même.',
points: [
'Zones de livraison par code postal avec minimum de commande',
'Paiement crypto (NowPayments) et bot Telegram intégrés',
"Couleurs de l'espace admin et de l'app client personnalisables",
],
mockup: <ParametresSection />,
reverse: true,
},
{
id: 'alertes',
kicker: 'Réactivité',
title: 'Réagissez immédiatement en cas de problème',
lead: "Si un livreur rencontre un souci sur le terrain, l'alerte remonte instantanément — avec son message et l'horodatage — jusqu'à ce qu'elle soit résolue.",
points: ['Distinction claire entre alerte active et résolue', 'Historique complet conservé'],
mockup: <AlertesSection />,
},
{
id: 'adresses',
kicker: 'Fiabilité livraison',
title: 'Zéro commande perdue à cause dune adresse mal saisie',
lead: "Quand un client tape une adresse imprécise, corrigez-la une bonne fois pour toutes : les prochaines commandes utiliseront automatiquement la bonne adresse.",
points: ['Association simple « adresse saisie → adresse correcte »', 'Moins derreurs de livraison, moins dallers-retours'],
mockup: <AdressesSection />,
reverse: true,
},
]
export function Documentation() {
return (
<Box>
{/* Hero */}
<Box bgGradient="linear(to-b, blackAlpha.50, transparent)" py={{ base: 14, md: 20 }}>
<Container maxW="container.lg">
<Stack spacing={5} textAlign="center" align="center">
<HStack spacing={2}>
<Box as="span" px={3} py={1} borderRadius="full" fontSize="xs" fontWeight="bold" bg="primary.500" color="white">
Espace admin
</Box>
<Box as="span" px={3} py={1} borderRadius="full" fontSize="xs" fontWeight="bold" borderWidth="1px">
Démo interactive
</Box>
</HStack>
<Heading size="2xl">Pilotez toute votre activité depuis un seul espace</Heading>
<Text fontSize="lg" color="gray.500" maxW="2xl">
Commandes, livraisons, produits, équipe, statistiques et paramètres. Chaque aperçu ci-dessous est
interactif : cliquez, filtrez, essayez comme dans l'application réelle.
</Text>
<Stack direction={{ base: 'column', sm: 'row' }} spacing={4}>
<Button as={RouterLink} to="/register" colorScheme="primary" size="lg">
Créer un compte
</Button>
<Button as={RouterLink} to="/tarifs" variant="outline" size="lg">
Voir les tarifs
</Button>
</Stack>
</Stack>
</Container>
</Box>
{/* Sommaire */}
<Box borderTopWidth="1px" borderBottomWidth="1px" bg="chakra-subtle-bg" position="sticky" top={0} zIndex={2}>
<Container maxW="container.lg" py={3} overflowX="auto">
<Wrap spacing={5} shouldWrapChildren>
{FEATURES.map((f) => (
<WrapItem key={f.id}>
<Text
as="a"
href={`#${f.id}`}
fontSize="sm"
fontWeight="semibold"
color="primary.500"
whiteSpace="nowrap"
_hover={{ textDecoration: 'underline' }}
>
{f.title.length > 28 ? f.kicker : f.title}
</Text>
</WrapItem>
))}
</Wrap>
</Container>
</Box>
{/* Sections */}
<Container maxW="container.lg">
{FEATURES.map((f) => (
<Stack
key={f.id}
id={f.id}
direction={{ base: 'column', md: f.reverse ? 'row-reverse' : 'row' }}
spacing={{ base: 10, md: 16 }}
align="center"
py={{ base: 14, md: 20 }}
borderBottomWidth="1px"
scrollMarginTop="60px"
>
<Box flex="0.9" minW={0}>
<Text fontSize="xs" fontWeight="extrabold" letterSpacing="wide" textTransform="uppercase" color="primary.500" mb={2}>
{f.kicker}
</Text>
<Heading size="lg" mb={4}>
{f.title}
</Heading>
<Text color="gray.500" mb={5}>
{f.lead}
</Text>
<List spacing={2}>
{f.points.map((p) => (
<ListItem key={p} fontSize="sm" color="gray.600" display="flex">
<ListIcon as={() => <FontAwesomeIcon icon={faCheck} />} color="green.400" mt={1} mr={2} />
<span>{p}</span>
</ListItem>
))}
</List>
</Box>
<Box flex="1" minW={0} w="full">
{f.mockup}
</Box>
</Stack>
))}
</Container>
{/* CTA */}
<Box bg="gray.900" color="white" py={20} textAlign="center">
<Container maxW="container.md">
<Heading size="xl" mb={4}>
Prêt à essayer votre propre espace admin ?
</Heading>
<Text color="whiteAlpha.700" mb={8}>
Une démo dédiée et isolée, prête en quelques minutes, pour tester la plateforme en conditions réelles.
</Text>
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={4} maxW="sm" mx="auto">
<Button as={RouterLink} to="/register" colorScheme="primary" size="lg">
Créer un compte
</Button>
<Button as={RouterLink} to="/tarifs" variant="outline" colorScheme="whiteAlpha" size="lg">
Voir les tarifs
</Button>
</SimpleGrid>
</Container>
</Box>
</Box>
)
}
+2 -2
View File
@@ -3,9 +3,9 @@ import { Link as RouterLink } from 'react-router-dom'
const features = [
{ title: 'Gestion de commandes', desc: 'Client, admin, cabine, livreur — un flux complet de bout en bout.' },
{ title: 'Livraison temps réel', desc: 'GPS TomTom, auto-assignation des livreurs, ETA et navigation.' },
{ title: 'Livraison temps réel', desc: 'GPS, auto-assignation des livreurs, ETA et navigation.' },
{ title: 'Paiements & notifications', desc: 'NowPayments (crypto), Telegram.' },
{ title: 'Sécurisé par design', desc: 'WAF ModSecurity/Coraza, TLS, JWT, isolation par démo.' },
{ title: 'Sécurisé par design', desc: 'Sécurité maximale avec RBAC, TLSv3, WAF et isolation de chaque plateforme.' },
]
export function Landing() {
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/theme.ts","./src/vite-env.d.ts","./src/components/BackofficeLayout.tsx","./src/components/ColorModeToggle.tsx","./src/components/ConfirmDialog.test.tsx","./src/components/ConfirmDialog.tsx","./src/components/CreateDemoModal.tsx","./src/components/DemoCard.tsx","./src/components/EditDomainModal.tsx","./src/components/Footer.tsx","./src/components/Header.tsx","./src/components/PasswordInput.tsx","./src/components/PodStatusPanel.tsx","./src/components/PublicLayout.tsx","./src/lib/api.ts","./src/lib/auth.tsx","./src/lib/format.test.ts","./src/lib/format.ts","./src/pages/AdminLogin.tsx","./src/pages/Landing.tsx","./src/pages/Login.tsx","./src/pages/Pricing.tsx","./src/pages/Register.tsx","./src/pages/backoffice/Codes.tsx","./src/pages/backoffice/Demos.tsx","./src/pages/backoffice/PremiumDemos.tsx","./src/pages/backoffice/Profile.tsx","./src/pages/backoffice/Subscription.tsx"],"version":"5.9.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/theme.ts","./src/vite-env.d.ts","./src/components/BackofficeLayout.tsx","./src/components/ColorModeToggle.tsx","./src/components/ConfirmDialog.test.tsx","./src/components/ConfirmDialog.tsx","./src/components/CreateDemoModal.tsx","./src/components/DemoCard.tsx","./src/components/EditDomainModal.tsx","./src/components/Footer.tsx","./src/components/Header.tsx","./src/components/PasswordInput.tsx","./src/components/PodStatusPanel.tsx","./src/components/PublicLayout.tsx","./src/components/docs/AdminMockup.tsx","./src/components/docs/sections/AdressesSection.tsx","./src/components/docs/sections/AlertesSection.tsx","./src/components/docs/sections/CategoriesSection.tsx","./src/components/docs/sections/CommandesSection.tsx","./src/components/docs/sections/DashboardSection.tsx","./src/components/docs/sections/LivraisonSection.tsx","./src/components/docs/sections/ParametresSection.tsx","./src/components/docs/sections/ProduitsSection.tsx","./src/components/docs/sections/StatistiquesSection.tsx","./src/components/docs/sections/UtilisateursSection.tsx","./src/lib/api.ts","./src/lib/auth.tsx","./src/lib/format.test.ts","./src/lib/format.ts","./src/pages/AdminLogin.tsx","./src/pages/Documentation.tsx","./src/pages/Landing.tsx","./src/pages/Login.tsx","./src/pages/Pricing.tsx","./src/pages/Register.tsx","./src/pages/backoffice/Codes.tsx","./src/pages/backoffice/Demos.tsx","./src/pages/backoffice/PremiumDemos.tsx","./src/pages/backoffice/Profile.tsx","./src/pages/backoffice/Subscription.tsx"],"version":"5.9.3"}