From 0335b0ad81169232a3dbb1be1341fdcfce548645 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 2 Jun 2026 02:12:57 -0700 Subject: migrate to pocketbase backend + auth/login --- src/app/game/page.tsx | 1011 ------------------------------------------------- 1 file changed, 1011 deletions(-) delete mode 100644 src/app/game/page.tsx (limited to 'src/app/game/page.tsx') diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx deleted file mode 100644 index bce01b3..0000000 --- a/src/app/game/page.tsx +++ /dev/null @@ -1,1011 +0,0 @@ -"use client"; -import { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useReducer, - useRef, - useState, - Suspense, -} from "react"; -import { useSearchParams, useRouter } from "next/navigation"; -import Link from "next/link"; -import { FaRedo } from "react-icons/fa"; -import { MdLibraryMusic } from "react-icons/md"; -import { toast, ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import { - GameRoot, - GameGlobalStyle, - GameNavbar, - GameContent, - HUD, - HudStat, - HudValue, - HudLabel, - ComboValue, - GameArea, - UpcomingWrap, - UpcomingLabel, - UpcomingText, - CurrentWrap, - LineTimingMeta, - LineTimingValue, - LineTimingRow, - LineTimingBar, - LineTimingFill, - CharRow, - WordWrap, - CharBox, - ClearToast, - GetReadyText, - BackgroundVideo, - OpacityControl, - OpacityLabel, - OpacitySlider, - OpacityValue, - PreviewWrap, - PreviewBtn, - PreviewHint, - CompletedLineFade, - GameFooter, - ControlBtn, - ProgressWrap, - ProgressFill, - TimeText, - StartOverlay, - StartCard, - CountdownNumber, - SongTitleText, - SongArtistText, - StartBtn, - CodeSection, - CodeInputRow, - CodeInputField, - CodeLoadBtn, - ResultsOverlay, - ResultsCard, - ResultsTitle, - BigScore, - StatsGrid, - StatBlock, - StatValue, - StatLabel, - ActionRow, - PlayAgainBtn, - HomeBtn, -} from "./page.styles"; -import { gReducer, initialGState } from "./game.stat"; -import { formatTime, parseLrcLines, calculateCPSNeeded } from "./game.utils"; - -type GamePhase = "idle" | "countdown" | "playing" | "paused" | "finished"; - -const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]); -const isVideoUrl = (url: string) => { - if (!url) return false; - const cleaned = url.split("?")[0].split("#")[0]; - const ext = cleaned.split(".").pop()?.toLowerCase(); - return !!ext && VIDEO_EXTENSIONS.has(ext); -}; -const BACKGROUND_OPACITY_KEY = "lrcType.backgroundOpacity"; -const AUDIO_VOLUME_KEY = "lrcType.audioVolume"; - -function GameInner() { - const searchParams = useSearchParams(); - const router = useRouter(); - - useEffect(() => { - window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - }, []); - - const audioRef = useRef(null); - const videoRef = useRef(null); - const gameStartTimeRef = useRef(0); - const lastHandledIdxRef = useRef(-1); - const lastLineAdvanceAtRef = useRef(0); - - const [phase, setPhase] = useState("idle"); - const [currentMs, setCurrentMs] = useState(0); - const [lineTimingPct, setLineTimingPct] = useState(0); - const [lineRemainingMs, setLineRemainingMs] = useState(0); - const [currentLineTime, setCurrentLineTime] = useState(0); - const [duration, setDuration] = useState(0); - const [progressPct, setProgressPct] = useState(0); - const [gameDurationMs, setGameDurationMs] = useState(0); - - const [lrcContent, setLrcContent] = useState(""); - const [audioUrl, setAudioUrl] = useState(""); - const [songTitle, setSongTitle] = useState("Unknown Title"); - const [songArtist, setSongArtist] = useState("Unknown Artist"); - const [offset, setOffset] = useState(0); - const [loadingLrc, setLoadingLrc] = useState(false); - - const [codeInput, setCodeInput] = useState(""); - const [wrongChar, setWrongChar] = useState(false); - const [clearShowing, setClearShowing] = useState(false); - const [comboAnimKey, setComboAnimKey] = useState(0); - const [countdown, setCountdown] = useState(0); - const [backgroundOpacity, setBackgroundOpacity] = useState(() => { - if (typeof window === "undefined") return 0; - const stored = localStorage.getItem(BACKGROUND_OPACITY_KEY); - if (stored === null) return 0; - const parsed = Number(stored); - if (!Number.isFinite(parsed)) return 0; - return Math.min(100, Math.max(0, parsed)); - }); - const [audioVolume, setAudioVolume] = useState(() => { - if (typeof window === "undefined") return 100; - const stored = localStorage.getItem(AUDIO_VOLUME_KEY); - if (stored === null) return 100; - const parsed = Number(stored); - if (!Number.isFinite(parsed)) return 100; - return Math.min(100, Math.max(0, parsed)); - }); - const [isPreviewPlaying, setIsPreviewPlaying] = useState(false); - const [skipBacking, setSkipBacking] = useState(false); - const isVideo = useMemo(() => isVideoUrl(audioUrl), [audioUrl]); - - - - useEffect(() => { - localStorage.setItem(BACKGROUND_OPACITY_KEY, String(backgroundOpacity)); - }, [backgroundOpacity]); - - useEffect(() => { - localStorage.setItem(AUDIO_VOLUME_KEY, String(audioVolume)); - }, [audioVolume]); - - useEffect(() => { - const media = isVideo ? videoRef.current : audioRef.current; - if (!media) return; - media.volume = audioVolume / 100; - }, [audioVolume, isVideo, audioUrl]); - - useEffect(() => { - const media = isVideo ? videoRef.current : audioRef.current; - if (!media) { - setIsPreviewPlaying(false); - return; - } - const handlePlay = () => setIsPreviewPlaying(true); - const handlePause = () => setIsPreviewPlaying(false); - const handleEnded = () => setIsPreviewPlaying(false); - media.addEventListener("play", handlePlay); - media.addEventListener("pause", handlePause); - media.addEventListener("ended", handleEnded); - return () => { - media.removeEventListener("play", handlePlay); - media.removeEventListener("pause", handlePause); - media.removeEventListener("ended", handleEnded); - }; - }, [isVideo, audioUrl]); - - useEffect(() => { - setIsPreviewPlaying(false); - }, [audioUrl]); - - const charRowRef = useRef(null); - const charRefs = useRef<(HTMLSpanElement | null)[]>([]); - const [wrapSpaceIndicators, setWrapSpaceIndicators] = useState([]); - const countdownIntervalRef = useRef(null); - - const [g, dispatch] = useReducer(gReducer, initialGState); - - const gameLines = useMemo( - () => parseLrcLines(lrcContent, { skipBacking }), - [lrcContent, skipBacking] - ); - const isReady = !loadingLrc && !!lrcContent && !!audioUrl; - - const accuracy = - g.totalCorrect + g.totalMiss > 0 - ? Math.round((g.totalCorrect / (g.totalCorrect + g.totalMiss)) * 100) - : 100; - - const elapsedMs = - phase === "playing" - ? Math.max(1, Date.now() - gameStartTimeRef.current) - : gameDurationMs; - - const wpm = - elapsedMs > 0 ? Math.round(g.totalCorrect / 5 / (elapsedMs / 60000)) : 0; - - const gRef = useRef(g); - const currentLineContent = - g.displayedLineIdx >= 0 ? gameLines[g.displayedLineIdx]?.content ?? "" : ""; - - useEffect(() => { - charRefs.current = []; - }, [currentLineContent]); - - useLayoutEffect(() => { - if (!charRowRef.current) return; - let frame = 0; - const text = currentLineContent.toLowerCase(); - - const recompute = () => { - const nodes = charRefs.current; - const indicators = new Array(text.length).fill(false); - for (let i = 0; i < text.length - 1; i += 1) { - if (text[i] !== " ") continue; - const curr = nodes[i]; - const next = nodes[i + 1]; - if (!curr || !next) continue; - const currRect = curr.getBoundingClientRect(); - const nextRect = next.getBoundingClientRect(); - if (nextRect.top - currRect.top > 1) { - indicators[i] = true; - } - } - setWrapSpaceIndicators(indicators); - }; - - const schedule = () => { - cancelAnimationFrame(frame); - frame = requestAnimationFrame(() => { - frame = requestAnimationFrame(recompute); - }); - }; - - schedule(); - - if (document.fonts?.ready) { - document.fonts.ready.then(schedule); - } - - const observer = new ResizeObserver(schedule); - observer.observe(charRowRef.current); - window.addEventListener("resize", schedule); - - return () => { - observer.disconnect(); - window.removeEventListener("resize", schedule); - cancelAnimationFrame(frame); - }; - }, [currentLineContent]); - useEffect(() => { - gRef.current = g; - }, [g]); - - const phaseRef = useRef("idle"); - useEffect(() => { - phaseRef.current = phase; - }, [phase]); - - const offsetRef = useRef(0); - useEffect(() => { - offsetRef.current = offset; - }, [offset]); - - useEffect(() => { - if (!("mediaSession" in navigator)) return; - const mediaSession = navigator.mediaSession; - mediaSession.setActionHandler("pause", () => {}); - return () => { - mediaSession.setActionHandler("pause", null); - }; - }, []); - - useEffect(() => { - return () => { - if (countdownIntervalRef.current !== null) { - clearInterval(countdownIntervalRef.current); - countdownIntervalRef.current = null; - } - }; - }, []); - - const lineAnimRef = useRef({ startMs: 0, endMs: 0, startPerf: 0 }); - - const timeBasedLineIdx = useMemo(() => { - if (!gameLines.length) return -1; - let idx = -1; - for (let i = 0; i < gameLines.length; i++) { - if (gameLines[i].millisecond <= currentMs) idx = i; - else break; - } - return idx; - }, [currentMs, gameLines]); - - const intermissionData = useMemo(() => { - const firstMs = gameLines[0]?.millisecond ?? 0; - const firstMediaMs = firstMs - offsetRef.current; - const remainingMs = Math.max(0, firstMs - currentMs); - if (!gameLines.length || firstMediaMs <= 0) { - return { pct: remainingMs === 0 ? 100 : 0, remainingMs }; - } - - const mediaCurrentMs = currentMs - offsetRef.current; - const pct = Math.min(100, Math.max(0, (mediaCurrentMs / firstMediaMs) * 100)); - - return { pct, remainingMs }; - }, [gameLines, currentMs, offset]); - - useEffect(() => { - const idx = g.displayedLineIdx; - if (idx < 0 || !gameLines[idx]) { - lineAnimRef.current = { startMs: 0, endMs: 0, startPerf: 0 }; - setLineTimingPct(0); - setLineRemainingMs(0); - setCurrentLineTime(-1); - return; - } - const start = gameLines[idx].millisecond; - const end = gameLines[idx + 1]?.millisecond ?? start + 5000; - lineAnimRef.current = { - startMs: start, - endMs: end, - startPerf: performance.now(), - }; - setLineTimingPct(0); - const currentLineTime = end - start; - setLineRemainingMs(Math.max(0, currentLineTime)); - setCurrentLineTime(Math.max(currentLineTime, currentLineTime)); - }, [g.displayedLineIdx, gameLines]); - - useEffect(() => { - if (phase !== "playing") return; - let rafId = 0; - const tick = () => { - const { startMs, endMs, startPerf } = lineAnimRef.current; - if (endMs <= startMs) { - setLineTimingPct(100); - setLineRemainingMs(0); - } else { - const elapsed = performance.now() - startPerf; - const duration = endMs - startMs; - const pct = Math.min(100, Math.max(0, (elapsed / duration) * 100)); - const remaining = Math.max(0, duration - elapsed); - setLineTimingPct(pct); - setLineRemainingMs(remaining); - } - rafId = requestAnimationFrame(tick); - }; - rafId = requestAnimationFrame(tick); - return () => cancelAnimationFrame(rafId); - }, [phase]); - - useEffect(() => { - const media = isVideo ? videoRef.current : audioRef.current; - if (!media) return; - const onTimeUpdate = () => { - setCurrentMs(media.currentTime * 1000 + offsetRef.current); - if (media.duration && !isNaN(media.duration)) { - setDuration(media.duration * 1000); - setProgressPct((media.currentTime / media.duration) * 100); - } - }; - const onLoadedMetadata = () => { - if (!isNaN(media.duration)) { - setDuration(media.duration * 1000); - setGameDurationMs(media.duration * 1000); - } - }; - const onEnded = () => { - if (phaseRef.current === "playing") { - setPhase("finished"); - setGameDurationMs(Date.now() - gameStartTimeRef.current); - return; - } - setIsPreviewPlaying(false); - }; - media.addEventListener("timeupdate", onTimeUpdate); - media.addEventListener("loadedmetadata", onLoadedMetadata); - media.addEventListener("ended", onEnded); - return () => { - media.removeEventListener("timeupdate", onTimeUpdate); - media.removeEventListener("loadedmetadata", onLoadedMetadata); - media.removeEventListener("ended", onEnded); - }; - }, [isVideo, audioUrl]); - - useEffect(() => { - if (phaseRef.current !== "playing") return; - if (timeBasedLineIdx < 0) return; - if (timeBasedLineIdx <= lastHandledIdxRef.current) return; - lastHandledIdxRef.current = timeBasedLineIdx; - lastLineAdvanceAtRef.current = performance.now(); - dispatch({ - type: "ADVANCE", - newIdx: timeBasedLineIdx, - prevCompleted: gRef.current.lineCompleted, - }); - }, [timeBasedLineIdx]); - - const loadData = useCallback((data: Record) => { - if (typeof data.lrc === "string" && data.lrc) { - setLoadingLrc(true); - fetch(data.lrc) - .then((r) => r.text()) - .then((t) => { - setLrcContent(t); - setLoadingLrc(false); - }); - } - if (typeof data.file1 === "string") setAudioUrl(data.file1); - if (typeof data.offset === "number") setOffset(data.offset); - if (typeof data.offset === "string" && data.offset.trim() !== "") - setOffset(Number(data.offset)); - if (typeof data.title === "string") setSongTitle(data.title); - if (typeof data.artist === "string") setSongArtist(data.artist); - if (typeof data.skip_backing === "boolean") - setSkipBacking(data.skip_backing); - if (typeof data.skip_backing === "string") - setSkipBacking(data.skip_backing === "true"); - }, []); - - useEffect(() => { - const code = searchParams.get("code"); - if (!code) return; - try { - const json = atob(code); - const data = JSON.parse(json) as Record; - loadData(data); - } catch {} - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const handlePreviewToggle = useCallback(() => { - if (phase !== "idle") return; - const media = isVideo ? videoRef.current : audioRef.current; - if (!media || !audioUrl) return; - - if (media.paused) { - void media.play().catch(() => { - toast.error("Unable to start preview. Try interacting with the page again.", { - theme: "dark", - }); - }); - return; - } - - media.pause(); - }, [phase, isVideo, audioUrl]); - - const handleStart = useCallback(() => { - const media = isVideo ? videoRef.current : audioRef.current; - if (!media || !lrcContent || !audioUrl) return; - if (countdownIntervalRef.current !== null) { - clearInterval(countdownIntervalRef.current); - countdownIntervalRef.current = null; - } - dispatch({ type: "RESET" }); - lastHandledIdxRef.current = -1; - media.pause(); - media.currentTime = 0; - setIsPreviewPlaying(false); - setPhase("countdown"); - setCountdown(5); - setGameDurationMs(0); - setProgressPct(0); - setCurrentMs(0); - - const beginPlayback = () => { - media.currentTime = 0; - media.play(); - setPhase("playing"); - gameStartTimeRef.current = Date.now(); - }; - - countdownIntervalRef.current = window.setInterval(() => { - setCountdown((c) => { - if (c <= 1) { - if (countdownIntervalRef.current !== null) { - clearInterval(countdownIntervalRef.current); - countdownIntervalRef.current = null; - } - beginPlayback(); - return 0; - } - return c - 1; - }); - }, 1000); - }, [lrcContent, audioUrl, gameLines, isVideo]); - - const handleRestart = useCallback(() => { - const media = isVideo ? videoRef.current : audioRef.current; - if (media) { - media.pause(); - media.currentTime = 0; - } - setIsPreviewPlaying(false); - if (countdownIntervalRef.current !== null) { - clearInterval(countdownIntervalRef.current); - countdownIntervalRef.current = null; - } - setCountdown(0); - dispatch({ type: "RESET" }); - lastHandledIdxRef.current = -1; - setPhase("idle"); - setCurrentMs(0); - setProgressPct(0); - }, [isVideo]); - - const handleLoadCode = useCallback(() => { - if (!codeInput.trim()) return; - try { - const json = atob(codeInput.trim()); - const data = JSON.parse(json) as Record; - loadData(data); - handleRestart(); - toast.success("Song loaded!", { theme: "dark" }); - } catch { - toast.error("Invalid code. Please check and try again.", { - theme: "dark", - }); - } - }, [codeInput, loadData, handleRestart]); - - const handleKeyPress = useCallback( - (char: string) => { - if (phaseRef.current !== "playing") return; - const line = gameLines[gRef.current.displayedLineIdx]; - if (!line || gRef.current.lineCompleted) return; - const expected = line.content[gRef.current.typedCount]; - if (expected === undefined) return; - if (char.toLowerCase() === expected.toLowerCase()) { - const willComplete = gRef.current.typedCount + 1 >= line.content.length; - dispatch({ type: "CORRECT", willComplete }); - if (willComplete) { - setClearShowing(true); - setTimeout(() => setClearShowing(false), 700); - setComboAnimKey((k) => k + 1); - } - } else { - if (performance.now() - lastLineAdvanceAtRef.current < 100) return; - dispatch({ type: "WRONG" }); - setWrongChar(true); - setTimeout(() => setWrongChar(false), 320); - } - }, - [gameLines], - ); - - useEffect(() => { - if (phase !== "playing") return; - const handler = (e: KeyboardEvent) => { - if (e.key === " ") { - const idx = gRef.current.displayedLineIdx; - if (idx < 0 && gameLines.length > 0) { - const firstMs = gameLines[0]?.millisecond ?? 0; - const media = isVideo ? videoRef.current : audioRef.current; - if (media) { - const currentMsLocal = media.currentTime * 1000 + offsetRef.current; - const intermissionRemaining = Math.max(0, firstMs - currentMsLocal); - if (intermissionRemaining > 5000) { - e.preventDefault(); - const targetMs = firstMs - 3000; - media.currentTime = Math.max(0, (targetMs - offsetRef.current) / 1000); - setCurrentMs(media.currentTime * 1000 + offsetRef.current); - return; - } - } - } - } - if (e.key.length === 1) { - e.preventDefault(); - handleKeyPress(e.key); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [phase, handleKeyPress, gameLines, isVideo]); - - return ( - - - {!isVideo && ( - - ); -} - -export default function GamePage() { - return ( - <> - - -
- Loading... -
- - } - > - -
- - ); -} -- cgit v1.2.3