From 24e8fcf4b1557dbccb25e0ef0bcc3bb7c94a9c85 Mon Sep 17 00:00:00 2001 From: Tara Aqeel Date: Mon, 19 Oct 2026 16:04:44 +0200 Subject: [PATCH 1/4] refactor: rydd opp i importer og legg til rute for tidligere handlelister (#70) --- frontend/src/App.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c5c1bb8..a7ffd3a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,4 @@ import { useState } from "react"; -import NavigationMenu from "./components/navigationMenu/NavigationMenu"; -import ShoppingListsPage from "./pages/shoppingLists/ShoppingListsPage"; - import { BrowserRouter, Routes, @@ -10,17 +7,24 @@ import { useNavigate, } from "react-router"; +// Felles komponenter import Header from "./components/header/Header"; +import NavigationMenu from "./components/navigationMenu/NavigationMenu"; + +// Sider import HomePage from "./pages/home/HomePage"; import SearchResultsPage from "./pages/SearchResults/SearchResultsPage"; +import ShoppingListsPage from "./pages/shoppingLists/ShoppingListsPage"; +import PreviousListsPage from "./pages/shoppingLists/PreviousListsPage"; +// Hjelpefunksjoner import { loadSearchPreferences, saveSearchPreferences, } from "./pages/SearchResults/searchPreferences"; - import { createCategoryUrl } from "./pages/home/categoryNavigation"; +// Styling import "./App.css"; function AppLayout() { @@ -131,6 +135,11 @@ function AppLayout() { element={} /> + } + /> + Date: Tue, 20 Oct 2026 17:29:05 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20implementer=20historikkoversikt=20m?= =?UTF-8?q?ed=20s=C3=B8k,=20sortering=20og=20paginering=20(#70)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/shoppingLists/PreviousListRow.tsx | 68 ++++ .../pages/shoppingLists/PreviousListsPage.css | 304 ++++++++++++++++++ .../pages/shoppingLists/PreviousListsPage.tsx | 296 +++++++++++++++++ .../pages/shoppingLists/ShoppingListCard.tsx | 8 +- .../pages/shoppingLists/ShoppingListsPage.css | 32 +- .../pages/shoppingLists/ShoppingListsPage.tsx | 19 +- 6 files changed, 713 insertions(+), 14 deletions(-) create mode 100644 frontend/src/pages/shoppingLists/PreviousListRow.tsx create mode 100644 frontend/src/pages/shoppingLists/PreviousListsPage.css create mode 100644 frontend/src/pages/shoppingLists/PreviousListsPage.tsx diff --git a/frontend/src/pages/shoppingLists/PreviousListRow.tsx b/frontend/src/pages/shoppingLists/PreviousListRow.tsx new file mode 100644 index 0000000..fd1bc10 --- /dev/null +++ b/frontend/src/pages/shoppingLists/PreviousListRow.tsx @@ -0,0 +1,68 @@ + +import type { ShoppingList } from "./shoppingListTypes"; + +interface PreviousListRowProps { + list: ShoppingList; + onReuse?: (id: string) => void; +} + +export default function PreviousListRow({ + list, + onReuse, +}: PreviousListRowProps) { + const formattedDate = new Date( + `${list.createdAt.slice(0, 10)}T12:00:00` + ).toLocaleDateString("nb-NO", { + day: "2-digit", + month: "2-digit", + }); + + const productCount = list.items.length; + + const itemCount = list.items.reduce( + (total, item) => total + item.quantity, + 0 +); + + const formattedPrice = + list.estimatedTotal !== null + ? list.estimatedTotal.toLocaleString("nb-NO", { + style: "currency", + currency: "NOK", + }) + : "Pris ikke tilgjengelig"; + + return ( +
+ + {formattedDate} + + + + {list.name} + + + + {productCount} produkter · {itemCount} varer + + + + {formattedPrice} + + + +
+ ); +} diff --git a/frontend/src/pages/shoppingLists/PreviousListsPage.css b/frontend/src/pages/shoppingLists/PreviousListsPage.css new file mode 100644 index 0000000..f0001c2 --- /dev/null +++ b/frontend/src/pages/shoppingLists/PreviousListsPage.css @@ -0,0 +1,304 @@ + +/* Historikkoversikt */ + +.previous-lists-page { + width: min(100% - 3rem, 1200px); + margin: 0 auto; + padding: 3rem 0 5rem; +} + +.previous-lists-page__header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1.5rem; + margin-bottom: 2.5rem; +} + +.previous-lists-page__heading { + flex: 1; + min-width: 0; +} + +.previous-lists-page__eyebrow { + display: block; + margin-bottom: 12px; + font: 500 11px var(--font-mono); + letter-spacing: 0.08em; + color: var(--text-accent); +} + +.previous-lists-page__header h1 { + margin: 0 0 8px; + font-family: var(--font-ui); + font-size: clamp(2rem, 4vw, 3rem); + line-height: 1.1; + color: var(--text-primary); +} + +.previous-lists-page__header h1 em { + font-family: var(--font-accent); + font-weight: 400; + color: var(--text-brand); +} + +.previous-lists-page__header p { + margin: 0; + font-size: 0.95rem; + color: var(--text-secondary); +} + +/* Søk og sortering */ + +.previous-lists-page__controls { + display: flex; + justify-content: space-between; + align-items: end; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.previous-lists-page__search, +.previous-lists-page__sort { + display: flex; + flex-direction: column; + gap: 6px; + font: 11px var(--font-mono); + color: var(--text-muted); +} + +.previous-lists-page__search input, +.previous-lists-page__sort select { + padding: 10px 14px; + font: 13px var(--font-ui); + color: var(--text-primary); + background: var(--bg-surface); + border: 1px solid var(--border-hairline); + border-radius: 6px; +} + +.previous-lists-page__search input { + width: min(100%, 320px); +} + +/* Historikkramme */ + +.previous-lists-page__history { + border: 1px solid var(--border-brand); + border-radius: 10px; + padding: 20px 24px; + background: var(--bg-surface); + box-shadow: 3px 3px 0 var(--note-tint); +} + +.previous-lists-page__month + .previous-lists-page__month { + margin-top: 2rem; +} + +.previous-lists-page__month-heading { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1rem; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-strong); +} + +.previous-lists-page__month-heading h2 { + margin: 0; + font: italic 1.7rem var(--font-accent); + color: var(--text-brand); + text-transform: capitalize; +} + +.previous-lists-page__month-heading span { + font: 10px var(--font-mono); + text-transform: uppercase; + color: var(--text-muted); +} + +/* Enkeltrad */ + +.previous-list-row { + display: grid; + grid-template-columns: 65px minmax(0, 1fr) 180px 110px 125px; + align-items: center; + gap: 1rem; + padding: 14px 0; + border-bottom: 1px solid var(--border-hairline); +} + +.previous-list-row__date { + font: 500 11px var(--font-mono); + color: var(--text-accent); +} + +.previous-list-row__name { + font: 500 14px var(--font-ui); + color: var(--text-primary); + overflow-wrap: anywhere; +} + +.previous-list-row__count { + font: 10px var(--font-mono); + color: var(--text-muted); +} + +.previous-list-row__price { + font: 500 14px var(--font-ui); + color: var(--text-primary); + text-align: right; + white-space: nowrap; +} + +.previous-list-row__reuse { + padding: 8px 10px; + border: 1px solid var(--border-hairline); + border-radius: 6px; + font: 12px var(--font-ui); + background: var(--bg-surface); + color: var(--text-primary); + cursor: pointer; +} + +.previous-list-row__reuse:hover:not(:disabled) { + border-color: var(--border-brand); +} + +.previous-list-row__reuse:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Tom tilstand og last inn flere */ + +.previous-lists-page__empty { + padding: 3rem 1rem; + text-align: center; + color: var(--text-secondary); +} + +.previous-lists-page__load-more { + display: block; + margin: 2rem 0 0 auto; + padding: 10px 16px; + border: 1px solid var(--border-hairline); + border-radius: 6px; + background: var(--bg-surface); + color: var(--text-primary); + font: 13px var(--font-ui); + cursor: pointer; +} + +.previous-lists-page__load-more:hover { + border-color: var(--border-brand); +} + +/* Tastaturnavigasjon */ + +.previous-lists-page input:focus-visible, +.previous-lists-page select:focus-visible, +.previous-lists-page button:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 3px; +} + +/* Mobil og nettbrett */ + +@media (max-width: 900px) { + .previous-list-row { + grid-template-columns: 55px minmax(0, 1fr) auto; + gap: 10px; + } + + .previous-list-row__count { + grid-column: 2; + grid-row: 2; + } + + .previous-list-row__price { + grid-column: 3; + grid-row: 1; + } + + .previous-list-row__reuse { + grid-column: 3; + grid-row: 2; + } +} + +@media (max-width: 600px) { + .previous-lists-page { + width: calc(100% - 1.5rem); + padding: 2rem 0 3rem; + } + + .previous-lists-page__controls { + align-items: stretch; + flex-direction: column; + } + + .previous-lists-page__search input { + width: 100%; + box-sizing: border-box; + } + + .previous-lists-page__history { + padding: 16px; + } + + .previous-list-row { + grid-template-columns: 1fr auto; + gap: 8px 12px; + } + + .previous-list-row__date { + grid-column: 1; + } + + .previous-list-row__name { + grid-column: 1; + } + + .previous-list-row__count { + grid-column: 1; + grid-row: auto; + } + + .previous-list-row__price { + grid-column: 2; + grid-row: 2; + } + + .previous-list-row__reuse { + grid-column: 2; + grid-row: 3; + } +} + +.previous-lists-page__pagination { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + margin-top: 2rem; +} + +.previous-lists-page__pagination-info { + margin: 0; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-muted); +} + +.previous-lists-page__pagination-buttons { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 12px; +} + +.previous-lists-page__pagination-buttons .previous-lists-page__load-more { + margin: 0; +} \ No newline at end of file diff --git a/frontend/src/pages/shoppingLists/PreviousListsPage.tsx b/frontend/src/pages/shoppingLists/PreviousListsPage.tsx new file mode 100644 index 0000000..2a55464 --- /dev/null +++ b/frontend/src/pages/shoppingLists/PreviousListsPage.tsx @@ -0,0 +1,296 @@ +import { Link } from "react-router"; +import { useMemo, useState } from "react"; +import PreviousListRow from "./PreviousListRow"; +import type { ShoppingList } from "./shoppingListTypes"; +import "./PreviousListsPage.css"; +import "./ShoppingListsPage.css"; + +// Midlertidige data frem til GraphQL-integrasjonen (#63). +const mockPreviousLists: ShoppingList[] = [ + { + id: "101", + name: "Ukeshandel", + status: "COMPLETED", + inUse: false, + createdAt: "2026-09-18", + estimatedTotal: 669.6, + items: [ + { id: "1", ean: "1", name: "Melk", quantity: 2, checked: true }, + { id: "2", ean: "2", name: "Brød", quantity: 1, checked: true }, + { id: "3", ean: "3", name: "Egg", quantity: 2, checked: true }, + ], + }, + { + id: "102", + name: "Pizzakveld", + status: "COMPLETED", + inUse: false, + createdAt: "2026-09-12", + estimatedTotal: 287.4, + items: [ + { id: "4", ean: "4", name: "Pizzadeig", quantity: 1, checked: true }, + { id: "5", ean: "5", name: "Ost", quantity: 2, checked: true }, + ], + }, + { + id: "103", + name: "Hyttetur", + status: "COMPLETED", + inUse: false, + createdAt: "2026-08-24", + estimatedTotal: 842.1, + items: [ + { id: "6", ean: "6", name: "Pasta", quantity: 3, checked: true }, + { id: "7", ean: "7", name: "Kaffe", quantity: 1, checked: true }, + ], + }, + { + id: "104", + name: "Sommerfest", + status: "COMPLETED", + inUse: false, + createdAt: "2026-08-10", + estimatedTotal: 1250, + items: [ + { id: "8", ean: "8", name: "Brus", quantity: 4, checked: true }, + { id: "9", ean: "9", name: "Potetgull", quantity: 3, checked: true }, + ], + }, + { + id: "105", + name: "Helgehandel", + status: "COMPLETED", + inUse: false, + createdAt: "2026-07-20", + estimatedTotal: 412.9, + items: [ + { id: "10", ean: "10", name: "Yoghurt", quantity: 2, checked: true }, + ], + }, +]; + +// Ekstra testdata for søk, sortering og paginering. +const extraMockLists: ShoppingList[] = Array.from( + { length: 25 }, + (_, index) => { + const number = index + 1; + + return { + id: `test-${number}`, + name: + number % 2 === 0 + ? `Ukeshandel ${number}` + : `Helgehandel ${number}`, + status: "COMPLETED", + inUse: false, + createdAt: new Date( + Date.UTC(2026, 8, 17 - index, 12) + ) + .toISOString() + .slice(0, 10), + estimatedTotal: 150 + number * 25, + items: [ + { + id: `item-${number}-1`, + ean: `test-${number}-1`, + name: "Melk", + quantity: 2, + checked: true, + }, + { + id: `item-${number}-2`, + ean: `test-${number}-2`, + name: "Brød", + quantity: 1, + checked: true, + }, + ], + }; + } +); + +const allPreviousLists = [ + ...mockPreviousLists, + ...extraMockLists, +]; + +const monthFormatter = new Intl.DateTimeFormat("nb-NO", { + month: "long", + year: "numeric", + timeZone: "UTC", +}); + +function getMonthKey(date: string) { + return date.slice(0, 7); +} + +function formatMonth(monthKey: string) { + return monthFormatter.format(new Date(`${monthKey}-01T12:00:00Z`)); +} + +const PAGE_SIZE = 10; + +export default function PreviousListsPage() { + const [search, setSearch] = useState(""); + const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest"); + const [visibleCount, setVisibleCount] = useState(10); + + const filteredLists = useMemo(() => { + return allPreviousLists + .filter( + (list) => + list.status === "COMPLETED" && + list.name.toLocaleLowerCase("nb-NO").includes( + search.trim().toLocaleLowerCase("nb-NO") + ) + ) + .sort((a, b) => + sortOrder === "newest" + ? b.createdAt.localeCompare(a.createdAt) + : a.createdAt.localeCompare(b.createdAt) + ); + }, [search, sortOrder]); + + const visibleLists = filteredLists.slice(0, visibleCount); + + const groupedLists = visibleLists.reduce( + (groups, list) => { + const month = getMonthKey(list.createdAt); + + if (!groups.has(month)) { + groups.set(month, []); + } + + groups.get(month)!.push(list); + + return groups; + }, + new Map() + ); + + return ( +
+ +
+
+ + 10 · HISTORIKK + + +

+ Tidligere handlelister +

+ +

+ Her finner du handlelistene du har handlet ferdig. +

+
+ + +
+ + +
+ + + +
+ + {visibleLists.length === 0 ? ( +

+ Ingen tidligere handlelister funnet. +

+ ) : ( +
+ {Array.from(groupedLists.entries()).map(([month, lists]) => ( +
+
+

{formatMonth(month)}

+ {lists?.length ?? 0} viste lister +
+ + {lists?.map((list) => ( + + ))} +
+ ))} +
+ )} + +
+

+ {visibleLists.length} av {filteredLists.length} handlelister vist +

+ +
+ {visibleCount > PAGE_SIZE && ( + + )} + + {visibleCount < filteredLists.length && ( + + )} +
+ +
+
+ ); +} diff --git a/frontend/src/pages/shoppingLists/ShoppingListCard.tsx b/frontend/src/pages/shoppingLists/ShoppingListCard.tsx index 6b0d91e..e601f22 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListCard.tsx +++ b/frontend/src/pages/shoppingLists/ShoppingListCard.tsx @@ -11,6 +11,8 @@ export default function ShoppingListCard({ list, onOpen, }: ShoppingListCardProps) { + const productCount = list.items.length; + const itemCount = list.items.reduce( (total, item) => total + item.quantity, 0 @@ -71,8 +73,10 @@ export default function ShoppingListCard({
- {itemCount} VARER - {formattedPrice} + + {productCount} PRODUKTER · {itemCount} VARER + + {formattedPrice}
diff --git a/frontend/src/pages/shoppingLists/ShoppingListsPage.css b/frontend/src/pages/shoppingLists/ShoppingListsPage.css index 0eddd0f..bc7c334 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListsPage.css +++ b/frontend/src/pages/shoppingLists/ShoppingListsPage.css @@ -181,12 +181,6 @@ align-items: stretch; } -@media (max-width: 1000px) { - .shopping-lists-page__grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - .shopping-lists-page__eyebrow { display: block; @@ -327,4 +321,30 @@ /* Kortene retter seg opp ved hover */ .shopping-list-card:hover { transform: translateY(-4px) rotate(0deg); +} + +.shopping-lists-page__tabs { + width: fit-content; + flex-shrink: 0; +} + +.shopping-lists-page__tab { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + color: var(--text-muted); +} + +.shopping-lists-page__tab--active { + color: var(--text-brand); +} + +.shopping-lists-page__tab:hover { + color: var(--text-accent); +} + +.shopping-lists-page__tab:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 3px; } \ No newline at end of file diff --git a/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx b/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx index 3d04e2f..7f7014a 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx +++ b/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router"; +import { Link, useNavigate } from "react-router"; import ShoppingListCard from "./ShoppingListCard"; import type { ShoppingList } from "./shoppingListTypes"; import "./ShoppingListsPage.css"; @@ -92,13 +92,20 @@ export default function ShoppingListsPage() {