diff --git a/app/(dashboard)/faculty/analytics/page.tsx b/app/(dashboard)/faculty/analytics/page.tsx new file mode 100644 index 0000000..c9eab3d --- /dev/null +++ b/app/(dashboard)/faculty/analytics/page.tsx @@ -0,0 +1,3 @@ +export default function FacultyAnalyticsPage() { + return

Faculty Analytics

+} diff --git a/app/(dashboard)/faculty/courses/page.tsx b/app/(dashboard)/faculty/courses/page.tsx new file mode 100644 index 0000000..aea01b8 --- /dev/null +++ b/app/(dashboard)/faculty/courses/page.tsx @@ -0,0 +1,3 @@ +export default function FacultyCoursesPage() { + return

Faculty Courses

+} diff --git a/app/(dashboard)/faculty/page.tsx b/app/(dashboard)/faculty/page.tsx new file mode 100644 index 0000000..c180f0a --- /dev/null +++ b/app/(dashboard)/faculty/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function FacultyPage() { + redirect("/faculty/courses") +} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..2631800 --- /dev/null +++ b/app/(dashboard)/layout.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react" + +import { AppSidebar } from "@/components/app-sidebar" +import { DashboardContentHeader } from "@/components/dashboard-content-header" +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" + +type DashboardLayoutProps = { + children: ReactNode +} + +export default function DashboardLayout({ children }: DashboardLayoutProps) { + return ( + + + + +
{children}
+
+
+ ) +} diff --git a/app/(dashboard)/student/courses/page.tsx b/app/(dashboard)/student/courses/page.tsx new file mode 100644 index 0000000..3a514c1 --- /dev/null +++ b/app/(dashboard)/student/courses/page.tsx @@ -0,0 +1,3 @@ +export default function StudentCoursesPage() { + return

Student Courses

+} diff --git a/app/(dashboard)/student/page.tsx b/app/(dashboard)/student/page.tsx new file mode 100644 index 0000000..a9abf43 --- /dev/null +++ b/app/(dashboard)/student/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function StudentPage() { + redirect("/student/courses") +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx index a0bb829..7d5e980 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -5,7 +5,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useLogin } from "@/hooks/api/use-login" import { LoginRequest, loginRequestSchema } from "@/types/kubb/gen"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation"; diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..e0c6a91 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,55 @@ +import { AppSidebar } from "@/components/app-sidebar" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" + +export default function Page() { + return ( + + + +
+
+ + + + + + + Build Your Application + + + + + Data Fetching + + + +
+
+
+
+
+
+
+
+
+
+ + + ) +} diff --git a/app/globals.css b/app/globals.css index bdfe4ca..a9c6266 100644 --- a/app/globals.css +++ b/app/globals.css @@ -50,7 +50,6 @@ --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); - /* Gradient Animation */ --animate-first: moveVertical 60s ease infinite; --animate-second: moveInCircle 40s reverse infinite; @@ -94,7 +93,6 @@ } /* End of Gradient Animation */ - :root { --radius: 0.625rem; --brand-blue: oklch(0.428 0.25 264.53); diff --git a/bun.lock b/bun.lock index fbeb9c1..3857510 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.34.3", - "lucide-react": "^0.564.0", + "lucide-react": "^0.575.0", "next": "16.1.6", "openapi-fetch": "^0.17.0", "openapi-react-query": "^0.5.4", @@ -1529,7 +1529,7 @@ "lru-queue": ["lru-queue@0.1.0", "", { "dependencies": { "es5-ext": "~0.10.2" } }, "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ=="], - "lucide-react": ["lucide-react@0.564.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg=="], + "lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..0ae0ab6 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,92 @@ +"use client" + +import type { ComponentProps, ComponentType } from "react" +import { BarChart3, BookOpen, GraduationCap } from "lucide-react" +import Link from "next/link" +import { usePathname } from "next/navigation" + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar" + +type NavItem = { + title: string + href: string + icon: ComponentType<{ className?: string }> +} + +const studentNavItems: NavItem[] = [ + { + title: "Courses", + href: "/student/courses", + icon: BookOpen, + }, +] + +const facultyNavItems: NavItem[] = [ + { + title: "Courses", + href: "/faculty/courses", + icon: BookOpen, + }, + { + title: "Analytics", + href: "/faculty/analytics", + icon: BarChart3, + }, +] + +export function AppSidebar({ ...props }: ComponentProps) { + const pathname = usePathname() + const isFaculty = pathname.startsWith("/faculty") + const roleLabel = isFaculty ? "Faculty" : "Student" + const navItems = isFaculty ? facultyNavItems : studentNavItems + + return ( + + +
+
+ +
+
+

Faculytics

+

{roleLabel}

+
+
+
+ + + {roleLabel} Navigation + + {navItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + +
+ ) +} diff --git a/components/dashboard-content-header.tsx b/components/dashboard-content-header.tsx new file mode 100644 index 0000000..95f8462 --- /dev/null +++ b/components/dashboard-content-header.tsx @@ -0,0 +1,61 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { Fragment } from "react" + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { SidebarTrigger } from "@/components/ui/sidebar" + +function formatSegment(segment: string) { + return segment + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" ") +} + +export function DashboardContentHeader() { + const pathname = usePathname() + const segments = pathname.split("/").filter(Boolean) + + return ( +
+ + + + + {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 && } + + ) + })} + + +
+ ) +} diff --git a/components/nav-main.tsx b/components/nav-main.tsx new file mode 100644 index 0000000..1d71af1 --- /dev/null +++ b/components/nav-main.tsx @@ -0,0 +1,73 @@ +"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 new file mode 100644 index 0000000..f50b20d --- /dev/null +++ b/components/nav-projects.tsx @@ -0,0 +1,89 @@ +"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 new file mode 100644 index 0000000..3d6d9f8 --- /dev/null +++ b/components/nav-user.tsx @@ -0,0 +1,114 @@ +"use client" + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/components/providers/app-provider.tsx b/components/providers/app-provider.tsx index 99297cc..172bca9 100644 --- a/components/providers/app-provider.tsx +++ b/components/providers/app-provider.tsx @@ -1,9 +1,12 @@ +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 new file mode 100644 index 0000000..083e9ec --- /dev/null +++ b/components/team-switcher.tsx @@ -0,0 +1,91 @@ +"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/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..1ac1570 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,109 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarBadge, + AvatarGroup, + AvatarGroupCount, +} diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..542e762 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { ChevronRight, MoreHorizontal } from "lucide-react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return