This commit is contained in:
eric
2026-03-12 00:34:50 -05:00
parent 0c2ccadb6b
commit 5caf970c7e
26 changed files with 1528 additions and 288 deletions

View File

@@ -1,30 +1,46 @@
"use client";
import { useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { Header } from "../../../components";
import { VideoPlayer } from "../../../components/VideoPlayer";
import { LessonMarkdown } from "./LessonMarkdown";
import { LessonLayout } from "@/app/components/content/LessonLayout";
import { getLesson } from "../../../config/lessons";
import { canWatchLesson } from "../../../lib/payment";
import { setLastWatched, markCompleted } from "../../../lib/learning";
import { DOWNLOAD_CONFIG } from "../../../config/course";
import { setLastWatched, markCompleted, getCompletedLessons } from "../../../lib/learning";
import { CURRICULUM_CONFIG, DOWNLOAD_CONFIG } from "../../../config/course";
import { CLOUDPHONE_COURSE_CONFIG } from "@/config/content.config";
import { getPrevNextLesson } from "@/lib/content/course-utils";
const TOTAL_LESSONS = CURRICULUM_CONFIG.modules.reduce((s, m) => s + m.lessons.length, 0);
export default function LessonPage() {
const params = useParams();
const router = useRouter();
const moduleIndex = Number(params.moduleIndex);
const lessonIndex = Number(params.lessonIndex);
const lesson = getLesson(moduleIndex, lessonIndex);
const unlocked = canWatchLesson(moduleIndex, lessonIndex);
const isFreeTrial = moduleIndex === 0 && lessonIndex < 2;
const [completed, setCompleted] = useState(() => getCompletedLessons().size);
const { prev: prevLesson, next: nextLesson } = getPrevNextLesson(
CURRICULUM_CONFIG.modules,
moduleIndex,
lessonIndex
);
useEffect(() => {
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
}, [moduleIndex, lessonIndex, lesson?.name, unlocked]);
useEffect(() => {
const onProgress = () => setCompleted(getCompletedLessons().size);
window.addEventListener("learning:progress", onProgress);
return () => window.removeEventListener("learning:progress", onProgress);
}, []);
if (!lesson || isNaN(moduleIndex) || isNaN(lessonIndex)) {
return (
<div className="min-h-screen bg-[var(--background)]">
@@ -32,9 +48,7 @@ export default function LessonPage() {
<main className="pt-20 pb-16">
<div className="mx-auto max-w-4xl px-4 py-16 text-center">
<h1 className="mb-4 text-2xl font-bold"></h1>
<p className="mb-6 text-[var(--muted-foreground)]">
</p>
<p className="mb-6 text-[var(--muted-foreground)]"></p>
<Link
href="/cloudphone"
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
@@ -50,100 +64,81 @@ export default function LessonPage() {
return (
<div className="min-h-screen bg-[var(--background)] text-[var(--foreground)]">
<Header />
<main className="pt-14 pb-16 md:pt-16">
<div className="mx-auto max-w-4xl px-4 sm:px-6">
<nav className="animate__animated animate__fadeInDown mb-6 flex items-center gap-2 text-sm" style={{ animationFillMode: "both" }}>
<button
type="button"
onClick={() => router.back()}
className="flex items-center gap-1 text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-[var(--muted-foreground)]">/</span>
<Link href="/cloudphone" className="text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]">
</Link>
</nav>
<h1 className="animate__animated animate__fadeInUp mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl" style={{ animationDelay: "0.1s", animationFillMode: "both" }}>
{lesson.name}
{isFreeTrial && (
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
</span>
)}
</h1>
<p className="mb-8 text-sm text-[var(--muted-foreground)]">
{lesson.duration}
</p>
<div className="animate__animated animate__fadeInUp relative mb-12" style={{ animationDelay: "0.2s", animationFillMode: "both" }}>
{unlocked ? (
<VideoPlayer
src={lesson.videoUrl}
title={lesson.name}
videoId={`cloudphone-${moduleIndex}-${lessonIndex}`}
className="shadow-xl"
onComplete={() => markCompleted(moduleIndex, lessonIndex)}
/>
) : (
<div className="relative overflow-hidden rounded-xl bg-black md:rounded-2xl">
<div className="aspect-video flex flex-col items-center justify-center gap-4 bg-zinc-900">
<span className="text-6xl">🔒</span>
<p className="text-lg text-white/90"></p>
<Link
href="/cloudphone/pay"
className="rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
>
</Link>
</div>
<LessonLayout
courseConfig={CLOUDPHONE_COURSE_CONFIG}
moduleIndex={moduleIndex}
lessonIndex={lessonIndex}
lessonName={lesson.name}
lessonDuration={lesson.duration}
isFreeTrial={isFreeTrial}
unlocked={unlocked}
totalLessons={TOTAL_LESSONS}
completedCount={completed}
prevLesson={prevLesson}
nextLesson={nextLesson}
courseHomeHref="/cloudphone"
payHref="/cloudphone/pay"
>
<div className="animate__animated animate__fadeInUp relative mb-12" style={{ animationDelay: "0.2s", animationFillMode: "both" }}>
{unlocked ? (
<VideoPlayer
src={lesson.videoUrl}
title={lesson.name}
videoId={`cloudphone-${moduleIndex}-${lessonIndex}`}
className="shadow-xl"
onComplete={() => markCompleted(moduleIndex, lessonIndex)}
/>
) : (
<div className="relative overflow-hidden rounded-xl bg-black md:rounded-2xl">
<div className="aspect-video flex flex-col items-center justify-center gap-4 bg-zinc-900">
<span className="text-6xl">🔒</span>
<p className="text-lg text-white/90"></p>
<Link
href="/cloudphone/pay"
className="rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
>
</Link>
</div>
)}
</div>
{unlocked && (
<div className="animate__animated animate__fadeInUp relative rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8" style={{ animationDelay: "0.3s", animationFillMode: "both" }}>
<h2 className="mb-6 flex items-center gap-2 text-lg font-semibold">
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</h2>
<LessonMarkdown content={lesson.markdown} />
</div>
)}
{DOWNLOAD_CONFIG.enabled && unlocked && (
<div className="mt-8 rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
{DOWNLOAD_CONFIG.title}
</h2>
<p className="mb-4 text-sm text-[var(--muted-foreground)]">
</p>
<Link
href={DOWNLOAD_CONFIG.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
>
{DOWNLOAD_CONFIG.buttonText}
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</Link>
</div>
)}
</div>
</main>
{unlocked && (
<div className="animate__animated animate__fadeInUp relative rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8" style={{ animationDelay: "0.3s", animationFillMode: "both" }}>
<h2 className="mb-6 flex items-center gap-2 text-lg font-semibold">
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</h2>
<LessonMarkdown content={lesson.markdown} />
</div>
)}
{DOWNLOAD_CONFIG.enabled && unlocked && (
<div className="mt-8 rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm md:rounded-2xl md:p-8">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
<svg className="h-5 w-5 text-[var(--accent)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
{DOWNLOAD_CONFIG.title}
</h2>
<p className="mb-4 text-sm text-[var(--muted-foreground)]"></p>
<Link
href={DOWNLOAD_CONFIG.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-6 py-3 font-medium text-[var(--accent-foreground)] transition hover:opacity-90"
>
{DOWNLOAD_CONFIG.buttonText}
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</Link>
</div>
)}
</LessonLayout>
</div>
);
}