feat(dashboard): PaywallBanner + DashboardPage conditionnel (Sprint 1 étape 5)
This commit is contained in:
parent
d0f77e04f9
commit
bf778a5a4d
3 changed files with 157 additions and 12 deletions
|
|
@ -4,22 +4,12 @@ import { Navigate, Routes, Route } from 'react-router-dom'
|
||||||
import { LoginPage } from '@/features/auth/pages/LoginPage'
|
import { LoginPage } from '@/features/auth/pages/LoginPage'
|
||||||
import { RegisterPage } from '@/features/auth/pages/RegisterPage'
|
import { RegisterPage } from '@/features/auth/pages/RegisterPage'
|
||||||
import { ProtectedRoute } from '@/features/auth/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/features/auth/components/ProtectedRoute'
|
||||||
|
import { DashboardPage } from '@/features/dashboard/pages/DashboardPage'
|
||||||
|
|
||||||
const DesignSystemPage = import.meta.env.DEV
|
const DesignSystemPage = import.meta.env.DEV
|
||||||
? React.lazy(() => import('@/features/design-system/DesignSystemPage'))
|
? React.lazy(() => import('@/features/design-system/DesignSystemPage'))
|
||||||
: () => null
|
: () => null
|
||||||
|
|
||||||
function DashboardStub() {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen bg-canvas p-6">
|
|
||||||
<h1 className="text-2xl font-semibold text-ink-1">Dashboard — stub</h1>
|
|
||||||
<p className="mt-2 text-sm text-ink-3">
|
|
||||||
Cette vue sera remplacée par DashboardPage au prochain lot du Sprint 1.
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppRouter() {
|
export function AppRouter() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
@ -30,7 +20,7 @@ export function AppRouter() {
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardStub />
|
<DashboardPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
25
src/features/dashboard/components/PaywallBanner.tsx
Normal file
25
src/features/dashboard/components/PaywallBanner.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Bannière inline affichée sur le dashboard pour les utilisateurs Free.
|
||||||
|
* Présente les features débloquées par Standard et oriente vers /pricing.
|
||||||
|
* Pas de modale — intégrée dans le flux de la page (cf. PARCOURS_UTILISATEURS §2).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Button } from '@/shared/components/ui/button'
|
||||||
|
|
||||||
|
export function PaywallBanner() {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-expria-100 bg-expria-50 p-4 dark:border-expria/20">
|
||||||
|
<p className="text-sm font-semibold text-ink-1">Passez à Standard pour débloquer :</p>
|
||||||
|
<ul className="mt-2 space-y-1 text-sm text-ink-3" role="list">
|
||||||
|
<li>Simulations illimitées</li>
|
||||||
|
<li>Rapport détaillé par critère</li>
|
||||||
|
<li>Historique de vos productions</li>
|
||||||
|
<li>Suivi de progression</li>
|
||||||
|
</ul>
|
||||||
|
<Button asChild size="sm" className="mt-4 w-full">
|
||||||
|
<Link to="/pricing">Passer à Standard</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
130
src/features/dashboard/pages/DashboardPage.tsx
Normal file
130
src/features/dashboard/pages/DashboardPage.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* Page dashboard — affichage conditionnel selon le plan utilisateur.
|
||||||
|
*
|
||||||
|
* Toute logique de permission passe par hasAccess() et canSimulate()
|
||||||
|
* (Règles D et H — jamais de if plan === '...').
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Logo } from '@/shared/components/Logo'
|
||||||
|
import { ThemeToggle } from '@/shared/components/ThemeToggle'
|
||||||
|
import { Button } from '@/shared/components/ui/button'
|
||||||
|
import { Badge } from '@/shared/components/ui/badge'
|
||||||
|
import { hasAccess, canSimulate } from '@/entities/user/lib'
|
||||||
|
import type { Plan } from '@/entities/user/types'
|
||||||
|
import { useAuth } from '@/features/auth/hooks/useAuth'
|
||||||
|
import { usePlan } from '../hooks/usePlan'
|
||||||
|
import { PaywallBanner } from '../components/PaywallBanner'
|
||||||
|
import { PLAN_QUERY_KEY } from '../hooks/usePlan'
|
||||||
|
|
||||||
|
const PLAN_LABELS: Record<Plan, string> = {
|
||||||
|
free: 'Plan Découverte',
|
||||||
|
standard: 'Plan Standard',
|
||||||
|
premium: 'Plan Premium',
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayName(user: { user_metadata?: { full_name?: string }; email?: string } | null): string {
|
||||||
|
const fullName = user?.user_metadata?.full_name
|
||||||
|
if (fullName) return fullName.split(' ')[0]
|
||||||
|
const email = user?.email
|
||||||
|
if (email) return email.split('@')[0]
|
||||||
|
return 'vous'
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashboardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6" aria-busy="true" aria-label="Chargement du tableau de bord">
|
||||||
|
<div className="h-8 w-48 animate-pulse rounded-md bg-canvas-2" />
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||||
|
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||||
|
</div>
|
||||||
|
<div className="h-9 animate-pulse rounded-md bg-canvas-2" />
|
||||||
|
<div className="h-16 animate-pulse rounded-lg bg-canvas-2" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardPage() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
const { data, isLoading, isError } = usePlan()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const displayName = getDisplayName(user)
|
||||||
|
|
||||||
|
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">
|
||||||
|
{isLoading && <DashboardSkeleton />}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="space-y-3 text-center">
|
||||||
|
<p className="text-sm text-danger">
|
||||||
|
Impossible de charger votre tableau de bord. Réessayez dans quelques instants.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => queryClient.refetchQueries({ queryKey: PLAN_QUERY_KEY })}
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Salutation */}
|
||||||
|
<section className="flex flex-wrap items-center gap-3">
|
||||||
|
<h1 className="text-2xl font-semibold text-ink-1">
|
||||||
|
Bonjour, {displayName}
|
||||||
|
</h1>
|
||||||
|
<Badge variant="secondary">{PLAN_LABELS[data.plan]}</Badge>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Bannière upgrade — plan Free uniquement */}
|
||||||
|
{!hasAccess(data.plan, 'dashboard') && <PaywallBanner />}
|
||||||
|
|
||||||
|
{/* Métriques */}
|
||||||
|
<section
|
||||||
|
className="grid grid-cols-2 gap-4"
|
||||||
|
aria-label="Métriques de préparation"
|
||||||
|
>
|
||||||
|
<div className="rounded-lg border border-line bg-surface p-4">
|
||||||
|
<p className="text-xs text-ink-4">Simulations restantes</p>
|
||||||
|
<p className="mt-1 text-2xl font-semibold text-ink-1">
|
||||||
|
{data.simulations_remaining === null
|
||||||
|
? 'Illimitées'
|
||||||
|
: data.simulations_remaining}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-line bg-surface p-4">
|
||||||
|
<p className="text-xs text-ink-4">Niveau NCLC estimé</p>
|
||||||
|
<p className="mt-1 text-2xl font-semibold text-ink-1">—</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Nouvelle simulation */}
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={!canSimulate(data.plan, data.simulations_used).allowed}
|
||||||
|
>
|
||||||
|
Nouvelle simulation
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Dernières simulations */}
|
||||||
|
<section aria-label="Dernières simulations">
|
||||||
|
<h2 className="text-base font-semibold text-ink-1">Dernières simulations</h2>
|
||||||
|
<p className="mt-2 text-sm text-ink-4">Aucune simulation pour l'instant.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue