Avnyr cab80e6aef feat: add dashboard, projects, and persons pages with reusable components
Implemented the following:
- `DashboardPage`: displays an overview of stats, recent projects, and tabs for future analytics/reports.
- `ProjectsPage` and `PersonsPage`: include searchable tables, actions, and mobile-friendly card views.
- Integrated reusable components like `AuthLoading`, `DropdownMenu`, `Table`, and `Card`.
2025-05-16 14:43:14 +02:00

581 lines
24 KiB
TypeScript

"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import {
Save,
RefreshCw,
Shield,
Bell,
Mail,
Database,
Server,
FileJson,
Loader2
} from "lucide-react";
export default function AdminSettingsPage() {
const [activeTab, setActiveTab] = useState("general");
const [isLoading, setIsLoading] = useState(false);
// Mock system settings
const systemSettings = {
general: {
siteName: "Application de Création de Groupes",
siteDescription: "Une application web moderne dédiée à la création et à la gestion de groupes",
contactEmail: "admin@example.com",
maxProjectsPerUser: "10",
maxPersonsPerProject: "100",
},
authentication: {
enableGithubAuth: true,
requireEmailVerification: false,
sessionTimeout: "7",
maxLoginAttempts: "5",
passwordMinLength: "8",
},
notifications: {
enableEmailNotifications: true,
enableSystemNotifications: true,
notifyOnNewUser: true,
notifyOnNewProject: false,
adminEmailRecipients: "admin@example.com",
},
maintenance: {
maintenanceMode: false,
maintenanceMessage: "Le site est actuellement en maintenance. Veuillez réessayer plus tard.",
debugMode: false,
logLevel: "error",
},
};
const { register: registerGeneral, handleSubmit: handleSubmitGeneral, formState: { errors: errorsGeneral } } = useForm({
defaultValues: systemSettings.general,
});
const { register: registerAuth, handleSubmit: handleSubmitAuth, formState: { errors: errorsAuth } } = useForm({
defaultValues: systemSettings.authentication,
});
const { register: registerNotif, handleSubmit: handleSubmitNotif, formState: { errors: errorsNotif } } = useForm({
defaultValues: systemSettings.notifications,
});
const { register: registerMaint, handleSubmit: handleSubmitMaint, formState: { errors: errorsMaint } } = useForm({
defaultValues: systemSettings.maintenance,
});
const onSubmitGeneral = async (data: any) => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Paramètres généraux mis à jour avec succès");
};
const onSubmitAuth = async (data: any) => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Paramètres d'authentification mis à jour avec succès");
};
const onSubmitNotif = async (data: any) => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Paramètres de notification mis à jour avec succès");
};
const onSubmitMaint = async (data: any) => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Paramètres de maintenance mis à jour avec succès");
};
const handleExportConfig = async () => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Configuration exportée avec succès");
};
const handleClearCache = async () => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
toast.success("Cache vidé avec succès");
};
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 className="text-2xl sm:text-3xl font-bold">Paramètres système</h1>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
<span className="text-sm text-muted-foreground">Configuration globale</span>
</div>
</div>
<Tabs defaultValue="general" className="space-y-4" onValueChange={setActiveTab}>
<TabsList className="w-full flex justify-start overflow-auto">
<TabsTrigger value="general" className="flex-1 sm:flex-none">Général</TabsTrigger>
<TabsTrigger value="authentication" className="flex-1 sm:flex-none">Authentification</TabsTrigger>
<TabsTrigger value="notifications" className="flex-1 sm:flex-none">Notifications</TabsTrigger>
<TabsTrigger value="maintenance" className="flex-1 sm:flex-none">Maintenance</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4">
<Card>
<form onSubmit={handleSubmitGeneral(onSubmitGeneral)}>
<CardHeader>
<CardTitle>Paramètres généraux</CardTitle>
<CardDescription>
Configurez les paramètres généraux de l'application
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="siteName">Nom du site</Label>
<Input
id="siteName"
{...registerGeneral("siteName", { required: "Le nom du site est requis" })}
/>
{errorsGeneral.siteName && (
<p className="text-sm text-destructive">{errorsGeneral.siteName.message as string}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="contactEmail">Email de contact</Label>
<Input
id="contactEmail"
type="email"
{...registerGeneral("contactEmail", {
required: "L'email de contact est requis",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Adresse email invalide"
}
})}
/>
{errorsGeneral.contactEmail && (
<p className="text-sm text-destructive">{errorsGeneral.contactEmail.message as string}</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="siteDescription">Description du site</Label>
<Textarea
id="siteDescription"
rows={3}
{...registerGeneral("siteDescription")}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="maxProjectsPerUser">Nombre max. de projets par utilisateur</Label>
<Input
id="maxProjectsPerUser"
type="number"
{...registerGeneral("maxProjectsPerUser", {
required: "Ce champ est requis",
min: { value: 1, message: "La valeur minimale est 1" }
})}
/>
{errorsGeneral.maxProjectsPerUser && (
<p className="text-sm text-destructive">{errorsGeneral.maxProjectsPerUser.message as string}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="maxPersonsPerProject">Nombre max. de personnes par projet</Label>
<Input
id="maxPersonsPerProject"
type="number"
{...registerGeneral("maxPersonsPerProject", {
required: "Ce champ est requis",
min: { value: 1, message: "La valeur minimale est 1" }
})}
/>
{errorsGeneral.maxPersonsPerProject && (
<p className="text-sm text-destructive">{errorsGeneral.maxPersonsPerProject.message as string}</p>
)}
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les modifications
</>
)}
</Button>
</CardFooter>
</form>
</Card>
</TabsContent>
<TabsContent value="authentication" className="space-y-4">
<Card>
<form onSubmit={handleSubmitAuth(onSubmitAuth)}>
<CardHeader>
<CardTitle>Paramètres d'authentification</CardTitle>
<CardDescription>
Configurez les options d'authentification et de sécurité
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="enableGithubAuth">Authentification GitHub</Label>
<p className="text-sm text-muted-foreground">
Activer l'authentification via GitHub OAuth
</p>
</div>
<Switch
id="enableGithubAuth"
{...registerAuth("enableGithubAuth")}
defaultChecked={systemSettings.authentication.enableGithubAuth}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="requireEmailVerification">Vérification d'email</Label>
<p className="text-sm text-muted-foreground">
Exiger la vérification de l'email lors de l'inscription
</p>
</div>
<Switch
id="requireEmailVerification"
{...registerAuth("requireEmailVerification")}
defaultChecked={systemSettings.authentication.requireEmailVerification}
/>
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="sessionTimeout">Durée de session (jours)</Label>
<Input
id="sessionTimeout"
type="number"
{...registerAuth("sessionTimeout", {
required: "Ce champ est requis",
min: { value: 1, message: "La valeur minimale est 1" }
})}
/>
{errorsAuth.sessionTimeout && (
<p className="text-sm text-destructive">{errorsAuth.sessionTimeout.message as string}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="maxLoginAttempts">Tentatives de connexion max.</Label>
<Input
id="maxLoginAttempts"
type="number"
{...registerAuth("maxLoginAttempts", {
required: "Ce champ est requis",
min: { value: 1, message: "La valeur minimale est 1" }
})}
/>
{errorsAuth.maxLoginAttempts && (
<p className="text-sm text-destructive">{errorsAuth.maxLoginAttempts.message as string}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="passwordMinLength">Longueur min. du mot de passe</Label>
<Input
id="passwordMinLength"
type="number"
{...registerAuth("passwordMinLength", {
required: "Ce champ est requis",
min: { value: 6, message: "La valeur minimale est 6" }
})}
/>
{errorsAuth.passwordMinLength && (
<p className="text-sm text-destructive">{errorsAuth.passwordMinLength.message as string}</p>
)}
</div>
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les modifications
</>
)}
</Button>
</CardFooter>
</form>
</Card>
</TabsContent>
<TabsContent value="notifications" className="space-y-4">
<Card>
<form onSubmit={handleSubmitNotif(onSubmitNotif)}>
<CardHeader>
<CardTitle>Paramètres de notification</CardTitle>
<CardDescription>
Configurez les options de notification système et email
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="enableEmailNotifications">Notifications par email</Label>
<p className="text-sm text-muted-foreground">
Activer l'envoi de notifications par email
</p>
</div>
<Switch
id="enableEmailNotifications"
{...registerNotif("enableEmailNotifications")}
defaultChecked={systemSettings.notifications.enableEmailNotifications}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="enableSystemNotifications">Notifications système</Label>
<p className="text-sm text-muted-foreground">
Activer les notifications dans l'application
</p>
</div>
<Switch
id="enableSystemNotifications"
{...registerNotif("enableSystemNotifications")}
defaultChecked={systemSettings.notifications.enableSystemNotifications}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notifyOnNewUser">Notification nouvel utilisateur</Label>
<p className="text-sm text-muted-foreground">
Notifier les administrateurs lors de l'inscription d'un nouvel utilisateur
</p>
</div>
<Switch
id="notifyOnNewUser"
{...registerNotif("notifyOnNewUser")}
defaultChecked={systemSettings.notifications.notifyOnNewUser}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notifyOnNewProject">Notification nouveau projet</Label>
<p className="text-sm text-muted-foreground">
Notifier les administrateurs lors de la création d'un nouveau projet
</p>
</div>
<Switch
id="notifyOnNewProject"
{...registerNotif("notifyOnNewProject")}
defaultChecked={systemSettings.notifications.notifyOnNewProject}
/>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="adminEmailRecipients">Destinataires des emails administratifs</Label>
<Input
id="adminEmailRecipients"
{...registerNotif("adminEmailRecipients", {
required: "Ce champ est requis",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Adresse email invalide"
}
})}
/>
<p className="text-xs text-muted-foreground">
Séparez les adresses email par des virgules pour plusieurs destinataires
</p>
{errorsNotif.adminEmailRecipients && (
<p className="text-sm text-destructive">{errorsNotif.adminEmailRecipients.message as string}</p>
)}
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les modifications
</>
)}
</Button>
</CardFooter>
</form>
</Card>
</TabsContent>
<TabsContent value="maintenance" className="space-y-4">
<Card>
<form onSubmit={handleSubmitMaint(onSubmitMaint)}>
<CardHeader>
<CardTitle>Maintenance et débogage</CardTitle>
<CardDescription>
Configurez les options de maintenance et de débogage
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="maintenanceMode" className="font-semibold text-destructive">Mode maintenance</Label>
<p className="text-sm text-muted-foreground">
Activer le mode maintenance (le site sera inaccessible aux utilisateurs)
</p>
</div>
<Switch
id="maintenanceMode"
{...registerMaint("maintenanceMode")}
defaultChecked={systemSettings.maintenance.maintenanceMode}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maintenanceMessage">Message de maintenance</Label>
<Textarea
id="maintenanceMessage"
rows={3}
{...registerMaint("maintenanceMessage")}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="debugMode">Mode débogage</Label>
<p className="text-sm text-muted-foreground">
Activer le mode débogage (affiche des informations supplémentaires)
</p>
</div>
<Switch
id="debugMode"
{...registerMaint("debugMode")}
defaultChecked={systemSettings.maintenance.debugMode}
/>
</div>
<div className="space-y-2">
<Label htmlFor="logLevel">Niveau de journalisation</Label>
<select
id="logLevel"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
{...registerMaint("logLevel")}
>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="trace">Trace</option>
</select>
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleExportConfig}
disabled={isLoading}
>
<FileJson className="mr-2 h-4 w-4" />
Exporter la configuration
</Button>
</div>
<div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleClearCache}
disabled={isLoading}
>
<RefreshCw className="mr-2 h-4 w-4" />
Vider le cache
</Button>
</div>
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les modifications
</>
)}
</Button>
</CardFooter>
</form>
</Card>
</TabsContent>
</Tabs>
</div>
);
}