frontend/app/components/settings/preferences.tsx
Nathan a3f16fc967
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 2m30s
Update app/components/settings/preferences.tsx
2025-09-12 21:10:51 +02:00

225 lines
7.5 KiB
TypeScript

import { Moon, Palette, Save, Sun, Undo } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "~/components/ui/card";
import { Label } from "~/components/ui/label";
import {
cn,
colors,
DEFAULT_COLOR,
DEFAULT_EMOJI,
getColorClass,
getSubjectColor,
getSubjectEmoji,
} from "~/lib/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import { updateUserPreferences, useSubjects, type User } from "~/lib/api";
import EmojiInput from "./emoji-input";
import { useState } from "react";
import { toast } from "sonner";
import { useQueryClient } from "@tanstack/react-query";
import { useDisplayedTheme } from "../theme-provider";
export default function Preferences({ user }: { user: User }) {
const [preferences, setPreferences] = useState(user.preferences || []);
const [unsavedChanges, setUnsavedChanges] = useState(false);
const { theme, setTheme } = useDisplayedTheme();
const queryClient = useQueryClient();
// TODO: Add a loading state and error handling
const { subjects, isLoading, isError } = useSubjects();
const resetPreferences = () => {
setPreferences(user.preferences || []);
setUnsavedChanges(false);
};
const setPref = (subjectName: string, key: string, value: string) => {
const existingPreference = preferences.find((p) => p.name === subjectName);
const updatedPreferences = existingPreference
? preferences.map((p) =>
p.name === subjectName ? { ...p, [key]: value } : p
)
: [
...preferences,
Object.assign(
{ name: subjectName, emoji: DEFAULT_EMOJI, color: DEFAULT_COLOR },
{
name: subjectName,
[key]: value,
}
),
];
if (!unsavedChanges) {
toast("Vos changements n'ont pas été sauvegardés", {
description:
"N'oubliez pas de sauvegarder vos préférences en bas de la page.",
duration: 3000,
});
setUnsavedChanges(true);
}
setPreferences(updatedPreferences);
};
function savePreferences() {
updateUserPreferences(
// Filter out any incomplete preferences
preferences.filter((p) => p.name && p.emoji && p.color)
)
.then(() => {
// Invalidate the user query to refresh the data
queryClient.removeQueries({ queryKey: ["user"] });
toast.success("Vos préférences ont été sauvegardé avec succès !");
setUnsavedChanges(false);
})
.catch((error) => {
console.error(
"Erreur lors de la sauvegarde de vos préférences :",
error
);
toast.error("Échec de la sauvegarde. Veuillez réessayer.");
});
}
return (
<div className="lg:col-span-2 space-y-6">
{/* Theme card: darmode light mode */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Moon className="h-5 w-5" />
Thème
</CardTitle>
<CardDescription>
Personnalisez l'apparence de l'application.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* TODO: Save user theme */}
{/* Theme toggle button */}
<Button
variant="default"
size="sm"
className="w-full"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? (
<>
<Sun className="mr-2 h-4 w-4" />
<span>Activer le mode clair</span>
</>
) : (
<>
<Moon className="mr-2 h-4 w-4" />
<span>Activer le mode sombre</span>
</>
)}
</Button>
</CardContent>
</Card>
{/* Subject Customization Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5" />
Personnalisation
</CardTitle>
<CardDescription>
Choisissez les couleurs et emojis des matières.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{subjects
.sort((a, b) => a.localeCompare(b))
.map((subjectName) => ({
subjectName,
subjectEmoji: getSubjectEmoji(subjectName, preferences),
subjectColor: getSubjectColor(subjectName, preferences),
}))
.map(({ subjectName, subjectEmoji, subjectColor }) => (
<div key={subjectName} className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-base font-medium truncate">
{subjectName}
</Label>
<Badge
className={cn(
"px-3 py-1 truncate",
getColorClass(subjectColor)
)}
>
{subjectEmoji} {subjectName}
</Badge>
</div>
{/* Color selector */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Couleur</Label>
<div className="flex flex-wrap gap-2">
{colors.map((colorName) => (
<button
key={colorName}
type="button"
className={cn(
"w-4 h-4 rounded-full border",
subjectColor === colorName
? "ring-2 ring-offset-2 ring-primary"
: "ring-0",
getColorClass(colorName)
)}
onClick={() =>
setPref(subjectName, "color", colorName)
}
title={colorName}
/>
))}
</div>
</div>
{/* Emoji selector */}
<div className="space-y-2">
<Label>Emoji</Label>
<div className="relative">
<EmojiInput
type="text"
defaultValue={subjectEmoji}
onChange={(emoji) =>
setPref(subjectName, "emoji", emoji)
}
className="w-full bg-transparent border border-gray-300 rounded-md px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
</div>
<Separator className="my-4" />
</div>
))}
<Button
className="w-full"
onClick={() => savePreferences()}
disabled={!unsavedChanges}
>
<Save className="h-4 w-4 mr-2" />
Enregistrer vos préférences
</Button>
<Button
variant="destructive"
className="w-full text-white"
onClick={resetPreferences}
disabled={!unsavedChanges}
>
<Undo className="h-4 w-4 mr-2" />
Annuler les modifications en cours
</Button>
</CardContent>
</Card>
</div>
);
}