From 9e4dd3970edfdac67b61eae0cee2ceb52597c761 Mon Sep 17 00:00:00 2001 From: Ingrihka Date: Tue, 20 Oct 2026 08:58:45 +0200 Subject: [PATCH 1/2] feat: side for aktiv handleliste fra Figma (#58) --- .../pages/shoppingList/ShoppingListPage.css | 375 ++++++++++++++++++ .../pages/shoppingList/ShoppingListPage.tsx | 332 ++++++++++++++++ 2 files changed, 707 insertions(+) create mode 100644 frontend/src/pages/shoppingList/ShoppingListPage.css create mode 100644 frontend/src/pages/shoppingList/ShoppingListPage.tsx diff --git a/frontend/src/pages/shoppingList/ShoppingListPage.css b/frontend/src/pages/shoppingList/ShoppingListPage.css new file mode 100644 index 0000000..38f742e --- /dev/null +++ b/frontend/src/pages/shoppingList/ShoppingListPage.css @@ -0,0 +1,375 @@ +/* Side for aktiv handleliste – fra Figma (Active shopping list) */ +.shopping-list-page { + min-height: 100vh; + max-width: 1312px; + margin: 0 auto; + padding: 32px 48px 0; + box-sizing: border-box; + background: var(--bg-page); + color: var(--text-primary); + font-family: var(--font-ui); + text-align: left; +} + +/* Linjen over lappen: tilbake-lenke og figurtekst */ +.shopping-list-page__top { + display: flex; + justify-content: space-between; + max-width: 780px; + margin-bottom: 20px; + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; +} + +.shopping-list-page__back { + color: var(--text-accent); + text-decoration: none; +} + +.shopping-list-page__back:hover { + text-decoration: underline; +} + +.shopping-list-page__fig { + margin: 0; + color: var(--text-muted); +} + +/* Lappen til venstre, oppsummeringen til høyre */ +.shopping-list-page__layout { + display: grid; + grid-template-columns: minmax(0, 780px) 332px; + justify-content: space-between; + align-items: start; + gap: 48px; + padding-bottom: 56px; +} + +/* ---------- Notatblokken ---------- */ +.notepad { + position: relative; +} + +/* To ark som ligger skjevt bak lappen */ +.notepad__sheet { + position: absolute; + inset: 0; + border: 1.25px solid var(--border-strong); + border-radius: 3px; + box-shadow: + 3px 16px 22px -10px rgba(64, 5, 18, 0.1), + 0 1px 2px rgba(64, 5, 18, 0.08); +} + +.notepad__sheet--back { + background: var(--note-tint); + transform: rotate(2deg); +} + +.notepad__sheet--middle { + background: var(--note-paper); + transform: rotate(-1.2deg); +} + +.notepad__pad { + position: relative; + overflow: hidden; + border: 1.25px solid var(--border-strong); + border-radius: 3px; + background: var(--note-paper); + box-shadow: + 3px 16px 22px -10px rgba(64, 5, 18, 0.1), + 0 1px 2px rgba(64, 5, 18, 0.08); +} + +/* Mørk stripe øverst */ +.notepad__bar { + display: flex; + justify-content: space-between; + align-items: center; + height: 34px; + padding: 0 24px; + background: var(--bg-brand); + color: var(--text-on-dark); + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; +} + +.notepad__paper { + position: relative; + padding: 30px 40px 30px 84px; +} + +/* Rød marglinje */ +.notepad__paper::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 62px; + width: 1.25px; + background: var(--note-margin); +} + +.notepad__header { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 16px; + padding-bottom: 14px; +} + +.notepad__title { + margin: 0 0 2px; + font: 700 46px/54px var(--font-note); + color: var(--note-ink); +} + +.notepad__meta { + margin: 0; + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; + color: var(--text-muted); +} + +/* Sorteringsvalg */ +.notepad__sort { + display: flex; + align-items: center; + gap: 4px; + height: 32px; + padding: 0 8px 0 12px; + background: var(--bg-surface); + border: 1px solid var(--border-hairline); + border-radius: 6px; + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; + color: var(--text-primary); +} + +.notepad__sort select { + border: none; + background: transparent; + color: inherit; + font: inherit; + letter-spacing: inherit; + cursor: pointer; +} + +.notepad__list { + margin: 0; + padding: 0; + list-style: none; +} + +/* Hele navnefeltet er klikkbart for å krysse av */ +.notepad__list .shopping-list-row__name { + cursor: pointer; +} + +.notepad__list .shopping-list-row__price { + text-transform: uppercase; +} + +.notepad__list .quantity-stepper__button:disabled { + opacity: 0.4; + cursor: default; +} + +/* «Legg til vare» nederst på lappen */ +.notepad__add { + display: flex; + align-items: center; + gap: 12px; + padding-top: 12px; +} + +.notepad__add-plus { + font: 700 22px var(--font-note); + color: var(--text-accent); +} + +.notepad__add input { + flex: 1; + min-width: 0; + padding: 0; + border: none; + background: transparent; + font: 400 19px var(--font-note); + color: var(--note-ink); +} + +.notepad__add input::placeholder { + color: var(--text-muted); +} + +.notepad__add input:focus-visible { + outline: none; + border-bottom: 1px solid var(--note-margin); +} + +/* ---------- Oppsummering ---------- */ +.list-summary { + display: flex; + flex-direction: column; + gap: 14px; + padding: 24px; + background: var(--bg-surface); + border: 1.25px solid var(--border-strong); + border-radius: 4px; +} + +.list-summary__label { + margin: 0; + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; + color: var(--text-accent); +} + +.list-summary__label--muted { + color: var(--text-muted); +} + +.list-summary__facts { + display: flex; + flex-direction: column; + gap: 14px; + margin: 0 0 4px; +} + +/* Én rad: navn ..... verdi */ +.list-summary__facts div { + display: flex; + align-items: flex-end; + gap: 8px; + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; +} + +.list-summary__facts dt { + color: var(--text-secondary); +} + +.list-summary__facts span { + flex: 1; + height: 8px; + border-bottom: 1px dashed var(--border-hairline); +} + +.list-summary__facts dd { + margin: 0; + color: var(--text-primary); +} + +.list-summary__total { + display: flex; + flex-direction: column; + gap: 2px; +} + +.list-summary__sum { + margin: 0; + font: 500 44px var(--font-ui); + letter-spacing: -0.03em; +} + +.list-summary__sum em { + font-family: var(--font-accent); + font-style: italic; + font-weight: 400; + color: var(--text-brand); +} + +.list-summary__button { + justify-content: center; + width: 100%; + box-sizing: border-box; + text-decoration: none; +} + +.list-summary__info { + margin: 0; + font: 400 12px/18px var(--font-ui); + color: var(--text-muted); +} + +/* ---------- Bunntekst ---------- */ +.shopping-list-page__footer { + display: flex; + justify-content: space-between; + padding: 16px 0 32px; + border-top: 1px solid var(--border-hairline); + font: 500 10px var(--font-mono); + letter-spacing: 0.06em; + color: var(--text-muted); +} + +.shopping-list-page__footer p { + margin: 0; +} + +/* ---------- Mindre skjermer ---------- */ +@media (max-width: 1000px) { + .shopping-list-page__layout { + grid-template-columns: 1fr; + } + + .list-summary { + max-width: 480px; + } +} + +@media (max-width: 600px) { + .shopping-list-page { + padding: 24px 16px 0; + } + + /* Arkene bak tar for mye plass på mobil */ + .notepad__sheet { + display: none; + } + + .notepad__paper { + padding: 20px 16px 20px 40px; + } + + .notepad__paper::before { + left: 26px; + } + + .notepad__header { + flex-direction: column; + align-items: flex-start; + } + + .notepad__title { + font-size: 36px; + line-height: 42px; + } + + /* Varenavnet får egen linje, antall og pris under */ + .notepad__list .shopping-list-row { + flex-wrap: wrap; + } + + .notepad__list .shopping-list-row__name { + flex-basis: calc(100% - 28px); + } + + .notepad__list .quantity-stepper { + margin-left: 28px; + } + + .notepad__list .shopping-list-row__price { + margin-left: auto; + } + + .notepad__add input { + font-size: 16px; + } + + .shopping-list-page__footer { + flex-direction: column; + gap: 4px; + } +} diff --git a/frontend/src/pages/shoppingList/ShoppingListPage.tsx b/frontend/src/pages/shoppingList/ShoppingListPage.tsx new file mode 100644 index 0000000..dbee425 --- /dev/null +++ b/frontend/src/pages/shoppingList/ShoppingListPage.tsx @@ -0,0 +1,332 @@ +import { useState } from "react"; +import type { FormEvent } from "react"; +import { Link } from "react-router"; +import "./ShoppingListPage.css"; +import "../../components/shoppingListRow/ShoppingListRow.css"; +import "../../components/checkbox/Checkbox.css"; +import "../../components/quantityStepper/QuantityStepper.css"; +import "../../components/button/Button.css"; + +type ListItem = { + id: string; + name: string; + note?: string; + quantity: number; + unitPrice: number | null; // null = vare skrevet fritt, uten pris + checked: boolean; +}; + +type SortOrder = "added" | "alphabetical"; + +// Midlertidige data. Byttes ut med shoppingList(id) fra GraphQL senere. +const initialItems: ListItem[] = [ + { id: "1", name: "Lettmelk", quantity: 2, unitPrice: 23.9, checked: false }, + { + id: "2", + name: "Norvegia 26 %", + note: "skivet, hvis de har", + quantity: 1, + unitPrice: 79.9, + checked: false, + }, + { id: "3", name: "Mozzarella", quantity: 2, unitPrice: 24.9, checked: false }, + { + id: "4", + name: "Egg, 12 stk", + note: "frittgående", + quantity: 1, + unitPrice: 54.9, + checked: false, + }, + { id: "5", name: "Bananer", quantity: 1, unitPrice: 29.9, checked: true }, + { id: "6", name: "Tomater", quantity: 4, unitPrice: 6.9, checked: false }, + { + id: "7", + name: "Kaffe", + note: "filtermalt", + quantity: 1, + unitPrice: 64.9, + checked: true, + }, + { + id: "8", + name: "Pasta", + note: "spaghetti", + quantity: 1, + unitPrice: 31.6, + checked: false, + }, +]; + +// Gir f.eks. "47,80" +function formatPrice(value: number) { + return value.toFixed(2).replace(".", ","); +} + +function ShoppingListPage() { + const [items, setItems] = useState(initialItems); + const [sortOrder, setSortOrder] = useState("added"); + const [newItem, setNewItem] = useState(""); + + const checkedCount = items.filter((item) => item.checked).length; + const total = items.reduce( + (sum, item) => sum + (item.unitPrice ?? 0) * item.quantity, + 0, + ); + + const sortedItems = + sortOrder === "alphabetical" + ? [...items].sort((a, b) => a.name.localeCompare(b.name, "nb")) + : items; + + function toggleChecked(id: string) { + setItems((current) => + current.map((item) => + item.id === id ? { ...item, checked: !item.checked } : item, + ), + ); + } + + function changeQuantity(id: string, change: number) { + setItems((current) => + current.map((item) => + item.id === id + ? { ...item, quantity: Math.max(1, item.quantity + change) } + : item, + ), + ); + } + + function removeItem(id: string) { + setItems((current) => current.filter((item) => item.id !== id)); + } + + function addItem(event: FormEvent) { + event.preventDefault(); + const name = newItem.trim(); + if (!name) return; + setItems((current) => [ + ...current, + { + id: crypto.randomUUID(), + name, + quantity: 1, + unitPrice: null, + checked: false, + }, + ]); + setNewItem(""); + } + + return ( +
+
+ + ← MINE HANDLELISTER + +

FIG. 8 — NOTATBLOKKEN

+
+ +
+
+ + +
+

HANDLELAPPEN — IT2810 · GRUPPE 27

+

SIDE 08 / 11

+
+
+ ); +} + +export default ShoppingListPage; From 7e31718b1685d6f8104ee1d63514037ce63c3a52 Mon Sep 17 00:00:00 2001 From: Ingrihka Date: Tue, 20 Oct 2026 09:56:46 +0200 Subject: [PATCH 2/2] feat: detaljside for aktiv handleliste koblet til /lists/:id (#58) --- frontend/src/App.tsx | 8 +- .../ShoppingListDetailPage.css} | 0 .../ShoppingListDetailPage.tsx} | 108 ++++------ .../pages/shoppingLists/ShoppingListsPage.tsx | 128 ++++-------- .../shoppingLists/shoppingListMockData.ts | 194 ++++++++++++++++++ .../pages/shoppingLists/shoppingListTypes.ts | 3 +- 6 files changed, 279 insertions(+), 162 deletions(-) rename frontend/src/pages/{shoppingList/ShoppingListPage.css => shoppingLists/ShoppingListDetailPage.css} (100%) rename frontend/src/pages/{shoppingList/ShoppingListPage.tsx => shoppingLists/ShoppingListDetailPage.tsx} (83%) create mode 100644 frontend/src/pages/shoppingLists/shoppingListMockData.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c5c1bb8..ce45843 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import NavigationMenu from "./components/navigationMenu/NavigationMenu"; import ShoppingListsPage from "./pages/shoppingLists/ShoppingListsPage"; +import ShoppingListDetailPage from "./pages/shoppingLists/ShoppingListDetailPage"; import { BrowserRouter, @@ -126,10 +127,9 @@ function AppLayout() { element={} /> - } - /> + } /> + + } /> (initialItems); +// Gir f.eks. "19.10.2026" +function formatDate(isoDate: string) { + return new Date(isoDate).toLocaleDateString("nb-NO", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function ShoppingListDetailPage() { + // Henter id fra adressen, f.eks. /lists/2 gir id = "2" + const { id } = useParams(); + const list = mockLists.find((candidate) => candidate.id === id); + + const [items, setItems] = useState(list?.items ?? []); const [sortOrder, setSortOrder] = useState("added"); const [newItem, setNewItem] = useState(""); @@ -79,26 +45,26 @@ function ShoppingListPage() { ? [...items].sort((a, b) => a.name.localeCompare(b.name, "nb")) : items; - function toggleChecked(id: string) { + function toggleChecked(itemId: string) { setItems((current) => current.map((item) => - item.id === id ? { ...item, checked: !item.checked } : item, + item.id === itemId ? { ...item, checked: !item.checked } : item, ), ); } - function changeQuantity(id: string, change: number) { + function changeQuantity(itemId: string, change: number) { setItems((current) => current.map((item) => - item.id === id + item.id === itemId ? { ...item, quantity: Math.max(1, item.quantity + change) } : item, ), ); } - function removeItem(id: string) { - setItems((current) => current.filter((item) => item.id !== id)); + function removeItem(itemId: string) { + setItems((current) => current.filter((item) => item.id !== itemId)); } function addItem(event: FormEvent) { @@ -109,6 +75,7 @@ function ShoppingListPage() { ...current, { id: crypto.randomUUID(), + ean: "", // fritt skrevet vare, ikke koblet til et produkt name, quantity: 1, unitPrice: null, @@ -118,6 +85,19 @@ function ShoppingListPage() { setNewItem(""); } + // Ugyldig id i adressen, f.eks. /lists/999 + if (!list) { + return ( +
+ + ← MINE HANDLELISTER + +

Fant ikke handlelisten

+

Listen finnes ikke, eller den er slettet.

+
+ ); + } + return (
@@ -140,15 +120,15 @@ function ShoppingListPage() {
- HANDLELAPPEN NR. 14 - 25.09.2026 + HANDLELAPPEN NR. {list.id} + {formatDate(list.createdAt)}

- Fredagshandel + {list.name}

{checkedCount} AV {items.length} KRYSSET AV @@ -223,7 +203,7 @@ function ShoppingListPage() {

- {item.unitPrice === null + {item.unitPrice == null ? "—" : `${formatPrice(item.unitPrice * item.quantity)} KR`} @@ -329,4 +309,4 @@ function ShoppingListPage() { ); } -export default ShoppingListPage; +export default ShoppingListDetailPage; diff --git a/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx b/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx index 3d04e2f..fd55424 100644 --- a/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx +++ b/frontend/src/pages/shoppingLists/ShoppingListsPage.tsx @@ -1,73 +1,12 @@ import { useNavigate } from "react-router"; import ShoppingListCard from "./ShoppingListCard"; -import type { ShoppingList } from "./shoppingListTypes"; +import { mockLists } from "./shoppingListMockData"; import "./ShoppingListsPage.css"; -const mockLists: ShoppingList[] = [ - { - id: "1", - name: "Fredagshandel", - status: "ACTIVE", - inUse: true, - createdAt: "2026-10-19", - estimatedTotal: 423.5, - items: [ - { id: "1", ean: "1", name: "Melk", quantity: 2, checked: false }, - { id: "2", ean: "2", name: "Brød", quantity: 1, checked: false }, - { id: "3", ean: "3", name: "Egg", quantity: 1, checked: false }, - { id: "14", ean: "14", name: "Yoghurt", quantity: 1, checked: false }, - { id: "15", ean: "15", name: "Bananer", quantity: 1, checked: false }, - ], - }, - { - id: "2", - name: "Taco-kveld", - status: "ACTIVE", - inUse: false, - createdAt: "2026-10-18", - estimatedTotal: 287, - items: [ - { id: "4", ean: "4", name: "Tortilla", quantity: 2, checked: false }, - { id: "5", ean: "5", name: "Kjøttdeig", quantity: 1, checked: false }, - { id: "6", ean: "6", name: "Ost", quantity: 1, checked: false }, - ], - }, - { - id: "3", - name: "Hyttetur", - status: "ACTIVE", - inUse: false, - createdAt: "2026-10-17", - estimatedTotal: 756, - items: [ - { id: "7", ean: "7", name: "Pasta", quantity: 2, checked: false }, - { id: "8", ean: "8", name: "Kaffe", quantity: 1, checked: false }, - { id: "9", ean: "9", name: "Sjokolade", quantity: 3, checked: false }, - ], - }, - { - id: "4", - name: "Matpakke-basics", - status: "ACTIVE", - inUse: false, - createdAt: "2026-10-14", - estimatedTotal: 148.2, - items: [ - { id: "10", ean: "10", name: "Knekkebrød", quantity: 1, checked: false }, - { id: "11", ean: "11", name: "Leverpostei", quantity: 1, checked: false }, - { id: "12", ean: "12", name: "Agurk", quantity: 1, checked: false }, - { id: "13", ean: "13", name: "Paprika", quantity: 1, checked: false }, - ], - }, - -]; - export default function ShoppingListsPage() { const navigate = useNavigate(); - const activeLists = mockLists.filter( - (list) => list.status === "ACTIVE" - ); + const activeLists = mockLists.filter((list) => list.status === "ACTIVE"); function handleOpenList(id: string) { navigate(`/lists/${id}`); @@ -77,55 +16,58 @@ export default function ShoppingListsPage() {
- - 07 · LISTER - + 07 · LISTER -

+

Mine handlelister -

+ -

+

{activeLists.length} AKTIVE LISTER · DEN DU BRUKER NÅ ER MERKET -

+

- - - + Aktive + + + Tidligere + + + +
{activeLists.length > 0 ? (
-
+
{activeLists.map((list) => ( - + /> ))} -
+
) : (

Du har ingen aktive handlelister ennå.

diff --git a/frontend/src/pages/shoppingLists/shoppingListMockData.ts b/frontend/src/pages/shoppingLists/shoppingListMockData.ts new file mode 100644 index 0000000..6be5388 --- /dev/null +++ b/frontend/src/pages/shoppingLists/shoppingListMockData.ts @@ -0,0 +1,194 @@ +import type { ShoppingList } from "./shoppingListTypes"; + +// Midlertidige data som brukes av både oversikten og handlelistesiden. +// Byttes ut med GraphQL når backend er klar. +export const mockLists: ShoppingList[] = [ + { + id: "1", + name: "Fredagshandel", + status: "ACTIVE", + inUse: true, + createdAt: "2026-10-19", + estimatedTotal: 386.4, + items: [ + { + id: "1", + ean: "1", + name: "Lettmelk", + quantity: 2, + unitPrice: 23.9, + checked: false, + }, + { + id: "2", + ean: "2", + name: "Norvegia 26 %", + note: "skivet, hvis de har", + quantity: 1, + unitPrice: 79.9, + checked: false, + }, + { + id: "3", + ean: "3", + name: "Mozzarella", + quantity: 2, + unitPrice: 24.9, + checked: false, + }, + { + id: "4", + ean: "4", + name: "Egg, 12 stk", + note: "frittgående", + quantity: 1, + unitPrice: 54.9, + checked: false, + }, + { + id: "5", + ean: "5", + name: "Bananer", + quantity: 1, + unitPrice: 29.9, + checked: true, + }, + { + id: "6", + ean: "6", + name: "Tomater", + quantity: 4, + unitPrice: 6.9, + checked: false, + }, + { + id: "7", + ean: "7", + name: "Kaffe", + note: "filtermalt", + quantity: 1, + unitPrice: 64.9, + checked: true, + }, + { + id: "8", + ean: "8", + name: "Pasta", + note: "spaghetti", + quantity: 1, + unitPrice: 31.6, + checked: false, + }, + ], + }, + { + id: "2", + name: "Taco-kveld", + status: "ACTIVE", + inUse: false, + createdAt: "2026-10-18", + estimatedTotal: 287, + items: [ + { + id: "9", + ean: "9", + name: "Tortilla", + quantity: 2, + unitPrice: 34.9, + checked: false, + }, + { + id: "10", + ean: "10", + name: "Kjøttdeig", + quantity: 1, + unitPrice: 89.9, + checked: false, + }, + { + id: "11", + ean: "11", + name: "Ost", + quantity: 1, + unitPrice: 127.3, + checked: false, + }, + ], + }, + { + id: "3", + name: "Hyttetur", + status: "ACTIVE", + inUse: false, + createdAt: "2026-10-17", + estimatedTotal: 256.8, + items: [ + { + id: "12", + ean: "12", + name: "Pasta", + quantity: 2, + unitPrice: 31.6, + checked: false, + }, + { + id: "13", + ean: "13", + name: "Kaffe", + quantity: 1, + unitPrice: 64.9, + checked: false, + }, + { + id: "14", + ean: "14", + name: "Sjokolade", + quantity: 3, + unitPrice: 42.9, + checked: false, + }, + ], + }, + { + id: "4", + name: "Matpakke-basics", + status: "ACTIVE", + inUse: false, + createdAt: "2026-10-14", + estimatedTotal: 103.6, + items: [ + { + id: "15", + ean: "15", + name: "Knekkebrød", + quantity: 1, + unitPrice: 39.9, + checked: false, + }, + { + id: "16", + ean: "16", + name: "Leverpostei", + quantity: 1, + unitPrice: 24.9, + checked: false, + }, + { + id: "17", + ean: "17", + name: "Agurk", + quantity: 1, + unitPrice: 16.9, + checked: false, + }, + { + id: "18", + ean: "18", + name: "Paprika", + quantity: 1, + unitPrice: 21.9, + checked: false, + }, + ], + }, +]; diff --git a/frontend/src/pages/shoppingLists/shoppingListTypes.ts b/frontend/src/pages/shoppingLists/shoppingListTypes.ts index e6648d0..93be9f5 100644 --- a/frontend/src/pages/shoppingLists/shoppingListTypes.ts +++ b/frontend/src/pages/shoppingLists/shoppingListTypes.ts @@ -1,10 +1,11 @@ - export interface ShoppingListItem { id: string; ean: string; name: string; quantity: number; checked: boolean; + note?: string; + unitPrice?: number | null; } export interface ShoppingList {