diff --git a/client/src/App.css b/client/src/App.css index 8cafc3d..1aab49e 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -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. */ @@ -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); diff --git a/client/src/components/PlaceFilter.tsx b/client/src/components/PlaceFilter.tsx new file mode 100644 index 0000000..534d996 --- /dev/null +++ b/client/src/components/PlaceFilter.tsx @@ -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 ( + + + onQueryChange(event.target.value)} + /> + + {options.length > 0 && ( + <> + + {/* Valget legges til som filter, så menyen står alltid på overskriften. */} + + + )} + + + + {/* Raden har fast høyde også uten filter, så resten av siden ligger i ro. */} + + + ) +} diff --git a/client/src/components/PlaceList.tsx b/client/src/components/PlaceList.tsx index 4c1b3e3..9c58762 100644 --- a/client/src/components/PlaceList.tsx +++ b/client/src/components/PlaceList.tsx @@ -8,6 +8,7 @@ type PlaceListProps = { featuredId: string loadedCount: number totalCount: number + isFiltered: boolean hasMore: boolean canShowLess: boolean isLoadingMore: boolean @@ -23,6 +24,7 @@ export function PlaceList({ featuredId, loadedCount, totalCount, + isFiltered, hasMore, canShowLess, isLoadingMore, @@ -63,7 +65,9 @@ export function PlaceList({

- Viser {loadedCount} av {totalCount} steder + {isFiltered + ? `Viser ${loadedCount} treff` + : `Viser ${loadedCount} av ${totalCount} steder`}