feat(settings): add account deletion feature and improve UI

- Introduced "Delete Account" functionality with confirmation dialog and success/error notifications.
- Enhanced general settings page UI, including updated card layouts and improved form elements.
- Added support for theme selection with a more user-friendly design.
- Refined typography and button styling for better visual consistency.
This commit is contained in:
Mathis HERRIOT
2026-01-21 15:43:43 +01:00
parent 7dce7ec286
commit e69156407e

View File

@@ -2,19 +2,34 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import {
AlertTriangle,
Laptop, Laptop,
Loader2, Loader2,
Moon, Moon,
Palette, Palette,
Save, Save,
Settings,
Sun, Sun,
Trash2,
User as UserIcon, User as UserIcon,
} from "lucide-react"; } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import * as React from "react"; import * as React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import * as z from "zod"; import * as z from "zod";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -49,8 +64,10 @@ type SettingsFormValues = z.infer<typeof settingsSchema>;
export default function SettingsPage() { export default function SettingsPage() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { user, isLoading, refreshUser } = useAuth(); const { user, isLoading, refreshUser, logout } = useAuth();
const router = useRouter();
const [isSaving, setIsSaving] = React.useState(false); const [isSaving, setIsSaving] = React.useState(false);
const [isDeleting, setIsDeleting] = React.useState(false);
const [mounted, setMounted] = React.useState(false); const [mounted, setMounted] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
@@ -111,146 +128,225 @@ export default function SettingsPage() {
} }
}; };
const handleDeleteAccount = async () => {
setIsDeleting(true);
try {
await UserService.removeMe();
toast.success("Votre compte a été supprimé.");
logout();
router.push("/");
} catch (error) {
console.error(error);
toast.error("Erreur lors de la suppression du compte.");
} finally {
setIsDeleting(false);
}
};
return ( return (
<div className="max-w-2xl mx-auto py-12 px-4"> <div className="max-w-2xl mx-auto py-12 px-4">
<div className="flex items-center gap-3 mb-8"> <div className="flex items-center gap-3 mb-8">
<div className="bg-primary/10 p-3 rounded-xl"> <div className="bg-primary/10 p-3 rounded-xl">
<UserIcon className="h-6 w-6 text-primary" /> <Settings className="h-6 w-6 text-primary" />
</div> </div>
<h1 className="text-3xl font-bold">Paramètres du profil</h1> <h1 className="text-3xl font-bold tracking-tight">Paramètres</h1>
</div> </div>
<Card> <div className="space-y-8">
<CardHeader> <Card className="border-none shadow-sm">
<CardTitle>Informations personnelles</CardTitle> <CardHeader className="pb-4">
<CardDescription> <div className="flex items-center gap-2 mb-1">
Mettez à jour vos informations publiques. Ces données seront visibles par <UserIcon className="h-5 w-5 text-primary" />
les autres utilisateurs. <CardTitle>Informations personnelles</CardTitle>
</CardDescription> </div>
</CardHeader> <CardDescription>
<CardContent> Mettez à jour vos informations publiques. Ces données seront visibles par
<Form {...form}> les autres utilisateurs.
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> </CardDescription>
<div className="grid gap-4"> </CardHeader>
<FormItem> <CardContent>
<FormLabel>Nom d'utilisateur</FormLabel> <Form {...form}>
<FormControl> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Input <div className="grid gap-6">
value={user.username} <div className="grid sm:grid-cols-2 gap-4">
disabled
className="bg-zinc-50 dark:bg-zinc-900"
/>
</FormControl>
<FormDescription>
Le nom d'utilisateur ne peut pas être modifié.
</FormDescription>
</FormItem>
<FormField
control={form.control}
name="displayName"
render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Nom d'affichage</FormLabel> <FormLabel>Nom d'utilisateur</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Votre nom" {...field} /> <Input
</FormControl> value={user.username}
<FormDescription> disabled
Le nom qui sera affiché sur votre profil et vos mèmes. className="bg-muted cursor-not-allowed"
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Racontez-nous quelque chose sur vous..."
className="resize-none"
{...field}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>Identifiant unique non modifiable.</FormDescription>
Une courte description de vous (max 255 caractères).
</FormDescription>
<FormMessage />
</FormItem> </FormItem>
)}
/> <FormField
control={form.control}
name="displayName"
render={({ field }) => (
<FormItem>
<FormLabel>Nom d'affichage</FormLabel>
<FormControl>
<Input placeholder="Votre nom" {...field} />
</FormControl>
<FormDescription>Nom visible sur votre profil.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Racontez-nous quelque chose sur vous..."
className="resize-none min-h-[100px]"
{...field}
/>
</FormControl>
<FormDescription>
Une courte description de vous (max 255 caractères).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end border-t pt-6">
<Button type="submit" disabled={isSaving} className="min-w-[150px]">
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
<Card className="border-none shadow-sm">
<CardHeader className="pb-4">
<div className="flex items-center gap-2 mb-1">
<Palette className="h-5 w-5 text-primary" />
<CardTitle>Apparence</CardTitle>
</div>
<CardDescription>
Personnalisez l'apparence de l'application selon vos préférences.
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup
value={mounted ? theme : "system"}
onValueChange={(value) => setTheme(value)}
className="grid grid-cols-1 sm:grid-cols-3 gap-4"
>
<div className="relative">
<RadioGroupItem value="light" id="light" className="peer sr-only" />
<Label
htmlFor="light"
className="flex flex-col items-center justify-between rounded-xl border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer transition-all"
>
<Sun className="mb-3 h-6 w-6" />
<span className="text-sm font-semibold">Clair</span>
</Label>
</div> </div>
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto"> <div className="relative">
{isSaving ? ( <RadioGroupItem value="dark" id="dark" className="peer sr-only" />
<> <Label
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> htmlFor="dark"
Enregistrement... className="flex flex-col items-center justify-between rounded-xl border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer transition-all"
</> >
) : ( <Moon className="mb-3 h-6 w-6" />
<> <span className="text-sm font-semibold">Sombre</span>
<Save className="mr-2 h-4 w-4" /> </Label>
Enregistrer les modifications </div>
</>
)} <div className="relative">
</Button> <RadioGroupItem value="system" id="system" className="peer sr-only" />
</form> <Label
</Form> htmlFor="system"
</CardContent> className="flex flex-col items-center justify-between rounded-xl border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer transition-all"
</Card> >
<Card className="mt-8"> <Laptop className="mb-3 h-6 w-6" />
<CardHeader> <span className="text-sm font-semibold">Système</span>
<div className="flex items-center gap-2"> </Label>
<Palette className="h-5 w-5 text-primary" /> </div>
<CardTitle>Apparence</CardTitle> </RadioGroup>
</div> </CardContent>
<CardDescription> </Card>
Personnalisez l'apparence de l'application selon vos préférences.
</CardDescription> <Card className="border-destructive/20 shadow-sm bg-destructive/5">
</CardHeader> <CardHeader className="pb-4">
<CardContent> <div className="flex items-center gap-2 mb-1">
<RadioGroup <AlertTriangle className="h-5 w-5 text-destructive" />
value={mounted ? theme : "system"} <CardTitle className="text-destructive">Zone de danger</CardTitle>
onValueChange={(value) => setTheme(value)}
className="grid grid-cols-1 sm:grid-cols-3 gap-4"
>
<div>
<RadioGroupItem value="light" id="light" className="peer sr-only" />
<Label
htmlFor="light"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
<Sun className="mb-3 h-6 w-6" />
<span>Clair</span>
</Label>
</div> </div>
<div> <CardDescription className="text-destructive/80 font-medium">
<RadioGroupItem value="dark" id="dark" className="peer sr-only" /> Actions irréversibles concernant votre compte.
<Label </CardDescription>
htmlFor="dark" </CardHeader>
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer" <CardContent>
> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-4 rounded-lg bg-white dark:bg-zinc-900 border border-destructive/20">
<Moon className="mb-3 h-6 w-6" /> <div className="space-y-1">
<span>Sombre</span> <p className="font-bold">Supprimer mon compte</p>
</Label> <p className="text-sm text-muted-foreground">
Toutes vos données seront supprimées définitivement.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="font-semibold">
<Trash2 className="h-4 w-4 mr-2" />
Supprimer le compte
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Êtes-vous absolument sûr ?</AlertDialogTitle>
<AlertDialogDescription>
Cette action est irréversible. Votre compte sera supprimé
définitivement ainsi que tous vos mèmes et vos favoris.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteAccount}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Suppression...
</>
) : (
"Confirmer la suppression"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
<div> </CardContent>
<RadioGroupItem value="system" id="system" className="peer sr-only" /> </Card>
<Label </div>
htmlFor="system"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
<Laptop className="mb-3 h-6 w-6" />
<span>Système</span>
</Label>
</div>
</RadioGroup>
</CardContent>
</Card>
</div> </div>
); );
} }