diff --git a/app/app.css b/app/app.css index 3b5012d..28942ec 100644 --- a/app/app.css +++ b/app/app.css @@ -119,6 +119,7 @@ } body { @apply bg-background text-foreground; + font-family: "Inter"; } } diff --git a/app/components/error.tsx b/app/components/error.tsx new file mode 100644 index 0000000..0e1e609 --- /dev/null +++ b/app/components/error.tsx @@ -0,0 +1,47 @@ +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { ChevronLeft, FileQuestion, RotateCw } from "lucide-react"; +import { Link } from "react-router"; +import { forceReload } from "~/lib/utils"; + +export default function Error({ + title = "Page introuvable", + message = "Nous n'avons pas pu trouver la page que vous recherchiez.", + code = 404, + description = "La page que vous recherchez a peut-être été supprimée, son nom a été modifié ou est temporairement indisponible.", +}) { + return ( +
+ + +
+ +
+ {title} + {message} +
+ +

{code}

+

{description}

+
+ + + + +
+
+ ); +} diff --git a/app/components/home/bottom-nav.tsx b/app/components/home/bottom-nav.tsx new file mode 100644 index 0000000..8aacbb4 --- /dev/null +++ b/app/components/home/bottom-nav.tsx @@ -0,0 +1,55 @@ +import { Star, User, Users } from "lucide-react" +import { cn } from "~/lib/utils" + +interface BottomNavigationProps { + activeTab: string + onTabChange: (tab: string) => void + favoriteCount: number +} + +export default function BottomNavigation({ activeTab, onTabChange, favoriteCount }: BottomNavigationProps) { + return ( +
+
+ + + + + +
+
+ ) +} diff --git a/app/components/home/colle-card.tsx b/app/components/home/colle-card.tsx new file mode 100644 index 0000000..93c1824 --- /dev/null +++ b/app/components/home/colle-card.tsx @@ -0,0 +1,178 @@ +import type React from "react"; +import type { Colle } from "~/lib/api"; + +import { DateTime } from "luxon"; +import { Link, useNavigate } from "react-router"; +import { + Card, + CardContent, + CardHeader, + CardFooter, +} from "~/components/ui/card"; +import { User, UserCheck, Paperclip, Star, MapPinHouse } from "lucide-react"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { titleCase } 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 +}; + +type ColleCardProps = { + colle: Colle; + onToggleFavorite: (id: number, favorite: boolean) => void; + beforeClick: () => void; + isFavorite: boolean; +}; + +export default function ColleCard({ + colle, + onToggleFavorite, + beforeClick, + isFavorite, +}: ColleCardProps) { + const navigate = useNavigate(); + + // TODO: Remove this if scroll restoration is not needed (test first) + const handleCardClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + beforeClick(); + setTimeout(() => navigate(`/colles/${colle.id}`), 100); + }; + + // TODO: Favorites + const handleToggleFavorite = (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent card click + e.preventDefault(); + const newValue = !isFavorite; + onToggleFavorite(colle.id, newValue); + }; + + const subjectColor = getSubjectColor(colle.subject.name); + const subjectEmoji = getSubjectEmoji(colle.subject.name); + + return ( + + + +
+
{formatDate(colle.date)}
+
+ {formatTime(colle.date)} +
+
+ {colle.grade && ( +
+ +
+ {formatGrade(colle.grade)}/20 +
+
+ )} +
+ + +
+
+
+ + {colle.student.fullName} +
+ +
+ + {colle.examiner.name} +
+ + {colle.room && ( +
+ + {colle.room.name} +
+ )} +
+
+
+ + +
+
+ + {colle.subject.name + " " + subjectEmoji} + + {isFavorite && ( + + + Favori + + )} +
+ {/* TODO: Attachments */} + {colle.attachmentsCount > 0 && ( +
+ + {colle.attachmentsCount} +
+ )} +
+
+
+ + ); +} + +const formatDate = (date: string) => { + const dt = DateTime.fromISO(date).setLocale("fr"); + const str = dt.toLocaleString({ + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", + }); + return titleCase(str); +}; + +const formatTime = (date: string) => { + const dt = DateTime.fromISO(date).setLocale("fr"); + return dt.toLocaleString({ + hour: "2-digit", + minute: "2-digit", + }); +}; + +const formatGrade = (grade?: number) => { + if (grade === undefined || grade === null || grade < 0 || grade > 20) + return "N/A"; + + const rounded = Math.round(grade * 10) / 10; + const str = + rounded % 1 === 0 + ? rounded.toFixed(0) // no decimals if .0 + : rounded.toFixed(1); // one decimal otherwise + + return str.replace(".", ",").padStart(2, "0"); // pad with zero if needed +}; diff --git a/app/components/home/date-picker.tsx b/app/components/home/date-picker.tsx new file mode 100644 index 0000000..7e1c1cb --- /dev/null +++ b/app/components/home/date-picker.tsx @@ -0,0 +1,67 @@ +import { DateTime } from "luxon"; +import { CalendarIcon } from "lucide-react"; +import { fr } from "date-fns/locale"; +import { Calendar } from "~/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; +import { cn } from "~/lib/utils"; + +export default function DatePickerWithRange({ + className, + startDate, + setStartDate, +}: { + className?: string; + startDate: DateTime; + setStartDate: (date: DateTime) => void; +}) { + const endDate = startDate.endOf("week"); + + function handleDateSelect(selectedDate?: Date) { + if (selectedDate) { + setStartDate(DateTime.fromJSDate(selectedDate)); + } + } + + return ( +
+ + + + + + + + +
+ ); +} + +const formatDate = (date: DateTime, includeYear = false) => { + const localDate = date.setLocale("fr"); + return includeYear + ? localDate.toFormat("dd MMM yyyy") + : localDate.toFormat("dd MMM"); +}; diff --git a/app/components/home/index.tsx b/app/components/home/index.tsx new file mode 100644 index 0000000..3876b5f --- /dev/null +++ b/app/components/home/index.tsx @@ -0,0 +1,352 @@ +import { DateTime } from "luxon"; +import { useState, useEffect } from "react"; +import { Button } from "~/components/ui/button"; +import { + ChevronLeft, + ChevronRight, + Star, + Filter, + X, + Users, + User, + ArrowUpDown, +} from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { Label } from "~/components/ui/label"; +import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/ui/collapsible"; +import DatePickerWithRange from "~/components/home/date-picker"; +import BottomNavigation from "~/components/home/bottom-nav"; +import Error from "~/components/error"; +import { useSearchParams } from "react-router"; +import { useColles } from "~/lib/api"; +import TabContent from "~/components/home/tab-content"; + +export default function Home() { + // Handle query parameters + const [query, setQuery] = useSearchParams(); + const updateQuery = (key: string, value: string) => { + if (query.get(key) !== value) { + setQuery((prev) => { + const newQuery = new URLSearchParams(prev); + newQuery.set(key, value); + return newQuery; + }); + } + }; + + // Tabs + const activeTab = query.get("view") || "you"; + const setActiveTab = (tab: string) => updateQuery("view", tab); + + // Date + const rawStartDate = query.get("start"); + const startDate = rawStartDate + ? DateTime.fromISO(rawStartDate, { zone: "local" }) + : DateTime.now().startOf("week"); + + const setStartDate = (date: DateTime) => + updateQuery("start", date.startOf("week").toISODate()!); + + const handlePreviousWeek = () => { + const previousWeek = startDate.minus({ weeks: 1 }); + setStartDate(previousWeek); + }; + + const handleNextWeek = () => { + const nextWeek = startDate.plus({ weeks: 1 }); + setStartDate(nextWeek); + }; + + // Fetch colles from API + const { studentColles, classColles, favoriteColles, error, isLoading } = + useColles(startDate); + console.log("Colles loaded:", { + studentColles, + classColles, + favoriteColles, + error, + isLoading, + }); + + // TODO: FAVORITES + const useToggleStar = (auth: any) => {}; + // TODO: FILTERS + const getSessionFilter = (key: string) => { + const filter = sessionStorage.getItem(key); + if (filter) { + return filter; + } + return "all"; + }; + const [subjectFilter, setSubjectFilter] = useState( + getSessionFilter("subject") + ); + const [examinerFilter, setExaminerFilter] = useState( + getSessionFilter("examiner") + ); + // const [studentFilter, setStudentFilter] = useState("all") + const [isFilterOpen, setIsFilterOpen] = useState( + getSessionFilter("subject") !== "all" || + getSessionFilter("examiner") !== "all" + ); + useEffect(() => { + sessionStorage.setItem("subject", subjectFilter); + sessionStorage.setItem("examiner", examinerFilter); + // sessionStorage.setItem('student', studentFilter) + }, [subjectFilter, examinerFilter]); + + // Restore scroll position + // TODO: Test and check if needed!! + // // SCROLL + // const setScrollPosition = (colleId: number) => { + // sessionStorage.setItem("colles_position", colleId.toString()); + // }; + // const restoreScrollPosition = () => { + // const position = sessionStorage.getItem("colles_position"); + // if (position) { + // const element = document.getElementById(`colle-${position}`); + // if (element) { + // element.scrollIntoView({ behavior: "smooth", block: "center" }); + // sessionStorage.removeItem("colles_position"); + // } + // } + // }; + // const location = useLocation(); + // useEffect(() => { + // setTimeout(restoreScrollPosition, 500); + // }, [location]); + + // Error handling (after all hooks) + if (error) + return ( + + ); + + return ( +
+ {/* Week Navigation */} +
+
+
+ +
+
+ + +
+
+
+ + +
+ + + + Vos colles ({studentColles.length}) + + + + Vos favoris ({0 /* TODO: stars.length */}) + + + + Votre classe ({classColles.length}) + + +
+
+ + {/* TODO: Filter component */} + +
+
+ {activeTab === "all" && ( + <> + +
+ + + +
+ + )} +
+ {/* TODO: DEBUG */} + {/* {activeTab === "all" && + (getWeekStart().getTime() != startDate.getTime() ? ( + + ) : ( + + ))} */} +
+ + +
+
+ + +
+ +
+ + +
+ + {/*
+ + +
*/} +
+ +
+ +
+
+
+ + {/* Tab Content */} +
+ {/* Your Colles Tab */} + {activeTab === "you" && ( + + )} + + {/* Favorites Tab */} + {activeTab === "favorites" && ( + + )} +
+ + {/* Class Colles Tab */} + {activeTab === "class" && ( + + )} + + {/* Bottom Navigation for Mobile */} + +
+ ); +} diff --git a/app/components/home/skeleton-card.tsx b/app/components/home/skeleton-card.tsx new file mode 100644 index 0000000..9e3b808 --- /dev/null +++ b/app/components/home/skeleton-card.tsx @@ -0,0 +1,55 @@ +import { + Card, + CardContent, + CardHeader, + CardFooter, +} from "~/components/ui/card"; +import { Skeleton } from "~/components/ui/skeleton"; + +export default function ColleCardSkeleton() { + return ( + + +
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); +} diff --git a/app/components/home/tab-content.tsx b/app/components/home/tab-content.tsx new file mode 100644 index 0000000..d3a45dc --- /dev/null +++ b/app/components/home/tab-content.tsx @@ -0,0 +1,104 @@ +import { DateTime } from "luxon"; +import type { Colle } from "~/lib/api"; +import { Badge } from "~/components/ui/badge"; +import ColleCard from "~/components/home/colle-card"; +import ColleCardSkeleton from "~/components/home/skeleton-card"; + +type TabContentProps = { + tabTitle: string; + emptyCollesText: string; + isLoading: boolean; + colles: Colle[]; +}; + +const WEEK_DAYS = [ + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi", + "Dimanche", +]; + +export default function TabContent({ + tabTitle, + emptyCollesText, + isLoading, + colles, +}: TabContentProps) { + const collesByDay: Record = {}; + colles.forEach((colle) => { + const date = DateTime.fromISO(colle.date); + const dayName = WEEK_DAYS[date.weekday - 1]; // Luxon weekday is 1-7 (Mon-Sun) + if (!collesByDay[dayName]) { + collesByDay[dayName] = []; + } + collesByDay[dayName].push(colle); + }); + + const days = WEEK_DAYS.filter( + (day) => collesByDay[day] && collesByDay[day].length > 0 + ); + + return ( + <> +

{tabTitle}

+ {isLoading ? ( +
+ {WEEK_DAYS.slice(0, 2).map((day) => ( +
+
+

{day}

+ + Chargement... + +
+
+ {Array(3) + .fill(0) + .map((_, i) => ( + + ))} +
+
+ ))} +
+ ) : colles.length === 0 ? ( +
+ {emptyCollesText} +
+ ) : ( +
+ {days.map((day) => ( +
+
+

{day}

+ + {collesByDay[day].length} colle + {collesByDay[day].length !== 1 ? "s" : ""} + +
+
+ {collesByDay[day].map((colle) => ( + {}} + onToggleFavorite={() => {}} + isFavorite={false} + // TODO: Implement scroll position handling + // beforeClick={() => setScrollPosition(colle.id)} + // TODO: Implement favorite toggle + // onToggleFavorite={handleToggleFavorite} + // isFavorite={isFavorite(colle)} + /> + ))} +
+
+ ))} +
+ )} + + ); +} diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx new file mode 100644 index 0000000..205a1a1 --- /dev/null +++ b/app/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "~/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx new file mode 100644 index 0000000..fc40406 --- /dev/null +++ b/app/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/app/components/ui/calendar.tsx b/app/components/ui/calendar.tsx new file mode 100644 index 0000000..052772f --- /dev/null +++ b/app/components/ui/calendar.tsx @@ -0,0 +1,211 @@ +import * as React from "react"; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react"; +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; + +import { cn } from "~/lib/utils"; +import { Button, buttonVariants } from "~/components/ui/button"; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"]; +}) { + const defaultClassNames = getDefaultClassNames(); + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ); + } + + if (orientation === "right") { + return ( + + ); + } + + return ( + + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); + }, + ...components, + }} + {...props} + /> + ); +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( + + + + +
+

+ {user.fullName} +

+

+ {user.className} +

+
+
+ + + setTheme(theme === "dark" ? "light" : "dark")} + > + {theme === "dark" ? ( + <> + + Mode clair + + ) : ( + <> + + Mode sombre + + )} + + { + e.stopPropagation(); + e.preventDefault(); + navigate("/settings"); + }}> + + + Paramètres + + + { + e.stopPropagation(); + e.preventDefault(); + navigate("/progress"); + }}> + + + Progression + + + + + + + Se déconnecter + +
+ + ); +} + +const getAvatar = (name: string) => { + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase(); +}; diff --git a/app/layout.tsx b/app/layout.tsx index 7545c85..083fc13 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,7 @@ import { Link } from "react-router"; const SUPPORT_MAIL = "mailto:" + import.meta.env.VITE_SUPPORT_EMAIL; -export default function AuthLayout({ +export function AuthLayout({ children, title = "Connexion", description = "Entrez votre email pour recevoir un lien de connexion ou un code de vérification.", @@ -48,3 +48,21 @@ export default function AuthLayout({ ); } + +export function MainLayout({ + children, + page +}: { + children: React.ReactNode; + page: React.ReactNode; +}) { + return ( +
+
+ {children} +
+ + {page} +
+ ); +} diff --git a/app/lib/api.ts b/app/lib/api.ts index f2f1a3c..92af104 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -1,3 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; +import { DateTime, Duration } from "luxon"; + const BASE_URL = import.meta.env.VITE_API_URL; const makePostRequest = async ( @@ -10,7 +13,7 @@ const makePostRequest = async ( method, headers: { "Content-Type": "application/json", - "Accept": "application/json", + Accept: "application/json", }, body: JSON.stringify(body), credentials: "include", // Include cookies for authentication @@ -23,11 +26,8 @@ const makePostRequest = async ( // TODO: Use swr or react-query for caching and revalidation // TODO: Cache all to localStorage or IndexedDB for offline support -const makeRequest = async ( - url: string, - error = "Une erreur est survenue", -) => { - const response = await fetch(BASE_URL + url, { credentials: "include"}); +const makeRequest = async (url: string, error = "Une erreur est survenue") => { + const response = await fetch(BASE_URL + url, { credentials: "include" }); const data = await response.json(); if (!response.ok) throw new Error(data.error || error); @@ -85,6 +85,125 @@ export const registerUser = async ( /** * === COLLES API === */ -export const getColles = async (weekNumber: number, year: number) => { - return makeRequest(`/colles?week=${weekNumber}&year=${year}`, "Échec de la récupération des colles"); +export const getColles = async (startDate: DateTime) => { + return makeRequest( + `/colles?startDate=${startDate.toISODate()}`, + "Échec de la récupération des colles" + ); +}; + +export interface Colle { + id: number; + date: string; // ISO date string + subject: { + id: number; + name: string; + }; + examiner: { + id: number; + name: string; + }; + room: { + id: number; + name: string; + }; + student: User; + grade?: number; // Nullable grade + bjsecret?: string; // Optional field + bjid?: string; // Optional field + content?: string; // Optional field + comment?: string; // Optional field + attachments?: string[]; // Optional field, array of attachment URLs +} + +interface CollePayload { + classColles: Colle[]; + studentColles: Colle[]; + favoriteColles: Colle[]; +} + +export const useColles = (startDate: DateTime) => { + // Check if the start date is the current week + // This is used to determine if the data is "actual" or not + const isActual = DateTime.now() + .startOf("week") + .equals(startDate.startOf("week")); + + const options = isActual + ? { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + staleTime: 0, + gcTime: 0, + } + : { + staleTime: Duration.fromObject({ + minutes: 5, // 5 minutes + }).toMillis(), + gcTime: Duration.fromObject({ + days: 3, // 3 days + }).toMillis(), + }; + + const { data, ...props } = useQuery({ + queryKey: ["colles", startDate.toISODate()], + queryFn: () => getColles(startDate), + ...options, + }); + const mergedData = Object.assign( + { + classColles: [], + studentColles: [], + favoriteColles: [], + }, + data || {} + ) as CollePayload; + + return { + ...mergedData, + ...props + } +}; + +/** + * === USER API === + */ +const fetchUser = async () => { + return makeRequest( + "/users/@me", + "Échec de la récupération des informations utilisateur" + ); +}; + +const defaultUser = { + id: 0, + firstName: "", + lastName: "", + fullName: "", + email: "", + className: "", +}; + +export type User = typeof defaultUser; + +export const useUser = () => { + const { data, ...props } = useQuery({ + queryKey: ["user"], + queryFn: fetchUser, + staleTime: Duration.fromObject({ + minutes: 5, // 5 minutes + }).toMillis(), + gcTime: Duration.fromObject({ + days: 3, // 3 days + }).toMillis(), + }); + return { + user: (data ? Object.assign(defaultUser, data) : defaultUser) as User, + ...props, + }; +}; + +export const logout = async () => { + // TODO: POST + // TODO: Invalidate user query (cache) }; diff --git a/app/lib/client.ts b/app/lib/client.ts new file mode 100644 index 0000000..d9bad41 --- /dev/null +++ b/app/lib/client.ts @@ -0,0 +1,138 @@ +import { QueryClient } from '@tanstack/react-query'; +import { persistQueryClient } from '@tanstack/query-persist-client-core'; +import { get, set, del } from 'idb-keyval'; +import LZString from 'lz-string'; + +const CACHE_KEY = 'khollise-cache'; // Key for IndexedDB storage + +// Check if we're in a browser environment with IndexedDB support +const isIndexedDBAvailable = () => { + return typeof window !== 'undefined' && + typeof window.indexedDB !== 'undefined' && + window.indexedDB !== null; +}; + +// Custom IndexedDB persister with LZ-string compression +const createIDBPersister = () => { + // Return a no-op persister if IndexedDB is not available + if (!isIndexedDBAvailable()) { + console.warn('IndexedDB not available - cache persistence disabled'); + return { + persistClient: async () => { }, + restoreClient: async () => undefined, + removeClient: async () => { }, + }; + } + + return { + persistClient: async (client: any) => { + try { + // Double-check IndexedDB availability before operation + if (!isIndexedDBAvailable()) { + console.warn('IndexedDB not available during persist operation'); + return; + } + + // Serialize the client data + const serializedClient = JSON.stringify(client); + + // Compress the serialized data + const compressedData = LZString.compress(serializedClient); + + // Store compressed data in IndexedDB + await set(CACHE_KEY, compressedData); + } catch (error) { + console.error('Failed to persist client cache:', error); + } + }, + + restoreClient: async () => { + try { + // Double-check IndexedDB availability before operation + if (!isIndexedDBAvailable()) { + console.warn('IndexedDB not available during restore operation'); + return undefined; + } + + // Get compressed data from IndexedDB + const compressedData = await get(CACHE_KEY); + + if (!compressedData) { + console.log('No cached data found in IndexedDB'); + return undefined; + } + + // Decompress the data + const decompressedData = LZString.decompress(compressedData); + + if (!decompressedData) { + console.warn('Failed to decompress cached data'); + return undefined; + } + + // Parse and return the client data + const client = JSON.parse(decompressedData); + console.log('Cache restored from IndexedDB'); + return client; + + } catch (error) { + console.error('Failed to restore client cache:', error); + // Clear corrupted cache if IndexedDB is available + if (isIndexedDBAvailable()) { + try { + await del(CACHE_KEY); + } catch (delError) { + console.error('Failed to clear corrupted cache:', delError); + } + } + return undefined; + } + }, + + removeClient: async () => { + try { + // Double-check IndexedDB availability before operation + if (!isIndexedDBAvailable()) { + console.warn('IndexedDB not available during remove operation'); + return; + } + + await del(CACHE_KEY); + console.log('Cache cleared from IndexedDB'); + } catch (error) { + console.error('Failed to remove client cache:', error); + } + }, + }; +}; + +// Create QueryClient with persistence +const createQueryClient = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + gcTime: 1000 * 60 * 60 * 24, // 24 hours (formerly cacheTime) + refetchOnWindowFocus: false, + retry: 2, + }, + }, + }); + + // Only set up persistence if IndexedDB is available + if (isIndexedDBAvailable()) { + persistQueryClient({ + queryClient, + persister: createIDBPersister(), + maxAge: 1000 * 60 * 60 * 24, // 24 hours + buster: 'v1', // Change this to invalidate cache + }) + } else { + console.warn('Cache persistence disabled - IndexedDB not available'); + } + + return queryClient; +}; + +const queryClient = createQueryClient(); +export default queryClient; diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 5f1e17c..4c4f2d6 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -8,3 +8,15 @@ export function cn(...inputs: ClassValue[]) { export function capitalizeFirstLetter(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } + +export function titleCase(str: string) { + return str.replace( + /\w\S*/g, + (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase() + ); +} + +// Force a reload of the page +export function forceReload() { + window.location.reload(); +} diff --git a/app/root.tsx b/app/root.tsx index 7331fe6..d758e6f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -9,6 +9,8 @@ import { import type { Route } from "./+types/root"; import "./app.css"; +import { QueryClientProvider } from "@tanstack/react-query"; +import queryClient from "./lib/client"; export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, @@ -38,7 +40,9 @@ export function Layout({ children }: { children: React.ReactNode }) { - {children} + + {children} + diff --git a/app/routes.ts b/app/routes.ts index cc6340d..c6ddef9 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,7 +1,7 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ - index("routes/index.tsx"), + index("routes/home.tsx"), route("/login", "routes/login.tsx"), route("/verify", "routes/verify.tsx"), route("/register", "routes/register.tsx"), diff --git a/app/routes/home.tsx b/app/routes/home.tsx new file mode 100644 index 0000000..af065c7 --- /dev/null +++ b/app/routes/home.tsx @@ -0,0 +1,22 @@ +import HomePage from "~/components/home"; +import { UserDropdown } from "~/components/user-dropdown"; +import { MainLayout } from "~/layout"; +import { useUser } from "~/lib/api"; +import { forceReload } from "~/lib/utils"; + +export default function Home() { + const { user, isLoading, error } = useUser(); + if (error) { + console.error(error); + // TODO: handle error (redirect to login or show error message) + } + return ( + }> +

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

+ {/* TODO: isLoading to display skeleton */} + +
+ ); +} diff --git a/app/routes/index.tsx b/app/routes/index.tsx deleted file mode 100644 index eb38dd6..0000000 --- a/app/routes/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from "react"; -import { getColles } from "~/lib/api"; - -export default function Index() { - const [colles, setColles] = useState([]); - - useEffect(() => { - // Fetch colles data from the API - getColles(12, 2025).then((data) => { - setColles(data); - }).catch((error) => { - console.error("Error fetching colles:", error); - }); - }, []); - - return ( -
-
-

Welcome to Khollisé!

-

Got {colles.length} colles this week.

-
-
- ); -} diff --git a/app/routes/login.tsx b/app/routes/login.tsx index c866633..37e080a 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -5,7 +5,7 @@ import { Label } from "~/components/ui/label"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; import { useState } from "react"; -import AuthLayout from "~/layout"; +import { AuthLayout } from "~/layout"; import { requestLogin } from "~/lib/api"; import { useNavigate } from "react-router"; import { Turnstile } from "@marsidev/react-turnstile"; diff --git a/app/routes/register.tsx b/app/routes/register.tsx index e83155b..a1f9d0e 100644 --- a/app/routes/register.tsx +++ b/app/routes/register.tsx @@ -1,17 +1,16 @@ -import type { Route } from "./+types/login"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { AlertCircleIcon, IdCardIcon, LoaderCircle, LogIn, MailIcon } from "lucide-react"; import { Label } from "~/components/ui/label"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; import { useEffect, useState } from "react"; -import AuthLayout from "~/layout"; +import { AuthLayout } from "~/layout"; import { useNavigate, useSearchParams } from "react-router"; import { capitalizeFirstLetter } from "~/lib/utils"; import { Combobox } from "~/components/combobox"; import { getClasses, registerUser } from "~/lib/api"; -export function meta({ }: Route.MetaArgs) { +export function meta() { return [ { title: "Khollisé - Inscription" }, { name: "description", content: "Connectez-vous à Khollisé" }, diff --git a/app/routes/verify.tsx b/app/routes/verify.tsx index b16702b..8977da1 100644 --- a/app/routes/verify.tsx +++ b/app/routes/verify.tsx @@ -11,7 +11,7 @@ import { useSearchParams } from "react-router"; import OtpInput from "~/components/input-otp"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Button } from "~/components/ui/button"; -import AuthLayout from "~/layout"; +import { AuthLayout } from "~/layout"; export function meta({ }: Route.MetaArgs) { return [ diff --git a/package.json b/package.json index e70c5f1..943e7f2 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,32 @@ }, "dependencies": { "@marsidev/react-turnstile": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "@react-router/node": "^7.5.3", "@react-router/serve": "^7.5.3", + "@tanstack/query-persist-client-core": "^5.83.0", + "@tanstack/react-query": "^5.83.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "idb-keyval": "^6.2.2", "input-otp": "^1.4.2", "isbot": "^5.1.27", "lucide-react": "^0.511.0", + "luxon": "^3.7.1", + "lz-string": "^1.5.0", "netlify-cli": "^22.1.3", "react": "^19.1.0", + "react-day-picker": "^9.8.1", "react-dom": "^19.1.0", "react-router": "^7.5.3", "tailwind-merge": "^3.3.0", @@ -33,6 +45,7 @@ "devDependencies": { "@react-router/dev": "^7.5.3", "@tailwindcss/vite": "^4.1.4", + "@types/luxon": "^3.7.0", "@types/node": "^20", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 165e357..41dc219 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,24 +11,45 @@ importers: '@marsidev/react-turnstile': specifier: ^1.1.0 version: 1.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-label': specifier: ^2.1.6 version: 2.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-popover': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-select': + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-tabs': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-router/node': specifier: ^7.5.3 version: 7.7.1(react-router@7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3) '@react-router/serve': specifier: ^7.5.3 version: 7.7.1(react-router@7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3) + '@tanstack/query-persist-client-core': + specifier: ^5.83.0 + version: 5.83.0 + '@tanstack/react-query': + specifier: ^5.83.0 + version: 5.83.0(react@19.1.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -38,6 +59,12 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + idb-keyval: + specifier: ^6.2.2 + version: 6.2.2 input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -47,12 +74,21 @@ importers: lucide-react: specifier: ^0.511.0 version: 0.511.0(react@19.1.1) + luxon: + specifier: ^3.7.1 + version: 3.7.1 + lz-string: + specifier: ^1.5.0 + version: 1.5.0 netlify-cli: specifier: ^22.1.3 - version: 22.4.0(@types/node@20.19.9)(picomatch@4.0.3)(rollup@4.46.1) + version: 22.4.0(@types/node@20.19.9)(idb-keyval@6.2.2)(picomatch@4.0.3)(rollup@4.46.1) react: specifier: ^19.1.0 version: 19.1.1 + react-day-picker: + specifier: ^9.8.1 + version: 9.8.1(react@19.1.1) react-dom: specifier: ^19.1.0 version: 19.1.1(react@19.1.1) @@ -72,6 +108,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.4 version: 4.1.11(vite@6.3.5(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.0)) + '@types/luxon': + specifier: ^3.7.0 + version: 3.7.0 '@types/node': specifier: ^20 version: 20.19.9 @@ -262,6 +301,9 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@date-fns/tz@1.2.0': + resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} + '@dependents/detective-less@5.0.1': resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} @@ -1174,6 +1216,9 @@ packages: resolution: {integrity: sha512-bWLDlHsBlgKY/05wDN/V3ETcn5G2SV/SiA2ZmNvKGGlmVX4G5li7GRDhHcgYvHJHyJ8TUStqg2xtHmCs0UbAbg==} engines: {node: '>=18'} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} @@ -1190,6 +1235,45 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.11': + resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -1221,6 +1305,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.10': resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} peerDependencies: @@ -1234,6 +1327,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: @@ -1278,6 +1384,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popover@1.1.14': resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} peerDependencies: @@ -1343,6 +1462,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -1352,6 +1497,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tabs@1.1.12': + resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -1388,6 +1546,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -1397,6 +1564,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.1': resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} peerDependencies: @@ -1415,6 +1591,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -1686,6 +1875,17 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/query-core@5.83.0': + resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} + + '@tanstack/query-persist-client-core@5.83.0': + resolution: {integrity: sha512-hdKgHkr1MYnwZX+QHj/9JjXZx9gL2RUCD5xSX0EHZiqUQhMk4Gcryq9xosn8LmYRMlhkjk7n9uV+X4UXRvgoIg==} + + '@tanstack/react-query@5.83.0': + resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} + peerDependencies: + react: ^18 || ^19 + '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1710,6 +1910,9 @@ packages: '@types/http-proxy@1.17.16': resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + '@types/luxon@3.7.0': + resolution: {integrity: sha512-zNduyjkTXTttOp0KUG7ueFORgq6eyFsjbCZ/B8UfrVmfkHD1V9xC9wN/Vk0XUg0R/k/aIX1NeFwZgwe5hsDXfA==} + '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} @@ -2357,6 +2560,12 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -3072,6 +3281,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3523,6 +3735,10 @@ packages: resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} engines: {node: '>=12'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + macos-release@3.4.0: resolution: {integrity: sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4167,6 +4383,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-day-picker@9.8.1: + resolution: {integrity: sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -4886,6 +5108,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5349,6 +5576,8 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@date-fns/tz@1.2.0': {} + '@dependents/detective-less@5.0.1': dependencies: gonzales-pe: 4.3.0 @@ -6251,6 +6480,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.1.2': {} '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': @@ -6262,6 +6493,47 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.1)': dependencies: react: 19.1.1 @@ -6296,6 +6568,12 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-direction@1.1.1(@types/react@19.1.8)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.8 + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -6309,6 +6587,21 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.1)': dependencies: react: 19.1.1 @@ -6342,6 +6635,32 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-menu@2.1.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1) + aria-hidden: 1.2.6 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-popover@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -6412,6 +6731,52 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + + '@radix-ui/react-select@2.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + aria-hidden: 1.2.6 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1) @@ -6419,6 +6784,22 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.1)': dependencies: react: 19.1.1 @@ -6447,12 +6828,25 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.8)(react@19.1.1)': + dependencies: + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.8)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.8 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.8)(react@19.1.1)': dependencies: '@radix-ui/rect': 1.1.1 @@ -6467,6 +6861,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/rect@1.1.1': {} '@react-router/dev@7.7.1(@react-router/serve@7.7.1(react-router@7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3))(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.7.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)(vite@6.3.5(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.0))(yaml@2.8.0)': @@ -6706,6 +7109,17 @@ snapshots: tailwindcss: 4.1.11 vite: 6.3.5(@types/node@20.19.9)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.0) + '@tanstack/query-core@5.83.0': {} + + '@tanstack/query-persist-client-core@5.83.0': + dependencies: + '@tanstack/query-core': 5.83.0 + + '@tanstack/react-query@5.83.0(react@19.1.1)': + dependencies: + '@tanstack/query-core': 5.83.0 + react: 19.1.1 + '@tokenizer/token@0.3.0': {} '@tsconfig/node10@1.0.11': {} @@ -6724,6 +7138,8 @@ snapshots: dependencies: '@types/node': 20.19.9 + '@types/luxon@3.7.0': {} + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 @@ -7457,6 +7873,10 @@ snapshots: data-uri-to-buffer@4.0.1: {} + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -8240,6 +8660,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.2: {} + ieee754@1.2.1: {} ignore@7.0.5: {} @@ -8298,7 +8720,7 @@ snapshots: ipaddr.js@1.9.1: {} - ipx@3.1.1(@netlify/blobs@10.0.7): + ipx@3.1.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2): dependencies: '@fastify/accept-negotiator': 2.0.1 citty: 0.1.6 @@ -8314,7 +8736,7 @@ snapshots: sharp: 0.34.3 svgo: 4.0.0 ufo: 1.6.1 - unstorage: 1.16.1(@netlify/blobs@10.0.7) + unstorage: 1.16.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2) xss: 1.0.15 transitivePeerDependencies: - '@azure/app-configuration' @@ -8660,6 +9082,8 @@ snapshots: luxon@3.7.1: {} + lz-string@1.5.0: {} + macos-release@3.4.0: {} magic-string@0.30.17: @@ -8810,7 +9234,7 @@ snapshots: negotiator@0.6.4: {} - netlify-cli@22.4.0(@types/node@20.19.9)(picomatch@4.0.3)(rollup@4.46.1): + netlify-cli@22.4.0(@types/node@20.19.9)(idb-keyval@6.2.2)(picomatch@4.0.3)(rollup@4.46.1): dependencies: '@fastify/static': 7.0.4 '@netlify/api': 14.0.3 @@ -8867,7 +9291,7 @@ snapshots: https-proxy-agent: 7.0.6(supports-color@10.0.0) inquirer: 8.2.6 inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.6) - ipx: 3.1.1(@netlify/blobs@10.0.7) + ipx: 3.1.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2) is-docker: 3.0.0 is-stream: 4.0.1 is-wsl: 3.1.0 @@ -9374,6 +9798,13 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-day-picker@9.8.1(react@19.1.1): + dependencies: + '@date-fns/tz': 1.2.0 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.1.1 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -10009,7 +10440,7 @@ snapshots: unpipe@1.0.0: {} - unstorage@1.16.1(@netlify/blobs@10.0.7): + unstorage@1.16.1(@netlify/blobs@10.0.7)(idb-keyval@6.2.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -10021,6 +10452,7 @@ snapshots: ufo: 1.6.1 optionalDependencies: '@netlify/blobs': 10.0.7 + idb-keyval: 6.2.2 untildify@4.0.0: {} @@ -10070,6 +10502,10 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + use-sync-external-store@1.5.0(react@19.1.1): + dependencies: + react: 19.1.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {}