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
15 changes: 14 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from 'react';
import { GameList } from './components/GameList/GameList';
import { mockGames } from './mocks/games';
import { SearchBar } from './components/SearchBar/SearchBar';
import { useDebounce } from './hooks/useDebounce';

/**
* Midlertidig forside: viser spillene med mockdata til GraphQL er klart.
Expand All @@ -10,6 +12,16 @@ function App() {
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [view, setView] = useState<'list' | 'grid'>('list');

// Det brukeren skriver nå, og det vi faktisk søker på etter 300 ms pause
const [input, setInput] = useState('');
const query = useDebounce(input.trim(), 300);

// MIDLERTIDIG: filtrerer mockdata på klienten for å kunne teste søket.
// Når GraphQL er klart, sendes `query` til backend og filtreres i databasen.
const games = mockGames.filter((g) =>
g.name.toLowerCase().includes(query.toLowerCase()),
);

const toggleFavorite = (id: string) => {
setFavorites((prev) => {
const next = new Set(prev);
Expand All @@ -22,6 +34,7 @@ function App() {
return (
<main style={{ padding: '0 24px' }}>
<h1>Spill</h1>
<SearchBar value={input} onChange={setInput} />
<h2>Søkeresultater</h2>

{/* Bytter mellom visning 1 (liste) og visning 2 (rutenett) */}
Expand All @@ -35,7 +48,7 @@ function App() {
</div>

<GameList
games={mockGames}
games={games}
view={view}
favorites={favorites}
onToggleFavorite={toggleFavorite}
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/SearchBar/SearchBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Skjuler labelen visuelt, men beholder den for skjermlesere. */
.search-bar__label {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}

.search-bar__input {
width: 100%;
padding: 0.6rem 0.8rem;
font: inherit;
border: 1px solid #767676;
border-radius: 0;
-webkit-appearance: textfield;
appearance: textfield;
}
30 changes: 30 additions & 0 deletions frontend/src/components/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import './SearchBar.css';

interface SearchBarProps {
value: string;
onChange: (value: string) => void;
}

// Søkefeltet øverst på siden. Komponenten eier ikke state selv:
// forelderen styrer verdien og bestemmer når det faktisk søkes (debounce).
export function SearchBar({ value, onChange }: SearchBarProps) {
return (
// role="search" gjør at skjermlesere kan hoppe rett til søket.
// preventDefault hindrer at Enter laster siden på nytt.
<form className="search-bar" role="search" onSubmit={(e) => e.preventDefault()}>
{/* Skjult visuelt, men lest opp av skjermlesere. Placeholder alene er ikke nok. */}
<label htmlFor="game-search" className="search-bar__label">
Søk etter spill
</label>
<input
id="game-search"
className="search-bar__input"
type="search"
placeholder="Søk etter spill …"
value={value}
onChange={(e) => onChange(e.target.value)}
autoComplete="off"
/>
</form>
);
}
12 changes: 12 additions & 0 deletions frontend/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';

export function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debounced;
}
Loading