Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
"welcomeTitle": "Find the videogames that your mind can't",
"welcomeFilters": "Use the filters on the left to narrow down your search.",
"welcomeSearch": "Hit Search when you're ready to explore.",
"cardView": "Cards",
"listView": "List",
"noResultsTitle": "No games found",
"noResultsDescription": "Try adjusting your filters or search term to find more games."
},
Expand Down
144 changes: 100 additions & 44 deletions src/features/dashboard/ResultsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import type { GameResult } from '@/models/AppTypes';
import type { MouseEvent } from 'react';
import { Badge } from '@/components/ui/badge';
import { Star } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { LayoutGrid, List, Star } from 'lucide-react';
import {
Pagination,
PaginationContent,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const ResultsGrid = ({

const [randomCatPath] = useState(() => getRandomCat());
const [animationData, setAnimationData] = useState<object | null>(null);
const [viewMode, setViewMode] = useState<'card' | 'list'>('card');

const ITEMS_PER_PAGE = 20;

Expand All @@ -74,9 +76,9 @@ export const ResultsGrid = ({
}
}, [hasSearched, results.length, randomCatPath]);

const handleCardClick = (e: MouseEvent<HTMLDivElement>, index: number) => {
const card = e.currentTarget;
const rect = card.getBoundingClientRect();
const handleCardClick = (e: MouseEvent<HTMLElement>, index: number) => {
const element = e.currentTarget;
const rect = element.getBoundingClientRect();
const origin: ClickOrigin = {
x: rect.left,
y: rect.top,
Expand Down Expand Up @@ -150,50 +152,104 @@ export const ResultsGrid = ({

return (
<div className="flex flex-col gap-6 pb-20">
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{paginatedResults.map((game, index) => (
<div
key={game.id}
onClick={(e) => handleCardClick(e, (currentPage - 1) * ITEMS_PER_PAGE + index)}
className="group relative aspect-[264/374] bg-white border-2 border-border rounded-base shadow-shadow flex flex-col justify-between overflow-visible hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none transition-all cursor-pointer"
<div className="flex justify-end">
<div className="inline-flex border-2 border-border rounded-base overflow-hidden shadow-shadow">
<Button
variant={viewMode === 'card' ? 'default' : 'neutral'}
size="sm"
className="rounded-none border-0 shadow-none"
onClick={() => setViewMode('card')}
>
{/* Badges - Top Left (outside box) */}
<div className="absolute -top-2 -left-2 z-10 flex gap-1">
{game.rating && (
<Badge>
<Star className="w-3 h-3 fill-current" />
{(game.rating / 10).toFixed(1)}
</Badge>
)}
{game.matchScore !== undefined && (
<Badge variant="neutral">
{t('results.match')}: {game.matchScore.toFixed(3)}
</Badge>
)}
</div>
<LayoutGrid className="h-4 w-4" />
{t('results.cardView')}
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'neutral'}
size="sm"
className="rounded-none border-0 shadow-none border-l-2 border-border"
onClick={() => setViewMode('list')}
>
<List className="h-4 w-4" />
{t('results.listView')}
</Button>
</div>
</div>

{/* Image Area */}
<div className="w-full h-full bg-secondary-background flex items-center justify-center overflow-hidden rounded-base">
{game.coverUrl ? (
<img
src={game.coverUrl}
alt={game.title}
className="w-full h-full object-cover"
/>
) : (
<span className="font-heading opacity-20">{t('results.noImage')}</span>
)}
<div className={viewMode === 'card' ? 'grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6' : 'flex flex-col gap-3'}>
{paginatedResults.map((game, index) => {
const gameIndex = (currentPage - 1) * ITEMS_PER_PAGE + index;
return viewMode === 'card' ? (
<div
key={game.id}
onClick={(e) => handleCardClick(e, gameIndex)}
className="group relative aspect-[264/374] bg-white border-2 border-border rounded-base shadow-shadow flex flex-col justify-between overflow-visible hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none transition-all cursor-pointer"
>
<div className="absolute -top-2 -left-2 z-10 flex gap-1">
{game.rating && (
<Badge>
<Star className="w-3 h-3 fill-current" />
{(game.rating / 10).toFixed(1)}
</Badge>
)}
{game.matchScore !== undefined && (
<Badge variant="neutral">
{t('results.match')}: {game.matchScore.toFixed(3)}
</Badge>
)}
</div>

<div className="w-full h-full bg-secondary-background flex items-center justify-center overflow-hidden rounded-base">
{game.coverUrl ? (
<img
src={game.coverUrl}
alt={game.title}
className="w-full h-full object-cover"
/>
) : (
<span className="font-heading opacity-20">{t('results.noImage')}</span>
)}
</div>

<div className="absolute bottom-0 w-full p-3 bg-background/90 border-t-2 border-border backdrop-blur-sm">
<h4 className="font-heading text-sm truncate">{game.title}</h4>
<span className="text-xs font-base opacity-70">
{game.year ?? '—'}
</span>
</div>
</div>

{/* Meta Overlay */}
<div className="absolute bottom-0 w-full p-3 bg-background/90 border-t-2 border-border backdrop-blur-sm">
<h4 className="font-heading text-sm truncate">{game.title}</h4>
<span className="text-xs font-base opacity-70">
{game.year ?? '—'}
</span>
) : (
<div
key={game.id}
onClick={(e) => handleCardClick(e, gameIndex)}
className="group border-2 border-border rounded-base bg-white shadow-shadow hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none transition-all cursor-pointer p-3 flex items-center gap-4"
>
<div className="h-16 w-12 shrink-0 rounded-base bg-secondary-background overflow-hidden flex items-center justify-center">
{game.coverUrl ? (
<img src={game.coverUrl} alt={game.title} className="h-full w-full object-cover" />
) : (
<span className="text-[10px] font-heading opacity-20">{t('results.noImage')}</span>
)}
</div>
<div className="min-w-0 flex-1">
<h4 className="font-heading text-sm truncate">{game.title}</h4>
<span className="text-xs font-base opacity-70">{game.year ?? '—'}</span>
</div>
<div className="flex gap-1 flex-wrap justify-end">
{game.rating && (
<Badge>
<Star className="w-3 h-3 fill-current" />
{(game.rating / 10).toFixed(1)}
</Badge>
)}
{game.matchScore !== undefined && (
<Badge variant="neutral">
{t('results.match')}: {game.matchScore.toFixed(3)}
</Badge>
)}
</div>
</div>
</div>
))}
);
})}
</div>

{/* Pagination */}
Expand Down
Loading