diff --git a/app/layout.tsx b/app/layout.tsx index 7754769..313708b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -30,7 +30,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/bun.lock b/bun.lock index 3857510..7574c1a 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "framer-motion": "^12.34.3", "lucide-react": "^0.575.0", "next": "16.1.6", + "next-themes": "^0.4.6", "openapi-fetch": "^0.17.0", "openapi-react-query": "^0.5.4", "radix-ui": "^1.4.3", @@ -1597,6 +1598,8 @@ "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], "nitropack": ["nitropack@2.13.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.2", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^0.4.4", "@vercel/nft": "^1.2.0", "archiver": "^7.0.1", "c12": "^3.3.3", "chokidar": "^5.0.0", "citty": "^0.1.6", "compatx": "^0.2.0", "confbox": "^0.2.2", "consola": "^3.4.2", "cookie-es": "^2.0.0", "croner": "^9.1.0", "crossws": "^0.3.5", "db0": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "dot-prop": "^10.1.0", "esbuild": "^0.27.2", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.8", "globby": "^16.1.0", "gzip-size": "^7.0.0", "h3": "^1.15.5", "hookable": "^5.5.3", "httpxy": "^0.1.7", "ioredis": "^5.9.1", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "listhen": "^1.9.0", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mime": "^4.1.0", "mlly": "^1.8.0", "node-fetch-native": "^1.6.7", "node-mock-http": "^1.0.4", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "pretty-bytes": "^7.1.0", "radix3": "^1.1.2", "rollup": "^4.55.1", "rollup-plugin-visualizer": "^6.0.5", "scule": "^1.3.0", "semver": "^7.7.3", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.1", "source-map": "^0.7.6", "std-env": "^3.10.0", "ufo": "^1.6.3", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unenv": "^2.0.0-rc.24", "unimport": "^5.6.0", "unplugin-utils": "^0.3.1", "unstorage": "^1.17.4", "untyped": "^2.0.0", "unwasm": "^0.5.3", "youch": "^4.1.0-beta.13", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-2dDj89C4wC2uzG7guF3CnyG+zwkZosPEp7FFBGHB3AJo11AywOolWhyQJFHDzve8COvGxJaqscye9wW2IrUsNw=="], diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 0ae0ab6..cac90fb 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -5,9 +5,12 @@ import { BarChart3, BookOpen, GraduationCap } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" +import { useMe } from "@/hooks/api/use-me" +import { NavUser } from "@/components/nav-user" import { Sidebar, SidebarContent, + SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, @@ -46,9 +49,11 @@ const facultyNavItems: NavItem[] = [ export function AppSidebar({ ...props }: ComponentProps) { const pathname = usePathname() + const { data } = useMe() const isFaculty = pathname.startsWith("/faculty") const roleLabel = isFaculty ? "Faculty" : "Student" const navItems = isFaculty ? facultyNavItems : studentNavItems + const userName = data?.fullName?.trim() || data?.userName || "User" return ( @@ -86,6 +91,15 @@ export function AppSidebar({ ...props }: ComponentProps) { + + + ) diff --git a/components/dashboard-content-header.tsx b/components/dashboard-content-header.tsx index 95f8462..f38e80e 100644 --- a/components/dashboard-content-header.tsx +++ b/components/dashboard-content-header.tsx @@ -12,6 +12,7 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb" +import { ThemeSwitch } from "@/components/theme-switch" import { Separator } from "@/components/ui/separator" import { SidebarTrigger } from "@/components/ui/sidebar" @@ -28,34 +29,37 @@ export function DashboardContentHeader() { return (
- - - - - {segments.map((segment, index) => { - const href = `/${segments.slice(0, index + 1).join("/")}` - const isLast = index === segments.length - 1 +
+ + + + + {segments.map((segment, index) => { + const href = `/${segments.slice(0, index + 1).join("/")}` + const isLast = index === segments.length - 1 - return ( - - - {!isLast ? ( - - {formatSegment(segment)} - - ) : ( - {formatSegment(segment)} - )} - - {!isLast && } - - ) - })} - - + return ( + + + {!isLast ? ( + + {formatSegment(segment)} + + ) : ( + {formatSegment(segment)} + )} + + {!isLast && } + + ) + })} + + +
+
) } diff --git a/components/nav-main.tsx b/components/nav-main.tsx deleted file mode 100644 index 1d71af1..0000000 --- a/components/nav-main.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client" - -import { ChevronRight, type LucideIcon } from "lucide-react" - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/components/ui/sidebar" - -export function NavMain({ - items, -}: { - items: { - title: string - url: string - icon?: LucideIcon - isActive?: boolean - items?: { - title: string - url: string - }[] - }[] -}) { - return ( - - Platform - - {items.map((item) => ( - - - - - {item.icon && } - {item.title} - - - - - - {item.items?.map((subItem) => ( - - - - {subItem.title} - - - - ))} - - - - - ))} - - - ) -} diff --git a/components/nav-projects.tsx b/components/nav-projects.tsx deleted file mode 100644 index f50b20d..0000000 --- a/components/nav-projects.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client" - -import { - Folder, - Forward, - MoreHorizontal, - Trash2, - type LucideIcon, -} from "lucide-react" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function NavProjects({ - projects, -}: { - projects: { - name: string - url: string - icon: LucideIcon - }[] -}) { - const { isMobile } = useSidebar() - - return ( - - Projects - - {projects.map((item) => ( - - - - - {item.name} - - - - - - - More - - - - - - View Project - - - - Share Project - - - - - Delete Project - - - - - ))} - - - - More - - - - - ) -} diff --git a/components/nav-user.tsx b/components/nav-user.tsx index 3d6d9f8..5749b0a 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -1,13 +1,11 @@ "use client" import { - BadgeCheck, - Bell, ChevronsUpDown, - CreditCard, LogOut, - Sparkles, } from "lucide-react" +import { useRouter } from "next/navigation" +import { useMemo } from "react" import { Avatar, @@ -17,12 +15,13 @@ import { import { DropdownMenu, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { useLogout } from "@/hooks/api/use-logout" +import { clearSession } from "@/lib/auth-storage" import { SidebarMenu, SidebarMenuButton, @@ -35,11 +34,30 @@ export function NavUser({ }: { user: { name: string - email: string + email?: string avatar: string } }) { const { isMobile } = useSidebar() + const router = useRouter() + const { mutate, isPending } = useLogout() + const initials = useMemo(() => { + const parts = user.name.trim().split(/\s+/).filter(Boolean) + if (parts.length === 0) return "?" + return parts + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? "") + .join("") + }, [user.name]) + + const handleLogout = () => { + mutate(undefined, { + onSettled: () => { + clearSession() + router.replace("/auth") + }, + }) + } return ( @@ -52,11 +70,13 @@ export function NavUser({ > - CN + {initials}
{user.name} - {user.email} + {user.email ? ( + {user.email} + ) : null}
@@ -71,40 +91,20 @@ export function NavUser({
- CN + {initials}
{user.name} - {user.email} + {user.email ? ( + {user.email} + ) : null}
- - - - Upgrade to Pro - - - - - - - Account - - - - Billing - - - - Notifications - - - - + - Log out + {isPending ? "Logging out..." : "Log out"} diff --git a/components/providers/app-provider.tsx b/components/providers/app-provider.tsx index 172bca9..7d2e728 100644 --- a/components/providers/app-provider.tsx +++ b/components/providers/app-provider.tsx @@ -1,12 +1,25 @@ -import { TooltipProvider } from "../ui/tooltip"; -import QueryProvider from "./query-provider"; +"use client" -export default function AppProvider({ children }: { children: React.ReactNode }) { +import { ThemeProvider } from "@/components/theme-provider" + +import { TooltipProvider } from "../ui/tooltip" +import QueryProvider from "./query-provider" + +export default function AppProvider({ + children, +}: { + children: React.ReactNode +}) { return ( - - - {children} - - + + + {children} + + ) } diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx deleted file mode 100644 index 083e9ec..0000000 --- a/components/team-switcher.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client" - -import * as React from "react" -import { ChevronsUpDown, Plus } from "lucide-react" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function TeamSwitcher({ - teams, -}: { - teams: { - name: string - logo: React.ElementType - plan: string - }[] -}) { - const { isMobile } = useSidebar() - const [activeTeam, setActiveTeam] = React.useState(teams[0]) - - if (!activeTeam) { - return null - } - - return ( - - - - - -
- -
-
- {activeTeam.name} - {activeTeam.plan} -
- -
-
- - - Teams - - {teams.map((team, index) => ( - setActiveTeam(team)} - className="gap-2 p-2" - > -
- -
- {team.name} - ⌘{index + 1} -
- ))} - - -
- -
-
Add team
-
-
-
-
-
- ) -} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..6a1ffe4 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} diff --git a/components/theme-switch.tsx b/components/theme-switch.tsx new file mode 100644 index 0000000..16194ac --- /dev/null +++ b/components/theme-switch.tsx @@ -0,0 +1,39 @@ +"use client" + +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function ThemeSwitch() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} diff --git a/components/ui/card.tsx b/components/ui/card.tsx deleted file mode 100644 index 681ad98..0000000 --- a/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/package.json b/package.json index 07e61f5..786733b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "framer-motion": "^12.34.3", "lucide-react": "^0.575.0", "next": "16.1.6", + "next-themes": "^0.4.6", "openapi-fetch": "^0.17.0", "openapi-react-query": "^0.5.4", "radix-ui": "^1.4.3",