import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router"; import { RPC_URL } from "@shared/constants"; import { useEffect, useRef, useState } from "react"; import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react"; import { HeaderUI } from "../../components/Header"; import { AnimatedBackground } from "../../components/AnimatedBackground"; import { useQuery } from "@tanstack/react-query"; import { Router } from "../.."; import Shortcuts from "../../components/Shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import Screenshots from "@/mainview/components/Screenshots"; import { HandleGoBack, scrollIntoViewHandler, useStickyDataAttr } from "@/mainview/scripts/utils"; import { FilterUI } from "@/mainview/components/Filters"; import StatList, { StatEntry } from "@/mainview/components/StatList"; import { useIntersectionObserver, useLocalStorage } from "usehooks-ts"; import { EmulatorsSection } from "@/mainview/components/store/EmulatorsSection"; import { zodValidator } from "@tanstack/zod-adapter"; import z from "zod"; import Achievements from "@/mainview/components/game/Achievements"; import { GameDetailsContext } from "@/mainview/scripts/contexts"; import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm"; import { GamesSection } from "@/mainview/components/store/GamesSection"; import Details, { DetailElement } from "@/mainview/components/game/Details"; export const Route = createFileRoute("/game/$source/$id")({ loader: async ({ params, context }) => { context.queryClient.prefetchQuery(gameQuery(params.source, params.id)); }, component: RouteComponent, pendingComponent: GameDetailsUIPending, errorComponent: Error, validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function useDetailsSection () { return useLocalStorage('details-section', 'screenshots'); } function Error (data: ErrorComponentProps) { const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" }); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); const { shortcuts } = useShortcutContext(); useEffect(() => { focusSelf(); }, []); return
{JSON.stringify(data.error, null, 3)}
; } function MainDetailsPending () { const { ref } = useFocusable({ focusKey: "main-details" }); return
} > } >
} >
; } function GameDetailsUIPending () { const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" }); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); const { shortcuts } = useShortcutContext(); useEffect(() => { focusSelf(); }, []); return
Screenshots
{Array.from({ length: 5 }).map((s, i) =>
)}
; } function MoreDetails (data: { game: FrontEndGameTypeDetailed | undefined; }) { const [details] = useDetailsSection(); const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: "game-more-details-section", onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'start', behavior: 'smooth' })(focusKey, ref.current, d), trackChildren: true }); return
{details === 'screenshots' && !!data.game &&
} {details === 'stats' && } {details === 'achievements' && !!data.game && }
; } function Stats (data: { game: FrontEndGameTypeDetailed | undefined; }) { const stats: StatEntry[] = []; if (data.game) { if (data.game.path_fs) stats.push({ label: "Location", content: data.game.path_fs, icon: }); if (data.game.companies) stats.push({ label: "Companies", content: data.game.companies }); if (data.game.genres) stats.push({ label: 'Genres', content: data.game.genres }); if (data.game.release_date) stats.push({ label: "Release Date", content: data.game.release_date.toLocaleDateString(), icon: }); if (data.game.emulators) stats.push({ label: "Emulators", content: data.game.emulators.map(e => e.name) }); } return ; } function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: FrontEndGameTypeDetailed | undefined; }) { const [details, setDetails] = useDetailsSection(); const { ref, focusKey } = useFocusable({ focusKey: "details-divider", onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'nearest', behavior: 'smooth' })(focusKey, ref.current, d), }); const detailFilter: Record = { stats: { label: "Stats", selected: details === 'stats', icon: }, screenshots: { label: "Screenshots", selected: details === 'screenshots', icon: }, }; if (data.game?.achievements) { detailFilter.achievements = { label: "Achievements", selected: details === 'achievements', icon: }; } return
; } function RouteComponent () { const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false); const { source, id } = Route.useParams(); const { data } = useQuery(gameQuery(source, id)); const { focus } = Route.useSearch(); const [, setUpdate] = useState(0); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" }); const headerRef = useRef(null); const sentinelRef = useRef(null); const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined; const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible }); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); const { shortcuts } = useShortcutContext(); useEffect(() => { if (focus) { setFocus(focus, { instant: true }); } else { focusSelf(); } }, []); useStickyDataAttr(headerRef, sentinelRef, ref); const recommendedEmulators = data?.emulators?.filter(e => e.validSource); const { ref: intersct } = useIntersectionObserver({ onChange: (isIntersecting, entry) => { setRecommendedGamesVisible(isIntersecting); } }); return ( setUpdate(v => v + 1) }} >
{!!recommendedEmulators && recommendedEmulators.length > 0 &&

Related Emulators

} onFocus={scrollIntoViewHandler({ block: 'center' })} onSelect={(id, focus) => { Router.navigate({ to: '/store/details/emulator/$id', params: { id } }); }} emulators={recommendedEmulators} />}

Related Games

{ Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } }); }} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
); }