Stage 2: student lesson viewer, Kinescope player, PDF files, prev/next nav, My Courses dashboard
This commit is contained in:
@@ -1,33 +1,106 @@
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { LogoutButton } from "@/components/layout/logout-button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function StudentDashboard() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
if (!session) redirect("/login");
|
||||
|
||||
const enrollments = await prisma.courseEnrollment.findMany({
|
||||
where: { userId: session.user.id },
|
||||
include: {
|
||||
course: {
|
||||
include: {
|
||||
modules: {
|
||||
include: { _count: { select: { lessons: true } } },
|
||||
},
|
||||
_count: { select: { modules: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { enrolledAt: "desc" },
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const active = enrollments.filter((e) => !e.expiresAt || e.expiresAt > now);
|
||||
const expired = enrollments.filter((e) => e.expiresAt && e.expiresAt <= now);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-amber-50">
|
||||
<header className="bg-white border-b border-amber-100 px-6 py-4 flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold text-amber-900">Second Brain</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">{session.user.name}</span>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
<main className="max-w-4xl mx-auto px-6 py-10">
|
||||
<h2 className="text-2xl font-semibold text-gray-800 mb-2">
|
||||
Добро пожаловать, {session.user.name}!
|
||||
</h2>
|
||||
<p className="text-gray-500 mb-8">Ваши курсы появятся здесь.</p>
|
||||
<div className="bg-white rounded-2xl border border-amber-100 p-8 text-center text-gray-400">
|
||||
<main className="max-w-4xl mx-auto px-6 py-10 w-full">
|
||||
<h1 className="text-2xl font-bold mb-1">Мои курсы</h1>
|
||||
<p className="text-sm mb-8" style={{ color: "var(--muted-foreground)" }}>
|
||||
{active.length} активных курсов
|
||||
</p>
|
||||
|
||||
{active.length === 0 ? (
|
||||
<div className="card-aubade p-12 text-center">
|
||||
<p className="text-4xl mb-3">📚</p>
|
||||
<p>Доступных курсов пока нет.</p>
|
||||
<p className="text-sm mt-1">Обратитесь к администратору за доступом.</p>
|
||||
<p className="font-medium">Доступных курсов пока нет</p>
|
||||
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
|
||||
Обратитесь к администратору за доступом
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{active.map(({ course, expiresAt }) => {
|
||||
const totalLessons = course.modules.reduce((s, m) => s + m._count.lessons, 0);
|
||||
return (
|
||||
<Link
|
||||
key={course.id}
|
||||
href={`/courses/${course.slug}`}
|
||||
className="card-aubade p-0 overflow-hidden flex flex-col"
|
||||
>
|
||||
{course.coverImage ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={course.coverImage} alt={course.title} className="w-full aspect-video object-cover" />
|
||||
) : (
|
||||
<div className="w-full aspect-video flex items-center justify-center text-4xl" style={{ backgroundColor: "var(--color-surface)" }}>
|
||||
📚
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex-1">
|
||||
<h2 className="font-bold text-base leading-tight">{course.title}</h2>
|
||||
{course.description && (
|
||||
<p className="text-xs mt-1 line-clamp-2" style={{ color: "var(--muted-foreground)" }}>
|
||||
{course.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
{course._count.modules} модулей · {totalLessons} уроков
|
||||
</span>
|
||||
{expiresAt && (
|
||||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
до {new Date(expiresAt).toLocaleDateString("ru-RU")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expired.length > 0 && (
|
||||
<div className="mt-10">
|
||||
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
|
||||
Доступ истёк
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{expired.map(({ course, expiresAt }) => (
|
||||
<div key={course.id} className="flex items-center justify-between px-4 py-3 opacity-50" style={{ border: "2px solid var(--border)" }}>
|
||||
<span className="text-sm font-medium">{course.title}</span>
|
||||
<span className="text-xs" style={{ color: "oklch(0.577 0.245 27.325)" }}>
|
||||
Истёк {new Date(expiresAt!).toLocaleDateString("ru-RU")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user