diff --git a/frontend/src/productDetails/ProductDetailsPage.css b/frontend/src/pages/productDetails/ProductDetailsPage.css similarity index 81% rename from frontend/src/productDetails/ProductDetailsPage.css rename to frontend/src/pages/productDetails/ProductDetailsPage.css index a24eb10..3c9a3ce 100644 --- a/frontend/src/productDetails/ProductDetailsPage.css +++ b/frontend/src/pages/productDetails/ProductDetailsPage.css @@ -2,12 +2,13 @@ /* ===== PRODUKTDETALJSIDE ===== */ .product-details-page { - width: 100%; + width: 100%; max-width: 1100px; margin: 0 auto; padding: 48px 24px 80px; box-sizing: border-box; - color: #171717; + background-color: var(--bg-surface); + color: var(--text-primary); text-align: left; } @@ -28,7 +29,7 @@ .product-details-page h1, .product-details-page h2 { - color: #171717; + color: var(--text-primary); text-align: left; } @@ -39,7 +40,7 @@ } .product-details-page .product-back-navigation a { - color: #d3045d; + color: var(--text-accent); text-decoration: none; font-size: 11px; font-weight: 600; @@ -80,33 +81,48 @@ .product-details-page .product-image { width: 100%; aspect-ratio: 1.2 / 1; - border: 1px solid #780b2b; border-radius: 10px; display: flex; justify-content: center; align-items: center; - - background-color: #fafafa; - background-image: - linear-gradient(#e8e8e8 1px, transparent 1px), - linear-gradient(90deg, #e8e8e8 1px, transparent 1px); - background-size: 20px 20px; } .product-details-page .product-image p { - color: #777; + color: var(--text-muted); font-size: 12px; letter-spacing: 1px; } .product-details-page .product-figure figcaption { margin-top: 12px; - color: #777; + color: var(--text-muted); font-size: 10px; letter-spacing: 1px; } +/* Produkt med bilde */ +.product-details-page .product-image.has-image { + background: transparent; + border: none; +} + +/* Når produktet mangler bilde */ +.product-details-page .product-image.no-image { + background-color: var(--bg-muted); + background-image: + linear-gradient(var(--border-grid) 1px, transparent 1px), + linear-gradient(90deg, var(--border-grid) 1px, transparent 1px); + background-size: 20px 20px; + border: 1px solid var(--border-brand); +} + +.product-details-page .product-image-content { + width: 100%; + height: 100%; + object-fit: contain; +} + /* ===== INGREDIENSER OG ALLERGENER ===== */ .product-details-page .product-ingredients h2, @@ -114,29 +130,29 @@ .product-details-page .product-nutrition h2 { padding-bottom: 12px; margin-bottom: 14px; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-brand); font-size: 11px; font-weight: 600; letter-spacing: 0.5px; } .product-details-page .section-letter { - color: #d3045d; + color: var(--text-accent); margin-right: 5px; } .product-details-page .product-ingredients p { font-size: 14px; line-height: 1.7; - color: #555; + color: var(--text-secondary); } .product-details-page .product-allergens p { display: inline-block; padding: 7px 10px; border-radius: 4px; - background: #fce8ef; - color: #780b2b; + background: var(--bg-tint); + color: var(--text-primary); font-size: 11px; font-weight: 600; } @@ -148,7 +164,7 @@ } .product-details-page .product-brand { - color: #d3045d; + color: var(--text-accent); font-size: 11px; font-weight: 600; letter-spacing: 1px; @@ -156,7 +172,7 @@ } .product-details-page .product-name { - color: #171717; + color: var(--text-primary); font-size: 42px; font-weight: 600; line-height: 1.12; @@ -165,7 +181,7 @@ .product-details-page .product-category { margin-top: 10px; - color: #777; + color: var(--text-muted); font-size: 11px; letter-spacing: 1px; } @@ -193,7 +209,7 @@ .product-details-page .price-number { font-size: 42px; font-weight: 500; - color: #171717; + color: var(--text-primary); } .product-details-page .price-currency { @@ -201,21 +217,14 @@ font-size: 60px; /* Større */ font-weight: 300; /* Tynnere */ font-style: italic; - color: #780b2b; + color: var(--text-accent); line-height: 1; } .product-details-page .product-cheapest-store { - margin: 0; - color: #777; + margin: 8px 0 0; + color: var(--text-muted); font-family: monospace; - font-size: 9px; - letter-spacing: 0.3px; -} - -.product-details-page .product-cheapest-store { - margin-top: 8px; - color: #777; font-size: 10px; letter-spacing: 0.5px; } @@ -223,7 +232,7 @@ /* ===== PRISSAMMENLIGNING ===== */ .product-details-page .store-prices { - border: 1px solid #780b2b; + border: 1px solid var(--border-brand); border-radius: 10px; overflow: hidden; box-shadow: 0 3px 0 rgba(120, 11, 43, 0.12); @@ -247,7 +256,7 @@ } .product-details-page .store-prices-header span { - color: #777; + color: var(--text-muted); font-size: 10px; white-space: nowrap; } @@ -265,27 +274,27 @@ gap: 8px; min-height: 46px; padding: 12px 16px; - border-top: 1px solid #eee; + border-top: 1px solid var(--border-brand); } .product-details-page .store-price-row.cheapest { - background: #fce8ef; + background: var(--bg-tint); } .product-details-page .store-number { - color: #d3045d; + color: var(--text-accent); font-size: 10px; font-weight: 700; } .product-details-page .store-name { - color: #171717; + color: var(--text-primary); font-size: 13px; font-weight: 500; } .product-details-page .store-difference { - color: #888; + color: var(--text-muted); font-size: 10px; text-align: right; white-space: nowrap; @@ -295,14 +304,14 @@ justify-self: end; padding: 5px 7px; border-radius: 3px; - background: #d3045d; + background: var(--text-accent); color: white; font-size: 9px; font-weight: 700; } .product-details-page .store-price { - color: #171717; + color: var(--text-primary); font-size: 13px; font-weight: 600; text-align: right; @@ -320,9 +329,9 @@ margin-top: 38px; padding: 26px 26px 24px; - border: 1px solid #171717; - border-left: 3px solid #d3045d; - background: white; + border: 1px solid var(--text-primary); + border-left: 3px solid var(--text-accent); + background: var(--bg-surface); box-shadow: 0 12px 18px rgba(0, 0, 0, 0.08); } @@ -334,7 +343,7 @@ top: -9px; left: 50%; transform: translateX(-50%) rotate(2deg); - background: #f6cada; + background: var(--note-tape); } .product-details-page .shopping-list-header { @@ -343,7 +352,7 @@ .product-details-page .shopping-list-header h2 { - color: #780b2b; + color: var(--text-accent); font-style: italic; font-weight: 600; line-height: 1.3; @@ -353,8 +362,7 @@ .product-details-page .quantity-controls { display: inline-flex; align-items: center; - margin-top: 14px; - border: 1px solid #e5e5e5; + border: 1px solid var(--border-hairline); border-radius: 5px; overflow: hidden; grid-column: 1; @@ -365,14 +373,14 @@ min-width: 32px; min-height: 32px; padding: 4px; - background: white; + background: var(--bg-surface); border: none; - color: #171717; + color: var(--text-primary); cursor: pointer; } .product-details-page .quantity-controls button:hover:not(:disabled) { - background: #fce8ef; + background: var(--bg-tint); } .product-details-page .quantity-controls button:disabled { @@ -384,7 +392,7 @@ min-width: 24px; text-align: center; font-size: 12px; - color: #171717; + color: var(--text-primary); } .product-details-page .remove-product { @@ -394,24 +402,24 @@ margin: 12px 0 0; padding: 9px 12px; - border: 1px solid #ddd; + border: 1px solid var(--border-brand); border-radius: 5px; - background: white; - color: #171717; + background: var(--bg-surface); + color: var(--text-primary); font-size: 11px; cursor: pointer; } .product-details-page .remove-product:hover { - background: #fce8ef; + background: var(--bg-tint); } .product-details-page .shopping-list-controls label { display: block; margin-top: 24px; margin-bottom: 10px; - color: #777; + color: var(--text-muted); font-size: 10px; letter-spacing: 1px; } @@ -419,13 +427,12 @@ .product-details-page .shopping-list-controls textarea { display: block; width: 100%; - min-height: 42px; padding: 8px 0; border: none; border-bottom: 1px solid #f3b4cc; border-radius: 0; background: transparent; - color: #171717; + color: var(--text-primary); font: inherit; font-size: 14px; resize: vertical; @@ -434,7 +441,7 @@ } .product-details-page .shopping-list-controls textarea::placeholder { - color: #888; + color: var(--text-muted); font-style: italic; } @@ -466,12 +473,12 @@ } .product-details-page .product-nutrition dt { - color: #666; + color: var(--text-secondary); } .product-details-page .product-nutrition dd { margin: 0; - color: #171717; + color: var(--text-primary); font-weight: 600; text-align: right; } diff --git a/frontend/src/productDetails/ProductDetailsPage.tsx b/frontend/src/pages/productDetails/ProductDetailsPage.tsx similarity index 51% rename from frontend/src/productDetails/ProductDetailsPage.tsx rename to frontend/src/pages/productDetails/ProductDetailsPage.tsx index aace43a..6723a18 100644 --- a/frontend/src/productDetails/ProductDetailsPage.tsx +++ b/frontend/src/pages/productDetails/ProductDetailsPage.tsx @@ -1,47 +1,109 @@ import { useState } from "react"; +import { useQuery } from "@apollo/client/react"; +import { PRODUCT_QUERY } from "./productQueries"; import "./ProductDetailsPage.css"; -type StorePrice = { - store: string; - price: number; +type ProductDetailsPageProps = { + ean: string; }; -const storePrices: StorePrice[] = [ - { store: "Kiwi", price: 79.9 }, - { store: "Rema 1000", price: 81.4 }, - { store: "Extra", price: 84.9 }, - { store: "Meny", price: 92.9 }, - { store: "Spar", price: 94.5 }, -]; +const storeNames: Record = { + KIWI: "Kiwi", + MENY_NO: "Meny", + SPAR_NO: "Spar", + BUNNPRIS: "Bunnpris", + COOP_NO: "Coop", + JOKER_NO: "Joker", + ODA_NO: "Oda", + EUROPRIS_NO: "Europris", + ENGROSSNETT_NO: "Engrosnett", +}; + +const getStoreName = (storeCode: string) => + storeNames[storeCode] ?? storeCode; -export default function ProductDetailsPage() { +export default function ProductDetailsPage({ + ean, +}: ProductDetailsPageProps) { + const { data, loading, error } = useQuery(PRODUCT_QUERY, { + variables: { ean }, + }); + + const product = data?.product; const [quantity, setQuantity] = useState(1); - const [isInList, setIsInList] = useState(true); + const [isInList, setIsInList] = useState(false); const [note, setNote] = useState(""); + const [failedImageUrl, setFailedImageUrl] = useState(null); + + if (loading) { + return
Laster produkt...
; + } + + if (error) { + return ( +
+

Kunne ikke hente produktet: {error.message}

+
+ ); + } + + if (!product) { + return ( +
+

Fant ikke produktet.

+
+ ); + } + + const storePrices = [...product.prices].sort( + (a, b) => a.price - b.price + ); + + const cheapest = storePrices[0]; + + const formatPrice = (price: number) => + price.toFixed(2).replace(".", ","); + + const hasImage = Boolean( + product.imageUrl && product.imageUrl !== failedImageUrl + ); + return (
{/* Venstre side: Produktbilde */}
-
-
+
+
+ {hasImage ? ( + {product.name} setFailedImageUrl(product.imageUrl)} + /> + ) : (

Mangler bilde

+ )}
- FIG. 5 — NORVEGIA 26 %, 500 G + FIG. 5 — {product.name.toUpperCase()}

A — INGREDIENSER

-

Pasteurisert kumelk, salt, syrekultur, løpe.

+

Ingrediensinformasjon er ikke tilgjengelig.

B — ALLERGENER

-

MELK

+

Allergeninformasjon er ikke tilgjengelig.

@@ -49,22 +111,34 @@ export default function ProductDetailsPage() { {/* Høyre side: Produktinformasjon */}
-

TINE

- -

Norvegia 26 %

+ {product.brand && ( +

{product.brand}

+ )} -

500 G · OST

+

{product.name}

-
-

- 79,90 - kr -

+ {product.category && ( +

{product.category.name}

+ )} -

- BILLIGST HOS KIWI · 159,80 KR/KG -

-
+
+ {cheapest ? ( + <> +

+ + {formatPrice(cheapest.price)} + + kr +

+ +

+ BILLIGST HOS {getStoreName(cheapest.storeCode).toUpperCase()} +

+ + ) : ( +

Ingen priser tilgjengelig.

+ )} +

PRIS I BUTIKKENE

@@ -72,27 +146,26 @@ export default function ProductDetailsPage() {
    - {[...storePrices] - .sort((a, b) => a.price - b.price) - .map((item, index, sortedPrices) => { - const cheapestPrice = sortedPrices[0].price; - const difference = item.price - cheapestPrice; + {storePrices.map((item, index) => { + const difference = item.price - (cheapest?.price ?? item.price); return (
  1. {String(index + 1).padStart(2, "0")} - {item.store} + + {getStoreName(item.storeCode)} + {index === 0 ? "BILLIGST" - : `+${difference.toFixed(2).replace(".", ",")} KR`} + : `+${formatPrice(difference)} KR`} @@ -162,28 +235,9 @@ export default function ProductDetailsPage() {
-

C — NÆRINGSINNHOLD PER 100 G

- -
-
Energi
-
1 469 kJ / 353 kcal
- -
Fett
-
26 g
- -
Hvorav mettede fettsyrer
-
17 g
- -
Karbohydrater
-
0 g
- -
Protein
-
27 g
- -
Salt
-
1,2 g
-
-
+

C — NÆRINGSINNHOLD

+

Næringsinnhold er ikke tilgjengelig.

+
diff --git a/frontend/src/pages/productDetails/productQueries.ts b/frontend/src/pages/productDetails/productQueries.ts new file mode 100644 index 0000000..83fca79 --- /dev/null +++ b/frontend/src/pages/productDetails/productQueries.ts @@ -0,0 +1,51 @@ +import { gql } from "@apollo/client"; +import type { TypedDocumentNode } from "@apollo/client"; + +export type Product = { + ean: string; + name: string; + brand: string | null; + imageUrl: string | null; + category: { + id: string; + name: string; + } | null; + prices: { + storeCode: string; + price: number; + unitPrice: number | null; + url: string | null; + }[]; +}; + +type ProductQueryData = { + product: Product | null; +}; + +type ProductQueryVariables = { + ean: string; +}; + +export const PRODUCT_QUERY: TypedDocumentNode< + ProductQueryData, + ProductQueryVariables +> = gql` + query Product($ean: String!) { + product(ean: $ean) { + ean + name + brand + imageUrl + category { + id + name + } + prices { + storeCode + price + unitPrice + url + } + } + } +`; \ No newline at end of file