Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 38 additions & 63 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useRequireAuth } from "@/lib/hooks/use-require-auth";
import { useCourses, useRecommendedCourses } from "@/lib/hooks/use-courses";
import { useRewards } from "@/lib/hooks/use-rewards";
import { useCredentials } from "@/lib/hooks/use-credentials";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card";
import { CourseCard } from "@/components/course/course-card";
import { ProgressBar } from "@/components/course/progress-bar";
import { CourseProgressCard } from "@/components/course/course-progress-card";
import { BalanceDisplay } from "@/components/wallet/balance-display";
import { CredentialCard } from "@/components/credentials/credential-card";
import { DashboardSkeleton } from "@/components/shared/loading-skeleton";
import { EmptyState } from "@/components/shared/empty-state";
import { StatsCard } from "@/components/shared/stats-card";
import { useCourseStore } from "@/store/course-store";
import {
BookOpen,
Expand Down Expand Up @@ -76,52 +77,30 @@ export default function DashboardPage() {
<TabsContent value="overview" className="space-y-8 focus-visible:outline-none focus-visible:ring-0">
{/* Stats */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-100">
<BookOpen className="h-5 w-5 text-primary-600" />
</div>
<div>
<p className="text-2xl font-bold">{enrollments.length}</p>
<p className="text-xs text-gray-500">Enrolled Courses</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
<TrendingUp className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold">
{enrollments.filter((e) => e.progress === 100).length}
</p>
<p className="text-xs text-gray-500">Completed</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-stellar-purple/10">
<Trophy className="h-5 w-5 text-stellar-purple" />
</div>
<div>
<p className="text-2xl font-bold">{totalBalance.toFixed(1)}</p>
<p className="text-xs text-gray-500">LEARN Tokens</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
<Award className="h-5 w-5 text-yellow-600" />
</div>
<div>
<p className="text-2xl font-bold">{credentials.length}</p>
<p className="text-xs text-gray-500">Credentials</p>
</div>
</CardContent>
</Card>
<StatsCard
icon={BookOpen}
label="Enrolled Courses"
value={enrollments.length}
color="primary"
/>
<StatsCard
icon={TrendingUp}
label="Completed"
value={enrollments.filter((e) => e.progress === 100).length}
color="success"
/>
<StatsCard
icon={Trophy}
label="LEARN Tokens"
value={totalBalance.toFixed(1)}
color="purple"
/>
<StatsCard
icon={Award}
label="Credentials"
value={credentials.length}
color="warning"
/>
</div>

<section>
Expand Down Expand Up @@ -174,21 +153,17 @@ export default function DashboardPage() {
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{enrolledCourses.map(({ enrollment, course }) => (
<Card key={enrollment.id}>
<CardContent className="p-4">
<h3 className="font-semibold text-gray-900 mb-2">
{course?.title || "Course"}
</h3>
<ProgressBar
value={progress[enrollment.courseId]?.progressPercent ?? enrollment.progress}
/>
<Link href={`/courses/${enrollment.courseId}`}>
<Button variant="ghost" size="sm" className="mt-3 w-full">
Continue Learning
</Button>
</Link>
</CardContent>
</Card>
<CourseProgressCard
key={enrollment.id}
courseId={enrollment.courseId}
courseTitle={course?.title || "Course"}
progress={
progress[enrollment.courseId]?.progressPercent ??
enrollment.progress
}
completedModules={enrollment.completedModules?.length}
moduleCount={course?.totalModules}
/>
))}
</div>
)}
Expand Down
2 changes: 0 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ export const metadata: Metadata = {
},
};

import { PageTransition } from "@/components/shared/page-transition";

export default function RootLayout({
children,
}: {
Expand Down
90 changes: 90 additions & 0 deletions src/components/course/course-progress-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { memo } from "react";
import Link from "next/link";
import { cn } from "@/lib/utils/cn";
import { Card, CardContent } from "@/components/ui/card";
import { ProgressBar } from "@/components/course/progress-bar";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ArrowRight, CheckCircle2, Layers } from "lucide-react";

export interface CourseProgressCardProps {
courseId: string;
courseTitle: string;
/** 0-100 completion percentage. */
progress: number;
completedModules?: number;
moduleCount?: number;
/** Optional href for the continue action. Defaults to the course detail page. */
continueHref?: string;
className?: string;
}

/**
* Dedicated card for an enrolled course on the dashboard. Shows the course
* title, an animated progress bar, module count and a quick continue action,
* plus explicit completion status.
*/
export const CourseProgressCard = memo(function CourseProgressCard({
courseId,
courseTitle,
progress,
completedModules,
moduleCount,
continueHref,
className,
}: CourseProgressCardProps) {
const clamped = Math.min(100, Math.max(0, progress));
const isComplete = clamped >= 100;

return (
<Card
className={cn(
"flex h-full flex-col transition-shadow hover:shadow-md",
className
)}
>
<CardContent className="flex flex-1 flex-col p-4">
<div className="mb-3 flex items-start justify-between gap-2">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{courseTitle}
</h3>
<Badge
variant={isComplete ? "success" : "secondary"}
className="flex-shrink-0"
>
{isComplete ? "Completed" : "In Progress"}
</Badge>
</div>

{moduleCount !== undefined && (
<p className="mb-2 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
<Layers className="h-3.5 w-3.5" aria-hidden="true" />
{completedModules !== undefined
? `${completedModules} of ${moduleCount} modules`
: `${moduleCount} modules`}
</p>
)}

<ProgressBar value={clamped} className="mb-4" />

<div className="mt-auto">
<Link href={continueHref ?? `/courses/${courseId}`} className="block">
<Button variant="ghost" size="sm" className="w-full gap-1">
{isComplete ? (
<>
Review Course <CheckCircle2 className="h-4 w-4" />
</>
) : (
<>
Continue Learning <ArrowRight className="h-4 w-4" />
</>
)}
</Button>
</Link>
</div>
</CardContent>
</Card>
);
});
2 changes: 0 additions & 2 deletions src/components/course/module-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ export function ModuleList({
aria-current={isCurrent ? "step" : undefined}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-gray-50 cursor-pointer dark:hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:outline-none",
isCurrent && "bg-primary-50 border border-primary-200"
"flex items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-gray-50 cursor-pointer dark:hover:bg-gray-800",
isCurrent && "bg-primary-50 border border-primary-200 dark:bg-primary-900/30 dark:border-primary-800"
)}
>
Expand Down
1 change: 1 addition & 0 deletions src/components/course/quiz-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export function QuizInterface({ quiz, onSubmit, onRetry, className }: QuizInterf
</Button>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
You need {quiz.passingScore}% to pass.
</p>
</div>
)}
{!attempt.passed && onRetry && (
Expand Down
2 changes: 2 additions & 0 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { LogOut, Settings, User } from "lucide-react";
import { Sun, Moon } from "lucide-react";
import { useTheme } from "@/components/theme/theme-provider";
import { NotificationBell } from "@/components/shared/notification-bell";

export function Header() {
const { isAuthenticated, walletAddress } = useAuthStore();
Expand Down Expand Up @@ -63,6 +64,7 @@ export function Header() {

{/* Right side */}
<div className="flex items-center gap-3">
{isAuthenticated && <NotificationBell />}
{isAuthenticated && walletAddress && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down
Loading