feat(auth): useAuth + ProtectedRoute + signUp dans auth-client (Sprint 1 étape 2)
This commit is contained in:
parent
107a37d197
commit
38777796aa
19 changed files with 2620 additions and 191 deletions
42
src/features/auth/components/ProtectedRoute.tsx
Normal file
42
src/features/auth/components/ProtectedRoute.tsx
Normal 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}</>
|
||||
}
|
||||
54
src/features/auth/hooks/useAuth.ts
Normal file
54
src/features/auth/hooks/useAuth.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue