Skip to content
Merged
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
118 changes: 118 additions & 0 deletions frontend/src/components/GameCard/GameCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* Én rad i resultatlista. Ingen boks, bare en skillelinje under. */
.game-card {
position: relative;
display: grid;
grid-template-columns: 192px 1fr;
gap: 20px;
padding: 20px 0;
text-align: left;
border-bottom: 1px solid var(--border);
}

.game-card__image {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
background: var(--code-bg); /* vises mens bildet laster / når det mangler */
}

.game-card__body {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}

.game-card__title {
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 1.25;
}

.game-card__title a {
color: var(--text-h);
text-decoration: none;
}

/* Strekker lenken over hele raden, så hele raden er klikkbar */
.game-card__title a::after {
content: '';
position: absolute;
inset: 0;
}

.game-card:hover .game-card__title a {
text-decoration: underline;
text-underline-offset: 3px;
}

/* Kantete fokusring rundt hele raden ved tastaturnavigasjon */
.game-card__title a:focus-visible {
outline: none;
}
.game-card:has(.game-card__title a:focus-visible) {
outline: 2px solid var(--text-h);
outline-offset: 4px;
}

.game-card__meta {
font-size: 15px;
}

.game-card__scores {
display: flex;
gap: 24px;
margin: 4px 0 8px;
font-size: 15px;
}

.game-card__scores div {
display: flex;
gap: 6px;
}

.game-card__scores dd {
margin: 0;
color: var(--text-h);
font-variant-numeric: tabular-nums; /* tallene står rett under hverandre i lista */
}

/* Tekstknapp med rett ramme, som [+ Favoritt] i skissen */
.game-card__favorite {
position: relative;
z-index: 1; /* over den strukne lenken */
margin-top: auto;
min-height: 44px; /* stort nok trykkmål */
padding: 0 14px;
font: inherit;
font-size: 15px;
color: var(--text-h);
background: none;
border: 1px solid var(--text-h);
border-radius: 0;
cursor: pointer;
}

.game-card__favorite:hover {
text-decoration: underline;
}

.game-card__favorite[aria-pressed='true'] {
color: var(--bg);
background: var(--text-h);
}

.game-card__favorite:focus-visible {
outline: 2px solid var(--text-h);
outline-offset: 2px;
}

@media (max-width: 480px) {
.game-card {
grid-template-columns: 1fr;
gap: 12px;
}
}
64 changes: 64 additions & 0 deletions frontend/src/components/GameCard/GameCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { GameSummary } from '../../types/game';
import './GameCard.css';

interface GameCardProps {
game: GameSummary;
isFavorite: boolean;
onToggleFavorite: (id: string) => void;
}

//Viser ett spill i resultatlista. Lenken i tittelen dekker hele kortet (via CSS), så hele kortet er klikkbart, men skjermlesere hører bare én lenke per kort.

export function GameCard({ game, isFavorite, onToggleFavorite }: GameCardProps) {
const year = game.released?.slice(0, 4) ?? null;
const meta = [year, game.genres.slice(0, 2).join(', ')].filter(Boolean).join(' · ');

return (
<article className="game-card">
{game.backgroundImage ? (
// alt="" fordi tittelen allerede beskriver kortet. Fast størrelse og lazy
// loading hindrer at layouten hopper og sparer data ved lange lister.
<img
className="game-card__image"
src={game.backgroundImage}
alt=""
width={320}
height={180}
loading="lazy"
decoding="async"
/>
) : (
<div className="game-card__image game-card__image--empty" aria-hidden="true" />
)}

<div className="game-card__body">
<h3 className="game-card__title">
<a href={`/game/${game.id}`}>{game.name}</a>
</h3>

{meta && <p className="game-card__meta">{meta}</p>}

<dl className="game-card__scores">
<div>
<dt>Rating</dt>
<dd>{game.rating != null ? `${game.rating.toFixed(1)} / 5` : 'Ingen'}</dd>
</div>
<div>
<dt>Metacritic</dt>
<dd>{game.metacritic ?? 'Ingen'}</dd>
</div>
</dl>

<button
type="button"
className="game-card__favorite"
aria-pressed={isFavorite}
aria-label={`Favoritt: ${game.name}`}
onClick={() => onToggleFavorite(game.id)}
>
{isFavorite ? 'Favoritt ✓' : '+ Favoritt'}
</button>
</div>
</article>
);
}
13 changes: 13 additions & 0 deletions frontend/src/types/game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//Inneholder dataene ett spillekort trenger. Feltene speiler tabellen 'games' + sjangernavn slik de vil komme fra GraphQL-APIet.

export interface GameSummary {
id: string;
name: string;
released: string | null;
backgroundImage: string | null;
// RAWG-rating, 0-5
rating: number | null;
// Metacritic score, 0-100
metacritic: number | null;
genres: string[];
}
Loading