From 967dedb847cbb432386e375ebeece63b51a9ed8b Mon Sep 17 00:00:00 2001 From: Lucas Dupias Date: Wed, 2 Jul 2025 17:37:40 +0200 Subject: [PATCH 1/2] Enhance project structure and UI components - Update package.json to include new dependencies: @radix-ui/react-progress, @radix-ui/react-tabs, and next-themes. - Refactor README.md to improve documentation clarity and add links to Developer Documentation and GitHub Actions Workflow Documentation. - Introduce new GitHub Actions documentation for CI/CD workflows. - Implement user cohort retrieval in the database with the getUserWithCohort function. - Update layout and styling for improved user experience across various components, including the addition of a theme toggle feature. - Remove the admin dashboard page and create a new dashboard layout and page for user-specific content. - Add loading skeletons for better user feedback during data fetching. - Enhance navigation components with badges and improved accessibility features. --- .github/{README.md => actions.md} | 0 README.md | 3 +- app/(admin)/page.tsx | 132 -------- app/{(admin) => (dashboard)}/layout.tsx | 11 +- app/(dashboard)/page.tsx | 413 ++++++++++++++++++++++++ app/api/user/route.ts | 33 +- app/db.ts | 26 +- app/globals.css | 187 +++++++---- app/layout.tsx | 21 +- app/login/page.tsx | 5 +- app/register/page.tsx | 5 +- components/app-sidebar.tsx | 203 +++--------- components/dashboard-breadcrumb.tsx | 72 +++++ components/header.tsx | 25 +- components/loading-skeleton.tsx | 121 +++++++ components/login-form.tsx | 41 ++- components/nav-main.tsx | 62 +++- components/nav-projects.tsx | 94 ++++++ components/nav-shortcuts.tsx | 84 +++++ components/nav-user.tsx | 120 +++++++ components/providers/theme-provider.tsx | 9 + components/register-form.tsx | 21 +- components/search.tsx | 12 +- components/team-switcher.tsx | 60 +++- components/theme-toggle.tsx | 40 +++ components/ui/alert.tsx | 66 ++++ components/ui/badge.tsx | 46 +++ components/ui/breadcrumb.tsx | 109 +++++++ components/ui/progress.tsx | 29 ++ components/ui/sheet.tsx | 4 +- components/ui/sidebar.tsx | 2 +- components/ui/tabs.tsx | 66 ++++ e2e/auth.spec.ts | 70 +--- e2e/example.spec.ts | 20 -- e2e/forms-validation.spec.ts | 141 -------- e2e/navigation.spec.ts | 7 - e2e/ui-interactions.spec.ts | 142 +------- package.json | 7 +- pnpm-lock.yaml | 72 +++++ 39 files changed, 1796 insertions(+), 785 deletions(-) rename .github/{README.md => actions.md} (100%) delete mode 100644 app/(admin)/page.tsx rename app/{(admin) => (dashboard)}/layout.tsx (58%) create mode 100644 app/(dashboard)/page.tsx create mode 100644 components/dashboard-breadcrumb.tsx create mode 100644 components/loading-skeleton.tsx create mode 100644 components/nav-projects.tsx create mode 100644 components/nav-shortcuts.tsx create mode 100644 components/nav-user.tsx create mode 100644 components/providers/theme-provider.tsx create mode 100644 components/theme-toggle.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/tabs.tsx 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..6e6594b --- /dev/null +++ b/app/(dashboard)/page.tsx @@ -0,0 +1,413 @@ +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 +} from 'lucide-react' +import * as React from 'react' +import { auth } from '@/app/auth' + +export default async function Dashboard() { + const session = await auth() + const userName = session?.user?.name?.split(' ')[0] || 'Student' + + return ( +
+ {/* Welcome Header */} +
+
+

Welcome back, {userName}! ๐Ÿ‘‹

+

+ Continue your learning journey with Code Academy Berlin +

+
+
+ + + Online + +
+
+ + {/* Quick Stats */} +
+ + + Courses Active + + + +
4
+

+ +2 from last month +

+
+
+ + + + Assignments Due + + + +
3
+

+ 2 due this week +

+
+
+ + + + Study Streak + + + +
12
+

+ days in a row +

+
+
+ + + + Study Hours + + + +
28.5
+

+ this week +

+
+
+
+ + {/* Important Alert */} + + + Upcoming: Web Development Bootcamp + + Your intensive bootcamp starts Monday, December 9th. Make sure to complete the pre-work assignments. + + + + {/* Main Content Tabs */} + + + Overview + Courses + Assignments + Progress + + + +
+ {/* Current Courses */} + + + + + Current Courses + + + Your active learning paths and progress + + + +
+
+
+

+ Full Stack JavaScript +

+

+ Module 3: React & State Management +

+
+
+ + 75% +
+
+ +
+
+

+ Database Design +

+

+ Module 2: Advanced SQL Queries +

+
+
+ + 60% +
+
+ +
+
+

+ DevOps Fundamentals +

+

+ Module 1: Docker & Containerization +

+
+
+ + 30% +
+
+
+
+
+ + {/* Quick Actions */} + + + Quick Actions + + Access your most used features + + + + + + + + + +
+
+ + +
+ + +
+ Full Stack JavaScript + Active +
+ + Complete web development with modern JavaScript + +
+ +
+ +
+ Module 3 of 5 + 75% Complete +
+ +
+
+
+ + + +
+ Database Design + In Progress +
+ + Master SQL and NoSQL database concepts + +
+ +
+ +
+ Module 2 of 4 + 60% Complete +
+ +
+
+
+ + + +
+ DevOps Fundamentals + Started +
+ + Learn deployment and infrastructure basics + +
+ +
+ +
+ Module 1 of 6 + 30% Complete +
+ +
+
+
+
+
+ + +
+ + + + Pending Assignments + 3 Due Soon + + + +
+
+
+

React Component Library Project

+

Full Stack JavaScript

+
+
+ Due Tomorrow + +
+
+ +
+
+

Database Schema Design

+

Database Design

+
+
+ Due Dec 10 + +
+
+ +
+
+

Docker Setup Assignment

+

DevOps Fundamentals

+
+
+ Due Dec 12 + +
+
+
+
+
+
+
+ + +
+ + + + + Overall Progress + + + +
+
+
+ Course Completion + 55% +
+ +
+ +
+
+ Assignment Completion + 78% +
+ +
+ +
+
+ Skill Development + 63% +
+ +
+
+
+
+ + + + + + Recent Achievements + + + +
+
+
+ +
+
+

First Project Complete

+

Completed your first coding project

+
+
+ +
+
+ +
+
+

Study Streak

+

12 days of consistent learning

+
+
+ +
+
+ +
+
+

Community Helper

+

Helped 5 fellow students

+
+
+
+
+
+
+
+
+
+ ) +} \ No newline at end of file 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..69a5f0c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -48,75 +48,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 +134,68 @@ } 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)); + } +} \ 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..19a729f 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,27 +1,39 @@ 'use client' -import { BookOpen, GraduationCap, Home, Settings, TrendingUp, UserCheck, Users } from 'lucide-react' +import { + Home, + Settings, + User, + GraduationCap, +} from 'lucide-react' import type * as React from 'react' +import { useSession } from 'next-auth/react' import { NavMain } from './nav-main' +import { NavProjects } from './nav-projects' +import { NavShortcuts, quickShortcuts } from './nav-shortcuts' +import { NavUser } from './nav-user' import { TeamSwitcher } from './team-switcher' -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from './ui/sidebar' +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail, SidebarSeparator } from './ui/sidebar' -// 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: [ +export function AppSidebar({ ...props }: React.ComponentProps) { + const { data: session } = useSession() + + // Simplified data for testing sidebar functionality + const displayUser = { + name: session?.user?.name || 'User', + email: session?.user?.email || 'user@example.com', + avatar: session?.user?.image || '/images/user.png', + } + + const displayTeam = { + name: 'Code Academy Berlin', + logo: GraduationCap, + plan: 'Student', + } + + // Simplified navigation data with only implemented routes + const navMainData = [ { title: 'Dashboard', url: '/dashboard', @@ -32,162 +44,47 @@ const data = { 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', + url: '/profile', icon: Settings, items: [ { title: 'Profile', - url: '/settings/profile', - }, - { - title: 'Notifications', - url: '/settings/notifications', - }, - { - title: 'Privacy', - url: '/settings/privacy', - }, - { - title: 'Account', - url: '/settings/account', + url: '/profile', }, ], }, - ], - projects: [ - { - name: 'Full Stack Development', - url: '/projects/full-stack', - icon: BookOpen, - }, - { - name: 'Data Science', - url: '/projects/data-science', - icon: TrendingUp, - }, + ] + + // Simplified projects data + const projectsData = [ { - name: 'Web Development', - url: '/projects/web-dev', - icon: GraduationCap, + name: 'Current Learning Path', + url: '/dashboard', + icon: User, + status: 'active' as const, + progress: 75, }, - ], -} + ] -export function AppSidebar({ ...props }: React.ComponentProps) { 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..3cfcce0 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,20 +1,29 @@ import { SessionProvider } from 'next-auth/react' // 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' export default function Header() { return ( -
-
- -
+
+
+ + +
+ +
+
- - -
+ + + +
) 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..c5173fc 100644 --- a/components/team-switcher.tsx +++ b/components/team-switcher.tsx @@ -1,6 +1,6 @@ 'use client' -import { ChevronsUpDown, Plus } from 'lucide-react' +import { ChevronsUpDown, Plus, Sparkles } from 'lucide-react' import * as React from 'react' import { @@ -13,6 +13,7 @@ import { DropdownMenuTrigger, } from './ui/dropdown-menu' import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar' +import { Badge } from './ui/badge' export function TeamSwitcher({ teams, @@ -26,6 +27,24 @@ export function TeamSwitcher({ const { isMobile } = useSidebar() const [activeTeam, setActiveTeam] = React.useState(teams[0]) + const getPlanBadge = (plan: string) => { + switch (plan.toLowerCase()) { + case 'premium': + return ( + + + Premium + + ) + case 'pro': + return Pro + case 'free': + return Free + default: + return {plan} + } + } + return ( @@ -33,44 +52,53 @@ export function TeamSwitcher({ -
+
- {activeTeam.name} - {activeTeam.plan} + {activeTeam.name} +
+ {getPlanBadge(activeTeam.plan)} +
- + - Teams + + Organizations + {teams.map((team, index) => ( setActiveTeam(team)} - className="gap-2 p-2" + className="gap-3 p-2.5 cursor-pointer hover:bg-accent/50 transition-colors" > -
- +
+ +
+
+
{team.name}
+
+ {getPlanBadge(team.plan)} +
- {team.name} - โŒ˜{index + 1} + โŒ˜{index + 1} ))} - -
+ +
-
Add team
+ Join Organization
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/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