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

@@ -14,13 +14,14 @@ export async function getCountries() {
}
}
export async function addCountry(data: { name: string, code: string, flag?: string }) {
export async function addCountry(data: { name: string, code: string, flag?: string, description?: string }) {
try {
const country = await prisma.country.create({
data: {
name: data.name,
code: data.code.toUpperCase(),
flag: data.flag || null,
description: data.description || null,
isActive: true
}
});
@@ -58,3 +59,17 @@ export async function toggleCountryStatus(id: string, isActive: boolean) {
return { success: false };
}
}
export async function updateCountryDescription(id: string, description: string) {
try {
await prisma.country.update({
where: { id },
data: { description }
});
revalidatePath('/countries');
return { success: true };
} catch (error) {
console.error('Error updating country description:', error);
return { success: false, error: 'Erreur lors de la mise à jour.' };
}
}

View File

@@ -28,7 +28,31 @@ export async function getSiteSettings() {
homeBlogSelection: "latest",
homeBlogIds: [],
homeBlogCategories: [],
homeCategories: ["Technologie & IT", "Agriculture & Agrobusiness", "Mode & Textile", "Cosmétique & Beauté"]
homeCategories: ["Technologie & IT", "Agriculture & Agrobusiness", "Mode & Textile", "Cosmétique & Beauté"],
resetPasswordSubject: "Réinitialisation de votre mot de passe - Afrohub",
resetPasswordTemplate: `<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
<h2 style="color: #0d9488; text-align: center;">{siteName}</h2>
<p>Bonjour {name},</p>
<p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{resetUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Réinitialiser mon mot de passe</a>
</div>
<p>Ce lien expirera dans 1 heure. Si vous n'avez pas demandé cette réinitialisation, vous pouvez ignorer cet email.</p>
<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;">
<p style="font-size: 12px; color: #6b7280; text-align: center;">&copy; 2025 {siteName}. Tous droits réservés.</p>
</div>`,
verifyEmailSubject: "Vérification de votre compte - Afrohub",
verifyEmailTemplate: `<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
<h2 style="color: #0d9488; text-align: center;">{siteName}</h2>
<p>Bonjour {name},</p>
<p>Merci de vous être inscrit sur {siteName}. Pour finaliser la création de votre compte, merci de vérifier votre adresse email en cliquant sur le bouton ci-dessous :</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{verifyUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Vérifier mon compte</a>
</div>
<p>Si vous n'avez pas créé de compte sur notre plateforme, vous pouvez ignorer cet email.</p>
<hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;">
<p style="font-size: 12px; color: #6b7280; text-align: center;">&copy; 2025 {siteName}. Tous droits réservés.</p>
</div>`
};
if (!settings) return defaultSettings;

View File

@@ -25,3 +25,19 @@ export async function getClients() {
return { success: false, error: "Erreur lors de la récupération des clients" };
}
}
export async function verifyUserEmail(userId: string) {
try {
await prisma.user.update({
where: { id: userId },
data: {
emailVerified: true,
verificationToken: null
}
});
return { success: true };
} catch (error) {
console.error("Failed to verify user email:", error);
return { success: false, error: "Erreur lors de la vérification de l'email" };
}
}

View File

@@ -1,110 +0,0 @@
import { getClients } from "@/app/actions/user";
import { User, Mail, Phone, MapPin } from "lucide-react";
import ClientActions from "@/components/ClientActions";
export default async function ClientsPage() {
const result = await getClients();
const clients = result.success ? (result.data || []) : [];
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Gestion des Clients</h1>
<p className="text-slate-400">Liste des utilisateurs n'ayant pas encore de boutique active.</p>
</div>
<div className="bg-slate-800/50 px-4 py-2 rounded-lg border border-slate-700/50">
<span className="text-slate-400 text-sm">Total Clients: </span>
<span className="text-indigo-400 font-bold ml-1">{clients.length}</span>
</div>
</div>
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="data-table">
<thead>
<tr>
<th>Utilisateur</th>
<th>Contact</th>
<th>Localisation</th>
<th>Statut</th>
<th className="text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{clients.length === 0 ? (
<tr>
<td colSpan={5} className="py-12 text-center text-slate-500">
<div className="flex flex-col items-center gap-2">
<User className="w-8 h-8 opacity-20" />
<p>Aucun client trouvé.</p>
</div>
</td>
</tr>
) : (
clients.map((client: any) => (
<tr key={client.id} className={`hover:bg-slate-800/30 transition-colors ${client.isSuspended ? 'opacity-60 grayscale-[0.5]' : ''}`}>
<td className="py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 font-bold text-sm overflow-hidden border border-slate-700">
{client.avatar ? (
<img src={client.avatar} alt={client.name} className="w-full h-full object-cover" />
) : (
client.name.charAt(0)
)}
</div>
<div>
<p className="text-white font-semibold">{client.name}</p>
<p className="text-slate-500 text-xs">ID: {client.id.split('-')[0]}...</p>
</div>
</div>
</td>
<td>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-xs text-slate-300">
<Mail className="w-3 h-3 text-slate-500" />
{client.email}
</div>
{client.phone && (
<div className="flex items-center gap-1.5 text-xs text-slate-300">
<Phone className="w-3 h-3 text-slate-500" />
{client.phone}
</div>
)}
</div>
</td>
<td>
<div className="flex items-center gap-1.5 text-xs text-slate-400">
<MapPin className="w-3 h-3" />
{client.location || "Non spécifié"}
</div>
</td>
<td>
<div className="flex flex-col gap-1">
{client.isSuspended ? (
<>
<span className="badge badge-error">Compte Suspendu</span>
<span className="text-[10px] text-rose-500/70 truncate max-w-[150px]" title={client.suspensionReason}>
{client.suspensionReason}
</span>
</>
) : client.businesses ? (
<span className="badge badge-pending">Shop Inactif</span>
) : (
<span className="badge bg-slate-800 text-slate-400">Actif</span>
)}
</div>
</td>
<td className="text-right">
<ClientActions userId={client.id} userName={client.name} isSuspended={!!client.isSuspended} />
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}

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">

View File

@@ -1,145 +0,0 @@
import { prisma } from '@/lib/prisma';
import ToggleVerifyButton from '@/components/ToggleVerifyButton';
import FeaturedModal from '@/components/FeaturedModal';
import EntrepreneurActions from '@/components/EntrepreneurActions';
import PlanSelector from '@/components/PlanSelector';
import { ShieldAlert, ShieldX, ShieldCheck } from 'lucide-react';
async function getBusinesses() {
return await prisma.business.findMany({
where: {
isActive: true
},
include: {
owner: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
export default async function EntrepreneursPage() {
const businesses = await getBusinesses();
const pendingCount = businesses.filter((b: any) =>
!b.verified &&
!b.isSuspended &&
['BOOSTER', 'EMPIRE'].includes(b.plan)
).length;
return (
<div>
<div className="mb-8 flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Gestion des Entrepreneurs</h1>
<p className="text-slate-400">Gérer les abonnements et certifier les comptes (Badge Bleu).</p>
</div>
{pendingCount > 0 && (
<div className="bg-blue-500/10 border border-blue-500/50 px-6 py-3 rounded-2xl flex items-center gap-4 animate-pulse">
<div className="w-10 h-10 bg-blue-500 rounded-xl flex items-center justify-center text-white">
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<div className="text-blue-400 font-bold">{pendingCount} en attente</div>
<div className="text-xs text-blue-400/80 font-medium uppercase tracking-wider">Certifications Booster</div>
</div>
</div>
)}
</div>
<div className="card overflow-hidden p-0">
<table className="data-table">
<thead>
<tr>
<th>Entreprise</th>
<th>Catégorie</th>
<th>Propriétaire</th>
<th>Statut</th>
<th className="text-center">Forfait</th>
<th className="text-center">Visibilité</th>
<th className="text-center">Mise en avant</th>
<th className="text-right">Action</th>
</tr>
</thead>
<tbody>
{businesses.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-10 text-slate-500">
Aucun entrepreneur trouvé.
</td>
</tr>
) : (
businesses.map((business: any) => (
<tr key={business.id} className={(business.isSuspended || business.owner?.isSuspended) ? 'opacity-60 grayscale-[0.5]' : ''}>
<td>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center overflow-hidden">
{business.logoUrl ? (
<img src={business.logoUrl} alt={business.name} className="w-full h-full object-cover" />
) : (
<span className="text-xs text-slate-500">LOGO</span>
)}
</div>
<div>
<div className="font-semibold text-white">{business.name}</div>
<div className="text-xs text-slate-500">{business.location}</div>
</div>
</div>
</td>
<td>
<span className="text-sm text-slate-300">{business.category}</span>
</td>
<td>
<div className="text-sm text-white">{business.owner.name}</div>
<div className="text-xs text-slate-500">{business.owner.email}</div>
</td>
<td>
<div className="flex flex-col gap-1">
{business.owner?.isSuspended ? (
<span className="badge badge-error flex items-center gap-1">
<ShieldX className="w-3 h-3" /> Compte Suspendu
</span>
) : business.isSuspended ? (
<span className="badge bg-orange-500/10 text-orange-500 border border-orange-500/20 flex items-center gap-1">
<ShieldAlert className="w-3 h-3" /> Shop Masqué
</span>
) : business.verified ? (
<span className="badge badge-verified">Vérifié</span>
) : (
<span className="badge badge-pending">En attente</span>
)}
</div>
</td>
<td className="text-center">
<PlanSelector businessId={business.id} currentPlan={business.plan} />
</td>
<td className="text-center font-medium text-slate-300">
{business.viewCount.toLocaleString()}
</td>
<td className="text-center">
<FeaturedModal business={business} />
</td>
<td className="text-right">
<div className="flex items-center justify-end gap-3">
<ToggleVerifyButton id={business.id} verified={business.verified} />
<div className="h-8 w-[1px] bg-slate-800 mx-1" />
<EntrepreneurActions
businessId={business.id}
businessName={business.name}
userId={business.owner.id}
userName={business.owner.name}
isBusinessSuspended={!!business.isSuspended}
isUserSuspended={!!business.owner?.isSuspended}
/>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { Save, Globe, Mail, Phone, MapPin, Share2, Link as LinkIcon, Loader2, Ne
import { getSiteSettings, updateSiteSettings } from '@/app/actions/settings';
import { toast } from 'react-hot-toast';
import { getBlogPosts } from '@/app/actions/blog';
import RichTextEditor from '@/components/RichTextEditor';
const BUSINESS_CATEGORIES = [
"Technologie & IT",
@@ -33,7 +34,11 @@ const BLOG_CATEGORIES = [
"Formation"
];
export default function SettingsPage() {
export default function SettingsPage({
searchParams
}: {
searchParams: Promise<{ tab?: string }>
}) {
const [isPending, startTransition] = useTransition();
const [settings, setSettings] = useState<any>(null);
const [allPosts, setAllPosts] = useState<any[]>([]);
@@ -41,6 +46,12 @@ export default function SettingsPage() {
const [selectedPostIds, setSelectedPostIds] = useState<string[]>([]);
const [selectedHomeCategories, setSelectedHomeCategories] = useState<string[]>([]);
const [selectedBlogCategories, setSelectedBlogCategories] = useState<string[]>([]);
const [resetPasswordTemplate, setResetPasswordTemplate] = useState('');
const [verifyEmailTemplate, setVerifyEmailTemplate] = useState('');
// Use a state for the tab if we want it to be fully client-side reactive without full reload
// But since the user wants it "like moderation center", I'll use URL search params
const [currentTab, setCurrentTab] = useState('general');
useEffect(() => {
async function loadData() {
@@ -53,6 +64,8 @@ export default function SettingsPage() {
setSelectedPostIds(settingsData?.homeBlogIds || []);
setSelectedHomeCategories(settingsData?.homeCategories || []);
setSelectedBlogCategories(settingsData?.homeBlogCategories || []);
setResetPasswordTemplate(settingsData?.resetPasswordTemplate || '');
setVerifyEmailTemplate(settingsData?.verifyEmailTemplate || '');
setLoading(false);
}
loadData();
@@ -80,6 +93,10 @@ export default function SettingsPage() {
homeBlogIds: selectedPostIds,
homeBlogCategories: selectedBlogCategories,
homeCategories: selectedHomeCategories,
resetPasswordSubject: formData.get('resetPasswordSubject'),
resetPasswordTemplate: resetPasswordTemplate,
verifyEmailSubject: formData.get('verifyEmailSubject'),
verifyEmailTemplate: verifyEmailTemplate,
};
startTransition(async () => {
@@ -123,176 +140,82 @@ export default function SettingsPage() {
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-white">Configuration du Site</h1>
<p className="text-slate-400">Gérez les informations globales et les coordonnées de contact du site principal.</p>
<p className="text-slate-400">Gérez les informations globales, les sections de l'accueil et les modèles d'emails.</p>
</div>
</div>
{/* Tabs Switcher */}
<div className="flex gap-2 mb-8 bg-slate-900/50 p-1 rounded-xl border border-slate-800 w-fit">
<button
onClick={() => setCurrentTab('general')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentTab === 'general' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<Globe className="w-4 h-4" />
Général
</button>
<button
onClick={() => setCurrentTab('emails')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentTab === 'emails' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<Mail className="w-4 h-4" />
Configuration Mail
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Section Générale */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Globe className="w-5 h-5 text-indigo-400" />
<h2 className="font-semibold text-white">Informations Générales</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nom du site</label>
<input
name="siteName"
defaultValue={settings?.siteName}
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">Slogan du site</label>
<input
name="siteSlogan"
defaultValue={settings?.siteSlogan}
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="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Texte du pied de page (Footer)</label>
<input
name="footerText"
defaultValue={settings?.footerText}
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>
{/* Section Page d'Accueil - Secteurs */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Briefcase className="w-5 h-5 text-yellow-400" />
<h2 className="font-semibold text-white">Secteurs en Vedette (Home)</h2>
</div>
<div className="p-6">
<p className="text-sm text-slate-400 mb-4">Sélectionnez les secteurs d'activité à afficher sur la page d'accueil.</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{BUSINESS_CATEGORIES.map(cat => (
<button
key={cat}
type="button"
onClick={() => toggleHomeCategory(cat)}
className={`text-left p-2 rounded-lg border text-xs transition-all ${
selectedHomeCategories.includes(cat)
? 'bg-indigo-600/20 border-indigo-500 text-indigo-300'
: 'bg-slate-950 border-slate-700 text-slate-400 hover:border-slate-500'
}`}
>
{cat}
</button>
))}
</div>
</div>
</div>
{/* Section Page d'Accueil - Blog */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Newspaper className="w-5 h-5 text-purple-400" />
<h2 className="font-semibold text-white">Section Blog sur l'Accueil</h2>
</div>
<div className="p-6 space-y-6">
<div className="flex items-center gap-3 p-3 bg-indigo-600/10 border border-indigo-500/20 rounded-xl">
<input
type="checkbox"
name="homeBlogShow"
id="homeBlogShow"
defaultChecked={settings?.homeBlogShow}
className="w-5 h-5 rounded border-slate-700 bg-slate-950 text-indigo-600 focus:ring-indigo-500"
/>
<label htmlFor="homeBlogShow" className="text-sm font-medium text-slate-300 font-bold uppercase tracking-wider">Activer cette section</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Titre de la section</label>
<input
name="homeBlogTitle"
defaultValue={settings?.homeBlogTitle}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
{currentTab === 'general' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
{/* Section Générale */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Globe className="w-5 h-5 text-indigo-400" />
<h2 className="font-semibold text-white">Informations Générales</h2>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Sous-titre</label>
<input
name="homeBlogSubtitle"
defaultValue={settings?.homeBlogSubtitle}
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="border-t border-slate-700 pt-6">
<label className="text-sm font-medium text-slate-400 block mb-3">Mode de sélection des articles</label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'latest' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="latest" defaultChecked={settings?.homeBlogSelection === 'latest'} className="sr-only" />
<span className="text-white font-bold text-sm">Les plus récents</span>
<span className="text-xs text-slate-500 mt-1">Affiche automatiquement les X derniers articles.</span>
</label>
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'manual' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="manual" defaultChecked={settings?.homeBlogSelection === 'manual'} className="sr-only" />
<span className="text-white font-bold text-sm">Sélection manuelle</span>
<span className="text-xs text-slate-500 mt-1">Choisissez précisément les articles à mettre en avant.</span>
</label>
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'category' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="category" defaultChecked={settings?.homeBlogSelection === 'category'} className="sr-only" />
<span className="text-white font-bold text-sm">Par catégorie</span>
<span className="text-xs text-slate-500 mt-1">Affiche les articles appartenant à certaines catégories.</span>
</label>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nombre maximum d'articles</label>
<input
type="number"
name="homeBlogCount"
defaultValue={settings?.homeBlogCount}
min="1" max="12"
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
{/* Manual Selection UI */}
<div className="space-y-3">
<label className="text-sm font-medium text-slate-400">Articles sélectionnés (si mode manuel)</label>
<div className="grid grid-cols-1 gap-2 max-h-60 overflow-y-auto p-2 bg-slate-950 rounded-xl border border-slate-700">
{allPosts.map(post => (
<button
key={post.id}
type="button"
onClick={() => togglePostId(post.id)}
className={`flex items-center justify-between p-3 rounded-lg text-sm transition-all ${
selectedPostIds.includes(post.id)
? 'bg-indigo-600/20 border-indigo-500/50 text-white'
: 'text-slate-400 hover:bg-slate-900'
}`}
>
<span>{post.title}</span>
{selectedPostIds.includes(post.id) && <span className="text-indigo-400 font-bold text-xs">SÉLECTIONNÉ</span>}
</button>
))}
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nom du site</label>
<input
name="siteName"
defaultValue={settings?.siteName}
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">Slogan du site</label>
<input
name="siteSlogan"
defaultValue={settings?.siteSlogan}
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="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Texte du pied de page (Footer)</label>
<input
name="footerText"
defaultValue={settings?.footerText}
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>
{/* Category Selection UI */}
<div className="space-y-3">
<label className="text-sm font-medium text-slate-400">Catégories d'articles (si mode catégorie)</label>
<div className="flex flex-wrap gap-2">
{BLOG_CATEGORIES.map(cat => (
{/* Section Page d'Accueil - Secteurs */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Briefcase className="w-5 h-5 text-yellow-400" />
<h2 className="font-semibold text-white">Secteurs en Vedette (Home)</h2>
</div>
<div className="p-6">
<p className="text-sm text-slate-400 mb-4">Sélectionnez les secteurs d'activité à afficher sur la page d'accueil.</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{BUSINESS_CATEGORIES.map(cat => (
<button
key={cat}
type="button"
onClick={() => toggleBlogCategory(cat)}
className={`px-4 py-2 rounded-full border text-xs transition-all ${
selectedBlogCategories.includes(cat)
? 'bg-purple-600 border-purple-500 text-white'
onClick={() => toggleHomeCategory(cat)}
className={`text-left p-2 rounded-lg border text-xs transition-all ${
selectedHomeCategories.includes(cat)
? 'bg-indigo-600/20 border-indigo-500 text-indigo-300'
: 'bg-slate-950 border-slate-700 text-slate-400 hover:border-slate-500'
}`}
>
@@ -302,109 +225,288 @@ export default function SettingsPage() {
</div>
</div>
</div>
</div>
</div>
{/* Section Contact */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Mail className="w-5 h-5 text-emerald-400" />
<h2 className="font-semibold text-white">Coordonnées de Contact</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Email de support</label>
<div className="relative">
<Mail className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactEmail"
defaultValue={settings?.contactEmail}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
{/* Section Page d'Accueil - Blog */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Newspaper className="w-5 h-5 text-purple-400" />
<h2 className="font-semibold text-white">Section Blog sur l'Accueil</h2>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Téléphone de contact</label>
<div className="relative">
<Phone className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactPhone"
defaultValue={settings?.contactPhone}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Adresse physique</label>
<div className="relative">
<MapPin className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="address"
defaultValue={settings?.address}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
<div className="p-6 space-y-6">
<div className="flex items-center gap-3 p-3 bg-indigo-600/10 border border-indigo-500/20 rounded-xl">
<input
type="checkbox"
name="homeBlogShow"
id="homeBlogShow"
defaultChecked={settings?.homeBlogShow}
className="w-5 h-5 rounded border-slate-700 bg-slate-950 text-indigo-600 focus:ring-indigo-500"
/>
<label htmlFor="homeBlogShow" className="text-sm font-medium text-slate-300 font-bold uppercase tracking-wider">Activer cette section</label>
</div>
{/* Section Réseaux Sociaux */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Share2 className="w-5 h-5 text-blue-400" />
<h2 className="font-semibold text-white">Réseaux Sociaux</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Facebook URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="facebookUrl"
defaultValue={settings?.facebookUrl}
placeholder="https://facebook.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Titre de la section</label>
<input
name="homeBlogTitle"
defaultValue={settings?.homeBlogTitle}
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">Sous-titre</label>
<input
name="homeBlogSubtitle"
defaultValue={settings?.homeBlogSubtitle}
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="border-t border-slate-700 pt-6">
<label className="text-sm font-medium text-slate-400 block mb-3">Mode de sélection des articles</label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'latest' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="latest" defaultChecked={settings?.homeBlogSelection === 'latest'} className="sr-only" />
<span className="text-white font-bold text-sm">Les plus récents</span>
<span className="text-xs text-slate-500 mt-1">Affiche automatiquement les X derniers articles.</span>
</label>
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'manual' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="manual" defaultChecked={settings?.homeBlogSelection === 'manual'} className="sr-only" />
<span className="text-white font-bold text-sm">Sélection manuelle</span>
<span className="text-xs text-slate-500 mt-1">Choisissez précisément les articles à mettre en avant.</span>
</label>
<label className={`flex flex-col p-4 rounded-xl border cursor-pointer transition-all ${settings?.homeBlogSelection === 'category' ? 'bg-indigo-600/10 border-indigo-500' : 'bg-slate-950 border-slate-700 hover:border-slate-500'}`}>
<input type="radio" name="homeBlogSelection" value="category" defaultChecked={settings?.homeBlogSelection === 'category'} className="sr-only" />
<span className="text-white font-bold text-sm">Par catégorie</span>
<span className="text-xs text-slate-500 mt-1">Affiche les articles appartenant à certaines catégories.</span>
</label>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nombre maximum d'articles</label>
<input
type="number"
name="homeBlogCount"
defaultValue={settings?.homeBlogCount}
min="1" max="12"
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
{/* Manual Selection UI */}
<div className="space-y-3">
<label className="text-sm font-medium text-slate-400">Articles sélectionnés (si mode manuel)</label>
<div className="grid grid-cols-1 gap-2 max-h-60 overflow-y-auto p-2 bg-slate-950 rounded-xl border border-slate-700">
{allPosts.map(post => (
<button
key={post.id}
type="button"
onClick={() => togglePostId(post.id)}
className={`flex items-center justify-between p-3 rounded-lg text-sm transition-all ${
selectedPostIds.includes(post.id)
? 'bg-indigo-600/20 border-indigo-500/50 text-white'
: 'text-slate-400 hover:bg-slate-900'
}`}
>
<span>{post.title}</span>
{selectedPostIds.includes(post.id) && <span className="text-indigo-400 font-bold text-xs">SÉLECTIONNÉ</span>}
</button>
))}
</div>
</div>
{/* Category Selection UI */}
<div className="space-y-3">
<label className="text-sm font-medium text-slate-400">Catégories d'articles (si mode catégorie)</label>
<div className="flex flex-wrap gap-2">
{BLOG_CATEGORIES.map(cat => (
<button
key={cat}
type="button"
onClick={() => toggleBlogCategory(cat)}
className={`px-4 py-2 rounded-full border text-xs transition-all ${
selectedBlogCategories.includes(cat)
? 'bg-purple-600 border-purple-500 text-white'
: 'bg-slate-950 border-slate-700 text-slate-400 hover:border-slate-500'
}`}
>
{cat}
</button>
))}
</div>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Twitter (X) URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="twitterUrl"
defaultValue={settings?.twitterUrl}
placeholder="https://twitter.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
{/* Section Contact */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Mail className="w-5 h-5 text-emerald-400" />
<h2 className="font-semibold text-white">Coordonnées de Contact</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Email de support</label>
<div className="relative">
<Mail className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactEmail"
defaultValue={settings?.contactEmail}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 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">Téléphone de contact</label>
<div className="relative">
<Phone className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactPhone"
defaultValue={settings?.contactPhone}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Adresse physique</label>
<div className="relative">
<MapPin className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="address"
defaultValue={settings?.address}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Instagram URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="instagramUrl"
defaultValue={settings?.instagramUrl}
placeholder="https://instagram.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
{/* Section Réseaux Sociaux */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Share2 className="w-5 h-5 text-blue-400" />
<h2 className="font-semibold text-white">Réseaux Sociaux</h2>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">LinkedIn URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="linkedinUrl"
defaultValue={settings?.linkedinUrl}
placeholder="https://linkedin.com/in/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Facebook URL</label>
<div className="relative">
<Share2 className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="facebookUrl"
defaultValue={settings?.facebookUrl}
placeholder="https://facebook.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 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">Twitter (X) URL</label>
<div className="relative">
<Share2 className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="twitterUrl"
defaultValue={settings?.twitterUrl}
placeholder="https://twitter.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 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">Instagram URL</label>
<div className="relative">
<Share2 className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="instagramUrl"
defaultValue={settings?.instagramUrl}
placeholder="https://instagram.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 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">LinkedIn URL</label>
<div className="relative">
<Share2 className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="linkedinUrl"
defaultValue={settings?.linkedinUrl}
placeholder="https://linkedin.com/in/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{currentTab === 'emails' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
{/* Section Mail */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Mail className="w-5 h-5 text-orange-400" />
<h2 className="font-semibold text-white">Gestion des Emails</h2>
</div>
<div className="p-6 space-y-8">
{/* Password Reset */}
<div className="space-y-4">
<h3 className="text-sm font-bold text-indigo-400 uppercase tracking-wider">Réinitialisation de mot de passe</h3>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Objet de l'email</label>
<input
name="resetPasswordSubject"
defaultValue={settings?.resetPasswordSubject}
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">Modèle d'email</label>
<RichTextEditor
value={resetPasswordTemplate}
onChange={setResetPasswordTemplate}
placeholder="Contenu de l'email..."
/>
<p className="text-[10px] text-slate-500">
Variables : <code className="text-indigo-400">{"{name}"}</code>, <code className="text-indigo-400">{"{resetUrl}"}</code>, <code className="text-indigo-400">{"{siteName}"}</code>
</p>
</div>
</div>
<div className="border-t border-slate-800"></div>
{/* Account Verification */}
<div className="space-y-4">
<h3 className="text-sm font-bold text-emerald-400 uppercase tracking-wider">Vérification de compte</h3>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Objet de l'email</label>
<input
name="verifyEmailSubject"
defaultValue={settings?.verifyEmailSubject}
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">Modèle d'email</label>
<RichTextEditor
value={verifyEmailTemplate}
onChange={setVerifyEmailTemplate}
placeholder="Contenu de l'email..."
/>
<p className="text-[10px] text-slate-500">
Variables : <code className="text-indigo-400">{"{name}"}</code>, <code className="text-indigo-400">{"{verifyUrl}"}</code>, <code className="text-indigo-400">{"{siteName}"}</code>
</p>
</div>
</div>
</div>
</div>
</div>
)}
<div className="flex justify-end">
<button
@@ -424,3 +526,4 @@ export default function SettingsPage() {
</div>
);
}

View File

@@ -0,0 +1,298 @@
import { prisma } from '@/lib/prisma';
import { getClients } from "@/app/actions/user";
import ToggleVerifyButton from '@/components/ToggleVerifyButton';
import FeaturedModal from '@/components/FeaturedModal';
import EntrepreneurActions from '@/components/EntrepreneurActions';
import PlanSelector from '@/components/PlanSelector';
import ClientActions from "@/components/ClientActions";
import ManualVerifyButton from "@/components/ManualVerifyButton";
import { ShieldAlert, ShieldX, ShieldCheck, Users, Briefcase, User, Mail, Phone, MapPin, MailCheck, MailX } from 'lucide-react';
import Link from 'next/link';
async function getBusinesses() {
return await prisma.business.findMany({
where: {
isActive: true
},
include: {
owner: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
export default async function UsersPage({
searchParams
}: {
searchParams: Promise<{ tab?: string }>
}) {
const { tab = 'entrepreneurs' } = await searchParams;
// Data for Entrepreneurs
const businesses = await getBusinesses();
const pendingCount = businesses.filter((b: any) =>
!b.verified &&
!b.isSuspended &&
['BOOSTER', 'EMPIRE'].includes(b.plan)
).length;
// Data for Clients/Visitors
const clientsResult = await getClients();
const clients = (clientsResult.success ? (clientsResult.data || []) : []) as any[];
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Gestion des Utilisateurs</h1>
<p className="text-slate-400">Gérer les entrepreneurs et les visiteurs de la plateforme.</p>
</div>
{/* Tabs */}
<div className="flex gap-2 mb-8 bg-slate-900/50 p-1 rounded-xl border border-slate-800 w-fit">
<Link
href="/users?tab=entrepreneurs"
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${tab === 'entrepreneurs' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<Briefcase className="w-4 h-4" />
Entrepreneurs ({businesses.length})
{pendingCount > 0 && (
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-full">
{pendingCount}
</span>
)}
</Link>
<Link
href="/users?tab=visiteurs"
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${tab === 'visiteurs' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<User className="w-4 h-4" />
Visiteurs ({clients.length})
</Link>
</div>
{tab === 'entrepreneurs' ? (
<div className="space-y-6">
{pendingCount > 0 && (
<div className="bg-blue-500/10 border border-blue-500/50 px-6 py-3 rounded-2xl flex items-center gap-4 animate-pulse max-w-fit">
<div className="w-10 h-10 bg-blue-500 rounded-xl flex items-center justify-center text-white">
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<div className="text-blue-400 font-bold">{pendingCount} en attente</div>
<div className="text-xs text-blue-400/80 font-medium uppercase tracking-wider">Certifications Booster</div>
</div>
</div>
)}
<div className="card overflow-hidden p-0">
<table className="data-table">
<thead>
<tr>
<th>Entreprise</th>
<th>Catégorie</th>
<th>Propriétaire</th>
<th>Email</th>
<th>Statut</th>
<th className="text-center">Forfait</th>
<th className="text-center">Mise en avant</th>
<th className="text-right">Action</th>
</tr>
</thead>
<tbody>
{businesses.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-10 text-slate-500">
Aucun entrepreneur trouvé.
</td>
</tr>
) : (
businesses.map((business: any) => (
<tr key={business.id} className={(business.isSuspended || business.owner?.isSuspended) ? 'opacity-60 grayscale-[0.5]' : ''}>
<td>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center overflow-hidden">
{business.logoUrl ? (
<img src={business.logoUrl} alt={business.name} className="w-full h-full object-cover" />
) : (
<span className="text-xs text-slate-500">LOGO</span>
)}
</div>
<div>
<div className="font-semibold text-white">{business.name}</div>
<div className="text-xs text-slate-500">{business.location}</div>
</div>
</div>
</td>
<td>
<span className="text-sm text-slate-300">{business.category}</span>
</td>
<td>
<div className="text-sm text-white">{business.owner.name}</div>
<div className="text-xs text-slate-500">{business.owner.email}</div>
</td>
<td>
{business.owner.emailVerified ? (
<span className="flex items-center gap-1.5 text-[10px] text-emerald-500 font-bold uppercase">
<MailCheck className="w-3.5 h-3.5" /> Vérifié
</span>
) : (
<div className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-[10px] text-rose-500 font-bold uppercase">
<MailX className="w-3.5 h-3.5" /> Non vérifié
</span>
<ManualVerifyButton userId={business.owner.id} />
</div>
)}
</td>
<td>
<div className="flex flex-col gap-1">
{business.owner?.isSuspended ? (
<span className="badge badge-error flex items-center gap-1">
<ShieldX className="w-3 h-3" /> Compte Suspendu
</span>
) : business.isSuspended ? (
<span className="badge bg-orange-500/10 text-orange-500 border border-orange-500/20 flex items-center gap-1">
<ShieldAlert className="w-3 h-3" /> Shop Masqué
</span>
) : business.verified ? (
<span className="badge badge-verified">Vérifié</span>
) : (
<span className="badge badge-pending">En attente</span>
)}
</div>
</td>
<td className="text-center">
<PlanSelector businessId={business.id} currentPlan={business.plan} />
</td>
<td className="text-center">
<FeaturedModal business={business} />
</td>
<td className="text-right">
<div className="flex items-center justify-end gap-3">
<ToggleVerifyButton id={business.id} verified={business.verified} />
<div className="h-8 w-[1px] bg-slate-800 mx-1" />
<EntrepreneurActions
businessId={business.id}
businessName={business.name}
userId={business.owner.id}
userName={business.owner.name}
isBusinessSuspended={!!business.isSuspended}
isUserSuspended={!!business.owner?.isSuspended}
/>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="data-table">
<thead>
<tr>
<th>Utilisateur</th>
<th>Contact</th>
<th>Vérification</th>
<th>Localisation</th>
<th>Statut</th>
<th className="text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{clients.length === 0 ? (
<tr>
<td colSpan={6} className="py-12 text-center text-slate-500">
<div className="flex flex-col items-center gap-2">
<Users className="w-8 h-8 opacity-20" />
<p>Aucun visiteur trouvé.</p>
</div>
</td>
</tr>
) : (
clients.map((client: any) => (
<tr key={client.id} className={`hover:bg-slate-800/30 transition-colors ${client.isSuspended ? 'opacity-60 grayscale-[0.5]' : ''}`}>
<td className="py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 font-bold text-sm overflow-hidden border border-slate-700">
{client.avatar ? (
<img src={client.avatar} alt={client.name} className="w-full h-full object-cover" />
) : (
client.name.charAt(0)
)}
</div>
<div>
<p className="text-white font-semibold">{client.name}</p>
<p className="text-slate-500 text-xs">ID: {client.id.split('-')[0]}...</p>
</div>
</div>
</td>
<td>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-xs text-slate-300">
<Mail className="w-3 h-3 text-slate-500" />
{client.email}
</div>
{client.phone && (
<div className="flex items-center gap-1.5 text-xs text-slate-300">
<Phone className="w-3 h-3 text-slate-500" />
{client.phone}
</div>
)}
</div>
</td>
<td>
{client.emailVerified ? (
<span className="flex items-center gap-1.5 text-[10px] text-emerald-500 font-bold uppercase">
<MailCheck className="w-3.5 h-3.5" /> Vérifié
</span>
) : (
<div className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-[10px] text-rose-500 font-bold uppercase">
<MailX className="w-3.5 h-3.5" /> Non vérifié
</span>
<ManualVerifyButton userId={client.id} />
</div>
)}
</td>
<td>
<div className="flex items-center gap-1.5 text-xs text-slate-400">
<MapPin className="w-3 h-3" />
{client.location || "Non spécifié"}
</div>
</td>
<td>
<div className="flex flex-col gap-1">
{client.isSuspended ? (
<>
<span className="badge badge-error">Compte Suspendu</span>
<span className="text-[10px] text-rose-500/70 truncate max-w-[150px]" title={client.suspensionReason}>
{client.suspensionReason}
</span>
</>
) : client.businesses ? (
<span className="badge badge-pending">Shop Inactif</span>
) : (
<span className="badge bg-slate-800 text-slate-400">Actif</span>
)}
</div>
</td>
<td className="text-right">
<ClientActions userId={client.id} userName={client.name} isSuspended={!!client.isSuspended} />
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,41 @@
"use client";
import { useTransition } from 'react';
import { verifyUserEmail } from '@/app/actions/user';
import { MailCheck, Loader2 } from 'lucide-react';
import { toast } from 'react-hot-toast';
interface Props {
userId: string;
}
export default function ManualVerifyButton({ userId }: Props) {
const [isPending, startTransition] = useTransition();
const handleVerify = () => {
startTransition(async () => {
const result = await verifyUserEmail(userId);
if (result.success) {
toast.success("Email vérifié avec succès");
} else {
toast.error(result.error || "Une erreur est survenue");
}
});
};
return (
<button
onClick={handleVerify}
disabled={isPending}
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-500/10 text-indigo-500 hover:bg-indigo-500 hover:text-white rounded-lg transition-all text-[10px] font-bold uppercase border border-indigo-500/20"
title="Vérifier manuellement l'email"
>
{isPending ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<MailCheck className="w-3 h-3" />
)}
Vérifier Email
</button>
);
}

View File

@@ -20,8 +20,7 @@ import {
const menuItems = [
{ name: 'Dashboard', icon: LayoutDashboard, href: '/dashboard' },
{ name: 'Entrepreneurs', icon: ShieldCheck, href: '/entrepreneurs' },
{ name: 'Clients', icon: Users, href: '/clients' },
{ name: 'Utilisateurs', icon: Users, href: '/users' },
{ name: 'Metrics & Analytics', icon: BarChart3, href: '/metrics' },
{ name: 'Commentaires', icon: MessageSquare, href: '/comments' },
{ name: 'Modération', icon: Flag, href: '/moderation' },
@@ -67,7 +66,7 @@ export default function Sidebar() {
<span>{item.name}</span>
</div>
{item.name === 'Entrepreneurs' && pendingCount > 0 && (
{item.name === 'Utilisateurs' && pendingCount > 0 && (
<span className="bg-red-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] text-center shadow-lg group-hover:scale-110 transition-transform">
{pendingCount}
</span>