diff --git a/public/index.html b/public/index.html index f49c833..b26d62c 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - + @@ -24,9 +24,18 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - - - + + + Edbridge Scholars @@ -39,8 +48,8 @@ You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the tag. - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. + To begin development, run `pnpm dev`. + To create a production bundle, use `pnpm build`. --> diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 2167cb6..819b10f 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -9,6 +9,7 @@ import { SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, + useSidebar, } from "../components/ui/sidebar"; import { @@ -32,14 +33,15 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; export function AppSidebar() { const [open, setOpen] = useState(true); const user = useAuthStore((s) => s.user); + const { state } = useSidebar(); return ( - + {/* HEADER */} -
+
-
+
-
- - Edbridge Scholars - - - Student - -
+ {state !== "collapsed" && ( +
+ + Edbridge Scholars + + + Student + +
+ )}
- + {state !== "collapsed" && }
@@ -197,18 +201,22 @@ export function AppSidebar() { {/* FOOTER */} -
+
{user?.name.slice(0, 1)} -
- {user?.name} - {user?.email} -
- + {state !== "collapsed" && ( +
+ {user?.name} + {user?.email} +
+ )} + {state !== "collapsed" && ( + + )}
diff --git a/src/components/GeoGebraGraph.tsx b/src/components/GeoGebraGraph.tsx index a89fc61..9318a07 100644 --- a/src/components/GeoGebraGraph.tsx +++ b/src/components/GeoGebraGraph.tsx @@ -86,7 +86,7 @@ export function Graph({ }, []); return ( -
+
); diff --git a/src/components/LessonModal.tsx b/src/components/LessonModal.tsx index 24955f7..3b66608 100644 --- a/src/components/LessonModal.tsx +++ b/src/components/LessonModal.tsx @@ -4,7 +4,6 @@ import { DialogContent, DialogHeader, DialogTitle, - DialogDescription, } from "../components/ui/dialog"; import { api } from "../utils/api"; import { useAuthStore } from "../stores/authStore"; diff --git a/src/components/SearchOverlay.tsx b/src/components/SearchOverlay.tsx index cad6429..8cf5089 100644 --- a/src/components/SearchOverlay.tsx +++ b/src/components/SearchOverlay.tsx @@ -100,14 +100,16 @@ export const SearchOverlay = ({ }: Props) => { const navigate = useNavigate(); const searchItems = useMemo(() => { - const sheetItems = sheets.map((sheet) => ({ - type: "sheet", - id: sheet.id, - title: sheet.title, - description: sheet.description, - route: `/student/practice/${sheet.id}`, - group: formatGroupTitle(sheet.user_status), // 👈 reuse your grouping - })); + const sheetItems = sheets.map( + (sheet): SearchItem => ({ + type: "sheet", + id: sheet.id, + title: sheet.title, + description: sheet.description, + route: `/student/practice/${sheet.id}`, + group: formatGroupTitle(sheet.user_status), // 👈 reuse your grouping + }), + ); return [...navigationItems, ...sheetItems]; }, [sheets]); diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index fd3a406..127f9ab 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +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" +import { cn } from "../../lib/utils"; const badgeVariants = cva( "inline-flex items-center justify-center rounded-full 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", @@ -22,8 +22,8 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); function Badge({ className, @@ -32,7 +32,7 @@ function Badge({ ...props }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + const Comp = asChild ? Slot : "span"; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 46b3a48..b4d6e96 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -2,7 +2,7 @@ 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"; +import { cn } from "../../lib/utils"; const buttonVariants = cva( "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", diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 681ad98..1be309a 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) { data-slot="card" className={cn( "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className + className, )} {...props} /> - ) + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-header" className={cn( "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className + className, )} {...props} /> - ) + ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { className={cn("leading-none font-semibold", className)} {...props} /> - ) + ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { @@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { @@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-action" className={cn( "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className + className, )} {...props} /> - ) + ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { @@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { className={cn("px-6", className)} {...props} /> - ) + ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} /> - ) + ); } export { @@ -89,4 +89,4 @@ export { CardAction, CardDescription, CardContent, -} +}; diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index 71cff4c..b7ee2ae 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -1,43 +1,43 @@ -import * as React from "react" +import * as React from "react"; import useEmblaCarousel, { type UseEmblaCarouselType, -} from "embla-carousel-react" -import { ArrowLeft, ArrowRight } from "lucide-react" +} from "embla-carousel-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from "../../lib/utils"; +import { Button } from "./button"; -type CarouselApi = UseEmblaCarouselType[1] -type UseCarouselParameters = Parameters -type CarouselOptions = UseCarouselParameters[0] -type CarouselPlugin = UseCarouselParameters[1] +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { - opts?: CarouselOptions - plugins?: CarouselPlugin - orientation?: "horizontal" | "vertical" - setApi?: (api: CarouselApi) => void -} + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; type CarouselContextProps = { - carouselRef: ReturnType[0] - api: ReturnType[1] - scrollPrev: () => void - scrollNext: () => void - canScrollPrev: boolean - canScrollNext: boolean -} & CarouselProps + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; -const CarouselContext = React.createContext(null) +const CarouselContext = React.createContext(null); function useCarousel() { - const context = React.useContext(CarouselContext) + const context = React.useContext(CarouselContext); if (!context) { - throw new Error("useCarousel must be used within a ") + throw new Error("useCarousel must be used within a "); } - return context + return context; } function Carousel({ @@ -54,53 +54,53 @@ function Carousel({ ...opts, axis: orientation === "horizontal" ? "x" : "y", }, - plugins - ) - const [canScrollPrev, setCanScrollPrev] = React.useState(false) - const [canScrollNext, setCanScrollNext] = React.useState(false) + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) return - setCanScrollPrev(api.canScrollPrev()) - setCanScrollNext(api.canScrollNext()) - }, []) + if (!api) return; + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); const scrollPrev = React.useCallback(() => { - api?.scrollPrev() - }, [api]) + api?.scrollPrev(); + }, [api]); const scrollNext = React.useCallback(() => { - api?.scrollNext() - }, [api]) + api?.scrollNext(); + }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent) => { if (event.key === "ArrowLeft") { - event.preventDefault() - scrollPrev() + event.preventDefault(); + scrollPrev(); } else if (event.key === "ArrowRight") { - event.preventDefault() - scrollNext() + event.preventDefault(); + scrollNext(); } }, - [scrollPrev, scrollNext] - ) + [scrollPrev, scrollNext], + ); React.useEffect(() => { - if (!api || !setApi) return - setApi(api) - }, [api, setApi]) + if (!api || !setApi) return; + setApi(api); + }, [api, setApi]); React.useEffect(() => { - if (!api) return - onSelect(api) - api.on("reInit", onSelect) - api.on("select", onSelect) + if (!api) return; + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); return () => { - api?.off("select", onSelect) - } - }, [api, onSelect]) + api?.off("select", onSelect); + }; + }, [api, onSelect]); return ( - ) + ); } function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { - const { carouselRef, orientation } = useCarousel() + const { carouselRef, orientation } = useCarousel(); return (
) { className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", - className + className, )} {...props} />
- ) + ); } function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { - const { orientation } = useCarousel() + const { orientation } = useCarousel(); return (
) { className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", - className + className, )} {...props} /> - ) + ); } function CarouselPrevious({ @@ -175,7 +175,7 @@ function CarouselPrevious({ size = "icon", ...props }: React.ComponentProps) { - const { orientation, scrollPrev, canScrollPrev } = useCarousel() + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); return ( - ) + ); } function CarouselNext({ @@ -205,7 +205,7 @@ function CarouselNext({ size = "icon", ...props }: React.ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel() + const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( - ) + ); } export { @@ -236,4 +236,4 @@ export { CarouselItem, CarouselPrevious, CarouselNext, -} +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index daf6bf4..0f8f709 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,32 +1,32 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from "../../lib/utils"; +import { Button } from "./button"; function Dialog({ ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ @@ -38,11 +38,11 @@ function DialogOverlay({ data-slot="dialog-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", - className + className, )} {...props} /> - ) + ); } function DialogContent({ @@ -51,7 +51,7 @@ function DialogContent({ showCloseButton = true, ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { return ( @@ -60,7 +60,7 @@ function DialogContent({ data-slot="dialog-content" className={cn( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", - className + className, )} {...props} > @@ -76,7 +76,7 @@ function DialogContent({ )} - ) + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -86,7 +86,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> - ) + ); } function DialogFooter({ @@ -95,14 +95,14 @@ function DialogFooter({ children, ...props }: React.ComponentProps<"div"> & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { return (
@@ -113,7 +113,7 @@ function DialogFooter({ )}
- ) + ); } function DialogTitle({ @@ -126,7 +126,7 @@ function DialogTitle({ className={cn("text-lg leading-none font-semibold", className)} {...props} /> - ) + ); } function DialogDescription({ @@ -139,7 +139,7 @@ function DialogDescription({ className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } export { @@ -153,4 +153,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index eaed9ba..eef120f 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,13 +1,13 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function DropdownMenu({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuPortal({ @@ -15,7 +15,7 @@ function DropdownMenuPortal({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuTrigger({ @@ -26,7 +26,7 @@ function DropdownMenuTrigger({ data-slot="dropdown-menu-trigger" {...props} /> - ) + ); } function DropdownMenuContent({ @@ -41,12 +41,12 @@ function DropdownMenuContent({ sideOffset={sideOffset} className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", - className + className, )} {...props} /> - ) + ); } function DropdownMenuGroup({ @@ -54,7 +54,7 @@ function DropdownMenuGroup({ }: React.ComponentProps) { return ( - ) + ); } function DropdownMenuItem({ @@ -63,8 +63,8 @@ function DropdownMenuItem({ variant = "default", ...props }: React.ComponentProps & { - inset?: boolean - variant?: "default" | "destructive" + inset?: boolean; + variant?: "default" | "destructive"; }) { return ( - ) + ); } function DropdownMenuCheckboxItem({ @@ -91,7 +91,7 @@ function DropdownMenuCheckboxItem({ data-slot="dropdown-menu-checkbox-item" className={cn( "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - className + className, )} checked={checked} {...props} @@ -103,7 +103,7 @@ function DropdownMenuCheckboxItem({ {children} - ) + ); } function DropdownMenuRadioGroup({ @@ -114,7 +114,7 @@ function DropdownMenuRadioGroup({ data-slot="dropdown-menu-radio-group" {...props} /> - ) + ); } function DropdownMenuRadioItem({ @@ -127,7 +127,7 @@ function DropdownMenuRadioItem({ data-slot="dropdown-menu-radio-item" className={cn( "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - className + className, )} {...props} > @@ -138,7 +138,7 @@ function DropdownMenuRadioItem({ {children} - ) + ); } function DropdownMenuLabel({ @@ -146,7 +146,7 @@ function DropdownMenuLabel({ inset, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( - ) + ); } function DropdownMenuSeparator({ @@ -171,7 +171,7 @@ function DropdownMenuSeparator({ className={cn("bg-border -mx-1 my-1 h-px", className)} {...props} /> - ) + ); } function DropdownMenuShortcut({ @@ -183,17 +183,17 @@ function DropdownMenuShortcut({ data-slot="dropdown-menu-shortcut" className={cn( "text-muted-foreground ml-auto text-xs tracking-widest", - className + className, )} {...props} /> - ) + ); } function DropdownMenuSub({ ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuSubTrigger({ @@ -202,7 +202,7 @@ function DropdownMenuSubTrigger({ children, ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { return ( {children} - ) + ); } function DropdownMenuSubContent({ @@ -229,11 +229,11 @@ function DropdownMenuSubContent({ data-slot="dropdown-menu-sub-content" className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", - className + className, )} {...props} /> - ) + ); } export { @@ -252,4 +252,4 @@ export { DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, -} +}; diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx index db0dc12..a45ed88 100644 --- a/src/components/ui/field.tsx +++ b/src/components/ui/field.tsx @@ -1,9 +1,9 @@ -import { useMemo } from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" +import { cn } from "../../lib/utils"; +import { Label } from "./label"; +import { Separator } from "./separator"; function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( @@ -12,11 +12,11 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { className={cn( "flex flex-col gap-6", "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", - className + className, )} {...props} /> - ) + ); } function FieldLegend({ @@ -32,11 +32,11 @@ function FieldLegend({ "mb-3 font-medium", "data-[variant=legend]:text-base", "data-[variant=label]:text-sm", - className + className, )} {...props} /> - ) + ); } function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -45,11 +45,11 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-group" className={cn( "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", - className + className, )} {...props} /> - ) + ); } const fieldVariants = cva( @@ -73,8 +73,8 @@ const fieldVariants = cva( defaultVariants: { orientation: "vertical", }, - } -) + }, +); function Field({ className, @@ -89,7 +89,7 @@ function Field({ className={cn(fieldVariants({ orientation }), className)} {...props} /> - ) + ); } function FieldContent({ className, ...props }: React.ComponentProps<"div">) { @@ -98,11 +98,11 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-content" className={cn( "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", - className + className, )} {...props} /> - ) + ); } function FieldLabel({ @@ -116,11 +116,11 @@ function FieldLabel({ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", - className + className, )} {...props} /> - ) + ); } function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -129,11 +129,11 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-label" className={cn( "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", - className + className, )} {...props} /> - ) + ); } function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { @@ -144,11 +144,11 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className + className, )} {...props} /> - ) + ); } function FieldSeparator({ @@ -156,7 +156,7 @@ function FieldSeparator({ className, ...props }: React.ComponentProps<"div"> & { - children?: React.ReactNode + children?: React.ReactNode; }) { return (
@@ -178,7 +178,7 @@ function FieldSeparator({ )}
- ) + ); } function FieldError({ @@ -187,37 +187,37 @@ function FieldError({ errors, ...props }: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined> + errors?: Array<{ message?: string } | undefined>; }) { const content = useMemo(() => { if (children) { - return children + return children; } if (!errors?.length) { - return null + return null; } const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values(), - ] + ]; if (uniqueErrors?.length == 1) { - return uniqueErrors[0]?.message + return uniqueErrors[0]?.message; } return (
    {uniqueErrors.map( (error, index) => - error?.message &&
  • {error.message}
  • + error?.message &&
  • {error.message}
  • , )}
- ) - }, [children, errors]) + ); + }, [children, errors]); if (!content) { - return null + return null; } return ( @@ -229,7 +229,7 @@ function FieldError({ > {content}
- ) + ); } export { @@ -243,4 +243,4 @@ export { FieldSet, FieldContent, FieldTitle, -} +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 8916905..1753b85 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( @@ -11,11 +11,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "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", - className + className, )} {...props} /> - ) + ); } -export { Input } +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index f752f82..a9610ff 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { Label as LabelPrimitive } from "radix-ui" +import * as React from "react"; +import { Label as LabelPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function Label({ className, @@ -12,11 +12,11 @@ function Label({ data-slot="label" className={cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", - className + className, )} {...props} /> - ) + ); } -export { Label } +export { Label }; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 4c24b2a..7695eec 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import { Separator as SeparatorPrimitive } from "radix-ui" +import * as React from "react"; +import { Separator as SeparatorPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function Separator({ className, @@ -18,11 +18,11 @@ function Separator({ orientation={orientation} className={cn( "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className + className, )} {...props} /> - ) + ); } -export { Separator } +export { Separator }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 5963090..f089706 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,31 +1,31 @@ -"use client" +"use client"; -import * as React from "react" -import { XIcon } from "lucide-react" -import { Dialog as SheetPrimitive } from "radix-ui" +import * as React from "react"; +import { XIcon } from "lucide-react"; +import { Dialog as SheetPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function Sheet({ ...props }: React.ComponentProps) { - return + return ; } function SheetTrigger({ ...props }: React.ComponentProps) { - return + return ; } function SheetClose({ ...props }: React.ComponentProps) { - return + return ; } function SheetPortal({ ...props }: React.ComponentProps) { - return + return ; } function SheetOverlay({ @@ -37,11 +37,11 @@ function SheetOverlay({ data-slot="sheet-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", - className + className, )} {...props} /> - ) + ); } function SheetContent({ @@ -51,8 +51,8 @@ function SheetContent({ showCloseButton = true, ...props }: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left" - showCloseButton?: boolean + side?: "top" | "right" | "bottom" | "left"; + showCloseButton?: boolean; }) { return ( @@ -69,7 +69,7 @@ function SheetContent({ "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", side === "bottom" && "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", - className + className, )} {...props} > @@ -82,7 +82,7 @@ function SheetContent({ )} - ) + ); } function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -92,7 +92,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex flex-col gap-1.5 p-4", className)} {...props} /> - ) + ); } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -102,7 +102,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> - ) + ); } function SheetTitle({ @@ -115,7 +115,7 @@ function SheetTitle({ className={cn("text-foreground font-semibold", className)} {...props} /> - ) + ); } function SheetDescription({ @@ -128,7 +128,7 @@ function SheetDescription({ className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } export { @@ -140,4 +140,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +}; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 6a50783..012c0fe 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -1,56 +1,56 @@ -"use client" +"use client"; -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { PanelLeftIcon } from "lucide-react" -import { Slot } from "radix-ui" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelLeftIcon } from "lucide-react"; +import { Slot } from "radix-ui"; -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" +import { useIsMobile } from "../../hooks/use-mobile"; +import { cn } from "../../lib/utils"; +import { Button } from "./button"; +import { Input } from "./input"; +import { Separator } from "./separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, -} from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" +} from "./sheet"; +import { Skeleton } from "./skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip" +} from "./tooltip"; -const SIDEBAR_COOKIE_NAME = "sidebar_state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; type SidebarContextProps = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; -const SidebarContext = React.createContext(null) +const SidebarContext = React.createContext(null); function useSidebar() { - const context = React.useContext(SidebarContext) + const context = React.useContext(SidebarContext); if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") + throw new Error("useSidebar must be used within a SidebarProvider."); } - return context + return context; } function SidebarProvider({ @@ -62,36 +62,36 @@ function SidebarProvider({ children, ...props }: React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; }) { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen) - const open = openProp ?? _open + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value + const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { - setOpenProp(openState) + setOpenProp(openState); } else { - _setOpen(openState) + _setOpen(openState); } // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, - [setOpenProp, open] - ) + [setOpenProp, open], + ); // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { @@ -100,18 +100,18 @@ function SidebarProvider({ event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { - event.preventDefault() - toggleSidebar() + event.preventDefault(); + toggleSidebar(); } - } + }; - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" + const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo( () => ({ @@ -123,8 +123,8 @@ function SidebarProvider({ setOpenMobile, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); return ( @@ -140,7 +140,7 @@ function SidebarProvider({ } className={cn( "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", - className + className, )} {...props} > @@ -148,7 +148,7 @@ function SidebarProvider({
- ) + ); } function Sidebar({ @@ -159,11 +159,18 @@ function Sidebar({ children, ...props }: React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + // Only apply translate-based hiding when using offcanvas collapsible mode. + const collapsedTranslate = + collapsible === "offcanvas" + ? side === "left" + ? "group-data-[state=collapsed]:-translate-x-[calc(var(--sidebar-width)-var(--sidebar-width-icon))]" + : "group-data-[state=collapsed]:translate-x-[calc(var(--sidebar-width)-var(--sidebar-width-icon))]" + : ""; if (collapsible === "none") { return ( @@ -171,13 +178,13 @@ function Sidebar({ data-slot="sidebar" className={cn( "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", - className + className, )} {...props} > {children}
- ) + ); } if (isMobile) { @@ -202,7 +209,7 @@ function Sidebar({
{children}
- ) + ); } return ( @@ -218,26 +225,27 @@ function Sidebar({
- ) + ); } function SidebarTrigger({ className, onClick, ...props -}: React.ComponentProps) { - const { toggleSidebar } = useSidebar() +}: React.ComponentProps & { + onClick?: (event: React.MouseEvent) => void; +}) { + const { toggleSidebar } = useSidebar(); return ( - ) + ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (
- ) + ); } function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { @@ -24,7 +24,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { className={cn("[&_tr]:border-b", className)} {...props} /> - ) + ); } function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { @@ -34,7 +34,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { className={cn("[&_tr:last-child]:border-0", className)} {...props} /> - ) + ); } function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { @@ -43,11 +43,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { data-slot="table-footer" className={cn( "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", - className + className, )} {...props} /> - ) + ); } function TableRow({ className, ...props }: React.ComponentProps<"tr">) { @@ -56,11 +56,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) { data-slot="table-row" className={cn( "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", - className + className, )} {...props} /> - ) + ); } function TableHead({ className, ...props }: React.ComponentProps<"th">) { @@ -69,11 +69,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) { data-slot="table-head" className={cn( "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCell({ className, ...props }: React.ComponentProps<"td">) { @@ -82,11 +82,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) { data-slot="table-cell" className={cn( "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> - ) + ); } function TableCaption({ @@ -99,7 +99,7 @@ function TableCaption({ className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} /> - ) + ); } export { @@ -111,4 +111,4 @@ export { TableRow, TableCell, TableCaption, -} +}; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index a3b416a..a459874 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { Tooltip as TooltipPrimitive } from "radix-ui" +import * as React from "react"; +import { Tooltip as TooltipPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils"; function TooltipProvider({ delayDuration = 0, @@ -13,19 +13,19 @@ function TooltipProvider({ delayDuration={delayDuration} {...props} /> - ) + ); } function Tooltip({ ...props }: React.ComponentProps) { - return + return ; } function TooltipTrigger({ ...props }: React.ComponentProps) { - return + return ; } function TooltipContent({ @@ -41,7 +41,7 @@ function TooltipContent({ sideOffset={sideOffset} className={cn( "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", - className + className, )} {...props} > @@ -49,7 +49,7 @@ function TooltipContent({ - ) + ); } -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/hooks/useSatTimer.ts b/src/hooks/useSatTimer.ts index 22bd4d5..86b439d 100644 --- a/src/hooks/useSatTimer.ts +++ b/src/hooks/useSatTimer.ts @@ -4,7 +4,6 @@ import { useSatExam } from "../stores/useSatExam"; export const useSatTimer = () => { const phase = useSatExam((s) => s.phase); const getRemainingTime = useSatExam((s) => s.getRemainingTime); - const startBreak = useSatExam((s) => s.startBreak); const skipBreak = useSatExam((s) => s.skipBreak); const finishExam = useSatExam((s) => s.finishExam); diff --git a/src/pages/student/Analytics.tsx b/src/pages/student/Analytics.tsx index c5b276f..abc2424 100644 --- a/src/pages/student/Analytics.tsx +++ b/src/pages/student/Analytics.tsx @@ -1,6 +1,4 @@ -import { List, SquarePen, DecimalsArrowRight, MapPin } from "lucide-react"; -import { Progress } from "../../components/ui/progress"; -import { Button } from "../../components/ui/button"; +import { DecimalsArrowRight, MapPin } from "lucide-react"; import { Card, CardHeader, @@ -9,7 +7,7 @@ import { CardFooter, } from "../../components/ui/card"; import { Field, FieldLabel } from "../../components/ui/field"; -import { CircularProgress } from "../../components/CircularProgress"; +import { Progress } from "../../components/ui/progress"; export const Analytics = () => { return ( diff --git a/src/pages/student/StudentLayout.tsx b/src/pages/student/StudentLayout.tsx index a641dca..376e1ad 100644 --- a/src/pages/student/StudentLayout.tsx +++ b/src/pages/student/StudentLayout.tsx @@ -1,6 +1,10 @@ -import { Outlet, NavLink } from "react-router-dom"; +import { Outlet, NavLink, useLocation } from "react-router-dom"; import { Home, BookOpen, Award, User, Video } from "lucide-react"; -import { SidebarProvider, SidebarTrigger } from "../../components/ui/sidebar"; +import { + SidebarProvider, + SidebarTrigger, + SidebarInset, +} from "../../components/ui/sidebar"; import { AppSidebar } from "../../components/AppSidebar"; export function StudentLayout() { @@ -12,15 +16,53 @@ export function StudentLayout() { { to: "/student/profile", icon: User, label: "Profile" }, ]; + function Breadcrumbs() { + const location = useLocation(); + const parts = location.pathname.split("/").filter(Boolean); + + // Only show breadcrumbs for student routes + if (!parts.length || parts[0] !== "student") return null; + + const crumbs = parts.map((part, idx) => { + const to = "/" + parts.slice(0, idx + 1).join("/"); + const label = + part === "student" + ? "Student" + : part.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); + + return { to, label }; + }); + + return ( + + ); + } return (
{/* Desktop Sidebar */} -
- - -
+ +
+ + + {/* Breadcrumbs (web only) */} + +
+ +
+ +
+