Initial commit

This commit is contained in:
CallMeVerity
2026-06-03 00:44:48 +01:00
commit 3369f22f69
36 changed files with 3419 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router-dom";
import VideoSidebar from "../components/VideoSidebar";
import { fetchVideo, formatRunTime } from "../api/client";
import type { VideoDetail } from "../types/video";
function getMapDisplayName(mapName: string): string {
return mapName.replace(/_/g, " ").toUpperCase();
}
function getMomUrl(mapName: string): string {
return `https://dashboard.momentum-mod.org/maps/${mapName}`;
}
function SkeletonBlock({ className }: { className?: string }) {
return (
<div
className={`rounded bg-white/5 animate-pulse ${className ?? ""}`}
/>
);
}
function PageSkeleton() {
return (
<div className="py-6">
<div className="flex items-baseline gap-3 mb-3">
<SkeletonBlock className="h-8 w-64" />
</div>
<div className="flex flex-col lg:flex-row gap-4 lg:items-start">
<div className="lg:w-1/2 min-w-0">
<SkeletonBlock className="aspect-video w-full rounded-lg" />
</div>
<div className="lg:w-1/2 flex flex-col gap-4">
<div className="rounded border border-white/5 bg-white/5 p-4">
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<div>
<SkeletonBlock className="h-2.5 w-8 mb-1.5" />
<SkeletonBlock className="h-4 w-28" />
</div>
<div>
<SkeletonBlock className="h-2.5 w-8 mb-1.5" />
<SkeletonBlock className="h-4 w-20" />
</div>
<div>
<SkeletonBlock className="h-2.5 w-10 mb-1.5" />
<SkeletonBlock className="h-4 w-24" />
</div>
<div>
<SkeletonBlock className="h-2.5 w-8 mb-1.5" />
<SkeletonBlock className="h-7 w-16" />
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default function VideoDetail() {
const { id } = useParams<{ id: string }>();
const [video, setVideo] = useState<VideoDetail | null>(null);
const [videoReady, setVideoReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (!id) return;
fetchVideo(id)
.then(setVideo)
.catch((err) => setError(err.message));
}, [id]);
if (error) {
return (
<div className="py-24 text-center">
<p className="text-red-400/60 mb-4">{error}</p>
</div>
);
}
if (!video || !videoReady) {
return (
<>
<PageSkeleton />
{video && (
<video
ref={videoRef}
src={video.videoUrl}
preload="auto"
onCanPlay={() => setVideoReady(true)}
className="fixed opacity-0 pointer-events-none"
aria-hidden
/>
)}
</>
);
}
return (
<div className="py-6">
<div className="rounded-lg border border-white/5 bg-white/5 px-4 py-2 inline-block mb-3">
<h1 className="text-2xl font-bold tracking-tight text-white/90">
<a
href={getMomUrl(video.mapName)}
target="_blank"
rel="noopener noreferrer"
className="no-underline text-white/90 hover:text-mom-accent transition-colors"
>
{getMapDisplayName(video.mapName)}
</a>
</h1>
</div>
<div className="flex flex-col lg:flex-row gap-4 lg:items-start">
<div className="lg:w-1/2 min-w-0">
<div className="rounded-lg border border-white/5 overflow-hidden">
<div className="aspect-video bg-black">
<video
ref={videoRef}
src={video.videoUrl}
controls
className="w-full h-full"
poster={video.thumbnailUrl}
>
Your browser does not support video playback.
</video>
</div>
<div className="px-4 py-3 flex items-center justify-between border-t border-white/5 bg-white/3">
<a
href={video.mtvUrl}
download
className="inline-flex items-center gap-2 rounded-md bg-white/4 border border-white/7 px-3 py-1.5 text-xs font-medium text-white/50 hover:bg-white/8 hover:text-white/80 hover:border-white/15 transition-colors no-underline"
>
<svg
className="w-3.5 h-3.5"
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>
.mtv replay
</a>
</div>
</div>
</div>
<div className="lg:w-1/2">
<VideoSidebar video={video} />
{video.previousPbs && video.previousPbs.length > 0 && (
<div className="rounded border border-white/5 bg-white/5 p-4 mt-4">
<div className="text-[10px] uppercase tracking-wider text-gray-500 mb-2">
Previous PBs
</div>
<ul className="space-y-1">
{video.previousPbs.map((pb) => (
<li
key={pb.createdAt}
className="text-sm text-white/60"
>
<span className="font-mono text-white/90">
{formatRunTime(pb.runTime)}
</span>
{" · "}
{new Date(
pb.createdAt,
).toLocaleDateString()}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
);
}