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
111 changes: 110 additions & 1 deletion client/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@
background: var(--header-end);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 8'%3E%3Cpath d='M0 4q12-8 24 0t24 0 24 0 24 0V0H0Z'/%3E%3C/svg%3E")
repeat-x top / 6rem 0.5rem;
animation: wave-drift 24s linear infinite;
}

/* Bølgen glir sidelengs. Den ene bølgelengden er 6rem, så løkken er sømløs. */
@keyframes wave-drift {
to {
mask-position: 6rem top;
}
}

@media (prefers-reduced-motion: reduce) {
.site-header::after {
animation: none;
}
}

/* Sidemargen holder tittelen unna hjørneknappene. */
Expand Down Expand Up @@ -310,11 +324,106 @@
.page {
display: grid;
gap: 2rem;
max-width: 72rem;
/* Fast bredde, ellers krymper siden etter innholdet og boksene flytter seg. */
width: 100%;
max-width: 54rem;
margin: 0 auto;
padding: 1.5rem 1rem 3rem;
}

/* Søk og filter ligger øverst, over begge kolonnene. */
.place-filter,
.no-results {
grid-column: 1 / -1;
}

.place-filter {
/* Trekker innholdet litt opp mot raden, som alltid har plass til filtrene. */
margin-bottom: -0.75rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}

.place-filter input {
flex: 1 1 14rem;
min-width: 0;
padding: 0.6rem 1rem;
border: 2px solid var(--navy-900);
border-radius: 999px;
background: var(--surface);
color: var(--text);
font: inherit;
transition: box-shadow 0.15s;
}

.place-filter input:hover {
box-shadow: 0 0 0 3px var(--blue-300);
}

.place-filter select {
padding: 0.5rem 0.8rem;
border: 2px solid var(--navy-900);
border-radius: 0.6rem;
background: var(--surface);
color: var(--text);
font: inherit;
}

.filter-chips {
display: flex;
flex: 0 0 100%;
gap: 0.5rem;
align-items: center;
height: 3rem;
margin: 0;
padding: 0.25rem;
overflow-x: auto;
list-style: none;
scrollbar-width: thin;
}

.filter-chips li {
flex: none;
}

.filter-hint {
padding-left: 0.5rem;
color: var(--muted);
}

.place-filter .button:disabled {
cursor: default;
}

.filter-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0.8rem;
border: 2px solid var(--navy-700);
border-radius: 999px;
background: var(--blue-100);
color: var(--navy-900);
font: inherit;
font-weight: 600;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
}

.filter-chip:hover {
background: var(--navy-900);
color: var(--eggshell);
}

.no-results {
margin: 0;
color: var(--muted);
}

@media (min-width: 56rem) {
.page {
grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
Expand Down
93 changes: 93 additions & 0 deletions client/src/components/PlaceFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
type PlaceFilterProps = {
query: string
/** Filtrene som er valgt, i den rekkefølgen de ble lagt til. */
filters: string[]
/** Filtrene som kan legges til. */
options: string[]
onQueryChange: (query: string) => void
onAddFilter: (filter: string) => void
onRemoveFilter: (filter: string) => void
onReset: () => void
}

/** Søk og filter øverst på hovedsiden. Valgte filter vises som merkelapper. */
export function PlaceFilter({
query,
filters,
options,
onQueryChange,
onAddFilter,
onRemoveFilter,
onReset,
}: PlaceFilterProps) {
const isActive = query !== '' || filters.length > 0

return (
<search className="place-filter">
<label className="visually-hidden" htmlFor="place-search-input">
Søk etter steder
</label>
<input
id="place-search-input"
type="search"
placeholder="Søk etter navn, område eller type …"
autoComplete="off"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
/>

{options.length > 0 && (
<>
<label className="visually-hidden" htmlFor="place-filter-add">
Legg til filter
</label>
{/* Valget legges til som filter, så menyen står alltid på overskriften. */}
<select
id="place-filter-add"
value=""
onChange={(event) => onAddFilter(event.target.value)}
>
<option value="" disabled>
Legg til filter …
</option>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</>
)}

<button
type="button"
className="button button-outline"
disabled={!isActive}
onClick={onReset}
>
Nullstill
</button>

{/* Raden har fast høyde også uten filter, så resten av siden ligger i ro. */}
<ul className="filter-chips" aria-label="Valgte filter">
{filters.length === 0 && (
<li className="filter-hint">Ingen filter valgt</li>
)}
{filters.map((filter) => (
<li key={filter}>
<button
type="button"
className="filter-chip"
aria-label={`Fjern filteret ${filter}`}
title={`Fjern filteret ${filter}`}
onClick={() => onRemoveFilter(filter)}
>
{filter}
<span aria-hidden="true">×</span>
</button>
</li>
))}
</ul>
</search>
)
}
6 changes: 5 additions & 1 deletion client/src/components/PlaceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type PlaceListProps = {
featuredId: string
loadedCount: number
totalCount: number
isFiltered: boolean
hasMore: boolean
canShowLess: boolean
isLoadingMore: boolean
Expand All @@ -23,6 +24,7 @@ export function PlaceList({
featuredId,
loadedCount,
totalCount,
isFiltered,
hasMore,
canShowLess,
isLoadingMore,
Expand Down Expand Up @@ -63,7 +65,9 @@ export function PlaceList({
</ul>

<p role="status" className="suggestions-count">
Viser {loadedCount} av {totalCount} steder
{isFiltered
? `Viser ${loadedCount} treff`
: `Viser ${loadedCount} av ${totalCount} steder`}
</p>

<footer className="list-actions">
Expand Down
21 changes: 12 additions & 9 deletions client/src/components/wheel/SpinWheel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
layoutSegments,
nextRotation,
segmentPath,
SPOKE_COUNT,
truncate,
} from '../../utils/wheel'
import { ResultDialog } from './ResultDialog'
Expand Down Expand Up @@ -41,9 +40,11 @@ type SpinWheelProps<T> = {

const SPIN_MS = 4500
const COLOR_COUNT = 4
/** Pinne med håndtak, tegnet rett opp fra midten av roret. */
const SPOKE_PATH =
'M-2.6 -14V-106L-3.4 -112Q-6.2 -120 -4.8 -128Q0 -135 4.8 -128Q6.2 -120 3.4 -112L2.6 -106V-14Z'
/** Håndtak som stikker ut fra kanten, tegnet rett opp fra midten av roret. */
const HANDLE_PATH =
'M-2.6 -106L-3.4 -112Q-6.2 -120 -4.8 -128Q0 -135 4.8 -128Q6.2 -120 3.4 -112L2.6 -106Z'
/** Antall håndtak rundt roret. */
const HANDLE_COUNT = 8

const prefersReducedMotion = () =>
window.matchMedia('(prefers-reduced-motion: reduce)').matches
Expand Down Expand Up @@ -207,18 +208,20 @@ export function SpinWheel<T>({
})}
<g className="wheel-wood">
<circle className="wheel-rim" r="106" />
{Array.from({ length: SPOKE_COUNT }, (_, spoke) => (
{Array.from({ length: HANDLE_COUNT }, (_, handle) => (
<g
key={spoke}
transform={`rotate(${spoke * (360 / SPOKE_COUNT)})`}
key={handle}
transform={`rotate(${handle * (360 / HANDLE_COUNT)})`}
>
<path className="wheel-spoke" d={SPOKE_PATH} />
<path className="wheel-handle" d={HANDLE_PATH} />
</g>
))}
</g>
</g>

<path className="wheel-pointer" d="M-9 -124 9 -124 0 -96Z" />
<g className="wheel-pointer" aria-hidden="true">
<path d="M-6-114H6L0-99Z" />
</g>

<g
className="wheel-hub"
Expand Down
2 changes: 2 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
line-height: 1.5;
color: var(--text);
background: var(--eggshell);
/* Plass til rullefeltet er alltid reservert, så siden ikke hopper sidelengs. */
scrollbar-gutter: stable;
}

* {
Expand Down
9 changes: 5 additions & 4 deletions client/src/pages/ChallengesPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@
pointer-events: none;
}

.wheel-spoke {
.wheel-handle {
fill: var(--wood);
}

/* Treverk mellom feltene, så pinnene går i ett med resten av roret. */
/* Treverk bak feltene, så roret ser helt ut også mellom navet og kanten. */
.wheel-backing {
fill: var(--wood);
}
Expand All @@ -173,10 +173,11 @@
paint-order: stroke;
}

.wheel-pointer {
/* Pekeren er en liten gulltrekant over kanten, som peker ned på feltet. */
.wheel-pointer path {
fill: var(--brass);
stroke: var(--navy-900);
stroke-width: 1.5;
stroke-width: 1.2;
stroke-linejoin: round;
}

Expand Down
Loading