Some checks failed
Deploy / deploy (push) Has been cancelled
- Add Docker Compose for OpenFusion (FusionFall), MapleStory 2, and Minecraft FTB Infinity Evolved game servers - Add MapleStory 2 multi-service compose (MySQL, World, Login, Web, Game) - Add OpenFusion Dockerfile and configuration files - Fix CMS Dockerfile, web Dockerfile, and documentary components - Add root layout, globals.css, not-found page, and text formatting utils - Update .gitignore to exclude large game server repos and data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import type { Documentary, Chapter } from "@afterlife/shared";
|
|
import { ChapterNav } from "./ChapterNav";
|
|
import { ChapterContent } from "./ChapterContent";
|
|
import { AudioPlayer } from "./AudioPlayer";
|
|
import { ReadingProgress } from "./ReadingProgress";
|
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
|
|
|
interface DocumentaryLayoutProps {
|
|
documentary: Documentary;
|
|
}
|
|
|
|
export function DocumentaryLayout({ documentary }: DocumentaryLayoutProps) {
|
|
const chapters = [...documentary.chapters].sort((a, b) => a.order - b.order);
|
|
const [activeChapter, setActiveChapter] = useState<Chapter>(chapters[0]);
|
|
|
|
const audio = useAudioPlayer();
|
|
|
|
useEffect(() => {
|
|
const audioTracks = chapters
|
|
.filter((ch) => ch.audioFile)
|
|
.map((ch) => ({
|
|
id: ch.id,
|
|
title: ch.title,
|
|
url: ch.audioFile!.url,
|
|
duration: ch.audioDuration ?? 0,
|
|
}));
|
|
audio.setTracks(audioTracks);
|
|
if (audioTracks.length > 0) {
|
|
audio.loadTrack(0);
|
|
}
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
function handleSelectChapter(chapterId: number, index: number) {
|
|
const chapter = chapters.find((c) => c.id === chapterId);
|
|
if (chapter) {
|
|
setActiveChapter(chapter);
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
const trackIndex = audio.tracks.findIndex((t) => t.id === chapterId);
|
|
if (trackIndex !== -1) {
|
|
audio.goToTrack(trackIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ReadingProgress />
|
|
|
|
{/* Documentary header */}
|
|
<header className="border-b border-white/[0.06]">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-10 pb-8">
|
|
<h1 className="text-3xl sm:text-4xl font-display font-bold tracking-tight">
|
|
{documentary.title}
|
|
</h1>
|
|
{documentary.description && (
|
|
<p className="mt-3 text-gray-400 font-body text-lg max-w-3xl leading-relaxed">
|
|
{documentary.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex gap-12">
|
|
<ChapterNav
|
|
chapters={chapters}
|
|
activeChapterId={activeChapter.id}
|
|
onSelectChapter={handleSelectChapter}
|
|
/>
|
|
<div className="flex-1 min-w-0 pb-24">
|
|
<ChapterContent chapter={activeChapter} />
|
|
</div>
|
|
</div>
|
|
<AudioPlayer
|
|
trackTitle={audio.currentTrack?.title ?? null}
|
|
isPlaying={audio.isPlaying}
|
|
progress={audio.progress}
|
|
duration={audio.duration}
|
|
playbackRate={audio.playbackRate}
|
|
continuousMode={audio.continuousMode}
|
|
onToggle={audio.toggle}
|
|
onSeek={audio.seek}
|
|
onChangeRate={audio.changeRate}
|
|
onToggleContinuous={() =>
|
|
audio.setContinuousMode(!audio.continuousMode)
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}
|