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

@ -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 }