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
127 changes: 36 additions & 91 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,46 @@
import { useState } from 'react';
import heroImg from './assets/hero.png';
import reactLogo from './assets/react.svg';
import viteLogo from './assets/vite.svg';
import './App.css';
import { GameList } from './components/GameList/GameList';
import { mockGames } from './mocks/games';

/**
* Midlertidig forside: viser spillene med mockdata til GraphQL er klart.
* Favoritter og valgt visning ligger bare i minnet her og skal senere inn i state management.
*/
function App() {
const [count, setCount] = useState(0);
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [view, setView] = useState<'list' | 'grid'>('list');

return (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button type="button" className="counter" onClick={() => setCount((count) => count + 1)}>
Count is {count}
</button>
</section>
const toggleFavorite = (id: string) => {
setFavorites((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};

<div className="ticks"></div>
return (
<main style={{ padding: '0 24px' }}>
<h1>Spill</h1>
<h2>Søkeresultater</h2>

<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg className="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg className="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg className="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg className="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
{/* Bytter mellom visning 1 (liste) og visning 2 (rutenett) */}
<div role="group" aria-label="Visning">
<button type="button" aria-pressed={view === 'list'} onClick={() => setView('list')}>
Liste
</button>
<button type="button" aria-pressed={view === 'grid'} onClick={() => setView('grid')}>
Rutenett
</button>
</div>

<div className="ticks"></div>
<section id="spacer"></section>
</>
<GameList
games={mockGames}
view={view}
favorites={favorites}
onToggleFavorite={toggleFavorite}
/>
</main>
);
}

Expand Down
67 changes: 60 additions & 7 deletions frontend/src/components/GameCard/GameCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.game-card {
position: relative;
display: grid;
grid-template-columns: 192px 1fr;
grid-template-columns: 150px 1fr;
gap: 20px;
padding: 20px 0;
text-align: left;
Expand Down Expand Up @@ -80,22 +80,27 @@
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;
position: absolute; /* plasseres i forhold til .game-card, som har position: relative */
top: 12px; /* samme som padding-toppen på raden */
right: 0;
z-index: 1; /* over den strukne lenken */
margin-top: auto;
min-height: 44px; /* stort nok trykkmål */
padding: 0 14px;
min-height: 32px;
padding: 0 10px;
font: inherit;
font-size: 15px;
font-size: 14px;
color: var(--text-h);
background: none;
border: 1px solid var(--text-h);
border-radius: 0;
cursor: pointer;
}

/* Plass til knappen, så lange titler ikke havner under den */
.game-card__body {
padding-right: 110px;
}

.game-card__favorite:hover {
text-decoration: underline;
}
Expand All @@ -116,3 +121,51 @@
gap: 12px;
}
}

/* ===== Visning 2: oppsummert rute (4 per rad) ===== */
.game-card--grid {
grid-template-columns: 1fr; /* bildet over teksten */
grid-template-rows: auto 1fr; /* teksten fyller resten av ruta */
height: 100%; /* like høye ruter i samme rad */
box-sizing: border-box;
gap: 10px;
padding: 0 0 14px;
}

.game-card--grid .game-card__body {
gap: 2px;
padding-right: 0; /* knappen ligger ikke ved tittelen her */
}

/* Maks to linjer tittel, så rutene blir like høye */
.game-card--grid .game-card__title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 16px;
}

.game-card--grid .game-card__meta,
.game-card--grid .game-card__scores {
font-size: 14px;
}

.game-card--grid .game-card__scores {
margin: auto 0 0; /* skyver rating ned til bunnen av ruta */
}

/* Favoritt nederst til høyre, på linje med rating */
.game-card--grid .game-card__favorite {
top: auto;
bottom: 8px;
}

/* Smale ruter (mobil): knappen under rating i stedet for ved siden av */
@media (max-width: 640px) {
.game-card--grid .game-card__favorite {
position: relative;
bottom: auto;
margin-top: 6px;
}
}
20 changes: 13 additions & 7 deletions frontend/src/components/GameCard/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ interface GameCardProps {
game: GameSummary;
isFavorite: boolean;
onToggleFavorite: (id: string) => void;
/** 'list' = visning 1 (rad), 'grid' = visning 2 (oppsummert rute) */
view?: 'list' | 'grid';
}

//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) {
export function GameCard({ game, isFavorite, onToggleFavorite, view = 'list' }: GameCardProps) {
const year = game.released?.slice(0, 4) ?? null;
const meta = [year, game.genres.slice(0, 2).join(', ')].filter(Boolean).join(' · ');
const meta = [year, game.genres.slice(0, view === 'grid' ? 1 : 2).join(', ')]
.filter(Boolean)
.join(' · ');

return (
<article className="game-card">
<article className={`game-card game-card--${view}`}>
{game.backgroundImage ? (
// alt="" fordi tittelen allerede beskriver kortet. Fast størrelse og lazy
// loading hindrer at layouten hopper og sparer data ved lange lister.
Expand Down Expand Up @@ -43,10 +47,12 @@ export function GameCard({ game, isFavorite, onToggleFavorite }: GameCardProps)
<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>
{view === 'list' && (
<div>
<dt>Metacritic</dt>
<dd>{game.metacritic ?? 'Ingen'}</dd>
</div>
)}
</dl>

<button
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/GameList/GameList.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.game-list {
margin: 0;
padding: 0;
list-style: none;
}

/* Visning 2: fire ruter per rad. minmax(0, 1fr) hindrer at lange titler sprenger kolonnene. */
.game-list--grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 28px 24px;
padding-top: 20px;
}

/* Lar kortet fylle hele høyden i ruta, så bunnlinjene står likt */
.game-list--grid > li {
display: flex;
flex-direction: column;
}

@media (max-width: 900px) {
.game-list--grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

@media (max-width: 640px) {
.game-list--grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px 16px;
}
}
31 changes: 31 additions & 0 deletions frontend/src/components/GameList/GameList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { GameSummary } from '../../types/game';
import { GameCard } from '../GameCard/GameCard';
import './GameList.css';

interface GameListProps {
games: GameSummary[];
favorites: Set<string>;
onToggleFavorite: (id: string) => void;
view: 'list' | 'grid';
}

/**
* Resultatlista. Samme data og samme kort i begge visninger,
* bare layouten endres. <ul>/<li> gjør at skjermlesere sier hvor mange spill som vises.
*/
export function GameList({ games, favorites, onToggleFavorite, view }: GameListProps) {
return (
<ul className={`game-list game-list--${view}`}>
{games.map((game) => (
<li key={game.id}>
<GameCard
game={game}
view={view}
isFavorite={favorites.has(game.id)}
onToggleFavorite={onToggleFavorite}
/>
</li>
))}
</ul>
);
}
Loading
Loading