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`.
This commit is contained in:
581
frontend/app/admin/settings/page.tsx
Normal file
581
frontend/app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,581 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user