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
13 changes: 11 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { ErrorBoundary } from "@/components/shared/error-boundary";
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 {
ThemeProvider,
themeInitScript,
} from "@/components/theme/theme-provider";
import { PageTransition } from "@/components/shared/page-transition";
import "./globals.css";

Expand Down Expand Up @@ -68,7 +71,13 @@ export default function RootLayout({
>
Skip to content
</a>
<ErrorBoundary>
<ErrorBoundary
showErrorDetails={process.env.NODE_ENV === "development"}
onError={(error, errorInfo) => {
console.error("Application error:", error);
console.error("Component stack:", errorInfo.componentStack);
}}
>
<WalletProvider>
<TooltipProvider>
<ToastProvider>
Expand Down
98 changes: 85 additions & 13 deletions src/components/course/course-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import Link from "next/link";
import { cn } from "@/lib/utils/cn";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Badge, difficultyVariant } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ProgressBar } from "@/components/course/progress-bar";
import { Clock, Users, Star, MoreVertical, Share, Bookmark } from "lucide-react";
import {
Clock,
Users,
Star,
MoreVertical,
Share,
Bookmark,
PlayCircle,
Plus,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -20,32 +30,83 @@ interface CourseCardProps {
course: Course;
enrolled?: boolean;
progress?: number;
onEnroll?: (courseId: string) => void;
onContinue?: (courseId: string) => void;
className?: string;
}


export const CourseCard = memo(function CourseCard({
course,
enrolled,
progress,
onEnroll,
onContinue,
className,
}: CourseCardProps) {
const handleEnroll = (e: React.MouseEvent) => {
e.preventDefault();
onEnroll?.(course.id);
};

const handleContinue = (e: React.MouseEvent) => {
e.preventDefault();
onContinue?.(course.id);
};

return (
<Link href={`/courses/${course.id}`}>
<Card
className={cn(
"group cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5",
className
"group cursor-pointer transition-all hover:shadow-lg hover:-translate-y-1",
className,
)}
>
{/* Image placeholder */}
<div className="h-40 rounded-t-xl bg-gradient-to-br from-stellar-purple/20 to-stellar-blue/20 flex items-center justify-center relative">
{/* Image placeholder with enrolled badge */}
<div className="h-40 rounded-t-xl bg-gradient-to-br from-stellar-purple/20 to-stellar-blue/20 flex items-center justify-center relative overflow-hidden">
<Star className="h-10 w-10 text-stellar-purple/40" />

<div className="absolute top-2 right-2" onClick={(e) => e.preventDefault()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') e.preventDefault(); }}>

{/* Enrolled badge */}
{enrolled && (
<div className="absolute top-2 left-2">
<Badge className="bg-green-600 hover:bg-green-700 text-white animate-in fade-in slide-in-from-top-2 duration-300">
✓ Enrolled
</Badge>
</div>
)}

{/* Action buttons on hover */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-2">
{enrolled ? (
<Button
size="sm"
variant="secondary"
className="gap-2"
onClick={handleContinue}
>
<PlayCircle className="h-4 w-4" />
Continue
</Button>
) : (
<Button size="sm" className="gap-2" onClick={handleEnroll}>
<Plus className="h-4 w-4" />
Enroll
</Button>
)}
</div>

<div
className="absolute top-2 right-2"
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") e.preventDefault();
}}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button aria-label="Course options" className="p-1.5 text-gray-500 hover:text-gray-900 bg-white/50 hover:bg-white/80 rounded-full transition-colors dark:text-gray-300 dark:hover:text-gray-100 dark:bg-gray-900/50 dark:hover:bg-gray-900/80">
<button
aria-label="Course options"
className="p-1.5 text-gray-500 hover:text-gray-900 bg-white/50 hover:bg-white/80 rounded-full transition-colors dark:text-gray-300 dark:hover:text-gray-100 dark:bg-gray-900/50 dark:hover:bg-gray-900/80"
>
<MoreVertical className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
Expand All @@ -70,18 +131,20 @@ export const CourseCard = memo(function CourseCard({
<Badge variant={difficultyVariant(course.difficulty)}>
{capitalize(course.difficulty)}
</Badge>
<span className="text-xs text-gray-500">{capitalize(course.category)}</span>
<span className="text-xs text-gray-500">
{capitalize(course.category)}
</span>
</div>

<h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors mb-1 dark:text-gray-100 dark:group-hover:text-primary-300">
<h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors mb-1 dark:text-gray-100 dark:group-hover:text-primary-300">
{course.title}
</h3>
<p className="text-sm text-gray-500 line-clamp-2 dark:text-gray-400">
{course.description}
</p>
</CardContent>

<CardFooter className="px-4 pb-4 pt-0">
<CardFooter className="px-4 pb-4 pt-0 flex-col gap-3">
<div className="flex items-center justify-between w-full text-xs text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
Expand All @@ -98,8 +161,17 @@ export const CourseCard = memo(function CourseCard({
</span>
</div>

{/* Progress bar with percentage label */}
{enrolled && progress !== undefined && (
<div className="w-full mt-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="w-full space-y-1 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
Progress
</span>
<span className="text-xs font-semibold text-stellar-purple">
{Math.round(progress)}%
</span>
</div>
<ProgressBar value={progress} size="sm" />
</div>
)}
Expand Down
Loading