feat(sprint-0.5-bis): AppLayout + primitives UI + refonte pages
- AppLayout (sidebar fixe, drawer mobile, BottomNav) - MobileHeader sticky + Sidebar avec verrouillage hasAccess() - Primitives src/shared/ui/ : Button, Card, Badge - SimulationPage + DashboardPage : suppression headers internes - TaskSelector : Card interactive + Badge EE/EO + eyebrow - router.tsx : layout routes + ComingSoon inline
This commit is contained in:
parent
997f39bd33
commit
8450265449
11 changed files with 752 additions and 161 deletions
|
|
@ -13,6 +13,8 @@
|
|||
import { Lock, Loader2 } from 'lucide-react'
|
||||
import { canSimulate } from '@/entities/user/lib'
|
||||
import { cn } from '@/shared/lib/utils'
|
||||
import { Card } from '@/shared/ui/Card'
|
||||
import { Badge } from '@/shared/ui/Badge'
|
||||
import type { Plan } from '@/entities/user/lib'
|
||||
import type { CreateSimulationPayload, Tache } from '@/entities/production/types'
|
||||
|
||||
|
|
@ -40,7 +42,6 @@ const TASK_CARDS: readonly TaskCard[] = [
|
|||
{ tache: null, label: 'Expression Orale', sublabel: 'Tâche 2 — Live', sprintLocked: true, lockLabel: 'Exclusivité Premium' },
|
||||
]
|
||||
|
||||
|
||||
export function TaskSelector({ plan, simulationsUsed, isLoading, onSelect }: Props) {
|
||||
const simulationCheck = canSimulate(plan, simulationsUsed)
|
||||
const quotaBlocked = !simulationCheck.allowed
|
||||
|
|
@ -70,41 +71,47 @@ export function TaskSelector({ plan, simulationsUsed, isLoading, onSelect }: Pro
|
|||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{TASK_CARDS.map((card) => {
|
||||
const locked = card.sprintLocked || quotaBlocked
|
||||
const disabled = locked || isLoading || card.tache === null
|
||||
const abbrev = (card.tache?.split('_')[0]) ?? 'EO'
|
||||
|
||||
if (locked || card.tache === null) {
|
||||
return (
|
||||
<Card
|
||||
key={card.tache ?? 'eo-t2'}
|
||||
variant="default"
|
||||
className="flex flex-col p-4 opacity-60"
|
||||
>
|
||||
<Lock className="mb-2 size-4 text-ink-4" aria-hidden="true" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
|
||||
{card.label}
|
||||
</span>
|
||||
<span className="mt-1 text-sm font-semibold text-ink-1">{card.sublabel}</span>
|
||||
{card.lockLabel && (
|
||||
<span className="mt-1.5 text-xs text-ink-4">{card.lockLabel}</span>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${card.tache ?? 'eo-t2'}`}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
<Card
|
||||
key={card.tache}
|
||||
variant="interactive"
|
||||
className={cn('relative flex flex-col p-4', isLoading && 'cursor-wait')}
|
||||
onClick={() => {
|
||||
if (card.tache && !locked) {
|
||||
onSelect({ tache: card.tache, mode: 'entrainement' })
|
||||
}
|
||||
if (!isLoading) onSelect({ tache: card.tache as Tache, mode: 'entrainement' })
|
||||
}}
|
||||
className={cn(
|
||||
'group relative flex flex-col rounded-lg border p-4 text-left transition-colors',
|
||||
locked || card.tache === null
|
||||
? 'cursor-not-allowed border-line bg-canvas-2 opacity-60'
|
||||
: 'cursor-pointer border-line bg-surface hover:border-expria hover:bg-expria-50',
|
||||
isLoading && !locked && 'cursor-wait',
|
||||
)}
|
||||
>
|
||||
{(card.sprintLocked || card.tache === null) && (
|
||||
<Lock
|
||||
className="mb-2 size-4 text-ink-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs font-medium text-ink-4">{card.label}</span>
|
||||
<span className="mt-0.5 text-sm font-semibold text-ink-1">{card.sublabel}</span>
|
||||
{card.lockLabel && (
|
||||
<span className="mt-1.5 text-xs text-ink-4">{card.lockLabel}</span>
|
||||
)}
|
||||
{isLoading && !locked && card.tache && (
|
||||
<Loader2 className="absolute right-3 top-3 size-3.5 animate-spin text-expria" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Badge variant="neutral">{abbrev}</Badge>
|
||||
{isLoading && (
|
||||
<Loader2 className="size-3.5 animate-spin text-expria" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
|
||||
{card.label}
|
||||
</span>
|
||||
<span className="mt-1 text-sm font-semibold text-ink-1">{card.sublabel}</span>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ import { useEffect } from 'react'
|
|||
import { useNavigate } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getPlanStatus } from '@/entities/user/api'
|
||||
import { Logo } from '@/shared/components/Logo'
|
||||
import { ThemeToggle } from '@/shared/components/ThemeToggle'
|
||||
import { Button } from '@/shared/components/ui/button'
|
||||
import { Button } from '@/shared/ui/Button'
|
||||
import { useSimulation } from '../hooks/useSimulation'
|
||||
import { TaskSelector } from '../components/TaskSelector'
|
||||
import { SimulationForm } from '../components/SimulationForm'
|
||||
|
|
@ -66,45 +64,38 @@ export function SimulationPage() {
|
|||
}, [step, production, navigate])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas">
|
||||
<header className="flex items-center justify-between border-b border-line bg-surface px-4 py-3">
|
||||
<Logo size="sm" />
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
<main className="mx-auto max-w-2xl px-4 py-6">
|
||||
{isPlanLoading && <SimulationSkeleton />}
|
||||
|
||||
<main className="mx-auto max-w-2xl px-4 py-6">
|
||||
{isPlanLoading && <SimulationSkeleton />}
|
||||
{isPlanError && (
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="text-sm text-danger">
|
||||
Impossible de charger vos informations. Réessayez dans quelques instants.
|
||||
</p>
|
||||
<Button variant="secondary" size="sm" onClick={() => refetchPlan()}>
|
||||
Réessayer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPlanError && (
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="text-sm text-danger">
|
||||
Impossible de charger vos informations. Réessayez dans quelques instants.
|
||||
</p>
|
||||
<Button variant="outline" size="sm" onClick={() => refetchPlan()}>
|
||||
Réessayer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{planData && step === 'idle' && (
|
||||
<TaskSelector
|
||||
plan={planData.plan}
|
||||
simulationsUsed={planData.simulations_used}
|
||||
isLoading={isCreating}
|
||||
onSelect={selectTask}
|
||||
/>
|
||||
)}
|
||||
|
||||
{planData && step === 'idle' && (
|
||||
<TaskSelector
|
||||
plan={planData.plan}
|
||||
simulationsUsed={planData.simulations_used}
|
||||
isLoading={isCreating}
|
||||
onSelect={selectTask}
|
||||
/>
|
||||
)}
|
||||
|
||||
{planData && (step === 'task-selected' || step === 'correcting') && production && (
|
||||
<SimulationForm
|
||||
tache={production.tache}
|
||||
isSubmitting={isCorrecting}
|
||||
error={correctError}
|
||||
onSubmit={submitText}
|
||||
onBack={reset}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
{planData && (step === 'task-selected' || step === 'correcting') && production && (
|
||||
<SimulationForm
|
||||
tache={production.tache}
|
||||
isSubmitting={isCorrecting}
|
||||
error={correctError}
|
||||
onSubmit={submitText}
|
||||
onBack={reset}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue