feat(auth): useAuth + ProtectedRoute + signUp dans auth-client (Sprint 1 étape 2)

This commit is contained in:
Hermann_Kitio 2026-04-18 02:09:46 +03:00
parent 107a37d197
commit 38777796aa
19 changed files with 2620 additions and 191 deletions

View file

@ -0,0 +1,42 @@
/**
* Wrapper de route qui exige un utilisateur authentifié.
*
* - Pendant le chargement de la session : affiche un spinner centré.
* - Si non authentifié : redirige vers `/login` avec replace (pas d'entrée
* parasite dans l'historique navigateur).
* - Si authentifié : rend les `children`.
*
* Le backend reste l'autorité finale : cette garde est de l'UX. Les routes
* sensibles sont protégées par les middlewares Hono côté API (ADR 002).
*/
import { Navigate } from 'react-router-dom'
import { Loader2 } from 'lucide-react'
import { useAuth } from '../hooks/useAuth'
interface ProtectedRouteProps {
children: React.ReactNode
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isLoading, isAuthenticated } = useAuth()
if (isLoading) {
return (
<div
className="flex min-h-screen items-center justify-center bg-canvas text-ink-4"
role="status"
aria-live="polite"
aria-label="Chargement de la session"
>
<Loader2 className="size-6 animate-spin" aria-hidden="true" />
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <>{children}</>
}

View file

@ -0,0 +1,54 @@
/**
* Hook source de vérité pour l'état d'authentification dans toute l'app.
*
* Au mount : récupère la session courante depuis Supabase (cookie + localStorage).
* S'abonne ensuite à `onAuthStateChange` pour réagir aux login/logout/refresh
* token ; se désabonne au unmount.
*
* Consommé par `ProtectedRoute` (redirect si non authentifié) et par toute page
* qui a besoin du profil Supabase (ex. prénom affiché dans le header).
*/
import { useEffect, useState } from 'react'
import {
getCurrentSession,
subscribeToAuthChanges,
type User,
} from '@/shared/lib/auth-client'
interface UseAuthResult {
user: User | null
isLoading: boolean
isAuthenticated: boolean
}
export function useAuth(): UseAuthResult {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
let cancelled = false
getCurrentSession().then((session) => {
if (cancelled) return
setUser(session?.user ?? null)
setIsLoading(false)
})
const unsubscribe = subscribeToAuthChanges((session) => {
setUser(session?.user ?? null)
setIsLoading(false)
})
return () => {
cancelled = true
unsubscribe()
}
}, [])
return {
user,
isLoading,
isAuthenticated: user !== null,
}
}

View file

@ -26,29 +26,39 @@ import {
// ─── palette data ────────────────────────────────────────────────────────────
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
{ token: 'surface-hover',var: '--color-surface-hover',light: '#F8FAFD', dark: '#1E2A42' },
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
{ token: 'surface-hover', var: '--color-surface-hover', light: '#F8FAFD', dark: '#1E2A42' },
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
{ token: 'success-bg', var: '--color-success-bg', light: '#E6F6F0', dark: 'rgba(61,214,140,.12)' },
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
{ token: 'warning-bg', var: '--color-warning-bg', light: '#FEF3E2', dark: 'rgba(245,184,73,.12)' },
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
{
token: 'success-bg',
var: '--color-success-bg',
light: '#E6F6F0',
dark: 'rgba(61,214,140,.12)',
},
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
{
token: 'warning-bg',
var: '--color-warning-bg',
light: '#FEF3E2',
dark: 'rgba(245,184,73,.12)',
},
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
]
// ─── section wrapper ─────────────────────────────────────────────────────────
@ -71,7 +81,6 @@ export default function DesignSystemPage() {
return (
<div className="min-h-screen bg-canvas px-6 py-10 space-y-14">
{/* ── header ── */}
<header className="flex items-center justify-between">
<div>
@ -98,12 +107,8 @@ export default function DesignSystemPage() {
/>
<div className="space-y-0.5">
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
<p className="text-xs font-mono text-ink-4 leading-tight">
{light}
</p>
<p className="text-xs font-mono text-ink-4 leading-tight">
{dark}
</p>
<p className="text-xs font-mono text-ink-4 leading-tight"> {light}</p>
<p className="text-xs font-mono text-ink-4 leading-tight"> {dark}</p>
</div>
</div>
))}
@ -143,7 +148,9 @@ export default function DesignSystemPage() {
</div>
<div className="flex flex-wrap gap-2 items-center">
<Button disabled>Disabled</Button>
<Button variant="outline" disabled>Outline disabled</Button>
<Button variant="outline" disabled>
Outline disabled
</Button>
</div>
</div>
</Section>
@ -208,7 +215,7 @@ export default function DesignSystemPage() {
</div>
<div className="flex flex-col items-center gap-2">
<AvatarGroup>
{['AB', 'CD', 'EF'].map(initials => (
{['AB', 'CD', 'EF'].map((initials) => (
<Avatar key={initials}>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
@ -231,8 +238,8 @@ export default function DesignSystemPage() {
<DialogHeader>
<DialogTitle>Example dialog</DialogTitle>
<DialogDescription>
This dialog uses Direction H tokens bg-surface, border-line, text-ink-4.
Toggle the theme to see it adapt.
This dialog uses Direction H tokens bg-surface, border-line, text-ink-4. Toggle
the theme to see it adapt.
</DialogDescription>
</DialogHeader>
<DialogFooter showCloseButton>
@ -242,7 +249,6 @@ export default function DesignSystemPage() {
</Dialog>
</div>
</Section>
</div>
)
}

View file

@ -5,50 +5,50 @@
@theme {
/* ─── Typographie ───────────────────────────────────────────── */
--font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
system-ui, sans-serif;
--font-sans:
'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
/* ─── Fonds ─────────────────────────────────────────────────── */
/* bg-canvas = fond de page (jamais pur blanc) */
/* bg-surface = cards — ressortent sur le canvas */
--color-canvas: #EEF2F8;
--color-canvas-2: #E6EBF4;
--color-surface: #FFFFFF;
--color-surface-hover: #F8FAFD;
--color-canvas: #eef2f8;
--color-canvas-2: #e6ebf4;
--color-surface: #ffffff;
--color-surface-hover: #f8fafd;
/* ─── Hairlines ──────────────────────────────────────────────── */
--color-line: #DDE3ED;
--color-line-strong: #C7D0E0;
--color-line: #dde3ed;
--color-line-strong: #c7d0e0;
/* ─── Encres ─────────────────────────────────────────────────── */
--color-ink-1: #0F172A;
--color-ink-2: #1E293B;
--color-ink-1: #0f172a;
--color-ink-2: #1e293b;
--color-ink-3: #475569;
--color-ink-4: #64748B;
--color-ink-5: #94A3B8;
--color-ink-4: #64748b;
--color-ink-5: #94a3b8;
/* ─── Brand Expria ───────────────────────────────────────────── */
--color-expria: #1B4FD8;
--color-expria-hover: #1741B8;
--color-expria-50: #EEF3FF;
--color-expria-100: #DCE6FF;
--color-expria-200: #B8CDFF;
--color-deep: #0B1F5C;
--color-deep-2: #142B6E;
--color-expria: #1b4fd8;
--color-expria-hover: #1741b8;
--color-expria-50: #eef3ff;
--color-expria-100: #dce6ff;
--color-expria-200: #b8cdff;
--color-deep: #0b1f5c;
--color-deep-2: #142b6e;
/* ─── Sémantiques ────────────────────────────────────────────── */
--color-success: #0E9F6E;
--color-success-bg: #E6F6F0;
--color-warning: #C77A00;
--color-warning-bg: #FEF3E2;
--color-danger: #C53030;
--color-danger-bg: #FDECEC;
--color-success: #0e9f6e;
--color-success-bg: #e6f6f0;
--color-warning: #c77a00;
--color-warning-bg: #fef3e2;
--color-danger: #c53030;
--color-danger-bg: #fdecec;
/* ─── Rayons (override des defaults Tailwind) ────────────────── */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-full: 999px;
/* ─── Ombres (light mode) ────────────────────────────────────── */
@ -60,35 +60,35 @@
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
.dark {
/* Fonds */
--color-canvas: #0D1220;
--color-canvas-2: #121A2D;
--color-surface: #182238;
--color-surface-hover: #1E2A42;
--color-canvas: #0d1220;
--color-canvas-2: #121a2d;
--color-surface: #182238;
--color-surface-hover: #1e2a42;
/* Hairlines */
--color-line: #27324B;
--color-line-strong: #364363;
--color-line: #27324b;
--color-line-strong: #364363;
/* Encres */
--color-ink-1: #F1F4FA;
--color-ink-2: #DDE3EF;
--color-ink-3: #A8B2C7;
--color-ink-4: #7A8499;
--color-ink-5: #525C73;
--color-ink-1: #f1f4fa;
--color-ink-2: #dde3ef;
--color-ink-3: #a8b2c7;
--color-ink-4: #7a8499;
--color-ink-5: #525c73;
/* Brand — remonté en luminance pour rester lisible sur fond sombre */
--color-expria: #5B7FFF;
--color-expria-hover: #6F8EFF;
--color-expria-50: rgba(91, 127, 255, 0.12);
--color-deep: #060B1A;
--color-expria: #5b7fff;
--color-expria-hover: #6f8eff;
--color-expria-50: rgba(91, 127, 255, 0.12);
--color-deep: #060b1a;
/* Sémantiques */
--color-success: #3DD68C;
--color-success: #3dd68c;
--color-success-bg: rgba(61, 214, 140, 0.12);
--color-warning: #F5B849;
--color-warning: #f5b849;
--color-warning-bg: rgba(245, 184, 73, 0.12);
--color-danger: #F06B6B;
--color-danger-bg: rgba(240, 107, 107, 0.12);
--color-danger: #f06b6b;
--color-danger-bg: rgba(240, 107, 107, 0.12);
/* Ombres — jouer sur les surfaces, pas les ombres claires */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);

View file

@ -24,7 +24,7 @@ export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
<div
className={cn(
'inline-flex items-center gap-2.5 font-bold tracking-tight text-ink-1',
className
className,
)}
role={variant === 'icon' ? 'img' : undefined}
aria-label={variant === 'icon' ? 'Expria' : undefined}
@ -32,7 +32,7 @@ export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
<span
className={cn(
'flex shrink-0 items-center justify-center rounded-sm bg-expria font-bold tracking-tight text-white',
markStyles[size]
markStyles[size],
)}
aria-hidden="true"
>

View file

@ -1,36 +1,33 @@
import * as React from "react"
import { Avatar as AvatarPrimitive } from "radix-ui"
import * as React from 'react'
import { Avatar as AvatarPrimitive } from 'radix-ui'
import { cn } from "@/shared/lib/utils"
import { cn } from '@/shared/lib/utils'
function Avatar({
className,
size = "default",
size = 'default',
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
size?: 'default' | 'sm' | 'lg'
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
className
'group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6',
className,
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
className={cn('aspect-square size-full', className)}
{...props}
/>
)
@ -44,64 +41,54 @@ function AvatarFallback({
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 group-data-[size=sm]/avatar:text-xs",
className
'flex size-full items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 group-data-[size=sm]/avatar:text-xs',
className,
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-expria text-white ring-2 ring-canvas select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-expria text-white ring-2 ring-canvas select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className,
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-canvas",
className
'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-canvas',
className,
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 ring-2 ring-canvas group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 ring-2 ring-canvas group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className,
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarBadge,
AvatarGroup,
AvatarGroupCount,
}
export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount }

View file

@ -1,31 +1,23 @@
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import * as React from 'react'
import { XIcon } from 'lucide-react'
import { Dialog as DialogPrimitive } from 'radix-ui'
import { cn } from "@/shared/lib/utils"
import { Button } from "@/shared/components/ui/button"
import { cn } from '@/shared/lib/utils'
import { Button } from '@/shared/components/ui/button'
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
@ -37,8 +29,8 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
@ -59,8 +51,8 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-line bg-surface p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
className
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-line bg-surface p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
className,
)}
{...props}
>
@ -79,11 +71,11 @@ function DialogContent({
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
)
@ -94,16 +86,13 @@ function DialogFooter({
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
}: React.ComponentProps<'div'> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
>
{children}
@ -116,14 +105,11 @@ function DialogFooter({
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
className={cn('text-lg leading-none font-semibold', className)}
{...props}
/>
)
@ -136,7 +122,7 @@ function DialogDescription({
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-sm text-ink-4", className)}
className={cn('text-sm text-ink-4', className)}
{...props}
/>
)

View file

@ -1,17 +1,17 @@
import * as React from "react"
import * as React from 'react'
import { cn } from "@/shared/lib/utils"
import { cn } from '@/shared/lib/utils'
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-line bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-expria selection:text-white file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-ink-2 placeholder:text-ink-4 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-surface/30",
"focus-visible:border-expria focus-visible:ring-[3px] focus-visible:ring-expria/30",
"aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40",
className
'h-9 w-full min-w-0 rounded-md border border-line bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-expria selection:text-white file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-ink-2 placeholder:text-ink-4 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-surface/30',
'focus-visible:border-expria focus-visible:ring-[3px] focus-visible:ring-expria/30',
'aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40',
className,
)}
{...props}
/>

View file

@ -1,18 +1,15 @@
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import * as React from 'react'
import { Label as LabelPrimitive } from 'radix-ui'
import { cn } from "@/shared/lib/utils"
import { cn } from '@/shared/lib/utils'
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>

View file

@ -1,7 +1,7 @@
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import * as React from 'react'
import { Progress as ProgressPrimitive } from 'radix-ui'
import { cn } from "@/shared/lib/utils"
import { cn } from '@/shared/lib/utils'
function Progress({
className,
@ -11,10 +11,7 @@ function Progress({
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-expria/20",
className
)}
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-expria/20', className)}
{...props}
>
<ProgressPrimitive.Indicator

View file

@ -1,11 +1,11 @@
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import * as React from 'react'
import { Separator as SeparatorPrimitive } from 'radix-ui'
import { cn } from "@/shared/lib/utils"
import { cn } from '@/shared/lib/utils'
function Separator({
className,
orientation = "horizontal",
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
@ -15,8 +15,8 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-line data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
'shrink-0 bg-line data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>

View file

@ -1,4 +1,4 @@
import { createClient } from '@supabase/supabase-js'
import { createClient, type Session, type User } from '@supabase/supabase-js'
import { env } from '@/shared/config/env'
import { logger } from './logger'
@ -17,6 +17,32 @@ export async function signIn(email: string, password: string) {
return supabase.auth.signInWithPassword({ email, password })
}
export async function signUp(email: string, password: string) {
return supabase.auth.signUp({ email, password })
}
export async function signOut() {
return supabase.auth.signOut()
}
export async function getCurrentSession(): Promise<Session | null> {
const { data, error } = await supabase.auth.getSession()
if (error) {
logger.error('Auth session fetch failed', { name: error.name })
return null
}
return data.session
}
/**
* S'abonne aux changements d'état d'authentification Supabase.
* Retourne une fonction de désabonnement à appeler au cleanup.
*/
export function subscribeToAuthChanges(callback: (session: Session | null) => void): () => void {
const { data } = supabase.auth.onAuthStateChange((_event, session) => {
callback(session)
})
return () => data.subscription.unsubscribe()
}
export type { Session, User }