Files
project-afterlife/apps/web/src/components/documentary/DocumentaryLayout.tsx
consultoria-as aea2283d8f
Some checks failed
Deploy / deploy (push) Has been cancelled
feat: add game server infrastructure and documentary improvements
- 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>
2026-02-23 12:11:12 +00:00

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