Compare commits
9 commits
b0fdf297ee
...
aa51ae523d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa51ae523d | ||
|
|
098d528413 | ||
|
|
a63dd0ab89 | ||
|
|
b4b74155c2 | ||
|
|
cdc41c4a51 | ||
|
|
26cc3bf4bb | ||
|
|
7067256a9f | ||
|
|
299b1efe8b | ||
|
|
52ca34a600 |
22 changed files with 1179 additions and 393 deletions
47
app/components/bottom-nav.tsx
Normal file
47
app/components/bottom-nav.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { CalendarClock, NotebookTabs, User, Utensils } from "lucide-react";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from "~/components/ui/navigation-menu";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const navigationMenuItems = [
|
||||||
|
{ title: "Colles", id: "colles", href: "/", icon: CalendarClock },
|
||||||
|
{ title: "Repas", id: "repas", href: "/repas", icon: Utensils },
|
||||||
|
{ title: "Notes", id: "grades", href: "/grades", icon: NotebookTabs },
|
||||||
|
{ title: "Profil", id: "settings", href: "/settings", icon: User },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function BottomNavigation({ activeId }: { activeId: string }) {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 bg-background border-t border-border z-50">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
{navigationMenuItems.map((item) => (
|
||||||
|
<NavigationMenuItem key={item.title}>
|
||||||
|
<NavigationMenuLink
|
||||||
|
className={cn(
|
||||||
|
navigationMenuTriggerStyle(),
|
||||||
|
"flex flex-col h-auto items-center px-5 py-2.5"
|
||||||
|
)}
|
||||||
|
data-active={item.id === activeId}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link to={item.href}>
|
||||||
|
<item.icon className="mb-1.5 h-5 w-5" />
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
))}
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-background border-t border-border md:hidden z-50">
|
|
||||||
<div className="flex items-center justify-around h-16">
|
|
||||||
<button
|
|
||||||
onClick={() => onTabChange("you")}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-center justify-center w-full h-full",
|
|
||||||
activeTab === "you" ? "text-primary" : "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
<span className="text-xs font-medium">Vos colles</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => onTabChange("favorites")}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-center justify-center w-full h-full gap-1",
|
|
||||||
activeTab === "favorites" ? "text-primary" : "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Star className="h-4 w-4" />
|
|
||||||
<span className="text-xs font-medium">Vos favoris ({favoriteCount})</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => onTabChange("class")}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-center justify-center w-full h-full",
|
|
||||||
activeTab === "class" ? "text-primary" : "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
<span className="text-xs font-medium">Votre classe</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +1,129 @@
|
||||||
import type React from "react";
|
import type { Colle, UserPreferences } from "~/lib/api";
|
||||||
import type { Colle } from "~/lib/api";
|
|
||||||
|
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import {
|
import { Card } from "~/components/ui/card";
|
||||||
Card,
|
import { User, Star, CalendarDays, MapPin } from "lucide-react";
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import { User, UserCheck, Paperclip, Star, MapPinHouse } from "lucide-react";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
import {
|
||||||
import { formatDate, formatGrade, formatTime } from "~/lib/utils";
|
cn,
|
||||||
|
formatDate,
|
||||||
// TODO: Preferences for subject colors
|
formatGrade,
|
||||||
const getSubjectColor = (_: string) => {
|
formatTime,
|
||||||
// Mock placeholder function
|
getColorClass,
|
||||||
return "bg-blue-100 text-blue-800"; // Default color
|
getSubjectColor,
|
||||||
};
|
getSubjectEmoji,
|
||||||
const getSubjectEmoji = (_: string) => {
|
} from "~/lib/utils";
|
||||||
// Mock placeholder function
|
|
||||||
return "📚"; // Default emoji
|
|
||||||
};
|
|
||||||
|
|
||||||
type ColleCardProps = {
|
type ColleCardProps = {
|
||||||
colle: Colle;
|
colle: Colle;
|
||||||
onToggleFavorite: (id: number, favorite: boolean) => void;
|
onToggleFavorite: (id: number, favorite: boolean) => void;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
|
preferences: UserPreferences;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ColleCard({
|
export default function ColleCard({
|
||||||
colle,
|
colle,
|
||||||
onToggleFavorite,
|
onToggleFavorite,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
preferences,
|
||||||
}: ColleCardProps) {
|
}: ColleCardProps) {
|
||||||
// TODO: Favorites
|
// TODO: Favorites
|
||||||
const handleToggleFavorite = (e: React.MouseEvent) => {
|
// const handleToggleFavorite = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation(); // Prevent card click
|
// e.stopPropagation(); // Prevent card click
|
||||||
e.preventDefault();
|
// e.preventDefault();
|
||||||
const newValue = !isFavorite;
|
// const newValue = !isFavorite;
|
||||||
onToggleFavorite(colle.id, newValue);
|
// onToggleFavorite(colle.id, newValue);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const subjectColor = getSubjectColor(colle.subject.name);
|
const subjectColor = getColorClass(getSubjectColor(colle.subject.name, preferences));
|
||||||
const subjectEmoji = getSubjectEmoji(colle.subject.name);
|
const subjectEmoji = getSubjectEmoji(colle.subject.name, preferences);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/colles/${colle.id}`}>
|
<Link to={`/colles/${colle.id}`}>
|
||||||
<Card
|
<Card
|
||||||
id={`colle-${colle.id}`}
|
id={`colle-${colle.id}`}
|
||||||
className="h-full cursor-pointer hover:shadow-md transition-shadow border-primary"
|
className="p-0 overflow-hidden hover:shadow-lg transition-shadow max-w-sm"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-0 pt-3 px-4 flex flex-row items-center justify-between">
|
<div className="flex min-h-40">
|
||||||
<div>
|
<div
|
||||||
<div className="font-medium">{formatDate(colle.date)}</div>
|
className={cn(
|
||||||
<div className="font-normal text-sm text-muted-foreground">
|
"p-2 flex flex-col justify-center items-center min-w-[4px]",
|
||||||
{formatTime(colle.date)}
|
subjectColor
|
||||||
</div>
|
)}
|
||||||
</div>
|
></div>
|
||||||
{colle.grade && (
|
|
||||||
<div className="flex items-center gap-2 h-full mb-3">
|
<div className="flex-1 p-3 flex flex-col justify-between">
|
||||||
<Button
|
<div>
|
||||||
variant="ghost"
|
<div className="flex items-center justify-between">
|
||||||
size="icon"
|
<h3 className="text-lg font-bold text-foreground">
|
||||||
className={`h-8 w-8 ${
|
{colle.student.fullName}
|
||||||
isFavorite ? "text-yellow-500" : "text-muted-foreground"
|
</h3>
|
||||||
}`}
|
{colle.grade && (
|
||||||
onClick={handleToggleFavorite}
|
<div className="flex items-center gap-2 h-full">
|
||||||
>
|
{/* <Button
|
||||||
<Star
|
variant="ghost"
|
||||||
className={`h-5 w-5 ${isFavorite ? "fill-yellow-500" : ""}`}
|
size="icon"
|
||||||
/>
|
className={`h-8 w-8 ${
|
||||||
<span className="sr-only">Ajouter aux favoris</span>
|
isFavorite ? "text-yellow-500" : "text-muted-foreground"
|
||||||
</Button>
|
}`}
|
||||||
<div
|
onClick={handleToggleFavorite}
|
||||||
className={`px-2 py-1 rounded-md text-sm font-medium ${subjectColor}`}
|
>
|
||||||
>
|
<Star
|
||||||
{formatGrade(colle.grade)}/20
|
className={`h-5 w-5 ${
|
||||||
|
isFavorite ? "fill-yellow-500" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Ajouter aux favoris</span>
|
||||||
|
</Button> */}
|
||||||
|
<div
|
||||||
|
className={`px-2 py-1 rounded-md text-sm font-medium ${subjectColor}`}
|
||||||
|
>
|
||||||
|
{formatGrade(colle.grade)}/20
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pb-0 pt-0 px-4">
|
<div className="text-sm text-primary grid gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-1">
|
||||||
<div className="space-y-1">
|
<CalendarDays className="h-3 w-3" />
|
||||||
<div className="flex items-center gap-2">
|
<span>
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
{formatDate(colle.date)} à {formatTime(colle.date)}
|
||||||
<span className="font-medium">{colle.student.fullName}</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<MapPin className="h-3 w-3" />
|
||||||
<UserCheck className="h-4 w-4 text-muted-foreground" />
|
<span>{colle.room?.name}</span>
|
||||||
<span>{colle.examiner.name}</span>
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span>{colle.examiner?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{colle.room && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MapPinHouse className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span>{colle.room.name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="pt-1 pb-3 px-4 flex flex-col items-start">
|
<div className="w-full flex justify-between items-center">
|
||||||
<div className="w-full flex justify-between items-center">
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<Badge className={subjectColor}>
|
||||||
<Badge className={subjectColor}>
|
{colle.subject.name + " " + subjectEmoji}
|
||||||
{colle.subject.name + " " + subjectEmoji}
|
|
||||||
</Badge>
|
|
||||||
{isFavorite && (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500 mr-1" />
|
|
||||||
Favori
|
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
{isFavorite && (
|
||||||
</div>
|
<Badge variant="secondary">
|
||||||
{/* TODO: Attachments */}
|
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500 mr-1" />
|
||||||
{/* {colle.attachmentsCount > 0 && (
|
Favori
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* TODO: Attachments */}
|
||||||
|
{/* {colle.attachmentsCount > 0 && (
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
<div className="flex items-center gap-1 text-muted-foreground">
|
||||||
<Paperclip className="h-3.5 w-3.5" />
|
<Paperclip className="h-3.5 w-3.5" />
|
||||||
<span className="text-xs">{colle.attachmentsCount}</span>
|
<span className="text-xs">{colle.attachmentsCount}</span>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function DatePickerWithRange({
|
||||||
<button
|
<button
|
||||||
id="date"
|
id="date"
|
||||||
className={
|
className={
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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 h-9 px-4 py-2 has-[>svg]:px-3 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 " +
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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 h-9 px-4 py-2 has-[>svg]:px-3 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:hover:bg-input/50 " +
|
||||||
cn(
|
cn(
|
||||||
"w-full justify-start text-left font-normal",
|
"w-full justify-start text-left font-normal",
|
||||||
!startDate && "text-muted-foreground"
|
!startDate && "text-muted-foreground"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Star,
|
Star,
|
||||||
Filter,
|
|
||||||
X,
|
|
||||||
Users,
|
Users,
|
||||||
User,
|
UserIcon,
|
||||||
ArrowUpDown,
|
SortAsc,
|
||||||
|
SortDesc,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -18,21 +17,15 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "~/components/ui/select";
|
} from "~/components/ui/select";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Tabs, TabsList, tabsStyle, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import DatePickerWithRange from "~/components/home/date-picker";
|
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 Error from "~/components/error";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { useColles } from "~/lib/api";
|
import { useColles, type User } from "~/lib/api";
|
||||||
import TabContent from "~/components/home/tab-content";
|
import TabContent from "~/components/home/tab-content";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home({ user }: { user: User }) {
|
||||||
// Handle query parameters
|
// Handle query parameters
|
||||||
const [query, setQuery] = useSearchParams();
|
const [query, setQuery] = useSearchParams();
|
||||||
const updateQuery = (key: string, value: string) => {
|
const updateQuery = (key: string, value: string) => {
|
||||||
|
|
@ -71,13 +64,6 @@ export default function Home() {
|
||||||
// Fetch colles from API
|
// Fetch colles from API
|
||||||
const { studentColles, classColles, favoriteColles, error, isLoading } =
|
const { studentColles, classColles, favoriteColles, error, isLoading } =
|
||||||
useColles(startDate);
|
useColles(startDate);
|
||||||
console.log("Colles loaded:", {
|
|
||||||
studentColles,
|
|
||||||
classColles,
|
|
||||||
favoriteColles,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error handling (after all hooks)
|
// Error handling (after all hooks)
|
||||||
if (error)
|
if (error)
|
||||||
|
|
@ -92,240 +78,194 @@ export default function Home() {
|
||||||
|
|
||||||
// TODO: FAVORITES
|
// TODO: FAVORITES
|
||||||
const useToggleStar = (auth: any) => {};
|
const useToggleStar = (auth: any) => {};
|
||||||
// TODO: FILTERS
|
|
||||||
const getSessionFilter = (key: string) => {
|
// Filter state
|
||||||
const filter = sessionStorage.getItem(key);
|
const rawSubject = query.get("subject");
|
||||||
if (filter) {
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
return "all";
|
|
||||||
};
|
|
||||||
const [subjectFilter, setSubjectFilter] = useState<string>(
|
const [subjectFilter, setSubjectFilter] = useState<string>(
|
||||||
getSessionFilter("subject")
|
rawSubject === "all" ? "" : rawSubject || ""
|
||||||
);
|
);
|
||||||
|
const setSubject = (subject: string) => {
|
||||||
|
updateQuery("subject", subject);
|
||||||
|
setSubjectFilter(subject);
|
||||||
|
};
|
||||||
|
const rawExaminer = query.get("examiner");
|
||||||
const [examinerFilter, setExaminerFilter] = useState<string>(
|
const [examinerFilter, setExaminerFilter] = useState<string>(
|
||||||
getSessionFilter("examiner")
|
rawExaminer === "all" ? "" : rawExaminer || ""
|
||||||
);
|
);
|
||||||
// const [studentFilter, setStudentFilter] = useState<string>("all")
|
const setExaminer = (examiner: string) => {
|
||||||
const [isFilterOpen, setIsFilterOpen] = useState(
|
updateQuery("examiner", examiner);
|
||||||
getSessionFilter("subject") !== "all" ||
|
setExaminerFilter(examiner);
|
||||||
getSessionFilter("examiner") !== "all"
|
};
|
||||||
|
const [sorted, setSort] = useState<string>(query.get("sort") || "desc");
|
||||||
|
const toggleSort = () => {
|
||||||
|
const newSort = sorted === "asc" ? "desc" : "asc";
|
||||||
|
updateQuery("sort", newSort);
|
||||||
|
setSort(newSort);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateFilter = (arr: string[], value: string) => {
|
||||||
|
const unique = [...new Set(arr)];
|
||||||
|
if (value && !unique.includes(value)) {
|
||||||
|
unique.push(value);
|
||||||
|
}
|
||||||
|
unique.sort((a, b) => a.localeCompare(b));
|
||||||
|
return unique;
|
||||||
|
};
|
||||||
|
const subjects = generateFilter(
|
||||||
|
classColles.map((colle) => colle.subject?.name),
|
||||||
|
subjectFilter
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
const examiners = generateFilter(
|
||||||
sessionStorage.setItem("subject", subjectFilter);
|
classColles.map((colle) => colle.examiner?.name),
|
||||||
sessionStorage.setItem("examiner", examinerFilter);
|
examinerFilter
|
||||||
// sessionStorage.setItem('student', studentFilter)
|
);
|
||||||
}, [subjectFilter, examinerFilter]);
|
|
||||||
|
const applyFilters = (colles: any[]) => {
|
||||||
|
return colles
|
||||||
|
.filter((colle) => {
|
||||||
|
const subjectMatch =
|
||||||
|
subjectFilter === "all" || !subjectFilter
|
||||||
|
? true
|
||||||
|
: colle.subject?.name === subjectFilter;
|
||||||
|
const examinerMatch =
|
||||||
|
examinerFilter === "all" || !examinerFilter
|
||||||
|
? true
|
||||||
|
: colle.examiner?.name === examinerFilter;
|
||||||
|
return subjectMatch && examinerMatch;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (sorted === "asc") {
|
||||||
|
return a.date.localeCompare(b.date);
|
||||||
|
} else {
|
||||||
|
return b.date.localeCompare(a.date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 pb-20 md:pb-0">
|
<div className="space-y-6 pb-20 md:pb-0">
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs
|
||||||
|
defaultValue="all"
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="max-w-md w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
|
||||||
|
<TabsTrigger value="you" className={tabsStyle}>
|
||||||
|
<UserIcon className="h-4 w-4" />
|
||||||
|
Vous
|
||||||
|
</TabsTrigger>
|
||||||
|
{/* <TabsTrigger value="favorites" className={tabsStyle}>
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
Favoris
|
||||||
|
</TabsTrigger> */}
|
||||||
|
<TabsTrigger value="class" className={tabsStyle}>
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
Classe
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
{/* Week Navigation */}
|
{/* Week Navigation */}
|
||||||
<div className="mb-4">
|
<div className="mb-0">
|
||||||
<div className="flex flex-row items-center justify-between gap-4">
|
<div className="flex flex-row items-center justify-between gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
|
||||||
|
<ChevronLeft className="h-10 w-10" />
|
||||||
|
</Button>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<DatePickerWithRange
|
<DatePickerWithRange
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
setStartDate={setStartDate}
|
setStartDate={setStartDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<Button variant="outline" size="sm" onClick={handleNextWeek}>
|
||||||
<Button variant="outline" size="sm" onClick={handlePreviousWeek}>
|
<ChevronRight className="h-10 w-10" />
|
||||||
<ChevronLeft className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleNextWeek}>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
{/* Filter component */}
|
||||||
defaultValue="all"
|
<div className="flex gap-1 pb-0 pt-2">
|
||||||
value={activeTab}
|
<Select value={subjectFilter} onValueChange={setSubject}>
|
||||||
onValueChange={setActiveTab}
|
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
||||||
className="hidden md:block"
|
<SelectValue placeholder="Matière" />
|
||||||
>
|
</SelectTrigger>
|
||||||
<div className="mb-4">
|
<SelectContent>
|
||||||
<TabsList className="w-full">
|
<SelectItem value="all">Toutes</SelectItem>
|
||||||
<TabsTrigger
|
{subjects.map((subject) => (
|
||||||
value="you"
|
<SelectItem key={subject} value={subject}>
|
||||||
className="flex-1 flex items-center justify-center gap-1"
|
{subject}
|
||||||
>
|
</SelectItem>
|
||||||
<User className="h-4 w-4" />
|
))}
|
||||||
Vos colles ({studentColles.length})
|
</SelectContent>
|
||||||
</TabsTrigger>
|
</Select>
|
||||||
<TabsTrigger
|
|
||||||
value="favorites"
|
|
||||||
className="flex-1 flex items-center justify-center gap-1"
|
|
||||||
>
|
|
||||||
<Star className="h-4 w-4" />
|
|
||||||
Vos favoris ({0 /* TODO: stars.length */})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="class"
|
|
||||||
className="flex-1 flex items-center justify-center gap-1"
|
|
||||||
>
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
Votre classe ({classColles.length})
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{/* TODO: Filter component */}
|
<Select value={examinerFilter} onValueChange={setExaminer}>
|
||||||
<Collapsible open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
<SelectTrigger className="rounded-full data-[placeholder]:text-primary">
|
||||||
<div className="flex justify-between">
|
<SelectValue placeholder="Colleur" />
|
||||||
<div className="flex gap-2">
|
</SelectTrigger>
|
||||||
{activeTab === "all" && (
|
<SelectContent>
|
||||||
<>
|
<SelectItem value="all">Tous</SelectItem>
|
||||||
<Button
|
{examiners.map((examiner) => (
|
||||||
variant="outline"
|
<SelectItem key={examiner} value={examiner}>
|
||||||
size="sm"
|
{examiner}
|
||||||
// TODO: Implement sorting
|
</SelectItem>
|
||||||
onClick={() => {}}
|
))}
|
||||||
>
|
</SelectContent>
|
||||||
<ArrowUpDown className="h-4 w-4" />
|
</Select>
|
||||||
</Button>
|
|
||||||
<div className="flex mb-4">
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
{isFilterOpen ? (
|
|
||||||
<>
|
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
Fermer
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
|
||||||
Recherche
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* TODO: DEBUG */}
|
|
||||||
{/* {activeTab === "all" &&
|
|
||||||
(getWeekStart().getTime() != startDate.getTime() ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={resetWeek}>
|
|
||||||
<CalendarArrowUp className="h-4 w-4 mr-2" />
|
|
||||||
Cette semaine
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button variant="outline" size="sm" onClick={goPastYear}>
|
|
||||||
<CalendarArrowDown className="h-4 w-4 mr-2" />
|
|
||||||
Colles des spés
|
|
||||||
</Button>
|
|
||||||
))} */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CollapsibleContent className="space-y-4">
|
<Button
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
variant="outline"
|
||||||
<div className="space-y-2">
|
size="sm"
|
||||||
<Label htmlFor="subject-filter">Filtrer par matière</Label>
|
className="rounded-full dark:bg-input/30 text-primary font-normal"
|
||||||
<Select value={subjectFilter} onValueChange={setSubjectFilter}>
|
onClick={toggleSort}
|
||||||
<SelectTrigger id="subject-filter">
|
>
|
||||||
<SelectValue placeholder="Toutes les matières" />
|
{sorted == "asc" ? (
|
||||||
</SelectTrigger>
|
<SortAsc className="h-5 w-5" />
|
||||||
<SelectContent>
|
) : (
|
||||||
<SelectItem value="all">Toutes les matières</SelectItem>
|
<SortDesc className="h-5 w-5" />
|
||||||
{/* TODO: */}
|
)}
|
||||||
{/* {uniqueTopics.map((subject) => (
|
Trier
|
||||||
<SelectItem key={subject} value={subject}>
|
</Button>
|
||||||
{subject}
|
|
||||||
</SelectItem>
|
|
||||||
))} */}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="examiner-filter">Filtrer par colleur</Label>
|
|
||||||
<Select value={examinerFilter} onValueChange={setExaminerFilter}>
|
|
||||||
<SelectTrigger id="examiner-filter">
|
|
||||||
<SelectValue placeholder="Tous les colleurs" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les colleurs</SelectItem>
|
|
||||||
{/* TODO: */}
|
|
||||||
{/* {uniqueExaminers.map((examiner) => (
|
|
||||||
<SelectItem key={examiner} value={examiner}>
|
|
||||||
{examiner}
|
|
||||||
</SelectItem>
|
|
||||||
))} */}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className="space-y-2">
|
|
||||||
<Label htmlFor="student-filter">Filtrer par élève</Label>
|
|
||||||
<Select value={studentFilter} onValueChange={setStudentFilter}>
|
|
||||||
<SelectTrigger id="student-filter">
|
|
||||||
<SelectValue placeholder="Tous les élèves" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les élèves</SelectItem>
|
|
||||||
{uniqueStudents.map((student) => (
|
|
||||||
<SelectItem key={student} value={student}>
|
|
||||||
{student}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
{/* //TODO: onClick={resetFilters} */}
|
|
||||||
Réinitialiser les filtres
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
<div>
|
|
||||||
{/* Your Colles Tab */}
|
|
||||||
{activeTab === "you" && (
|
|
||||||
<TabContent
|
|
||||||
tabTitle="Vos colles"
|
|
||||||
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
|
|
||||||
isLoading={isLoading}
|
|
||||||
colles={studentColles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Favorites Tab */}
|
|
||||||
{activeTab === "favorites" && (
|
|
||||||
<TabContent
|
|
||||||
tabTitle="Vos favoris"
|
|
||||||
emptyCollesText="Vous n'avez pas encore de colle favorite, cliquez sur l'étoile pour ajouter une colle à vos favoris."
|
|
||||||
isLoading={isLoading}
|
|
||||||
colles={favoriteColles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Your Colles Tab */}
|
||||||
|
{activeTab === "you" && (
|
||||||
|
<TabContent
|
||||||
|
tabTitle="Vos colles"
|
||||||
|
emptyCollesText="Vous n'avez pas encore de colle cette semaine."
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSorted={sorted === "desc"}
|
||||||
|
colles={applyFilters(studentColles)}
|
||||||
|
preferences={user.preferences}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Favorites Tab
|
||||||
|
{activeTab === "favorites" && (
|
||||||
|
<TabContent
|
||||||
|
tabTitle="Vos favoris"
|
||||||
|
emptyCollesText="Vous n'avez pas encore de colle favorite, cliquez sur l'étoile pour ajouter une colle à vos favoris."
|
||||||
|
isLoading={isLoading}
|
||||||
|
colles={favoriteColles}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
|
||||||
{/* Class Colles Tab */}
|
{/* Class Colles Tab */}
|
||||||
{activeTab === "class" && (
|
{activeTab === "class" && (
|
||||||
<TabContent
|
<TabContent
|
||||||
tabTitle="Les colles de la classe"
|
tabTitle="Les colles de la classe"
|
||||||
emptyCollesText="Aucune colle trouvée."
|
emptyCollesText="Aucune colle trouvée."
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
colles={classColles}
|
isSorted={sorted === "desc"}
|
||||||
|
colles={applyFilters(classColles)}
|
||||||
|
preferences={user.preferences}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Navigation for Mobile */}
|
{/* Bottom Navigation for Mobile */}
|
||||||
<BottomNavigation
|
<BottomNavigation activeId="colles" />
|
||||||
activeTab={activeTab}
|
|
||||||
onTabChange={setActiveTab}
|
|
||||||
// TODO: Implement favorite count
|
|
||||||
favoriteCount={0}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import type { Colle } from "~/lib/api";
|
import type { Colle, UserPreferences } from "~/lib/api";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import ColleCard from "~/components/home/colle-card";
|
import ColleCard from "~/components/home/colle-card";
|
||||||
import ColleCardSkeleton from "~/components/home/skeleton-card";
|
import ColleCardSkeleton from "~/components/home/skeleton-card";
|
||||||
|
|
@ -9,6 +9,8 @@ type TabContentProps = {
|
||||||
emptyCollesText: string;
|
emptyCollesText: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
colles: Colle[];
|
colles: Colle[];
|
||||||
|
isSorted?: boolean;
|
||||||
|
preferences: UserPreferences;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WEEK_DAYS = [
|
const WEEK_DAYS = [
|
||||||
|
|
@ -26,6 +28,8 @@ export default function TabContent({
|
||||||
emptyCollesText,
|
emptyCollesText,
|
||||||
isLoading,
|
isLoading,
|
||||||
colles,
|
colles,
|
||||||
|
isSorted,
|
||||||
|
preferences,
|
||||||
}: TabContentProps) {
|
}: TabContentProps) {
|
||||||
const collesByDay: Record<string, Colle[]> = {};
|
const collesByDay: Record<string, Colle[]> = {};
|
||||||
colles.forEach((colle) => {
|
colles.forEach((colle) => {
|
||||||
|
|
@ -40,6 +44,10 @@ export default function TabContent({
|
||||||
const days = WEEK_DAYS.filter(
|
const days = WEEK_DAYS.filter(
|
||||||
(day) => collesByDay[day] && collesByDay[day].length > 0
|
(day) => collesByDay[day] && collesByDay[day].length > 0
|
||||||
);
|
);
|
||||||
|
// If not sorted, reverse the order of days
|
||||||
|
if (!isSorted) {
|
||||||
|
days.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -86,6 +94,7 @@ export default function TabContent({
|
||||||
colle={colle}
|
colle={colle}
|
||||||
onToggleFavorite={() => {}}
|
onToggleFavorite={() => {}}
|
||||||
isFavorite={false}
|
isFavorite={false}
|
||||||
|
preferences={preferences}
|
||||||
// TODO: Implement favorite toggle
|
// TODO: Implement favorite toggle
|
||||||
// onToggleFavorite={handleToggleFavorite}
|
// onToggleFavorite={handleToggleFavorite}
|
||||||
// isFavorite={isFavorite(colle)}
|
// isFavorite={isFavorite(colle)}
|
||||||
|
|
|
||||||
141
app/components/settings/emoji-input.tsx
Normal file
141
app/components/settings/emoji-input.tsx
Normal file
|
|
@ -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<HTMLInputElement>) {
|
||||||
|
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<HTMLInputElement>) {
|
||||||
|
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<HTMLInputElement>) {
|
||||||
|
// 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 (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInput}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
app/components/settings/index.tsx
Normal file
74
app/components/settings/index.tsx
Normal file
|
|
@ -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: <UserIcon className="h-4 w-4" />,
|
||||||
|
content: <Profile />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "preferences",
|
||||||
|
label: "Préférences",
|
||||||
|
icon: <Sliders className="h-4 w-4" />,
|
||||||
|
content: <Preferences user={user} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "notifications",
|
||||||
|
label: "Notifications",
|
||||||
|
icon: <Bell className="h-4 w-4" />,
|
||||||
|
content: <div>WIP</div>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// user / notifications / preferences tabs
|
||||||
|
const [activeTab, setActiveTab] = useState(tabs[0].value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 pb-20 md:pb-0">
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs
|
||||||
|
defaultValue={tabs[0].value}
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="max-w-md w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full p-0 bg-background justify-start border-b rounded-none">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
className={tabsStyle}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
{tab.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="pt-2">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.value}
|
||||||
|
className={`${
|
||||||
|
activeTab === tab.value ? "block" : "hidden"
|
||||||
|
} transition-all duration-300`}
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BottomNavigation activeId="settings" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
app/components/settings/preferences.tsx
Normal file
187
app/components/settings/preferences.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Subject Customization Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Palette className="h-5 w-5" />
|
||||||
|
Personnalisation
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Choisissez les couleurs et emojis des matières.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{subjects
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((subjectName) => ({
|
||||||
|
subjectName,
|
||||||
|
subjectEmoji: getSubjectEmoji(subjectName, preferences),
|
||||||
|
subjectColor: getSubjectColor(subjectName, preferences),
|
||||||
|
}))
|
||||||
|
.map(({ subjectName, subjectEmoji, subjectColor }) => (
|
||||||
|
<div key={subjectName} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-base font-medium truncate">
|
||||||
|
{subjectName}
|
||||||
|
</Label>
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1 truncate",
|
||||||
|
getColorClass(subjectColor)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subjectEmoji} {subjectName}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{/* Color selector */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Couleur</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{colors.map((colorName) => (
|
||||||
|
<button
|
||||||
|
key={colorName}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 rounded-full border",
|
||||||
|
subjectColor === colorName
|
||||||
|
? "ring-2 ring-offset-2 ring-primary"
|
||||||
|
: "ring-0",
|
||||||
|
getColorClass(colorName)
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
setPref(subjectName, "color", colorName)
|
||||||
|
}
|
||||||
|
title={colorName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Emoji selector */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Emoji</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<EmojiInput
|
||||||
|
type="text"
|
||||||
|
defaultValue={subjectEmoji}
|
||||||
|
onChange={(emoji) =>
|
||||||
|
setPref(subjectName, "emoji", emoji)
|
||||||
|
}
|
||||||
|
className="w-full bg-transparent border border-gray-300 rounded-md px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => savePreferences()}
|
||||||
|
disabled={!unsavedChanges}
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Enregistrer vos préférences
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full text-white"
|
||||||
|
onClick={resetPreferences}
|
||||||
|
disabled={!unsavedChanges}
|
||||||
|
>
|
||||||
|
<Undo className="h-4 w-4 mr-2" />
|
||||||
|
Annuler les modifications en cours
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
app/components/settings/profile.tsx
Normal file
14
app/components/settings/profile.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Trash } from "lucide-react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { clearCache } from "~/lib/utils";
|
||||||
|
|
||||||
|
export default function Profile() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button variant="destructive" className="w-full text-white" onClick={clearCache}>
|
||||||
|
<Trash className="mr-2 h-4 w-4" />
|
||||||
|
Vider le cache
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,11 @@ const isLocalStorageAvailable = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HEADER_COLORS = {
|
||||||
|
light: "#ffffff",
|
||||||
|
dark: "#0d0d0d",
|
||||||
|
};
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
defaultTheme = "system",
|
defaultTheme = "system",
|
||||||
|
|
@ -53,10 +58,19 @@ export function ThemeProvider({
|
||||||
? "dark"
|
? "dark"
|
||||||
: "light";
|
: "light";
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("meta[name=theme-color]")
|
||||||
|
?.setAttribute(
|
||||||
|
"content",
|
||||||
|
HEADER_COLORS[systemTheme] || HEADER_COLORS.light
|
||||||
|
);
|
||||||
root.classList.add(systemTheme);
|
root.classList.add(systemTheme);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("meta[name=theme-color]")
|
||||||
|
?.setAttribute("content", HEADER_COLORS[theme] || HEADER_COLORS.light);
|
||||||
root.classList.add(theme);
|
root.classList.add(theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
|
|
@ -86,14 +100,18 @@ export const useTheme = () => {
|
||||||
|
|
||||||
const isWindowAvailable = () => {
|
const isWindowAvailable = () => {
|
||||||
return typeof window !== "undefined" && window !== null;
|
return typeof window !== "undefined" && window !== null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useDisplayedTheme = () => {
|
export const useDisplayedTheme = () => {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
return {
|
return {
|
||||||
theme: isWindowAvailable() && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
theme:
|
||||||
|
isWindowAvailable() &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light",
|
||||||
setTheme,
|
setTheme,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
app/components/ui/navigation-menu.tsx
Normal file
168
app/components/ui/navigation-menu.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
);
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({
|
||||||
className,
|
className,
|
||||||
|
|
@ -13,7 +13,7 @@ function Tabs({
|
||||||
className={cn("flex flex-col gap-2", className)}
|
className={cn("flex flex-col gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({
|
||||||
|
|
@ -29,7 +29,7 @@ function TabsList({
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({
|
||||||
|
|
@ -45,7 +45,7 @@ function TabsTrigger({
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({
|
||||||
|
|
@ -58,7 +58,9 @@ function TabsContent({
|
||||||
className={cn("flex-1 outline-none", className)}
|
className={cn("flex-1 outline-none", className)}
|
||||||
{...props}
|
{...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!";
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,12 @@ const defaultUser = {
|
||||||
fullName: "",
|
fullName: "",
|
||||||
email: "",
|
email: "",
|
||||||
className: "",
|
className: "",
|
||||||
|
preferences: [] as UserPreferences,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = typeof defaultUser;
|
export type User = typeof defaultUser;
|
||||||
|
|
||||||
export const AUTH_ERROR = "Unauthorized access"
|
export const AUTH_ERROR = "Unauthorized access";
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const { data, ...props } = useQuery({
|
const { data, ...props } = useQuery({
|
||||||
|
|
@ -132,6 +133,24 @@ export const logout = async () => {
|
||||||
return makePostRequest("/auth/logout", {}, "Échec de la déconnexion", "POST");
|
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 ===
|
* === COLLES API ===
|
||||||
*/
|
*/
|
||||||
|
|
@ -187,9 +206,7 @@ export const useColles = (startDate: DateTime) => {
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
staleTime: Duration.fromObject({
|
staleTime: 0,
|
||||||
minutes: 5, // 5 minutes
|
|
||||||
}).toMillis(),
|
|
||||||
gcTime: Duration.fromObject({
|
gcTime: Duration.fromObject({
|
||||||
days: 3, // 3 days
|
days: 3, // 3 days
|
||||||
}).toMillis(),
|
}).toMillis(),
|
||||||
|
|
@ -254,3 +271,26 @@ export const useColle = (id: number) => {
|
||||||
...props,
|
...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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { QueryClient } from "@tanstack/react-query";
|
||||||
import { persistQueryClient } from "@tanstack/react-query-persist-client";
|
import { persistQueryClient } from "@tanstack/react-query-persist-client";
|
||||||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
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
|
// Check if we're in a browser environment with IndexedDB support
|
||||||
const isIndexedDBAvailable = () => {
|
const isIndexedDBAvailable = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { del } from "idb-keyval";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import type { NavigateFunction } from "react-router";
|
import type { NavigateFunction } from "react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { CACHE_KEY } from "./client";
|
||||||
|
import type { User, UserPreferences } from "./api";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
|
@ -60,10 +63,14 @@ export const formatDate = (date: string) => {
|
||||||
|
|
||||||
export const formatTime = (date: string) => {
|
export const formatTime = (date: string) => {
|
||||||
const dt = DateTime.fromISO(date).setLocale("fr");
|
const dt = DateTime.fromISO(date).setLocale("fr");
|
||||||
return dt.toLocaleString({
|
return (
|
||||||
hour: "2-digit",
|
dt
|
||||||
minute: "2-digit",
|
.toLocaleString({
|
||||||
});
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
?.replace(":", "h") || "N/A"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatGrade = (grade?: number) => {
|
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
|
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);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
{/* PWA Manifest */}
|
{/* PWA Manifest */}
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<meta name="title" content="Khollisé ⚔️" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="BJColle but faster, better and prettier ⚡️"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="manifest.webmanifest" />
|
<link rel="manifest" href="manifest.webmanifest" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,5 @@ export default [
|
||||||
route("/verify", "routes/verify.tsx"),
|
route("/verify", "routes/verify.tsx"),
|
||||||
route("/register", "routes/register.tsx"),
|
route("/register", "routes/register.tsx"),
|
||||||
route("/colles/:colleId", "routes/colles.tsx"),
|
route("/colles/:colleId", "routes/colles.tsx"),
|
||||||
|
route("/settings", "routes/settings.tsx"),
|
||||||
] satisfies RouteConfig;
|
] satisfies RouteConfig;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout page={<HomePage />}>
|
<MainLayout page={<HomePage user={user} />}>
|
||||||
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||||
Khollisé - {user.className} ⚔️
|
Khollisé - {user.className} ⚔️
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
||||||
31
app/routes/settings.tsx
Normal file
31
app/routes/settings.tsx
Normal file
|
|
@ -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 <Loader />;
|
||||||
|
}
|
||||||
|
if (error?.message === AUTH_ERROR) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <Error message={error.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout page={<SettingsPage user={user} />}>
|
||||||
|
<h1 className="text-2xl font-bold" onClick={forceReload}>
|
||||||
|
Khollisé - {user.className} ⚔️
|
||||||
|
</h1>
|
||||||
|
<UserDropdown user={user} />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
|
|
||||||
92
pnpm-lock.yaml
generated
92
pnpm-lock.yaml
generated
|
|
@ -26,6 +26,9 @@ importers:
|
||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.1.6
|
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)
|
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-navigation-menu':
|
||||||
|
specifier: ^1.2.14
|
||||||
|
version: 1.2.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-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.1.14
|
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)
|
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)
|
||||||
|
|
@ -1639,6 +1642,9 @@ packages:
|
||||||
'@radix-ui/primitive@1.1.2':
|
'@radix-ui/primitive@1.1.2':
|
||||||
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
||||||
|
|
||||||
|
'@radix-ui/primitive@1.1.3':
|
||||||
|
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.7':
|
'@radix-ui/react-arrow@1.1.7':
|
||||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1744,6 +1750,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@1.1.11':
|
||||||
|
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
||||||
|
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-dropdown-menu@2.1.15':
|
'@radix-ui/react-dropdown-menu@2.1.15':
|
||||||
resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==}
|
resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1814,6 +1833,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.14':
|
||||||
|
resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
|
||||||
|
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':
|
'@radix-ui/react-popover@1.1.14':
|
||||||
resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==}
|
resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1866,6 +1898,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-presence@1.1.5':
|
||||||
|
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
|
||||||
|
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-primitive@2.1.3':
|
'@radix-ui/react-primitive@2.1.3':
|
||||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -8119,6 +8164,8 @@ snapshots:
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.2': {}
|
'@radix-ui/primitive@1.1.2': {}
|
||||||
|
|
||||||
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|
||||||
'@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)':
|
'@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)':
|
||||||
dependencies:
|
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)
|
'@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)
|
||||||
|
|
@ -8222,6 +8269,19 @@ snapshots:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@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.3
|
||||||
|
'@radix-ui/react-compose-refs': 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-escape-keydown': 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-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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
|
|
@ -8296,6 +8356,28 @@ snapshots:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.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.3
|
||||||
|
'@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.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-id': 1.1.1(@types/react@19.1.8)(react@19.1.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.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-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)
|
||||||
|
'@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)
|
||||||
|
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-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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
|
|
@ -8357,6 +8439,16 @@ snapshots:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-presence@1.1.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/react-compose-refs': 1.1.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-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-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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.1)
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.1)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue