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
2,009 changes: 1,516 additions & 493 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/app/(auth)/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function OnboardingPage() {
const [saving, setSaving] = useState(false);

const goNext = useCallback(() => {
setStep((prev) => Math.min(prev + 1, steps.length - 1));
setStep((prev) => Math.min(prev + 1, 3));
}, []);

const goPrev = useCallback(() => {
Expand Down
18 changes: 6 additions & 12 deletions src/app/credentials/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CredentialCard } from "@/components/credentials/credential-card";
import { CredentialBadge } from "@/components/credentials/credential-badge";
import { Card, CardContent } from "@/components/ui/card";
import { LoadingSkeleton } from "@/components/shared/loading-skeleton";
import { EmptyState } from "@/components/shared/empty-state";
import { Award, Shield } from "lucide-react";

export default function CredentialsPage() {
Expand Down Expand Up @@ -34,18 +35,11 @@ export default function CredentialsPage() {
</div>

{credentials.length === 0 ? (
<Card>
<CardContent className="py-16 text-center">
<Award className="mx-auto h-12 w-12 text-gray-300 mb-4" />
<h2 className="text-lg font-semibold text-gray-900 mb-2">
No Credentials Yet
</h2>
<p className="text-sm text-gray-500 max-w-md mx-auto">
Complete courses and pass quizzes to earn verifiable credential
NFTs on the Stellar network.
</p>
</CardContent>
</Card>
<EmptyState
icon={Award}
title="No Credentials Yet"
description="Complete courses and pass quizzes to earn verifiable credential NFTs on the Stellar network."
/>
) : (
<>
{/* Badge showcase */}
Expand Down
30 changes: 14 additions & 16 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ProgressBar } from "@/components/course/progress-bar";
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 { useCourseStore } from "@/store/course-store";
import {
BookOpen,
Expand Down Expand Up @@ -160,17 +161,16 @@ export default function DashboardPage() {
</Link>
</div>
{enrolledCourses.length === 0 ? (
<Card>
<CardContent className="py-8 text-center">
<BookOpen className="mx-auto h-10 w-10 text-gray-300 mb-3" />
<p className="text-gray-500">You haven&apos;t enrolled in any courses yet.</p>
<EmptyState
icon={BookOpen}
title="No Courses Yet"
description="You haven't enrolled in any courses yet."
action={
<Link href="/courses">
<Button className="mt-4" size="sm">
Explore Courses
</Button>
<Button size="sm">Explore Courses</Button>
</Link>
</CardContent>
</Card>
}
/>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{enrolledCourses.map(({ enrollment, course }) => (
Expand Down Expand Up @@ -201,13 +201,11 @@ export default function DashboardPage() {
My Credentials
</h2>
{credentials.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<Award className="mx-auto h-12 w-12 text-gray-300 mb-3" />
<p className="text-gray-500">You haven&apos;t earned any credentials yet.</p>
<p className="text-sm text-gray-400 mt-1">Complete courses to earn verifiable credentials on the Stellar network.</p>
</CardContent>
</Card>
<EmptyState
icon={Award}
title="No Credentials Yet"
description="You haven't earned any credentials yet. Complete courses to earn verifiable credentials on the Stellar network."
/>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{credentials.map((cred) => (
Expand Down
4 changes: 4 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
animation: fadeIn 0.2s ease-out;
}

#page-content.no-animation {
animation: none !important;
}

@keyframes fadeIn {
from {
opacity: 0;
Expand Down
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ToastProvider } from "@/components/shared/toast-provider";
import { AuthProvider } from "@/components/auth/auth-provider";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider, themeInitScript } from "@/components/theme/theme-provider";
import { PageTransition } from "@/components/shared/page-transition";
import "./globals.css";

const inter = Inter({
Expand Down Expand Up @@ -76,7 +77,7 @@ export default function RootLayout({
<Header />
<AuthProvider>
<div id="main-content" className="flex-1">
{children}
<PageTransition>{children}</PageTransition>
</div>
</AuthProvider>
<Footer />
Expand Down
16 changes: 8 additions & 8 deletions src/app/rewards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TokenBalance } from "@/components/rewards/token-balance";
import { ClaimButton } from "@/components/rewards/claim-button";
import { LoadingSkeleton } from "@/components/shared/loading-skeleton";
import { EmptyState } from "@/components/shared/empty-state";
import { useToastContext } from "@/components/shared/toast";
import { Gift, Coins, TrendingUp } from "lucide-react";

Expand Down Expand Up @@ -57,14 +58,13 @@ export default function RewardsPage() {
{/* Token Balances */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 mb-8">
{balances.length === 0 ? (
<Card>
<CardContent className="py-8 text-center">
<Coins className="mx-auto h-10 w-10 text-gray-300 mb-3" />
<p className="text-sm text-gray-500">
No tokens yet. Complete courses to earn LEARN tokens.
</p>
</CardContent>
</Card>
<div className="col-span-full">
<EmptyState
icon={Coins}
title="No Tokens Yet"
description="Complete courses to earn LEARN tokens."
/>
</div>
) : (
balances.map((balance) => (
<TokenBalance key={balance.tokenCode} balance={balance} />
Expand Down
1 change: 0 additions & 1 deletion src/components/course/module-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export function ModuleList({
<div
key={mod.id}
role="listitem"
aria-disabled="true"
tabIndex={-1}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-3",
Expand Down
14 changes: 8 additions & 6 deletions src/components/course/quiz-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from "react";
import { cn } from "@/lib/utils/cn";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/shared/empty-state";
import { CheckCircle, XCircle, ChevronRight, Trophy } from "lucide-react";
import type { Quiz, QuizAttempt, QuizQuestion } from "@/types/quiz";

Expand All @@ -26,11 +27,11 @@ export function QuizInterface({ quiz, onSubmit, onRetry, className }: QuizInterf

if (!question) {
return (
<Card className={cn("max-w-2xl mx-auto", className)}>
<CardContent className="py-16 text-center">
<p className="text-gray-500">This quiz has no questions yet.</p>
</CardContent>
</Card>
<EmptyState
className={cn("max-w-2xl mx-auto", className)}
title="No Questions"
description="This quiz has no questions yet."
/>
);
}

Expand Down Expand Up @@ -124,7 +125,8 @@ 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 && (
<div className="pt-4 text-center">
<Button onClick={onRetry} variant="outline">
Expand Down
79 changes: 79 additions & 0 deletions src/components/shared/confirm-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { ReactNode } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils/cn";

export interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel?: string;
cancelLabel?: string;
onConfirm: () => void;
loading?: boolean;
destructive?: boolean;
icon?: ReactNode;
}

export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
onConfirm,
loading = false,
destructive = false,
icon,
}: ConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader className="sm:text-center">
<div className={cn(
"mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full",
destructive ? "bg-red-100" : "bg-primary-100"
)}>
{icon ? (
icon
) : (
<AlertTriangle className={cn("h-6 w-6", destructive ? "text-red-600" : "text-primary-600")} />
)}
</div>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-center">
<DialogClose asChild>
<Button variant="outline" disabled={loading}>
{cancelLabel}
</Button>
</DialogClose>
<Button
variant={destructive ? "destructive" : "default"}
onClick={(e) => {
e.preventDefault();
onConfirm();
}}
disabled={loading}
>
{loading ? "Please wait..." : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
31 changes: 31 additions & 0 deletions src/components/shared/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from "react";
import { LucideIcon } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils/cn";

export interface EmptyStateProps {
icon?: LucideIcon;
title: string;
description?: string;
action?: ReactNode;
className?: string;
}

export function EmptyState({
icon: Icon,
title,
description,
action,
className,
}: EmptyStateProps) {
return (
<Card className={cn("border-dashed", className)}>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
{Icon && <Icon className="mx-auto h-12 w-12 text-gray-300 mb-4" />}
<h3 className="text-lg font-semibold text-gray-900 mb-1">{title}</h3>
{description && <p className="text-gray-500 mb-4 max-w-sm">{description}</p>}
{action && <div>{action}</div>}
</CardContent>
</Card>
);
}
18 changes: 10 additions & 8 deletions src/components/shared/loading-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
SkeletonCircle,
SkeletonStack,
SkeletonText,
SkeletonRect,
SkeletonCard,
} from "@/components/ui/skeleton";

interface LoadingSkeletonProps {
Expand All @@ -28,16 +30,16 @@ export function LoadingSkeleton({
if (variant === "circle") {
return <SkeletonCircle key={i} />;
}
return <Skeleton key={i} className="h-48 w-full rounded-xl" />;
return <SkeletonCard key={i} />;
})}
</div>
);
}

export function CourseCardSkeleton() {
return (
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<Skeleton className="h-40 w-full rounded-none" />
<SkeletonCard>
<SkeletonRect className="h-40 w-full rounded-none" />
<div className="p-4 space-y-3">
<div className="flex gap-2">
<Skeleton className="h-5 w-16 rounded-full" />
Expand All @@ -46,7 +48,7 @@ export function CourseCardSkeleton() {
<SkeletonText className="h-5 w-3/4" />
<SkeletonStack lines={2} />
</div>
</div>
</SkeletonCard>
);
}

Expand All @@ -55,7 +57,7 @@ export function DashboardSkeleton() {
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-xl" />
<SkeletonRect key={i} className="h-24 w-full rounded-xl" />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
Expand All @@ -70,7 +72,7 @@ export function DashboardSkeleton() {
export function CredentialCardSkeleton() {
return (
<div className="rounded-xl border border-gray-200 bg-white p-4 flex gap-4">
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-xl" />
<SkeletonRect className="h-12 w-12 flex-shrink-0 rounded-xl" />
<div className="flex-1 space-y-2">
<SkeletonText className="h-5 w-1/2" />
<SkeletonStack lines={2} />
Expand Down Expand Up @@ -107,12 +109,12 @@ export function VerifySkeleton() {
export function CourseDetailSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-48 w-full rounded-xl" />
<SkeletonRect className="h-48 w-full rounded-xl" />
<SkeletonText className="h-8 w-2/3" />
<SkeletonStack lines={4} />
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full rounded-lg" />
<SkeletonRect key={i} className="h-16 w-full rounded-lg" />
))}
</div>
</div>
Expand Down
Loading