diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ce45843..92d8830 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,4 @@ import { useState } from "react"; -import NavigationMenu from "./components/navigationMenu/NavigationMenu"; -import ShoppingListsPage from "./pages/shoppingLists/ShoppingListsPage"; -import ShoppingListDetailPage from "./pages/shoppingLists/ShoppingListDetailPage"; - import { BrowserRouter, Routes, @@ -11,17 +7,25 @@ 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 ShoppingListDetailPage from "./pages/shoppingLists/ShoppingListDetailPage"; +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() { } /> + } + /> + 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..bcd5778 --- /dev/null +++ b/frontend/src/pages/shoppingLists/PreviousListsPage.css @@ -0,0 +1,350 @@ + +/* 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); +} + + +.previous-list-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 125px; + align-items: center; + gap: 1rem; + padding: 14px 0; + border-bottom: 1px solid var(--border-hairline); +} + +/* Klikkbart område for å åpne en tidligere handleliste */ +.previous-list-row__link { + display: grid; + grid-template-columns: 65px minmax(0, 1fr) 180px 110px; + align-items: center; + gap: 1rem; + min-width: 0; + text-decoration: none; + border-radius: 4px; +} + +.previous-list-row__link:hover .previous-list-row__name { + color: var(--text-brand); + text-decoration: underline; +} + +.previous-list-row__link:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 3px; +} + +.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 */ + + +/* Mobil og nettbrett */ + +@media (max-width: 900px) { + .previous-list-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + } + + .previous-list-row__link { + grid-template-columns: 55px minmax(0, 1fr) auto; + gap: 8px 10px; + } + + .previous-list-row__date { + grid-column: 1; + grid-row: 1; + } + + .previous-list-row__name { + grid-column: 2; + grid-row: 1; + } + + .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: 2; + grid-row: 1; + } +} + +@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: minmax(0, 1fr); + gap: 10px; + } + + .previous-list-row__link { + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px 12px; + } + + .previous-list-row__date { + grid-column: 1; + grid-row: 1; + } + + .previous-list-row__name { + grid-column: 1; + grid-row: 2; + } + + .previous-list-row__count { + grid-column: 1; + grid-row: 3; + } + + .previous-list-row__price { + grid-column: 2; + grid-row: 2; + } + + .previous-list-row__reuse { + grid-column: 1; + grid-row: 2; + justify-self: end; + } +} + +.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..d59aa88 --- /dev/null +++ b/frontend/src/pages/shoppingLists/PreviousListsPage.tsx @@ -0,0 +1,252 @@ +import { Link } from "react-router"; +import { useEffect, useMemo, useState } from "react"; +import PreviousListRow from "./PreviousListRow"; +import type { ShoppingList } from "./shoppingListTypes"; +import "./PreviousListsPage.css"; +import "./ShoppingListsPage.css"; +import { mockLists } from "./shoppingListMockData"; + + + +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 = 5; + +const STORAGE_KEY = "previous-lists-preferences"; + +type SortOrder = "newest" | "oldest"; + +interface PreviousListsPreferences { + search: string; + sortOrder: SortOrder; + visibleCount: number; +} + +function loadPreferences(): PreviousListsPreferences { + try { + const saved = localStorage.getItem(STORAGE_KEY); + + if (saved) { + const parsed = JSON.parse(saved); + + return { + search: typeof parsed.search === "string" ? parsed.search : "", + sortOrder: + parsed.sortOrder === "oldest" ? "oldest" : "newest", + visibleCount: + typeof parsed.visibleCount === "number" && + Number.isInteger(parsed.visibleCount) && + parsed.visibleCount >= PAGE_SIZE + ? parsed.visibleCount + : PAGE_SIZE, + }; + } + } catch { + // Bruk standardverdier dersom lagrede data er ugyldige. + } + + return { + search: "", + sortOrder: "newest" as SortOrder, + visibleCount: PAGE_SIZE, + }; +} + +export default function PreviousListsPage() { + const [preferences, setPreferences] = useState(loadPreferences); + + const { search, sortOrder, visibleCount } = preferences; + + useEffect(() => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences)); + } catch { + // Siden fungerer fortsatt hvis lagring ikke er tilgjengelig. + } +}, [preferences]); + + const filteredLists = useMemo(() => { + return mockLists + .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 99935e0..cd68215 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListCard.tsx +++ b/frontend/src/pages/shoppingLists/ShoppingListCard.tsx @@ -72,12 +72,12 @@ export default function ShoppingListCard({ )} -
- - {productCount} PRODUKTER · {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 fd9ef6a..ff3ab31 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; @@ -329,6 +323,35 @@ transform: translateY(-4px) rotate(0deg); } + +/* Faner mellom aktive og tidligere handlelister */ +.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; +} + +/* Sortering av aktive handlelister */ .shopping-lists-page__sort { display: flex; flex-direction: column; @@ -352,4 +375,4 @@ .shopping-lists-page__sort select: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 057fa0a..1243b31 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx +++ b/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useNavigate } from "react-router"; +import { Link, useNavigate } from "react-router"; import ShoppingListCard from "./ShoppingListCard"; import { mockLists } from "./shoppingListMockData"; import "./ShoppingListsPage.css"; @@ -82,6 +82,7 @@ export default function ShoppingListsPage() {

+
+