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. */}
+
+ {filters.length === 0 && (
+ - Ingen filter valgt
+ )}
+ {filters.map((filter) => (
+ -
+
+
+ ))}
+
+
+ )
+}
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`}