game page refinements
This commit is contained in:
@@ -12,9 +12,10 @@ interface FocusableItemProps {
|
||||
focusKey?: string;
|
||||
scrollOptions?: ScrollIntoViewOptions;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, scrollOptions, active }: FocusableItemProps) => {
|
||||
const FocusableItem = ({ onFocus, onEnterPress, onClick, children, className, focusKey, scrollOptions, active }: FocusableItemProps) => {
|
||||
const { mode } = useInputMode();
|
||||
const { ref, focused: rawFocused, focusKey: internalFocusKey } = useFocusable({
|
||||
onFocus,
|
||||
@@ -22,10 +23,10 @@ const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, s
|
||||
focusKey
|
||||
});
|
||||
|
||||
const focused = mode === 'gamepad' ? rawFocused : false;
|
||||
const focused = mode !== 'mouse' ? rawFocused : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'gamepad' && (focused || active) && ref.current) {
|
||||
if (mode !== 'mouse' && (focused || active) && ref.current) {
|
||||
if (scrollOptions) {
|
||||
ref.current.scrollIntoView(scrollOptions);
|
||||
} else {
|
||||
@@ -35,7 +36,14 @@ const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, s
|
||||
}, [focused, active, mode, scrollOptions]);
|
||||
|
||||
return (
|
||||
<div ref={ref} data-focusable-key={internalFocusKey} className={`${className} ${focused ? 'focused' : ''}`}>
|
||||
<div
|
||||
ref={ref}
|
||||
data-focusable-key={internalFocusKey}
|
||||
className={`${className} ${focused ? 'focused' : ''}`}
|
||||
onClick={() => {
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
>
|
||||
{children(focused)}
|
||||
</div>
|
||||
);
|
||||
@@ -44,6 +52,7 @@ const FocusableItem = ({ onFocus, onEnterPress, children, className, focusKey, s
|
||||
const PlatformItem = ({ platform, active, onSelect }: { platform: Platform, active: boolean, onSelect: () => void }) => (
|
||||
<FocusableItem
|
||||
onFocus={onSelect}
|
||||
onClick={onSelect}
|
||||
className="shrink-0"
|
||||
focusKey={`PLATFORM_${platform.id}`}
|
||||
active={active}
|
||||
@@ -77,6 +86,7 @@ const PlatformItem = ({ platform, active, onSelect }: { platform: Platform, acti
|
||||
const GameListItem = ({ game, active, onFocus }: { game: Game, active: boolean, onFocus: () => void }) => (
|
||||
<FocusableItem
|
||||
onFocus={onFocus}
|
||||
onClick={onFocus}
|
||||
className="w-full"
|
||||
focusKey={`GAME_${game.id}`}
|
||||
scrollOptions={{ behavior: 'smooth', block: 'nearest' }}
|
||||
@@ -116,6 +126,7 @@ const AlphabetScroller = ({
|
||||
key={letter}
|
||||
onFocus={() => {}}
|
||||
onEnterPress={() => onLetterPress(letter)}
|
||||
onClick={() => onLetterPress(letter)}
|
||||
className="w-full flex justify-center py-1"
|
||||
focusKey={`LETTER_${letter}`}
|
||||
scrollOptions={{ behavior: 'smooth', block: 'center' }}
|
||||
@@ -195,13 +206,13 @@ export const GamesPage = () => {
|
||||
}, [filteredPlatforms, selectedPlatformId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gamesQuery.data && gamesQuery.data.length > 0 && !selectedGameId) {
|
||||
setSelectedGameId(gamesQuery.data[0].id);
|
||||
if (mode !== 'mouse' && (gamesQuery.data?.length ?? 0) > 0 && !selectedGameId) {
|
||||
setSelectedGameId(gamesQuery.data?.[0]?.id ?? null);
|
||||
}
|
||||
}, [gamesQuery.data, selectedGameId]);
|
||||
}, [gamesQuery.data, selectedGameId, mode]);
|
||||
|
||||
const handlePlatformFocus = (id: number, options: { skipZoneChange?: boolean } = {}) => {
|
||||
if (mode === 'gamepad') {
|
||||
if (mode !== 'mouse') {
|
||||
if (!options.skipZoneChange) {
|
||||
setActiveZone('platforms');
|
||||
}
|
||||
@@ -266,7 +277,7 @@ export const GamesPage = () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (closestKey && mode === 'gamepad') {
|
||||
if (closestKey && mode !== 'mouse') {
|
||||
console.log("[MagneticFocus] Setting focus to:", closestKey);
|
||||
setFocus(closestKey);
|
||||
}
|
||||
@@ -370,7 +381,7 @@ export const GamesPage = () => {
|
||||
game={game}
|
||||
active={selectedGameId === game.id}
|
||||
onFocus={() => {
|
||||
if (mode === 'gamepad') {
|
||||
if (mode !== 'mouse') {
|
||||
setActiveZone('games');
|
||||
if (detailTimeoutRef.current) clearTimeout(detailTimeoutRef.current);
|
||||
detailTimeoutRef.current = setTimeout(() => {
|
||||
@@ -403,8 +414,8 @@ export const GamesPage = () => {
|
||||
<FocusContext.Provider value={detailsFocusKey}>
|
||||
<div ref={detailsRef} className={`flex-1 overflow-y-auto relative scrollbar-hoverable ${activeZone === 'details' ? 'scrollbar-active' : ''}`}>
|
||||
{detailsQuery.data ? (
|
||||
<div className="p-16">
|
||||
<div className="flex gap-12 items-start">
|
||||
<div className="pt-[20px] pl-[20px] pr-16 pb-16">
|
||||
<div className="flex gap-[20px] items-start">
|
||||
{/* Poster */}
|
||||
<div className="w-[320px] rounded-[16px] overflow-hidden shadow-[0_20px_50px_rgba(0,0,0,0.5)] border border-white/10 shrink-0 sticky top-0 group">
|
||||
<img
|
||||
@@ -422,23 +433,68 @@ export const GamesPage = () => {
|
||||
<span className="text-white/40 text-[10px] geist-mono uppercase tracking-widest">ID: ROM-{detailsQuery.data.id}</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl font-black text-white mb-4 tracking-tighter leading-none">{detailsQuery.data.title}</h1>
|
||||
<h1 className="text-5xl font-black text-white tracking-tighter leading-none mb-6">{detailsQuery.data.title}</h1>
|
||||
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-2 mb-10 items-center">
|
||||
{/* Row 1: Regions, Release Date & Rating */}
|
||||
<div className="flex items-center gap-6 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">developer_board</span>
|
||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.developer || 'Unknown Dev'}</span>
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Region:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 uppercase geist-mono tracking-wider">
|
||||
{detailsQuery.data.regions?.join(', ') || 'Global'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">calendar_today</span>
|
||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.releaseDate || 'N/A'}</span>
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Release Date:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 uppercase geist-mono tracking-wider">
|
||||
{detailsQuery.data.releaseDate || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
{detailsQuery.data.rating && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Rating:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 geist-mono tracking-wider">{detailsQuery.data.rating}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Companies & Franchises */}
|
||||
<div className="flex items-center gap-6 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-[#2563eb]">sports_esports</span>
|
||||
<span className="text-[11px] font-bold text-white/80 uppercase geist-mono">{detailsQuery.data.system}</span>
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Companies:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 uppercase geist-mono tracking-wider">
|
||||
{[...(detailsQuery.data.developers || []), ...(detailsQuery.data.publishers || [])].filter((val, idx, self) => self.indexOf(val) === idx).join(' / ') || 'Unknown Company'}
|
||||
</span>
|
||||
</div>
|
||||
{(detailsQuery.data.franchises?.length ?? 0) > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Franchise:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 uppercase geist-mono tracking-wider">
|
||||
{detailsQuery.data.franchises?.join(' & ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Genres */}
|
||||
<div className="flex items-center gap-6 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Genres:</span>
|
||||
<span className="text-[11px] font-bold text-white/40 uppercase geist-mono tracking-wider italic">
|
||||
{detailsQuery.data.genres?.join(' • ') || 'No Genres'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Age Rating Text */}
|
||||
{detailsQuery.data.esrbRating && (
|
||||
<div className="flex items-center gap-2 mb-10">
|
||||
<span className="text-[10px] font-black text-[#2563eb] uppercase tracking-widest geist-mono">Age Rating:</span>
|
||||
<span className="text-[11px] font-bold text-white/60 uppercase geist-mono tracking-wider">
|
||||
{detailsQuery.data.esrbRating}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 mb-12">
|
||||
<FocusableItem
|
||||
onFocus={() => { if (mode === 'gamepad') setActiveZone('details'); }}
|
||||
@@ -476,17 +532,6 @@ export const GamesPage = () => {
|
||||
{detailsQuery.data.summary || "This title does not have a comprehensive analysis in our local database yet. Our researchers are working to archive more metadata for this specific entry."}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{detailsQuery.data.genres && detailsQuery.data.genres.length > 0 && (
|
||||
<section>
|
||||
<h3 className="text-[10px] font-black text-[#2563eb] uppercase tracking-[0.4em] mb-4">Classifications</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detailsQuery.data.genres.map(g => (
|
||||
<span key={g} className="px-3 py-1 bg-white/5 border border-white/5 rounded-full text-[9px] font-bold text-white/60 uppercase tracking-widest">{g}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user