Add cross-module lesson navigation and admin edit button
- Admin lesson editor now navigates across module boundaries (full course flat list) - Student lesson view shows "Edit" button for admins linking to lesson editor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,18 @@ export default async function LessonPage({ params }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="max-w-3xl mx-auto px-6 py-8">
|
<article className="max-w-3xl mx-auto px-6 py-8">
|
||||||
|
{/* Admin edit bar */}
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="flex items-center justify-end mb-4">
|
||||||
|
<Link
|
||||||
|
href={`/admin/courses/${lesson.module.course.id}/modules/${lesson.moduleId}/lessons/${lessonId}`}
|
||||||
|
className="btn-aubade text-xs px-3 py-1.5"
|
||||||
|
>
|
||||||
|
Редактировать урок
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h1 className="text-2xl font-bold mb-6 leading-snug">{lesson.title}</h1>
|
<h1 className="text-2xl font-bold mb-6 leading-snug">{lesson.title}</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props {
|
|||||||
export default async function LessonEditorPage({ params }: Props) {
|
export default async function LessonEditorPage({ params }: Props) {
|
||||||
const { courseId, moduleId, lessonId } = await params;
|
const { courseId, moduleId, lessonId } = await params;
|
||||||
|
|
||||||
const [lesson, siblings] = await Promise.all([
|
const [lesson, allCourseLessons] = await Promise.all([
|
||||||
prisma.lesson.findUnique({
|
prisma.lesson.findUnique({
|
||||||
where: { id: lessonId },
|
where: { id: lessonId },
|
||||||
include: {
|
include: {
|
||||||
@@ -28,29 +28,27 @@ export default async function LessonEditorPage({ params }: Props) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.lesson.findMany({
|
prisma.lesson.findMany({
|
||||||
where: { moduleId },
|
where: { module: { courseId } },
|
||||||
orderBy: { order: "asc" },
|
orderBy: [{ module: { order: "asc" } }, { order: "asc" }],
|
||||||
select: { id: true, title: true },
|
select: { id: true, title: true, moduleId: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!lesson || lesson.moduleId !== moduleId) notFound();
|
if (!lesson || lesson.moduleId !== moduleId) notFound();
|
||||||
|
|
||||||
const idx = siblings.findIndex((l) => l.id === lessonId);
|
const idx = allCourseLessons.findIndex((l) => l.id === lessonId);
|
||||||
const prevLesson = idx > 0 ? siblings[idx - 1] : null;
|
const prevLesson = idx > 0 ? allCourseLessons[idx - 1] : null;
|
||||||
const nextLesson = idx < siblings.length - 1 ? siblings[idx + 1] : null;
|
const nextLesson = idx < allCourseLessons.length - 1 ? allCourseLessons[idx + 1] : null;
|
||||||
|
|
||||||
// Serialize all Prisma proxy objects (DateTime, relations) before passing to Client Components
|
// Serialize all Prisma proxy objects (DateTime, relations) before passing to Client Components
|
||||||
const plain = JSON.parse(JSON.stringify({
|
const plain = JSON.parse(JSON.stringify({
|
||||||
files: lesson.files,
|
files: lesson.files,
|
||||||
homework: lesson.homework,
|
homework: lesson.homework,
|
||||||
quiz: lesson.quiz,
|
quiz: lesson.quiz,
|
||||||
siblings,
|
|
||||||
})) as {
|
})) as {
|
||||||
files: typeof lesson.files;
|
files: typeof lesson.files;
|
||||||
homework: typeof lesson.homework;
|
homework: typeof lesson.homework;
|
||||||
quiz: typeof lesson.quiz;
|
quiz: typeof lesson.quiz;
|
||||||
siblings: typeof siblings;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,10 +74,9 @@ export default async function LessonEditorPage({ params }: Props) {
|
|||||||
published: lesson.published,
|
published: lesson.published,
|
||||||
}}
|
}}
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
moduleId={moduleId}
|
|
||||||
courseSlug={lesson.module.course.slug}
|
courseSlug={lesson.module.course.slug}
|
||||||
prevLesson={plain.siblings[idx - 1] ?? null}
|
prevLesson={prevLesson}
|
||||||
nextLesson={plain.siblings[idx + 1] ?? null}
|
nextLesson={nextLesson}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,18 @@ interface LessonData {
|
|||||||
interface SiblingLesson {
|
interface SiblingLesson {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
moduleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LessonEditor({
|
export function LessonEditor({
|
||||||
lesson,
|
lesson,
|
||||||
courseId,
|
courseId,
|
||||||
moduleId,
|
|
||||||
courseSlug,
|
courseSlug,
|
||||||
prevLesson,
|
prevLesson,
|
||||||
nextLesson,
|
nextLesson,
|
||||||
}: {
|
}: {
|
||||||
lesson: LessonData;
|
lesson: LessonData;
|
||||||
courseId: string;
|
courseId: string;
|
||||||
moduleId: string;
|
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
prevLesson?: SiblingLesson | null;
|
prevLesson?: SiblingLesson | null;
|
||||||
nextLesson?: SiblingLesson | null;
|
nextLesson?: SiblingLesson | null;
|
||||||
@@ -156,8 +155,8 @@ export function LessonEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(lessonId: string) {
|
function navigateTo(target: SiblingLesson) {
|
||||||
router.push(`/admin/courses/${courseId}/modules/${moduleId}/lessons/${lessonId}`);
|
router.push(`/admin/courses/${courseId}/modules/${target.moduleId}/lessons/${target.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -213,18 +212,18 @@ export function LessonEditor({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => prevLesson && navigateTo(prevLesson.id)}
|
onClick={() => prevLesson && navigateTo(prevLesson)}
|
||||||
disabled={!prevLesson}
|
disabled={!prevLesson}
|
||||||
title={prevLesson ? `← ${prevLesson.title}` : "Первый урок в модуле"}
|
title={prevLesson ? `← ${prevLesson.title}` : "Первый урок курса"}
|
||||||
className="btn-aubade flex items-center gap-1 px-2 py-2 text-sm disabled:opacity-30"
|
className="btn-aubade flex items-center gap-1 px-2 py-2 text-sm disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={14} />
|
<ChevronLeft size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => nextLesson && navigateTo(nextLesson.id)}
|
onClick={() => nextLesson && navigateTo(nextLesson)}
|
||||||
disabled={!nextLesson}
|
disabled={!nextLesson}
|
||||||
title={nextLesson ? `${nextLesson.title} →` : "Последний урок в модуле"}
|
title={nextLesson ? `${nextLesson.title} →` : "Последний урок курса"}
|
||||||
className="btn-aubade flex items-center gap-1 px-2 py-2 text-sm disabled:opacity-30"
|
className="btn-aubade flex items-center gap-1 px-2 py-2 text-sm disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<ChevronRight size={14} />
|
<ChevronRight size={14} />
|
||||||
|
|||||||
Reference in New Issue
Block a user