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

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File