From 098d528413defcd73e451da81e64d528f76dfc67 Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Tue, 19 Aug 2025 16:58:12 +0200 Subject: [PATCH] feat: add preferences --- app/components/{home => }/bottom-nav.tsx | 2 +- app/components/home/colle-card.tsx | 29 ++-- app/components/home/index.tsx | 17 +-- app/components/home/tab-content.tsx | 9 +- app/components/settings/emoji-input.tsx | 141 +++++++++++++++++ app/components/settings/index.tsx | 74 +++++++++ app/components/settings/preferences.tsx | 187 +++++++++++++++++++++++ app/components/settings/profile.tsx | 14 ++ app/components/ui/tabs.tsx | 18 ++- app/lib/api.ts | 44 +++++- app/lib/client.ts | 2 +- app/lib/utils.ts | 82 +++++++++- app/routes.ts | 1 + app/routes/home.tsx | 2 +- app/routes/settings.tsx | 31 ++++ 15 files changed, 610 insertions(+), 43 deletions(-) rename app/components/{home => }/bottom-nav.tsx (96%) create mode 100644 app/components/settings/emoji-input.tsx create mode 100644 app/components/settings/index.tsx create mode 100644 app/components/settings/preferences.tsx create mode 100644 app/components/settings/profile.tsx create mode 100644 app/routes/settings.tsx diff --git a/app/components/home/bottom-nav.tsx b/app/components/bottom-nav.tsx similarity index 96% rename from app/components/home/bottom-nav.tsx rename to app/components/bottom-nav.tsx index 8ea46dc..0946f26 100644 --- a/app/components/home/bottom-nav.tsx +++ b/app/components/bottom-nav.tsx @@ -29,7 +29,7 @@ export default function BottomNavigation({ activeId }: { activeId: string }) { navigationMenuTriggerStyle(), "flex flex-col h-auto items-center px-5 py-2.5" )} - active={item.id === activeId} + data-active={item.id === activeId} asChild > diff --git a/app/components/home/colle-card.tsx b/app/components/home/colle-card.tsx index 27d552e..7fa4a3a 100644 --- a/app/components/home/colle-card.tsx +++ b/app/components/home/colle-card.tsx @@ -1,32 +1,31 @@ -import type { Colle } from "~/lib/api"; +import type { Colle, UserPreferences } from "~/lib/api"; import { Link } from "react-router"; import { Card } from "~/components/ui/card"; import { User, Star, CalendarDays, MapPin } from "lucide-react"; import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; -import { cn, formatDate, formatGrade, formatTime } from "~/lib/utils"; - -// TODO: Preferences for subject colors -const getSubjectColor = (_: string) => { - // Mock placeholder function - return "bg-blue-100 text-blue-800"; // Default color -}; -const getSubjectEmoji = (_: string) => { - // Mock placeholder function - return "📚"; // Default emoji -}; +import { + cn, + formatDate, + formatGrade, + formatTime, + getColorClass, + getSubjectColor, + getSubjectEmoji, +} from "~/lib/utils"; type ColleCardProps = { colle: Colle; onToggleFavorite: (id: number, favorite: boolean) => void; isFavorite: boolean; + preferences: UserPreferences; }; export default function ColleCard({ colle, onToggleFavorite, isFavorite, + preferences, }: ColleCardProps) { // TODO: Favorites // const handleToggleFavorite = (e: React.MouseEvent) => { @@ -36,8 +35,8 @@ export default function ColleCard({ // onToggleFavorite(colle.id, newValue); // }; - const subjectColor = getSubjectColor(colle.subject.name); - const subjectEmoji = getSubjectEmoji(colle.subject.name); + const subjectColor = getColorClass(getSubjectColor(colle.subject.name, preferences)); + const subjectEmoji = getSubjectEmoji(colle.subject.name, preferences); return ( diff --git a/app/components/home/index.tsx b/app/components/home/index.tsx index 72563ef..5439708 100644 --- a/app/components/home/index.tsx +++ b/app/components/home/index.tsx @@ -6,7 +6,7 @@ import { ChevronRight, Star, Users, - User, + UserIcon, SortAsc, SortDesc, } from "lucide-react"; @@ -17,18 +17,15 @@ import { SelectTrigger, SelectValue, } from "~/components/ui/select"; -import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { Tabs, TabsList, tabsStyle, TabsTrigger } from "~/components/ui/tabs"; import DatePickerWithRange from "~/components/home/date-picker"; -import BottomNavigation from "~/components/home/bottom-nav"; +import BottomNavigation from "~/components/bottom-nav"; import Error from "~/components/error"; import { useSearchParams } from "react-router"; -import { useColles } from "~/lib/api"; +import { useColles, type User } from "~/lib/api"; import TabContent from "~/components/home/tab-content"; -const tabsStyle = - "rounded-none bg-background data-[state=active]:bg-background! h-full data-[state=active]:shadow-none border-b-2 border-transparent border-t-0 border-r-0 border-l-0 data-[state=active]:border-primary!"; - -export default function Home() { +export default function Home({ user }: { user: User }) { // Handle query parameters const [query, setQuery] = useSearchParams(); const updateQuery = (key: string, value: string) => { @@ -147,7 +144,7 @@ export default function Home() { > - + Vous {/* @@ -233,6 +230,7 @@ export default function Home() { isLoading={isLoading} isSorted={sorted === "desc"} colles={applyFilters(studentColles)} + preferences={user.preferences} /> )} @@ -254,6 +252,7 @@ export default function Home() { isLoading={isLoading} isSorted={sorted === "desc"} colles={applyFilters(classColles)} + preferences={user.preferences} /> )} diff --git a/app/components/home/tab-content.tsx b/app/components/home/tab-content.tsx index e61ec85..683a2fd 100644 --- a/app/components/home/tab-content.tsx +++ b/app/components/home/tab-content.tsx @@ -1,5 +1,5 @@ import { DateTime } from "luxon"; -import type { Colle } from "~/lib/api"; +import type { Colle, UserPreferences } from "~/lib/api"; import { Badge } from "~/components/ui/badge"; import ColleCard from "~/components/home/colle-card"; import ColleCardSkeleton from "~/components/home/skeleton-card"; @@ -10,7 +10,8 @@ type TabContentProps = { isLoading: boolean; colles: Colle[]; isSorted?: boolean; -} + preferences: UserPreferences; +}; const WEEK_DAYS = [ "Lundi", @@ -27,7 +28,8 @@ export default function TabContent({ emptyCollesText, isLoading, colles, - isSorted + isSorted, + preferences, }: TabContentProps) { const collesByDay: Record = {}; colles.forEach((colle) => { @@ -92,6 +94,7 @@ export default function TabContent({ colle={colle} onToggleFavorite={() => {}} isFavorite={false} + preferences={preferences} // TODO: Implement favorite toggle // onToggleFavorite={handleToggleFavorite} // isFavorite={isFavorite(colle)} diff --git a/app/components/settings/emoji-input.tsx b/app/components/settings/emoji-input.tsx new file mode 100644 index 0000000..9aad496 --- /dev/null +++ b/app/components/settings/emoji-input.tsx @@ -0,0 +1,141 @@ +import { useState } from "react"; + +export default function EmojiInput({ + value, + onChange, + defaultValue = "", + ...props +}: { + value?: string; + onChange?: (value: string) => void; + [key: string]: any; // Allow other props to be passed +}) { + const [inputValue, setInputValue] = useState(value || defaultValue); + + // Function to get emoji segments using Intl.Segmenter + function getEmojiSegments(text: string) { + if (Intl.Segmenter) { + const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); + return Array.from(segmenter.segment(text)).map( + (segment) => segment.segment + ); + } else { + // Fallback for browsers without Intl.Segmenter + return [...text]; + } + } + + // Function to check if a string contains only emojis + function isOnlyEmojis(text: string) { + if (!text.trim()) return false; + + const segments = getEmojiSegments(text); + + for (const segment of segments) { + // Skip whitespace + if (/^\s+$/.test(segment)) continue; + + // Check if it's likely an emoji (contains emoji-range characters or common emoji symbols) + const hasEmojiChars = + /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{3000}-\u{303F}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]|[\u{00A9}\u{00AE}\u{2122}\u{2194}-\u{21AA}\u{231A}-\u{231B}\u{2328}\u{23CF}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{24C2}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2600}-\u{2604}\u{260E}\u{2611}\u{2614}-\u{2615}\u{2618}\u{261D}\u{2620}\u{2622}-\u{2623}\u{2626}\u{262A}\u{262E}-\u{262F}\u{2638}-\u{263A}\u{2640}\u{2642}\u{2648}-\u{2653}\u{265F}-\u{2660}\u{2663}\u{2665}-\u{2666}\u{2668}\u{267B}\u{267E}-\u{267F}\u{2692}-\u{2697}\u{2699}\u{269B}-\u{269C}\u{26A0}-\u{26A1}\u{26AA}-\u{26AB}\u{26B0}-\u{26B1}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26C8}\u{26CE}\u{26CF}\u{26D1}\u{26D3}-\u{26D4}\u{26E9}-\u{26EA}\u{26F0}-\u{26F5}\u{26F7}-\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270D}\u{270F}\u{2712}\u{2714}\u{2716}\u{271D}\u{2721}\u{2728}\u{2733}-\u{2734}\u{2744}\u{2747}\u{274C}\u{274E}\u{2753}-\u{2755}\u{2757}\u{2763}-\u{2764}\u{2795}-\u{2797}\u{27A1}\u{27B0}\u{27BF}\u{2934}-\u{2935}]/u.test( + segment + ); + + // Check if it's regular text (letters, numbers, basic punctuation) + const isRegularText = + /^[a-zA-Z0-9\s\.,!?;:'"()\-_+=<>@#$%^&*`~{}[\]|\\\/]*$/.test(segment); + + if (isRegularText && !hasEmojiChars) { + return false; + } + } + + return true; + } + + function handleInput(event: React.ChangeEvent) { + const text = event.target.value; + + if (!text) { + setInputValue(""); + onChange?.(""); + return; + } + + let processedValue = text; + + // Check if input contains only emojis + if (!isOnlyEmojis(text)) { + // Filter out non-emoji characters + processedValue = text.replace( + /[a-zA-Z0-9\s\.,!?;:'"()\-_+=<>@#$%^&*`~{}[\]|\\\/]/g, + "" + ); + if (!processedValue) { + setInputValue(""); + onChange?.(""); + return; + } + } + + // Get emoji segments and keep only the last one + const segments = getEmojiSegments(processedValue); + if (segments.length > 1) { + processedValue = segments[segments.length - 1]; + } + + setInputValue(processedValue); + onChange?.(processedValue); + } + + function handlePaste(event: React.ClipboardEvent) { + event.preventDefault(); + const paste = event.clipboardData.getData("text"); + + if (isOnlyEmojis(paste)) { + const segments = getEmojiSegments(paste); + if (segments.length > 0) { + const lastEmoji = segments[segments.length - 1]; + setInputValue(lastEmoji); + onChange?.(lastEmoji); + } + } + } + + function handleKeyDown(event: React.KeyboardEvent) { + // Allow control keys + if (event.ctrlKey || event.metaKey || event.altKey) return; + + // Allow navigation and deletion keys + const allowedKeys = [ + "Backspace", + "Delete", + "Tab", + "Escape", + "Enter", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "Home", + "End", + ]; + + if (allowedKeys.includes(event.key)) return; + + // For regular character input, let the input event handle emoji filtering + } + + return ( + + ); +} diff --git a/app/components/settings/index.tsx b/app/components/settings/index.tsx new file mode 100644 index 0000000..0e424e2 --- /dev/null +++ b/app/components/settings/index.tsx @@ -0,0 +1,74 @@ +import { Bell, Sliders, UserIcon } from "lucide-react"; +import { useState } from "react"; +import { Tabs, TabsList, tabsStyle, TabsTrigger } from "~/components/ui/tabs"; +import Preferences from "./preferences"; +import BottomNavigation from "~/components/bottom-nav"; +import Profile from "./profile"; +import type { User } from "~/lib/api"; + +export default function SettingsPage({ user }: { user: User }) { + const tabs = [ + { + value: "user", + label: "Profil", + icon: , + content: , + }, + { + value: "preferences", + label: "Préférences", + icon: , + content: , + }, + { + value: "notifications", + label: "Notifications", + icon: , + content:
WIP
, + }, + ]; + + // user / notifications / preferences tabs + const [activeTab, setActiveTab] = useState(tabs[0].value); + + return ( +
+ {/* Tabs */} + + + {tabs.map((tab) => ( + + {tab.icon} + {tab.label} + + ))} + + + + {/* Tab Content */} +
+ {tabs.map((tab) => ( +
+ {tab.content} +
+ ))} +
+ + +
+ ); +} diff --git a/app/components/settings/preferences.tsx b/app/components/settings/preferences.tsx new file mode 100644 index 0000000..0dbbfec --- /dev/null +++ b/app/components/settings/preferences.tsx @@ -0,0 +1,187 @@ +import { Palette, Save, 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"; + +export default function Preferences({ user }: { user: User }) { + const [preferences, setPreferences] = useState(user.preferences || []); + const [unsavedChanges, setUnsavedChanges] = useState(false); + // 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 + const queryClient = useQueryClient(); + 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 ( +
+ {/* Subject Customization Section */} + + + + + Personnalisation + + + Choisissez les couleurs et emojis des matières. + + + + {subjects + .sort((a, b) => a.localeCompare(b)) + .map((subjectName) => ({ + subjectName, + subjectEmoji: getSubjectEmoji(subjectName, preferences), + subjectColor: getSubjectColor(subjectName, preferences), + })) + .map(({ subjectName, subjectEmoji, subjectColor }) => ( +
+
+ + + {subjectEmoji} {subjectName} + +
+ {/* Color selector */} +
+
+ +
+ {colors.map((colorName) => ( +
+
+ {/* Emoji selector */} +
+ +
+ + 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" + /> +
+
+
+ +
+ ))} + + +
+
+
+ ); +} diff --git a/app/components/settings/profile.tsx b/app/components/settings/profile.tsx new file mode 100644 index 0000000..436debb --- /dev/null +++ b/app/components/settings/profile.tsx @@ -0,0 +1,14 @@ +import { Trash } from "lucide-react"; +import { Button } from "../ui/button"; +import { clearCache } from "~/lib/utils"; + +export default function Profile() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/components/ui/tabs.tsx b/app/components/ui/tabs.tsx index ac2c3f0..7104be2 100644 --- a/app/components/ui/tabs.tsx +++ b/app/components/ui/tabs.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Tabs({ className, @@ -13,7 +13,7 @@ function Tabs({ className={cn("flex flex-col gap-2", className)} {...props} /> - ) + ); } function TabsList({ @@ -29,7 +29,7 @@ function TabsList({ )} {...props} /> - ) + ); } function TabsTrigger({ @@ -45,7 +45,7 @@ function TabsTrigger({ )} {...props} /> - ) + ); } function TabsContent({ @@ -58,7 +58,9 @@ function TabsContent({ className={cn("flex-1 outline-none", className)} {...props} /> - ) + ); } -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; +export const tabsStyle = + "rounded-none bg-background data-[state=active]:bg-background! h-full data-[state=active]:shadow-none border-b-2 border-transparent border-t-0 border-r-0 border-l-0 data-[state=active]:border-primary!"; diff --git a/app/lib/api.ts b/app/lib/api.ts index cb25712..f01fbc2 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -105,11 +105,12 @@ const defaultUser = { fullName: "", email: "", className: "", + preferences: [] as UserPreferences, }; export type User = typeof defaultUser; -export const AUTH_ERROR = "Unauthorized access" +export const AUTH_ERROR = "Unauthorized access"; export const useUser = () => { const { data, ...props } = useQuery({ @@ -132,6 +133,24 @@ export const logout = async () => { return makePostRequest("/auth/logout", {}, "Échec de la déconnexion", "POST"); }; +export const updateUserPreferences = async ( + preferences: { name: string; emoji: string; color: string }[] +) => { + return makePostRequest( + "/users/@me", + { preferences }, + "Échec de la mise à jour des préférences utilisateur", + "POST" + ); +}; + +interface UserPreference { + name: string; + emoji: string; + color: string; +} +export type UserPreferences = UserPreference[]; + /** * === COLLES API === */ @@ -252,3 +271,26 @@ export const useColle = (id: number) => { ...props, }; }; + +/** + * === SUBJECTS API === + */ +export const getSubjects = async () => { + return makeRequest("/subjects", "Échec de la récupération des matières"); +}; +export const useSubjects = () => { + const { data, ...props } = useQuery({ + queryKey: ["subjects"], + queryFn: getSubjects, + staleTime: Duration.fromObject({ + hours: 1, // 1 day + }).toMillis(), + gcTime: Duration.fromObject({ + days: 3, // 3 days + }).toMillis(), + }); + return { + subjects: (data as string[]) || [], + ...props, + }; +}; diff --git a/app/lib/client.ts b/app/lib/client.ts index 3409aef..4b50893 100644 --- a/app/lib/client.ts +++ b/app/lib/client.ts @@ -3,7 +3,7 @@ import { QueryClient } from "@tanstack/react-query"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; -const CACHE_KEY = "khollise-cache"; // Key for IndexedDB storage +export const CACHE_KEY = "khollise-cache"; // Key for IndexedDB storage // Check if we're in a browser environment with IndexedDB support const isIndexedDBAvailable = () => { diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 15fe984..2a7133f 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,7 +1,10 @@ import { type ClassValue, clsx } from "clsx"; +import { del } from "idb-keyval"; import { DateTime } from "luxon"; import type { NavigateFunction } from "react-router"; import { twMerge } from "tailwind-merge"; +import { CACHE_KEY } from "./client"; +import type { User, UserPreferences } from "./api"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -60,10 +63,14 @@ export const formatDate = (date: string) => { export const formatTime = (date: string) => { const dt = DateTime.fromISO(date).setLocale("fr"); - return dt.toLocaleString({ - hour: "2-digit", - minute: "2-digit", - })?.replace(":", "h") || "N/A"; + return ( + dt + .toLocaleString({ + hour: "2-digit", + minute: "2-digit", + }) + ?.replace(":", "h") || "N/A" + ); }; export const formatGrade = (grade?: number) => { @@ -78,3 +85,70 @@ export const formatGrade = (grade?: number) => { return str.replace(".", ",").padStart(2, "0"); // pad with zero if needed }; + +// === COLORS UTILS === +export const colors = [ + "red", + "orange", + "amber", + "yellow", + "lime", + "green", + "emerald", + "teal", + "cyan", + "sky", + "blue", + "indigo", + "violet", + "purple", + "fuchsia", + "pink", + "rose", + "slate", + "zinc", +]; + +export const getColorClass = (colorCode: string) => + `bg-${colorCode}-100 text-${colorCode}-800 dark:bg-${colorCode}-300 dark:text-${colorCode}-900 hover:bg-${colorCode}-100 dark:hover:bg-${colorCode}-300`; + +// Used for Tailwind CSS to generate the classes +export const classNames = [ + [ + "bg-red-100 text-red-800 dark:bg-red-300 dark:text-red-900 hover:bg-red-100 dark:hover:bg-red-300", + "bg-orange-100 text-orange-800 dark:bg-orange-300 dark:text-orange-900 hover:bg-orange-100 dark:hover:bg-orange-300", + "bg-amber-100 text-amber-800 dark:bg-amber-300 dark:text-amber-900 hover:bg-amber-100 dark:hover:bg-amber-300", + "bg-yellow-100 text-yellow-800 dark:bg-yellow-300 dark:text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-300", + "bg-lime-100 text-lime-800 dark:bg-lime-300 dark:text-lime-900 hover:bg-lime-100 dark:hover:bg-lime-300", + "bg-green-100 text-green-800 dark:bg-green-300 dark:text-green-900 hover:bg-green-100 dark:hover:bg-green-300", + "bg-emerald-100 text-emerald-800 dark:bg-emerald-300 dark:text-emerald-900 hover:bg-emerald-100 dark:hover:bg-emerald-300", + "bg-teal-100 text-teal-800 dark:bg-teal-300 dark:text-teal-900 hover:bg-teal-100 dark:hover:bg-teal-300", + "bg-cyan-100 text-cyan-800 dark:bg-cyan-300 dark:text-cyan-900 hover:bg-cyan-100 dark:hover:bg-cyan-300", + "bg-sky-100 text-sky-800 dark:bg-sky-300 dark:text-sky-900 hover:bg-sky-100 dark:hover:bg-sky-300", + "bg-blue-100 text-blue-800 dark:bg-blue-300 dark:text-blue-900 hover:bg-blue-100 dark:hover:bg-blue-300", + "bg-indigo-100 text-indigo-800 dark:bg-indigo-300 dark:text-indigo-900 hover:bg-indigo-100 dark:hover:bg-indigo-300", + "bg-violet-100 text-violet-800 dark:bg-violet-300 dark:text-violet-900 hover:bg-violet-100 dark:hover:bg-violet-300", + "bg-purple-100 text-purple-800 dark:bg-purple-300 dark:text-purple-900 hover:bg-purple-100 dark:hover:bg-purple-300", + "bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-300 dark:text-fuchsia-900 hover:bg-fuchsia-100 dark:hover:bg-fuchsia-300", + "bg-pink-100 text-pink-800 dark:bg-pink-300 dark:text-pink-900 hover:bg-pink-100 dark:hover:bg-pink-300", + "bg-rose-100 text-rose-800 dark:bg-rose-300 dark:text-rose-900 hover:bg-rose-100 dark:hover:bg-rose-300", + "bg-slate-100 text-slate-800 dark:bg-slate-300 dark:text-slate-900 hover:bg-slate-100 dark:hover:bg-slate-300", + "bg-zinc-100 text-zinc-800 dark:bg-zinc-300 dark:text-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-300", + "bg-gray-100 text-gray-800 dark:bg-gray-300 dark:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-300", + ], +]; + +export const DEFAULT_COLOR = "blue"; // Default color for subjects +export const DEFAULT_EMOJI = "📚"; // Default emoji for subjects + +export const getSubjectEmoji = (subject: string, pref: UserPreferences) => { + const preference = pref.find((p) => p.name === subject); + return preference ? preference.emoji : DEFAULT_EMOJI; +}; +export const getSubjectColor = (subject: string, pref: UserPreferences) => { + const preference = pref.find((p) => p.name === subject); + return preference ? preference.color : DEFAULT_COLOR; +}; + +// === DEBUG UTILS === +export const clearCache = () => del(CACHE_KEY); diff --git a/app/routes.ts b/app/routes.ts index 6bebb05..3ca58a7 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -6,4 +6,5 @@ export default [ route("/verify", "routes/verify.tsx"), route("/register", "routes/register.tsx"), route("/colles/:colleId", "routes/colles.tsx"), + route("/settings", "routes/settings.tsx"), ] satisfies RouteConfig; diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 2ec6502..c826643 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -21,7 +21,7 @@ export default function Home() { } return ( - }> + }>

Khollisé - {user.className} ⚔️

diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx new file mode 100644 index 0000000..11bf049 --- /dev/null +++ b/app/routes/settings.tsx @@ -0,0 +1,31 @@ +import { Navigate } from "react-router"; +import Error from "~/components/error"; +import SettingsPage from "~/components/settings"; +import Loader from "~/components/loader"; +import UserDropdown from "~/components/user-dropdown"; +import { MainLayout } from "~/layout"; +import { AUTH_ERROR, useUser } from "~/lib/api"; +import { forceReload } from "~/lib/utils"; + +export default function Home() { + const { user, isLoading, error } = useUser(); + + if (isLoading) { + return ; + } + if (error?.message === AUTH_ERROR) { + return ; + } + if (error) { + return ; + } + + return ( + }> +

+ Khollisé - {user.className} ⚔️ +

+ +
+ ); +}