257 lines
9.5 KiB
TypeScript
257 lines
9.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import {
|
|
Mail,
|
|
Lock,
|
|
User,
|
|
Shield,
|
|
Eye,
|
|
EyeOff,
|
|
Loader2,
|
|
ArrowLeft,
|
|
RefreshCw,
|
|
Check,
|
|
} from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
function generatePassword() {
|
|
const chars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789!@#$'
|
|
return Array.from({ length: 12 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
|
|
}
|
|
|
|
export default function CreateUserPage() {
|
|
const router = useRouter()
|
|
const [email, setEmail] = useState('')
|
|
const [username, setUsername] = useState('')
|
|
const [password, setPassword] = useState(generatePassword())
|
|
const [role, setRole] = useState<'formateur' | 'admin'>('formateur')
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch('/api/admin/users/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, username, password, role }),
|
|
})
|
|
const data = await res.json()
|
|
if (!res.ok) {
|
|
setError(data.error)
|
|
} else {
|
|
setSuccess(true)
|
|
setTimeout(() => router.push('/dashboard/admin/users'), 1500)
|
|
}
|
|
} catch {
|
|
setError('Erreur réseau')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
return (
|
|
<div className="p-8 flex items-center justify-center min-h-[60vh]">
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Check size={32} className="text-green-400" />
|
|
</div>
|
|
<h2 className="text-xl font-bold text-text-primary mb-2">Compte créé !</h2>
|
|
<p className="text-text-secondary">Redirection en cours...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="p-4 md:p-8">
|
|
<div className="max-w-xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<Link
|
|
href="/dashboard/admin/users"
|
|
className="flex items-center gap-1.5 text-sm text-text-muted hover:text-text-primary transition-colors mb-4"
|
|
>
|
|
<ArrowLeft size={15} />
|
|
Retour à la liste
|
|
</Link>
|
|
<h1 className="text-3xl font-bold text-text-primary mb-2">Créer un compte</h1>
|
|
<p className="text-text-secondary">
|
|
Le nouveau membre pourra se connecter immédiatement avec ses identifiants.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="card overflow-hidden">
|
|
{/* Identité */}
|
|
<div className="p-6 border-b border-border">
|
|
<div className="flex items-center gap-2 mb-5">
|
|
<User size={18} className="text-primary" />
|
|
<h2 className="font-semibold text-text-primary">Identité</h2>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm text-text-secondary mb-1.5">
|
|
Nom d'utilisateur *
|
|
</label>
|
|
<div className="relative">
|
|
<User size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="Ex: jean.dupont"
|
|
className="input-field pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-text-secondary mb-1.5">
|
|
Adresse email *
|
|
</label>
|
|
<div className="relative">
|
|
<Mail size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="Ex: jean.dupont@solyti.fr"
|
|
className="input-field pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rôle */}
|
|
<div className="p-6 border-b border-border">
|
|
<div className="flex items-center gap-2 mb-5">
|
|
<Shield size={18} className="text-primary" />
|
|
<h2 className="font-semibold text-text-primary">Rôle</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{([
|
|
{
|
|
value: 'formateur',
|
|
label: 'Formateur',
|
|
desc: 'Gère ses quiz et sessions',
|
|
icon: '👨🏫',
|
|
},
|
|
{
|
|
value: 'admin',
|
|
label: 'Administrateur',
|
|
desc: 'Accès complet + gestion des comptes',
|
|
icon: '🛡️',
|
|
},
|
|
] as const).map((r) => (
|
|
<button
|
|
key={r.value}
|
|
type="button"
|
|
onClick={() => setRole(r.value)}
|
|
className={cn(
|
|
'text-left p-4 rounded-xl border-2 transition-all',
|
|
role === r.value
|
|
? 'border-primary bg-primary/10'
|
|
: 'border-border bg-background-elevated/30 hover:border-border-light'
|
|
)}
|
|
>
|
|
<span className="text-2xl mb-2 block">{r.icon}</span>
|
|
<p className={cn(
|
|
'font-semibold text-sm',
|
|
role === r.value ? 'text-primary' : 'text-text-primary'
|
|
)}>
|
|
{r.label}
|
|
</p>
|
|
<p className="text-xs text-text-muted mt-0.5">{r.desc}</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mot de passe */}
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-2 mb-5">
|
|
<Lock size={18} className="text-primary" />
|
|
<h2 className="font-semibold text-text-primary">Mot de passe temporaire</h2>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-text-secondary mb-1.5">
|
|
Mot de passe *{' '}
|
|
<span className="text-text-muted">(min. 8 caractères)</span>
|
|
</label>
|
|
<div className="relative">
|
|
<Lock size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="input-field pl-9 pr-20"
|
|
required
|
|
minLength={8}
|
|
/>
|
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setPassword(generatePassword())}
|
|
className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted hover:text-text-primary transition-colors"
|
|
title="Générer un nouveau mot de passe"
|
|
>
|
|
<RefreshCw size={14} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword((v) => !v)}
|
|
className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted hover:text-text-primary transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-text-muted mt-1.5">
|
|
Communiquez ce mot de passe à l'utilisateur. Il pourra le modifier depuis ses paramètres.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-4 border-t border-border bg-background-secondary/50 flex items-center justify-between">
|
|
<Link href="/dashboard/admin/users" className="btn-secondary">
|
|
Annuler
|
|
</Link>
|
|
<div className="flex items-center gap-3">
|
|
{error && <p className="text-sm text-red-400 max-w-xs text-right">{error}</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className={cn('btn-primary', loading && 'opacity-70 cursor-not-allowed')}
|
|
>
|
|
{loading ? (
|
|
<><Loader2 size={16} className="animate-spin" /> Création...</>
|
|
) : (
|
|
'Créer le compte'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|