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",