This commit is contained in:
Marcelo
2026-02-17 00:07:00 +00:00
parent b7a86a2d1c
commit be4ca2ed78
92 changed files with 6850 additions and 1188 deletions

123
components/CourseCard.tsx Normal file → Executable file
View File

@@ -1,43 +1,104 @@
import Link from "next/link";
import type { Course } from "@/types/course";
import type { Prisma } from "@prisma/client";
import ProgressBar from "@/components/ProgressBar";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
type PublicCourseCardCourse = Prisma.CourseGetPayload<{
include: {
author: {
select: {
fullName: true;
};
};
modules: {
select: {
_count: {
select: {
lessons: true;
};
};
};
};
_count: {
select: {
enrollments: true;
};
};
};
}>;
type CourseCardProps = {
course: Course;
course: PublicCourseCardCourse;
progress?: number;
};
export default function CourseCard({ course, progress = 0 }: CourseCardProps) {
return (
<Link
href={`/courses/${course.slug}`}
className="group flex h-full flex-col justify-between rounded-2xl border border-slate-300 bg-white p-6 shadow-sm transition hover:-translate-y-0.5 hover:border-brand/60"
>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="rounded-full bg-[#ead7a0] px-3 py-1 text-xs font-semibold text-[#8a6b00]">{course.level}</span>
<span className="text-sm font-semibold text-slate-700">Rating {course.rating.toFixed(1)}</span>
</div>
<h3 className="text-2xl leading-tight text-[#212937] md:text-4xl">{course.title}</h3>
<p className="text-base text-slate-600 md:text-lg">{course.summary}</p>
</div>
const levelBadgeClass = (level: PublicCourseCardCourse["level"]) => {
if (level === "BEGINNER") return "bg-emerald-100 text-emerald-900 border border-emerald-200";
if (level === "INTERMEDIATE") return "bg-sky-100 text-sky-900 border border-sky-200";
return "bg-violet-100 text-violet-900 border border-violet-200";
};
<div className="mt-5 border-t border-slate-200 pt-4 text-base text-slate-600 md:text-lg">
{progress > 0 ? (
<div className="mb-4">
<ProgressBar value={progress} label={`Progress ${progress}%`} />
const levelLabel = (level: PublicCourseCardCourse["level"]) => {
if (level === "BEGINNER") return "Beginner";
if (level === "INTERMEDIATE") return "Intermediate";
return "Advanced";
};
const getText = (value: unknown): string => {
if (!value) return "";
if (typeof value === "string") return value;
if (typeof value === "object") {
const record = value as Record<string, unknown>;
if (typeof record.en === "string") return record.en;
if (typeof record.es === "string") return record.es;
}
return "";
};
export default function CourseCard({ course, progress = 0 }: CourseCardProps) {
const lessonsCount = course.modules.reduce((total, module) => total + module._count.lessons, 0);
const title = getText(course.title) || "Untitled course";
const summary = getText(course.description) || "Course details will be published soon.";
const instructor = course.author.fullName || "ACVE Team";
return (
<Link href={`/courses/${course.slug}`} className="group block h-full">
<Card className="h-full border-border hover:-translate-y-0.5 hover:border-primary/60 transition-all duration-200">
<CardHeader className="space-y-3 pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="secondary" className={levelBadgeClass(course.level)}>
{levelLabel(course.level)}
</Badge>
<Badge variant="outline">{course.status.toLowerCase()}</Badge>
</div>
<span className="text-sm font-semibold text-muted-foreground">{lessonsCount} lessons</span>
</div>
) : null}
<div className="mb-1 flex items-center justify-between">
<span>{course.weeks} weeks</span>
<span>{course.lessonsCount} lessons</span>
</div>
<div className="mb-3 flex items-center justify-between">
<span>{course.students.toLocaleString()} students</span>
<span className="text-brand transition-transform group-hover:translate-x-1">{">"}</span>
</div>
<div className="text-sm font-semibold text-slate-700 md:text-base">By {course.instructor}</div>
</div>
<CardTitle className="text-[1.85rem] leading-tight md:text-[2rem]">{title}</CardTitle>
<CardDescription className="text-base leading-relaxed">{summary}</CardDescription>
</CardHeader>
<CardContent className="pt-4 text-base text-muted-foreground">
{progress > 0 ? (
<div className="mb-4">
<ProgressBar value={progress} label={`Progress ${progress}%`} />
</div>
) : null}
<div className="mb-1 flex items-center justify-between">
<span>{course._count.enrollments.toLocaleString()} enrolled</span>
<span>{lessonsCount} lessons</span>
</div>
</CardContent>
<CardFooter className="flex items-center justify-between border-t bg-muted/50 px-6 py-4 text-sm text-muted-foreground">
<span>By {instructor}</span>
<div className="flex items-center gap-2">
<span className="font-semibold text-primary">View Course</span>
<span className="text-primary opacity-0 transition-opacity group-hover:opacity-100">{">"}</span>
</div>
</CardFooter>
</Card>
</Link>
);
}