@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
|
||||
@@ -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 d’assignation', 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 l’app 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é aujourd’hui' },
|
||||
{ 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>
|
||||
)
|
||||
}
|
||||
@@ -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 d’une 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 d’erreurs de livraison, moins d’allers-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>
|
||||
)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user