feat: add authentication support and user model
- Updated `env.example` to include NextAuth configuration for authentication. - Added `next-auth` dependency to manage user sessions. - Introduced `User` model in Prisma schema with fields for user details and password hashing. - Integrated `AuthProvider` in layout for session management across the app. - Enhanced `Header` component with `AuthButton` for user authentication controls.
This commit is contained in:
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import NextAuth from "next-auth"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
|
||||
const handler = NextAuth(authOptions)
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
59
src/app/api/auth/register/route.ts
Normal file
59
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { usersService } from '@/services/users'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, name, firstName, lastName, password } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email et mot de passe requis' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Vérifier si l'email existe déjà
|
||||
const emailExists = await usersService.emailExists(email)
|
||||
if (emailExists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Un compte avec cet email existe déjà' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Créer l'utilisateur
|
||||
const user = await usersService.createUser({
|
||||
email,
|
||||
name,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Compte créé avec succès',
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la création du compte' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
||||
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
|
||||
import { userPreferencesService } from "@/services/core/user-preferences";
|
||||
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
||||
import { AuthProvider } from "../components/AuthProvider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -36,19 +37,21 @@ export default async function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
initialTheme={initialPreferences.viewPreferences.theme}
|
||||
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
||||
>
|
||||
<KeyboardShortcutsProvider>
|
||||
<KeyboardShortcuts />
|
||||
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||
{children}
|
||||
</UserPreferencesProvider>
|
||||
</JiraConfigProvider>
|
||||
</KeyboardShortcutsProvider>
|
||||
</ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
initialTheme={initialPreferences.viewPreferences.theme}
|
||||
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
||||
>
|
||||
<KeyboardShortcutsProvider>
|
||||
<KeyboardShortcuts />
|
||||
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||
{children}
|
||||
</UserPreferencesProvider>
|
||||
</JiraConfigProvider>
|
||||
</KeyboardShortcutsProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
127
src/app/login/page.tsx
Normal file
127
src/app/login/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { signIn, getSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { TowerLogo } from '@/components/TowerLogo'
|
||||
import { TowerBackground } from '@/components/TowerBackground'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const result = await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
setError('Email ou mot de passe incorrect')
|
||||
} else {
|
||||
// Vérifier que la session est bien créée
|
||||
const session = await getSession()
|
||||
if (session) {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Une erreur est survenue')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
<TowerBackground />
|
||||
|
||||
{/* Contenu principal */}
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo et titre */}
|
||||
<TowerLogo size="md" className="mb-8" />
|
||||
|
||||
{/* Formulaire */}
|
||||
<div className="bg-[var(--card)]/80 backdrop-blur-sm rounded-2xl shadow-xl border border-[var(--border)] p-8">
|
||||
<h2 className="text-2xl font-mono font-bold text-[var(--foreground)] text-center mb-6">
|
||||
Connexion
|
||||
</h2>
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder=""
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Mot de passe
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder=""
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-[var(--destructive)] text-sm text-center bg-[var(--destructive)]/10 p-3 rounded-lg border border-[var(--destructive)]/20">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-3 text-lg font-mono"
|
||||
>
|
||||
{isLoading ? 'Connexion...' : 'Se connecter'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-[var(--muted-foreground)]">
|
||||
<p>
|
||||
Pas encore de compte ?{' '}
|
||||
<Link href="/register" className="text-[var(--primary)] hover:underline font-medium">
|
||||
Créer un compte
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
279
src/app/profile/page.tsx
Normal file
279
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useTransition } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Header } from '@/components/ui/Header'
|
||||
import { updateProfile, getProfile } from '@/actions/profile'
|
||||
|
||||
interface UserProfile {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
avatar: string | null
|
||||
role: string
|
||||
createdAt: string
|
||||
lastLoginAt: string | null
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session, update } = useSession()
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
|
||||
// Form data
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
avatar: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
fetchProfile()
|
||||
}, [session, router])
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const result = await getProfile()
|
||||
if (!result.success || !result.user) {
|
||||
throw new Error(result.error || 'Erreur lors du chargement du profil')
|
||||
}
|
||||
setProfile(result.user)
|
||||
setFormData({
|
||||
name: result.user.name || '',
|
||||
firstName: result.user.firstName || '',
|
||||
lastName: result.user.lastName || '',
|
||||
avatar: result.user.avatar || '',
|
||||
})
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Erreur inconnue')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await updateProfile(formData)
|
||||
|
||||
if (!result.success || !result.user) {
|
||||
setError(result.error || 'Erreur lors de la mise à jour')
|
||||
return
|
||||
}
|
||||
|
||||
setProfile(result.user)
|
||||
setSuccess('Profil mis à jour avec succès')
|
||||
|
||||
// Mettre à jour la session NextAuth
|
||||
await update({
|
||||
...session,
|
||||
user: {
|
||||
...session?.user,
|
||||
name: result.user.name || `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || result.user.email,
|
||||
firstName: result.user.firstName,
|
||||
lastName: result.user.lastName,
|
||||
avatar: result.user.avatar,
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Erreur inconnue')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="text-center text-[var(--muted-foreground)]">
|
||||
Chargement du profil...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="text-center text-[var(--destructive)]">
|
||||
Erreur lors du chargement du profil
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Informations générales */}
|
||||
<div className="bg-[var(--card)] rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||
Informations générales
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Email
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{profile.email}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
L'email ne peut pas être modifié
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Rôle
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{profile.role}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Membre depuis
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{new Date(profile.createdAt).toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profile.lastLoginAt && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Dernière connexion
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaire de modification */}
|
||||
<form onSubmit={handleSubmit} className="bg-[var(--card)] rounded-lg p-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||
Modifier le profil
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Prénom
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleChange('firstName', e.target.value)}
|
||||
placeholder="Votre prénom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom de famille
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleChange('lastName', e.target.value)}
|
||||
placeholder="Votre nom de famille"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom d'affichage (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="Nom d'affichage personnalisé"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Si vide, sera généré automatiquement à partir du prénom et nom
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="avatar" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
URL de l'avatar (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="avatar"
|
||||
value={formData.avatar}
|
||||
onChange={(e) => handleChange('avatar', e.target.value)}
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-[var(--destructive)] text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mt-4 text-[var(--success)] text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{isPending ? 'Sauvegarde...' : 'Sauvegarder les modifications'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
213
src/app/register/page.tsx
Normal file
213
src/app/register/page.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import Link from 'next/link'
|
||||
import { TowerLogo } from '@/components/TowerLogo'
|
||||
import { TowerBackground } from '@/components/TowerBackground'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [lastName, setLastName] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
// Validation côté client
|
||||
if (password !== confirmPassword) {
|
||||
setError('Les mots de passe ne correspondent pas')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setError('Le mot de passe doit contenir au moins 6 caractères')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
name,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Une erreur est survenue')
|
||||
}
|
||||
|
||||
// Rediriger vers la page de login avec un message de succès
|
||||
router.push('/login?message=Compte créé avec succès')
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Une erreur est survenue')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
<TowerBackground />
|
||||
|
||||
{/* Contenu principal */}
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo et titre */}
|
||||
<TowerLogo size="md" className="mb-8" />
|
||||
|
||||
{/* Formulaire */}
|
||||
<div className="bg-[var(--card)]/80 backdrop-blur-sm rounded-2xl shadow-xl border border-[var(--border)] p-8">
|
||||
<h2 className="text-2xl font-mono font-bold text-[var(--foreground)] text-center mb-6">
|
||||
Créer un compte
|
||||
</h2>
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="votre@email.com"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Prénom
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
placeholder="Prénom"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
placeholder="Nom"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom d'affichage (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Nom d'affichage personnalisé"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Mot de passe
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Minimum 6 caractères"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Répétez le mot de passe"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-[var(--destructive)] text-sm text-center bg-[var(--destructive)]/10 p-3 rounded-lg border border-[var(--destructive)]/20">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-3 text-lg font-mono"
|
||||
>
|
||||
{isLoading ? 'Création du compte...' : 'Créer le compte'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-[var(--muted-foreground)]">
|
||||
<p>
|
||||
Déjà un compte ?{' '}
|
||||
<Link href="/login" className="text-[var(--primary)] hover:underline font-medium">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user