feat: implement authentication system with email verification, user management dashboard, and administrative business controls
Some checks failed
Build and Push App / build (push) Failing after 1m9s

This commit is contained in:
2026-04-26 22:46:32 +02:00
parent 31fa2fcde3
commit 281fb764c4
19 changed files with 999 additions and 561 deletions

View File

@@ -1,14 +1,16 @@
"use client";
import React, { useState, useEffect, useTransition } from 'react';
import { getCountries, addCountry, deleteCountry, toggleCountryStatus } from '@/app/actions/countries';
import { Globe, Plus, Trash2, MapPin, Search, Loader2, Flag } from 'lucide-react';
import { getCountries, addCountry, deleteCountry, toggleCountryStatus, updateCountryDescription } from '@/app/actions/countries';
import { Globe, Plus, Trash2, MapPin, Search, Loader2, Flag, Edit2, Check, X } from 'lucide-react';
import { toast } from 'react-hot-toast';
export default function CountriesPage() {
const [countries, setCountries] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isAdding, setIsAdding] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editDesc, setEditDesc] = useState("");
const [isPending, startTransition] = useTransition();
const loadData = async () => {
@@ -27,9 +29,10 @@ export default function CountriesPage() {
const name = formData.get('name') as string;
const code = formData.get('code') as string;
const flag = formData.get('flag') as string;
const description = formData.get('description') as string;
startTransition(async () => {
const result = await addCountry({ name, code, flag });
const result = await addCountry({ name, code, flag, description });
if (result.success) {
toast.success("Pays ajouté avec succès");
setIsAdding(false);
@@ -55,10 +58,22 @@ export default function CountriesPage() {
const handleToggleStatus = async (id: string, current: boolean) => {
const result = await toggleCountryStatus(id, !current);
if (result.success) {
toast.success(!current ? "Pays activé" : "Pays désactivé");
loadData();
}
};
const handleSaveDescription = async (id: string) => {
const result = await updateCountryDescription(id, editDesc);
if (result.success) {
toast.success("Description mise à jour");
setEditingId(null);
loadData();
} else {
toast.error("Erreur lors de la mise à jour");
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-20">
@@ -85,26 +100,34 @@ export default function CountriesPage() {
{isAdding && (
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl p-6 mb-8 animate-in fade-in slide-in-from-top-2">
<h2 className="text-lg font-bold text-white mb-4">Nouveau Pays</h2>
<form onSubmit={handleAdd} className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nom du pays</label>
<input name="name" required placeholder="Ex: Sénégal" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500" />
<form onSubmit={handleAdd} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nom du pays</label>
<input name="name" required placeholder="Ex: Sénégal" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Code ISO (2 lettres)</label>
<input name="code" required maxLength={2} placeholder="Ex: SN" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500 uppercase" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Emoji Drapeau / URL Icone</label>
<input name="flag" placeholder="Ex: 🇸🇳" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Code ISO (2 lettres)</label>
<input name="code" required maxLength={2} placeholder="Ex: SN" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500 uppercase" />
<label className="text-sm font-medium text-slate-400">Note / Description (interne)</label>
<textarea name="description" placeholder="Ajouter un commentaire sur ce pays..." rows={2} className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Emoji Drapeau / URL Icone</label>
<input name="flag" placeholder="Ex: 🇸🇳" className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500" />
</div>
<div className="flex gap-2">
<button disabled={isPending} type="submit" className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white p-3 rounded-xl font-bold transition-all disabled:opacity-50">
{isPending ? 'Chargement...' : 'Enregistrer'}
</button>
<button type="button" onClick={() => setIsAdding(false)} className="bg-slate-800 hover:bg-slate-700 text-white p-3 rounded-xl font-bold transition-all">
<div className="flex gap-2 justify-end">
<button type="button" onClick={() => setIsAdding(false)} className="bg-slate-800 hover:bg-slate-700 text-white px-6 py-3 rounded-xl font-bold transition-all">
Annuler
</button>
<button disabled={isPending} type="submit" className="bg-emerald-600 hover:bg-emerald-700 text-white px-10 py-3 rounded-xl font-bold transition-all disabled:opacity-50">
{isPending ? 'Chargement...' : 'Enregistrer'}
</button>
</div>
</form>
</div>
@@ -116,6 +139,7 @@ export default function CountriesPage() {
<tr>
<th className="px-6 py-4">Pays</th>
<th className="px-6 py-4">Code</th>
<th className="px-6 py-4">Description / Notes</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
@@ -123,7 +147,7 @@ export default function CountriesPage() {
<tbody className="divide-y divide-slate-800">
{countries.length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-10 text-center text-slate-500">Aucun pays configuré.</td>
<td colSpan={5} className="px-6 py-10 text-center text-slate-500">Aucun pays configuré.</td>
</tr>
) : countries.map((country) => (
<tr key={country.id} className="hover:bg-slate-800/30 transition-colors">
@@ -134,15 +158,55 @@ export default function CountriesPage() {
</div>
</td>
<td className="px-6 py-4 font-mono text-indigo-400">{country.code}</td>
<td className="px-6 py-4">
{editingId === country.id ? (
<div className="flex items-center gap-2">
<input
value={editDesc}
onChange={(e) => setEditDesc(e.target.value)}
className="bg-slate-950 border border-slate-700 rounded-lg px-2 py-1 text-sm text-white focus:outline-none focus:border-indigo-500 w-full"
autoFocus
/>
<button onClick={() => handleSaveDescription(country.id)} className="text-emerald-500 hover:text-emerald-400">
<Check className="w-4 h-4" />
</button>
<button onClick={() => setEditingId(null)} className="text-slate-500 hover:text-slate-400">
<X className="w-4 h-4" />
</button>
</div>
) : (
<div className="group flex items-center gap-2 max-w-xs">
<span className="text-slate-400 text-sm truncate">
{country.description || <span className="italic text-slate-600">Pas de note</span>}
</span>
<button
onClick={() => {
setEditingId(country.id);
setEditDesc(country.description || "");
}}
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-white transition-opacity"
>
<Edit2 className="w-3 h-3" />
</button>
</div>
)}
</td>
<td className="px-6 py-4">
<button
onClick={() => handleToggleStatus(country.id, country.isActive)}
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-bold ${
country.isActive ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-500/10 text-slate-500'
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
country.isActive ? 'bg-indigo-600' : 'bg-slate-700'
}`}
>
{country.isActive ? 'Actif' : 'Inactif'}
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
country.isActive ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
<span className={`ml-3 text-xs font-bold ${country.isActive ? 'text-emerald-500' : 'text-slate-500'}`}>
{country.isActive ? 'ACTIF' : 'INACTIF'}
</span>
</td>
<td className="px-6 py-4 text-right">
<button onClick={() => handleDelete(country.id, country.name)} className="p-2 text-slate-500 hover:text-red-500 transition-colors">