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: 15 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ function App() {
const [filters, setFilters] = useState<GameFilters>(defaultFilters);
const debouncedFilters = useDebounce(filters, 300);

// Øker hver gang man trykker på logoen. Brukes som key på søkefeltet, så det tømmes
const [searchResetKey, setSearchResetKey] = useState(0);

// Logoen: tilbake til forsiden med standardlista (mest populære, uten filtre).
// Selve sidebyttet gjør lenken i Logo, her nullstilles bare innholdet.
const goHome = () => {
setQuery('');
setFilters(defaultFilters);
setShowFavorites(false);
setSearchResetKey((k) => k + 1);
window.scrollTo({ top: 0 });
};

// Valgene i dropdownene (sjangre og plattformer) fra databasen.
// Filtrene vises bare på forsiden, så de hentes ikke på detaljsiden.
const { data: options } = useQuery(FILTER_OPTIONS, { skip: !onHome });
Expand Down Expand Up @@ -93,6 +106,8 @@ function App() {
return (
<>
<Header
onHome={goHome}
searchResetKey={searchResetKey}
onSearch={handleSearch}
onSelectGame={handleSelectGame}
favoritesCount={favorites.size}
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/components/GameSearch/GameSearch.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/* Bare en linje under, som resten av toppfeltet */
.game-search__input {
width: 100%;
padding: 0.4rem 0;
padding: 0.4rem 2rem 0.4rem 0; /* topp, høyre, bunn, venstre: plass til X-en til høyre */
font: inherit;
color: var(--text-h);
background: transparent;
Expand All @@ -39,6 +39,32 @@
box-shadow: 0 1px 0 var(--text-h);
}

/* X-knappen helt til høyre inne i søkefeltet, uten boks som resten av tekstknappene */
.game-search__clear {
position: absolute; /* plasseres i forhold til .game-search, som har position: relative */
top: 0;
right: 0;
height: 100%;
min-width: 32px; /* stort nok til å treffe med finger */
padding: 0;
font: inherit;
font-size: 1.3em;
line-height: 1;
color: var(--text);
background: none;
border: 0;
cursor: pointer;
}

.game-search__clear:hover {
color: var(--text-h);
}

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

/* Forslagene legger seg over innholdet under, med tynn linje rundt i stedet for skygge */
.game-search__popup {
position: absolute;
Expand Down
25 changes: 23 additions & 2 deletions frontend/src/components/GameSearch/GameSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useId, useState, type KeyboardEvent } from 'react';
import { useId, useRef, useState, type KeyboardEvent } from 'react';
import { useQuery } from '@apollo/client/react';
import { useDebounce } from '../../hooks/useDebounce';
import { SEARCH_SUGGESTIONS, type Suggestion } from '../../graphql/searchSuggestions';

import './GameSearch.css';

// Kortere tekst enn dette gir for mange tilfeldige treff, så vi spør ikke serveren
Expand Down Expand Up @@ -29,6 +30,7 @@ export function GameSearch({ onSearch, onSelectGame }: GameSearchProps) {
const [active, setActive] = useState(-1); // markert forslag, -1 = ingen
const id = useId();
const listId = `${id}-list`;
const inputRef = useRef<HTMLInputElement>(null);

// Spør først 300 ms etter siste tastetrykk, og ikke ved for kort tekst
const term = useDebounce(input.trim(), 300);
Expand All @@ -39,14 +41,26 @@ export function GameSearch({ onSearch, onSelectGame }: GameSearchProps) {

// Viser forrige svar mens det nye lastes, så lista ikke blinker
const suggestions = (data ?? previousData)?.games.edges.map((e) => e.node) ?? [];
const showPopup = open && term.length >= MIN_CHARS;

// Sjekker også det som står i feltet nå, ikke bare den forsinkede teksten.
// Ellers dukker gamle forslag opp et øyeblikk etter at feltet er tømt.
const showPopup = open && input.trim().length >= MIN_CHARS && term.length >= MIN_CHARS;

function select(game: Suggestion) {
setOpen(false);
setActive(-1);
onSelectGame(game);
}

// X-knappen: tømmer feltet og viser standardlista (forsiden)
function clear() {
setInput('');
setOpen(false);
setActive(-1);
onSearch(''); // App tømmer søket, og fra en detaljside går den til forsiden
inputRef.current?.focus(); // fokus blir i feltet, ellers forsvinner det med knappen
}

function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'ArrowDown') {
e.preventDefault();
Expand Down Expand Up @@ -100,6 +114,13 @@ export function GameSearch({ onSearch, onSelectGame }: GameSearchProps) {
onBlur={() => setOpen(false)}
/>

{/* Vises bare når det står noe i feltet. aria-label fordi "×" alene ikke sier noe */}
{input && (
<button type="button" className="game-search__clear" aria-label="Tøm søk" onClick={clear}>
×
</button>
)}

{showPopup && (
<div className="game-search__popup">
{suggestions.length > 0 ? (
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface HeaderProps {
favoritesCount: number;
showFavorites: boolean;
onToggleFavorites: () => void;
onHome: () => void;
searchResetKey: number;
}

/**
Expand All @@ -19,15 +21,17 @@ interface HeaderProps {
export function Header({
onSearch,
onSelectGame,
onHome,
searchResetKey,
favoritesCount,
showFavorites,
onToggleFavorites,
}: HeaderProps) {
return (
<header className="header">
<Logo />
<Logo onHome={onHome} />
<div className="header__search">
<GameSearch onSearch={onSearch} onSelectGame={onSelectGame} />
<GameSearch key={searchResetKey} onSearch={onSearch} onSelectGame={onSelectGame} />
</div>
<nav className="header__nav" aria-label="Meny">
<button
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/Logo/Logo.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
gap: 10px;
}

/* Logoen er en lenke: ingen understrek eller lenkefarge, men synlig fokus for tastatur */
.logo {
color: inherit;
text-decoration: none;
}

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

.logo img {
display: block;
height: 44px;
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/components/Logo/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Link } from 'react-router';
import turbine from '../../assets/drawings/turbin.webp';
import './Logo.css';

/** Damp-logoen: turbintegningen med navnet under. */
export function Logo() {
interface LogoProps {
/** Nullstiller søk og filtre når man går til forsiden */
onHome: () => void;
}

/** Damp-logoen: turbintegningen med navnet under. Lenke til forsiden. */
export function Logo({ onHome }: LogoProps) {
return (
<div className="logo">
{/* Tegningen er pynt (alt=""), navnet under er det skjermlesere leser */}
<Link to="/" className="logo" aria-label="Damp, til forsiden" onClick={onHome}>
{/* Tegningen er pynt (alt=""), lenken får navnet sitt fra aria-label */}
<img className="logo__img" src={turbine} alt="" />
<span className="logo__name">Damp</span>
</div>
</Link>
);
}