diff --git a/.github/README.md b/.github/actions.md similarity index 100% rename from .github/README.md rename to .github/actions.md diff --git a/README.md b/README.md index dbab70c..e5d334e 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,8 @@ git reset --soft HEAD~1 - **GitHub Discussions**: Ask questions about contributing - **Issues**: Report bugs or request features - **Discord/Slack**: Real-time chat with the community -- **Documentation**: Check our [Contributing Guide](./docs/dev/contributing.md) +- **Developer Documentation**: Check our [Developer Documentation](/docs/dev/README.md) +- **Github Actions Workflow Documentation**: Check our [Github Actions Workflow Documentation](/.github/actions.md) Remember: **Everyone was a beginner once!** The codac community is here to help you learn and grow as a developer. diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx deleted file mode 100644 index 20866bb..0000000 --- a/app/(admin)/page.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { BookOpen, GraduationCap, TrendingUp, Users } from 'lucide-react' -import * as React from 'react' - -export default function AdminDashboard() { - return ( -
-
-
-
-
- -

codac Admin Dashboard

-
-
-
-
- -
-
- - - Total Students - - - -
1,234
-

+12% from last month

-
-
- - - - Active Courses - - - -
24
-

+3 new this month

-
-
- - - - Alumni - - - -
542
-

+8% graduation rate

-
-
- - - - Completion Rate - - - -
87%
-

+5% from last cohort

-
-
-
- -
- - - Recent Activity - Latest updates from the codac platform - - -
-
-
-
-

New student enrolled

-

2 minutes ago

-
-
-
-
-
-

- Course "React Fundamentals" updated -

-

1 hour ago

-
-
-
-
-
-

Assignment submission pending review

-

3 hours ago

-
-
-
- - - - - - Quick Actions - Common administrative tasks - - -
- - - - -
-
-
-
-
-
- ) -} diff --git a/app/(admin)/layout.tsx b/app/(dashboard)/layout.tsx similarity index 58% rename from app/(admin)/layout.tsx rename to app/(dashboard)/layout.tsx index 268d45c..22ff85a 100644 --- a/app/(admin)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,10 +1,10 @@ -import { SidebarProvider } from '@/components/ui/sidebar' +import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar' import '@/app/globals.css' import { AppSidebar } from '@/components/app-sidebar' import Header from '@/components/header' -const title = 'Admin Page' +const title = 'Dashboard' const description = '' export const metadata = { @@ -15,17 +15,16 @@ export const metadata = { title, description, }, - metadataBase: new URL('https://nextjs-postgres-auth.vercel.app'), } -export default function AdminLayout({ children }: { children: React.ReactNode }) { +export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( -
+
{children} -
+
) } diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx new file mode 100644 index 0000000..2411d73 --- /dev/null +++ b/app/(dashboard)/page.tsx @@ -0,0 +1,266 @@ +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Progress } from '@/components/ui/progress' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { + BookOpen, + TrendingUp, + Users, + Calendar, + Target, + Award, + Clock, + AlertCircle, + CheckCircle, + Activity, + Heart, + Shield, + BarChart3, + MessageSquare +} from 'lucide-react' +import * as React from 'react' +import { auth } from '@/app/auth' +import { UserRole, getRolePermissions } from '@/lib/user-roles' +import { UserRoleBadge } from '@/components/user-role-badge' + +export default async function Dashboard() { + const session = await auth() + const userName = session?.user?.name?.split(' ')[0] || 'User' + const userRole = (session?.user as any)?.role as UserRole || 'student' + const permissions = getRolePermissions(userRole) + + // Role-based greeting and description + const getRoleBasedWelcome = () => { + switch (userRole) { + case 'student': + return { + greeting: `Welcome back, ${userName}! 📚`, + description: 'Continue your learning journey with Code Academy Berlin' + } + case 'alumni': + return { + greeting: `Hello ${userName}! 🎓`, + description: 'Stay connected with the Code Academy Berlin community' + } + case 'mentor': + return { + greeting: `Welcome back, ${userName}! 💡`, + description: 'Guide and inspire the next generation of developers' + } + case 'admin': + return { + greeting: `Dashboard Overview, ${userName} ⚡`, + description: 'Monitor and manage the Code Academy Berlin platform' + } + } + } + + const welcome = getRoleBasedWelcome() + + // Role-based stats + const getRoleBasedStats = () => { + switch (userRole) { + case 'student': + return [ + { title: 'Active Courses', value: '4', change: '+2 from last month', icon: BookOpen }, + { title: 'Assignments Due', value: '3', change: '2 due this week', icon: Target }, + { title: 'Study Streak', value: '12', change: 'days in a row', icon: Award }, + { title: 'Study Hours', value: '28.5', change: 'this week', icon: Clock } + ] + case 'alumni': + return [ + { title: 'Community Posts', value: '8', change: '+3 this month', icon: MessageSquare }, + { title: 'Mentoring Sessions', value: '2', change: 'upcoming', icon: Heart }, + { title: 'Network Connections', value: '45', change: '+5 new', icon: Users }, + { title: 'Profile Views', value: '124', change: 'this month', icon: Activity } + ] + case 'mentor': + return [ + { title: 'Active Mentees', value: '6', change: '+1 this month', icon: Users }, + { title: 'Sessions This Week', value: '8', change: '2 pending', icon: Calendar }, + { title: 'Success Rate', value: '94%', change: 'student completion', icon: Award }, + { title: 'Hours Mentored', value: '32', change: 'this month', icon: Clock } + ] + case 'admin': + return [ + { title: 'Total Users', value: '1,247', change: '+23 this week', icon: Users }, + { title: 'Active Courses', value: '18', change: '3 new courses', icon: BookOpen }, + { title: 'System Health', value: '98%', change: 'uptime', icon: Activity }, + { title: 'Support Tickets', value: '4', change: '2 resolved today', icon: AlertCircle } + ] + } + } + + const stats = getRoleBasedStats() + + // Role-based alert + const getRoleBasedAlert = () => { + switch (userRole) { + case 'student': + return { + title: 'Upcoming: Web Development Bootcamp', + description: 'Your intensive bootcamp starts Monday, December 9th. Make sure to complete the pre-work assignments.' + } + case 'alumni': + return { + title: 'Alumni Networking Event', + description: 'Join us for the monthly alumni meetup on December 15th. Connect with fellow graduates and share your experiences.' + } + case 'mentor': + return { + title: 'New Mentee Assignment', + description: 'You have been assigned 2 new mentees for the upcoming cohort. Please review their profiles and schedule initial meetings.' + } + case 'admin': + return { + title: 'System Maintenance Scheduled', + description: 'Planned maintenance on December 10th from 2-4 AM UTC. All users have been notified via email.' + } + } + } + + const alertInfo = getRoleBasedAlert() + + return ( +
+ {/* Welcome Header */} +
+
+

{welcome.greeting}

+

+ {welcome.description} +

+
+
+ + + + Online + +
+
+ + {/* Quick Stats */} +
+ {stats.map((stat, index) => ( + + + {stat.title} + + + +
{stat.value}
+

+ {stat.change} +

+
+
+ ))} +
+ + {/* Role-based Alert */} + + + {alertInfo.title} + + {alertInfo.description} + + + + {/* Main Content Tabs - Role-based */} + + + Overview + {permissions.canViewCourses && Courses} + {userRole === 'mentor' && Mentoring} + {userRole === 'admin' && Admin} + {(userRole === 'student' || userRole === 'alumni') && Progress} + + + + {/* Role-based overview content */} + {userRole === 'student' && ( +
+ + + + + Current Courses + + + +
+
+
+

Full Stack JavaScript

+

Module 3: React & State Management

+
+
+ + 75% +
+
+
+
+
+ + + + Quick Actions + + + + + + +
+ )} + + {userRole === 'mentor' && ( +
+ + + + + Active Mentees + + + +
6
+

Across 3 cohorts

+
+
+
+ )} + + {userRole === 'admin' && ( +
+ + + + + Platform Analytics + + + +
98.5%
+

User satisfaction

+
+
+
+ )} +
+ + {/* Additional tab contents would go here based on role */} +
+
+ ) +} \ No newline at end of file diff --git a/app/profile/page.tsx b/app/(dashboard)/profile/page.tsx similarity index 100% rename from app/profile/page.tsx rename to app/(dashboard)/profile/page.tsx diff --git a/app/api/user/route.ts b/app/api/user/route.ts index 9a9263d..95ace11 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -1,4 +1,31 @@ -export async function POST(request: Request) { - const res = await request.json() - return Response.json({ res }) +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/app/auth' +import { getUserWithCohort } from '@/app/db' + +export async function GET(request: NextRequest) { + const session = await auth() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const userId = searchParams.get('id') + + if (!userId || userId !== session.user.id) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + try { + const userData = await getUserWithCohort(userId) + + if (!userData) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + return NextResponse.json(userData) + } catch (error) { + console.error('Error fetching user data:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } } diff --git a/app/db.ts b/app/db.ts index 4495a15..e583272 100644 --- a/app/db.ts +++ b/app/db.ts @@ -2,7 +2,7 @@ import { genSaltSync, hashSync } from 'bcrypt-ts' import { eq } from 'drizzle-orm' // Import the database connection and schema from db/schema.ts -import { db, users } from '../db/schema' +import { db, users, cohorts } from '../db/schema' export { db } @@ -16,3 +16,27 @@ export async function createUser(email: string, password: string) { return await db.insert(users).values({ email: email, password: hash }) } + +export async function getUserWithCohort(userId: string) { + const result = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + image: users.image, + role: users.role, + bio: users.bio, + github: users.github, + linkedin: users.linkedin, + portfolio: users.portfolio, + cohortId: users.cohortId, + cohortName: cohorts.name, + cohortDescription: cohorts.description, + }) + .from(users) + .leftJoin(cohorts, eq(users.cohortId, cohorts.id)) + .where(eq(users.id, userId)) + .limit(1) + + return result[0] || null +} diff --git a/app/globals.css b/app/globals.css index 65ae045..9146974 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,7 @@ @import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); + /* Configure class-based dark mode */ @variant dark (&:where(.dark, .dark *)); @@ -48,75 +50,82 @@ @layer base { :root { + /* Light theme - Modern educational colors */ --background: 0 0% 100%; - --foreground: 240 10% 3.9%; + --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + /* Modern blue for education */ + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 84% 4.9%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 84% 4.9%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --chart-1: 221.2 83.2% 53.3%; + --chart-2: 142.1 76.2% 36.3%; + --chart-3: 25.1 95% 53.1%; + --chart-4: 280.4 89.2% 62.7%; + --chart-5: 17.5 88.2% 58.6%; + --radius: 0.75rem; + /* Slightly more rounded for modern feel */ + /* Enhanced sidebar colors */ --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-foreground: 220 8.9% 46.1%; + --sidebar-primary: 221.2 83.2% 53.3%; + --sidebar-primary-foreground: 210 40% 98%; + --sidebar-accent: 220 14.3% 95.9%; + --sidebar-accent-foreground: 220.9 39.3% 11%; --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-ring: 221.2 83.2% 53.3%; } .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; + /* Dark theme - Sophisticated educational colors */ + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + /* Brighter blue for dark mode */ + --primary-foreground: 222.2 84% 4.9%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 217.2 91.2% 59.8%; + --chart-1: 217.2 91.2% 59.8%; + --chart-2: 142.1 70.6% 45.3%; + --chart-3: 47.9 95.8% 53.1%; + --chart-4: 280.4 89.2% 62.7%; + --chart-5: 17.5 88.2% 58.6%; + + /* Enhanced dark sidebar */ + --sidebar-background: 220 13% 9%; + --sidebar-foreground: 220 8.9% 46.1%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-primary-foreground: 220 13% 9%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; --sidebar-ring: 217.2 91.2% 59.8%; } } @@ -127,6 +136,110 @@ } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans antialiased; + } + + /* Enhanced typography for better readability */ + h1, + h2, + h3, + h4, + h5, + h6 { + @apply font-semibold tracking-tight; + } + + h1 { + @apply text-3xl lg:text-4xl; + } + + h2 { + @apply text-2xl lg:text-3xl; + } + + h3 { + @apply text-xl lg:text-2xl; + } + + /* Smooth transitions for better UX */ + .transition-smooth { + @apply transition-all duration-200 ease-in-out; + } + + /* Custom focus styles */ + .focus-visible { + @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; + } + + /* Glass effect for modern cards */ + .glass-effect { + @apply bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60; + } +} + +/* Custom scrollbar */ +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted-foreground)) transparent; + } + + .scrollbar-thin::-webkit-scrollbar { + width: 6px; + } + + .scrollbar-thin::-webkit-scrollbar-track { + background: transparent; + } + + .scrollbar-thin::-webkit-scrollbar-thumb { + background-color: hsl(var(--muted-foreground)); + border-radius: 3px; + } + + .scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--foreground)); } } + +:root { + --sidebar: hsl(0 0% 98%); + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +.dark { + --sidebar: hsl(240 5.9% 10%); + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +@theme inline { + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 788e820..95cbede 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,10 @@ import './globals.css' -import { GeistSans } from 'geist/font/sans' +import { GeistSans } from 'geist/font' import { Toaster } from '@/components/ui/toaster' -import { SessionProvider } from 'next-auth/react' +import AuthProvider from '@/components/providers/session-provider' +import { ThemeProvider } from '@/components/providers/theme-provider' const title = 'codac - Learning Management System' const description = @@ -16,16 +17,22 @@ export const metadata = { card: 'summary_large_image', title, description, - }, - metadataBase: new URL('https://codac.vercel.app'), + } } export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + - {children} - + + {children} + + ) diff --git a/app/login/page.tsx b/app/login/page.tsx index 692bf7a..7335a80 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -3,7 +3,10 @@ import LoginForm from '@/components/login-form' export default function Login() { return (
- +
+

Login

+ +
) } diff --git a/app/register/page.tsx b/app/register/page.tsx index 9015733..c1ab607 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -3,7 +3,10 @@ import RegisterForm from '@/components/register-form' export default function register() { return (
- +
+

Create Account

+ +
) } diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 2173c63..4480fb9 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,193 +1,86 @@ -'use client' +"use client" -import { BookOpen, GraduationCap, Home, Settings, TrendingUp, UserCheck, Users } from 'lucide-react' -import type * as React from 'react' +import * as React from "react" +import { + BarChart3, + User, + GraduationCap, + type LucideIcon, +} from "lucide-react" -import { NavMain } from './nav-main' -import { TeamSwitcher } from './team-switcher' -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from './ui/sidebar' +import { NavMain } from "@/components/nav-main" +import { NavUser } from "@/components/nav-user" +import { TeamSwitcher } from "@/components/team-switcher" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@/components/ui/sidebar" +import { UserRole } from "@/lib/user-roles" -// Codac-specific navigation data -const data = { - user: { - name: 'Student', - email: 'student@codeacademy.berlin', - avatar: '/images/user.png', - }, - teams: [ - { - name: 'Code Academy Berlin', - logo: GraduationCap, - plan: 'Premium', - }, - ], - navMain: [ +interface NavItem { + title: string + url: string + icon: LucideIcon + isActive?: boolean + items?: { + title: string + url: string + }[] +} + +// Get navigation data - only for existing pages +const getNavData = () => { + const navMain: NavItem[] = [ { - title: 'Dashboard', - url: '/dashboard', - icon: Home, + title: "Dashboard", + url: "/", + icon: BarChart3, isActive: true, - items: [ - { - title: 'Overview', - url: '/dashboard', - }, - { - title: 'Progress', - url: '/dashboard/progress', - }, - { - title: 'Achievements', - url: '/dashboard/achievements', - }, - ], - }, - { - title: 'Learning', - url: '/learning', - icon: BookOpen, - items: [ - { - title: 'Courses', - url: '/learning/courses', - }, - { - title: 'Assignments', - url: '/learning/assignments', - }, - { - title: 'Resources', - url: '/learning/resources', - }, - { - title: 'Schedule', - url: '/learning/schedule', - }, - ], - }, - { - title: 'Community', - url: '/community', - icon: Users, - items: [ - { - title: 'Posts', - url: '/community/posts', - }, - { - title: 'Discussions', - url: '/community/discussions', - }, - { - title: 'Student Directory', - url: '/community/directory', - }, - { - title: 'Alumni Network', - url: '/community/alumni', - }, - ], - }, - { - title: 'Mentorship', - url: '/mentorship', - icon: UserCheck, - items: [ - { - title: 'Find a Mentor', - url: '/mentorship/find', - }, - { - title: 'My Mentors', - url: '/mentorship/mentors', - }, - { - title: 'Mentoring', - url: '/mentorship/mentoring', - }, - { - title: 'Sessions', - url: '/mentorship/sessions', - }, - ], - }, - { - title: 'Career', - url: '/career', - icon: TrendingUp, - items: [ - { - title: 'Job Board', - url: '/career/jobs', - }, - { - title: 'Portfolio', - url: '/career/portfolio', - }, - { - title: 'Interview Prep', - url: '/career/interview-prep', - }, - { - title: 'Resources', - url: '/career/resources', - }, - ], - }, - { - title: 'Settings', - url: '/settings', - icon: Settings, - items: [ - { - title: 'Profile', - url: '/settings/profile', - }, - { - title: 'Notifications', - url: '/settings/notifications', - }, - { - title: 'Privacy', - url: '/settings/privacy', - }, - { - title: 'Account', - url: '/settings/account', - }, - ], }, - ], - projects: [ { - name: 'Full Stack Development', - url: '/projects/full-stack', - icon: BookOpen, + title: "Profile", + url: "/profile", + icon: User, }, - { - name: 'Data Science', - url: '/projects/data-science', - icon: TrendingUp, - }, - { - name: 'Web Development', - url: '/projects/web-dev', - icon: GraduationCap, - }, - ], + ] + + return { + navMain, + } } export function AppSidebar({ ...props }: React.ComponentProps) { + const navData = getNavData() + + // Default user data - in real app, this would come from session + const defaultUser = { + name: "Student", + email: "student@codeacademy.berlin", + avatar: "/images/user.png", + role: 'student' as UserRole, + } + + // Organization data for Code Academy Berlin + const organization = { + name: "Code Academy Berlin", + logo: GraduationCap, + plan: "Education", + } + return ( - + - - {/* */} + - {/* */} + + + ) diff --git a/components/dashboard-breadcrumb.tsx b/components/dashboard-breadcrumb.tsx new file mode 100644 index 0000000..aa4d6e2 --- /dev/null +++ b/components/dashboard-breadcrumb.tsx @@ -0,0 +1,72 @@ +'use client' + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from './ui/breadcrumb' +import { Separator } from './ui/separator' +import { SidebarTrigger } from './ui/sidebar' + +export function DashboardBreadcrumb({ + items +}: { + items: Array<{ + title: string + url?: string + isCurrentPage?: boolean + }> +}) { + return ( +
+
+ + + + + {items.map((item, index) => ( +
+ + {item.isCurrentPage ? ( + + {item.title} + + ) : ( + + {item.title} + + )} + + {index < items.length - 1 && ( + + )} +
+ ))} +
+
+
+
+ ) +} + +// Common breadcrumb patterns for the app +export const dashboardBreadcrumbs = [ + { title: 'Dashboard', url: '/dashboard', isCurrentPage: true } +] + +export const learningBreadcrumbs = [ + { title: 'Dashboard', url: '/dashboard' }, + { title: 'Learning', url: '/learning', isCurrentPage: true } +] + +export const coursesBreadcrumbs = [ + { title: 'Dashboard', url: '/dashboard' }, + { title: 'Learning', url: '/learning' }, + { title: 'Courses', url: '/learning/courses', isCurrentPage: true } +] \ No newline at end of file diff --git a/components/header.tsx b/components/header.tsx index cfed9db..34961c5 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,20 +1,61 @@ import { SessionProvider } from 'next-auth/react' -// import { auth } from '@/app/auth'; +import { auth } from '@/app/auth' import { Search } from './search' -import { SidebarTrigger } from './ui/sidebar' import { UserNav } from './user-nav' +import { Separator } from './ui/separator' +import { SidebarTrigger } from './ui/sidebar' +import { ThemeToggle } from './theme-toggle' +import { Badge } from './ui/badge' +import { DashboardBreadcrumb } from './dashboard-breadcrumb' +import { UserRole } from './user-role-badge' +import { Bell } from 'lucide-react' +import { Button } from './ui/button' + +export default async function Header() { + const session = await auth() + const userRole = (session?.user as any)?.role as UserRole || 'student' -export default function Header() { return ( -
-
- -
+
+
+ + + +
+ +
+
- - -
+ + {/* Role-based header elements */} + {session?.user && ( + <> + {userRole === 'admin' && ( + + Admin Mode + + )} + + {/* Notifications */} + + + )} + + + + +
) diff --git a/components/loading-skeleton.tsx b/components/loading-skeleton.tsx new file mode 100644 index 0000000..66aca74 --- /dev/null +++ b/components/loading-skeleton.tsx @@ -0,0 +1,121 @@ +import { Skeleton } from '@/components/ui/skeleton' +import { Card, CardContent, CardHeader } from '@/components/ui/card' + +export function DashboardSkeleton() { + return ( +
+ {/* Header Skeleton */} +
+ + +
+ + {/* Stats Cards Skeleton */} +
+ {[...Array(4)].map((_, i) => ( + + + + + + + + + + + ))} +
+ + {/* Alert Skeleton */} + + +
+ +
+ + +
+
+
+
+ + {/* Tabs Skeleton */} +
+
+ {[...Array(4)].map((_, i) => ( + + ))} +
+ +
+ + + + + + + {[...Array(3)].map((_, i) => ( +
+
+ + +
+
+ + +
+
+ ))} +
+
+ + + + + + + + {[...Array(4)].map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+
+
+
+
+ ) +} + +export function CourseSkeleton() { + return ( +
+ {[...Array(6)].map((_, i) => ( + + +
+ + +
+ +
+ +
+ +
+ + +
+ +
+
+
+ ))} +
+ ) +} \ No newline at end of file diff --git a/components/login-form.tsx b/components/login-form.tsx index ea7a678..d360c0e 100644 --- a/components/login-form.tsx +++ b/components/login-form.tsx @@ -1,5 +1,6 @@ 'use client' +import { useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { zodResolver } from '@hookform/resolvers/zod' import { signIn } from 'next-auth/react' @@ -30,6 +31,7 @@ interface AuthError { } export default function LoginForm() { + const [isLoading, setIsLoading] = useState(false) const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { @@ -39,20 +41,25 @@ export default function LoginForm() { }) async function onSubmit(data: z.infer) { - const res = await signIn('credentials', { - redirect: false, - email: data.email, - password: data.password, - }) - if (res?.error) { - toast({ - variant: 'destructive', - title: 'Uh oh! Something went wrong.', - description: (res as AuthError).code, + setIsLoading(true) + try { + const res = await signIn('credentials', { + redirect: false, + email: data.email, + password: data.password, }) - form.setError('password', { type: 'manual', message: (res as AuthError).code }) - } else { - window.location.href = '/' + if (res?.error) { + toast({ + variant: 'destructive', + title: 'Uh oh! Something went wrong.', + description: (res as AuthError).code, + }) + form.setError('password', { type: 'manual', message: (res as AuthError).code }) + } else { + window.location.href = '/' + } + } finally { + setIsLoading(false) } } @@ -73,7 +80,7 @@ export default function LoginForm() { Email - + @@ -92,14 +99,14 @@ export default function LoginForm() {
- + )} /> - diff --git a/components/search.tsx b/components/search.tsx index 9c15d42..b332b3f 100644 --- a/components/search.tsx +++ b/components/search.tsx @@ -1,9 +1,17 @@ +'use client' + +import { Search as SearchIcon } from 'lucide-react' import { Input } from './ui/input' export function Search() { return ( -
- +
+ +
) } diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx index 485191a..99c7fbc 100644 --- a/components/team-switcher.tsx +++ b/components/team-switcher.tsx @@ -1,7 +1,5 @@ -'use client' - -import { ChevronsUpDown, Plus } from 'lucide-react' -import * as React from 'react' +import * as React from "react" +import { ChevronsUpDown, Plus } from "lucide-react" import { DropdownMenu, @@ -11,8 +9,13 @@ import { DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, -} from './ui/dropdown-menu' -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar' +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" export function TeamSwitcher({ teams, @@ -26,6 +29,10 @@ export function TeamSwitcher({ const { isMobile } = useSidebar() const [activeTeam, setActiveTeam] = React.useState(teams[0]) + if (!activeTeam) { + return null + } + return ( @@ -35,31 +42,33 @@ export function TeamSwitcher({ size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > -
+
- {activeTeam.name} + {activeTeam.name} {activeTeam.plan}
- Teams + + Teams + {teams.map((team, index) => ( setActiveTeam(team)} className="gap-2 p-2" > -
- +
+
{team.name} ⌘{index + 1} @@ -67,10 +76,10 @@ export function TeamSwitcher({ ))} -
+
-
Add team
+
Add team
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..00e614c --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,40 @@ +'use client' + +import * as React from 'react' +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 ThemeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme('light')}> + Light + + setTheme('dark')}> + Dark + + setTheme('system')}> + System + + + + ) +} \ No newline at end of file diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx index f009937..71e428b 100644 --- a/components/ui/avatar.tsx +++ b/components/ui/avatar.tsx @@ -1,47 +1,53 @@ -'use client' +"use client" -import * as AvatarPrimitive from '@radix-ui/react-avatar' -import * as React from 'react' +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +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" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md 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", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return