advance
This commit is contained in:
42
app/(protected)/teacher/courses/[slug]/edit/page.tsx
Normal file → Executable file
42
app/(protected)/teacher/courses/[slug]/edit/page.tsx
Normal file → Executable file
@@ -1,13 +1,41 @@
|
||||
import { db } from "@/lib/prisma";
|
||||
import { requireTeacher } from "@/lib/auth/requireTeacher";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import TeacherEditCourseForm from "@/components/teacher/TeacherEditCourseForm";
|
||||
|
||||
type TeacherEditCoursePageProps = {
|
||||
params: Promise<{ slug: string }>;
|
||||
};
|
||||
export default async function CourseEditPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) redirect("/auth/login");
|
||||
|
||||
export default async function TeacherEditCoursePage({ params }: TeacherEditCoursePageProps) {
|
||||
await requireTeacher();
|
||||
const { slug } = await params;
|
||||
|
||||
return <TeacherEditCourseForm slug={slug} />;
|
||||
}
|
||||
// Fetch Course + Modules + Lessons
|
||||
const course = await db.course.findUnique({
|
||||
where: { slug: slug },
|
||||
include: {
|
||||
modules: {
|
||||
orderBy: { orderIndex: "asc" },
|
||||
include: {
|
||||
lessons: {
|
||||
orderBy: { orderIndex: "asc" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!course) notFound();
|
||||
|
||||
// Security Check
|
||||
if (course.authorId !== user.id && user.role !== "SUPER_ADMIN") {
|
||||
return <div>No autorizado</div>;
|
||||
}
|
||||
|
||||
// Transform Decimal to number for the UI component
|
||||
const courseData = {
|
||||
...course,
|
||||
price: course.price.toNumber(),
|
||||
};
|
||||
|
||||
return <TeacherEditCourseForm course={courseData} />;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateLesson } from "@/app/(protected)/teacher/actions";
|
||||
import VideoUpload from "@/components/teacher/VideoUpload"; // The component you created earlier
|
||||
|
||||
interface LessonEditorFormProps {
|
||||
lesson: {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
videoUrl?: string | null;
|
||||
};
|
||||
courseSlug: string;
|
||||
}
|
||||
|
||||
export function LessonEditorForm({ lesson, courseSlug }: LessonEditorFormProps) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [title, setTitle] = useState(lesson.title);
|
||||
const [description, setDescription] = useState(lesson.description ?? "");
|
||||
|
||||
// 1. Auto-save Video URL when upload finishes
|
||||
const handleVideoUploaded = async (url: string) => {
|
||||
toast.loading("Guardando video...");
|
||||
const res = await updateLesson(lesson.id, { videoUrl: url });
|
||||
|
||||
if (res.success) {
|
||||
toast.dismiss();
|
||||
toast.success("Video guardado correctamente");
|
||||
router.refresh(); // Update the UI to show the new video player
|
||||
} else {
|
||||
toast.error("Error al guardar el video en la base de datos");
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Save Text Changes (Title/Desc)
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
const res = await updateLesson(lesson.id, { title, description });
|
||||
if (res.success) {
|
||||
toast.success("Cambios guardados");
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error("Error al guardar cambios");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* LEFT: Video & Content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
|
||||
{/* Video Upload Section */}
|
||||
<section className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<h2 className="font-semibold text-slate-900">Video del Curso</h2>
|
||||
<p className="text-sm text-slate-500">Sube el video principal de esta lección.</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50/50">
|
||||
<VideoUpload
|
||||
lessonId={lesson.id}
|
||||
currentVideoUrl={lesson.videoUrl}
|
||||
onUploadComplete={handleVideoUploaded}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Text Content */}
|
||||
<section className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Título de la Lección</label>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2 outline-none focus:border-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Descripción / Notas</label>
|
||||
<textarea
|
||||
rows={6}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2 outline-none focus:border-black"
|
||||
placeholder="Escribe aquí el contenido de la lección..."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Settings / Actions */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Acciones</h3>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className="w-full bg-black text-white rounded-lg py-2.5 font-medium hover:bg-slate-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? "Guardando..." : "Guardar Cambios"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push(`/teacher/courses/${courseSlug}/edit`)}
|
||||
className="w-full mt-3 bg-white border border-slate-300 text-slate-700 rounded-lg py-2.5 font-medium hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Volver al Curso
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { db } from "@/lib/prisma";
|
||||
import { requireTeacher } from "@/lib/auth/requireTeacher";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { LessonEditorForm } from "./LessonEditorForm";
|
||||
|
||||
function getText(value: unknown): string {
|
||||
if (!value) return "";
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "object") {
|
||||
const text = value as Record<string, string>;
|
||||
return text.es || text.en || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string; lessonId: string }>;
|
||||
}
|
||||
|
||||
export default async function LessonPage({ params }: PageProps) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) redirect("/auth/login");
|
||||
|
||||
const { slug, lessonId } = await params;
|
||||
|
||||
// 1. Fetch Lesson + Course Info (for breadcrumbs)
|
||||
const lesson = await db.lesson.findUnique({
|
||||
where: { id: lessonId },
|
||||
include: {
|
||||
module: {
|
||||
include: {
|
||||
course: {
|
||||
select: { title: true, slug: true, authorId: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Security & Null Checks
|
||||
if (!lesson) notFound();
|
||||
if (lesson.module.course.authorId !== user.id) redirect("/teacher");
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 mb-6">
|
||||
<Link href="/teacher" className="hover:text-black">Cursos</Link>
|
||||
<span>/</span>
|
||||
<Link href={`/teacher/courses/${slug}/edit`} className="hover:text-black">
|
||||
{getText(lesson.module.course.title)}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-slate-900 font-medium">{getText(lesson.title)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">Editar Lección</h1>
|
||||
</div>
|
||||
|
||||
{/* The Client Form */}
|
||||
<LessonEditorForm
|
||||
lesson={{
|
||||
...lesson,
|
||||
title: getText(lesson.title),
|
||||
description: getText(lesson.description),
|
||||
}}
|
||||
courseSlug={slug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/(protected)/teacher/courses/[slug]/lessons/new/page.tsx
Normal file → Executable file
0
app/(protected)/teacher/courses/[slug]/lessons/new/page.tsx
Normal file → Executable file
14
app/(protected)/teacher/courses/[slug]/page.tsx
Normal file
14
app/(protected)/teacher/courses/[slug]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export default async function CourseDashboardPage({ params }: PageProps) {
|
||||
// 1. Get the slug from the URL
|
||||
const { slug } = await params;
|
||||
|
||||
// 2. Automatically redirect to the Edit page
|
||||
// This saves us from building a separate "Stats" page for now
|
||||
redirect(`/teacher/courses/${slug}/edit`);
|
||||
}
|
||||
0
app/(protected)/teacher/courses/new/page.tsx
Normal file → Executable file
0
app/(protected)/teacher/courses/new/page.tsx
Normal file → Executable file
Reference in New Issue
Block a user