Shop page rebuilt into four brand colour-coded use-group sections (Küche/lime, Reinigung/grapefruit, Wäsche/plum, Pflege/care/orange): colour banners with diagonal dividers, per-section grid titles, crimson 'add to cart' + white-outline 'learn more' CTAs, plain-name eyebrows, first-fold halved + thin white band. Split old Haushalt group into clean+wash. Also in this checkpoint (brand-owner feedback items): - L11: Kaiser-Natron® on every visible mention, hyphenated (de+en, products, logo, nav) - L4: product-hero headline reworded to the brand versatility voice - L5/L6: faint 'AI Edited' badge on AI bundle images (data-driven aiEdited flag) - L10: Revitalization animation row + early-access CTA removed - U2: membership 'become a member' button removed - New category tokens (cat-kitchen/clean/wash/care) + Hero/WaveDivider tones - New /kueche category page (route + copy) — not yet linked (see STYLE-CHANGES caveat) - Homepage reverted to original ProductTeaser (banners belong on /shop) dist/ rebuilt and included for the Portainer/gitea deploy. Open decisions tracked in STYLE-CHANGES.md §20 and memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
// Product catalog — frontend fixture until the PHP / MySQL backend takes over.
|
||
// Shape and helper are the same surface the API module will eventually expose.
|
||
|
||
export const products = [
|
||
{
|
||
id: 'kaiser-natron-pulver-50-g-beutel',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Pulver',
|
||
size: '50 g Beutel',
|
||
category: 'Pulver',
|
||
price: 1.29,
|
||
image: '/products/kaiser-natron-pulver-50-g-beutel.webp',
|
||
href: '/shop/kaiser-natron-pulver-50-g-beutel',
|
||
inStock: true,
|
||
keywords: ['natron', 'backsoda', 'bicarbonat', 'natriumhydrogencarbonat', 'baking soda'],
|
||
properties: ['glutenfrei', 'laktosefrei', 'vegan', 'neutralisiert-saeuren', 'enthaertet', 'mild-alkalisch'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-pulver-250-g-grosspackung',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Pulver',
|
||
size: '250 g Großpackung',
|
||
category: 'Pulver',
|
||
price: 4.49,
|
||
image: '/products/kaiser-natron-pulver-250-g-grosspackung.webp',
|
||
href: '/shop/kaiser-natron-pulver-250-g-grosspackung',
|
||
inStock: true,
|
||
keywords: ['natron', 'backsoda', 'großpackung', 'bicarbonat', 'baking soda', 'vorrat'],
|
||
properties: ['glutenfrei', 'laktosefrei', 'vegan', 'neutralisiert-saeuren', 'enthaertet', 'mild-alkalisch'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-pulver-3490-g-eimer',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Pulver',
|
||
size: '3.490 g Eimer',
|
||
category: 'Pulver',
|
||
price: 29.9,
|
||
image: '/products/kaiser-natron-pulver-3.490-g-eimer.webp',
|
||
href: '/shop/kaiser-natron-pulver-3490-g-eimer',
|
||
inStock: true,
|
||
keywords: ['natron', 'eimer', 'großgebinde', 'gastro', 'gewerbe', 'baking soda'],
|
||
properties: ['glutenfrei', 'laktosefrei', 'vegan', 'neutralisiert-saeuren', 'enthaertet', 'mild-alkalisch'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-tabletten-100-g-dose',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Tabletten',
|
||
size: '100 g Dose',
|
||
category: 'Tabletten',
|
||
price: 3.49,
|
||
image: '/products/kaiser-natron-tabletten-100-g-dose.webp',
|
||
href: '/shop/kaiser-natron-tabletten-100-g-dose',
|
||
inStock: true,
|
||
keywords: ['natron', 'tabletten', 'magen', 'sodbrennen', 'verdauung'],
|
||
properties: ['glutenfrei', 'laktosefrei', 'vegan', 'fuer-kueche-und-haushalt'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-bad-500-g',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Bad',
|
||
size: '500 g',
|
||
category: 'Körperpflege',
|
||
price: 5.49,
|
||
image: '/products/kaiser-natron-bad-500-g.webp',
|
||
href: '/shop/kaiser-natron-bad-500-g',
|
||
inStock: true,
|
||
keywords: ['basenbad', 'wellness', 'entspannung', 'haut', 'wanne'],
|
||
properties: ['mit-entspannendem-apfelsinenduft', 'ohne-konservierungsstoffe', 'ohne-mineraloel', 'frei-von-tierischen-substanzen'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-fussbad-500-g',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Fußbad',
|
||
size: '500 g',
|
||
category: 'Körperpflege',
|
||
price: 5.49,
|
||
image: '/products/kaiser-natron-fussbad-500-g.webp',
|
||
href: '/shop/kaiser-natron-fussbad-500-g',
|
||
inStock: true,
|
||
keywords: ['fusspflege', 'basenbad', 'wellness', 'entspannung'],
|
||
properties: ['mit-dem-duft-der-latschenkiefer', 'wohltuend-und-erfrischend', 'ohne-konservierungsstoffe', 'ohne-mineraloel', 'frei-von-tierischen-substanzen'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-daunenwasch-250-ml',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Daunenwasch',
|
||
size: '250 ml',
|
||
category: 'Wäsche',
|
||
price: 6.9,
|
||
image: '/products/kaiser-natron-daunenwasch-250-ml.webp',
|
||
href: '/shop/kaiser-natron-daunenwasch-250-ml',
|
||
inStock: true,
|
||
keywords: ['daunen', 'wäsche', 'bettdecke', 'kissen', 'pflege'],
|
||
properties: ['mit-pflegendem-lanolin', 'frei-von-mikroplastik'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-sport-profi-250-ml',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Sport Profi',
|
||
size: '250 ml',
|
||
category: 'Sport',
|
||
price: 7.9,
|
||
image: '/products/kaiser-natron-sport-profi-250-ml.webp',
|
||
href: '/shop/kaiser-natron-sport-profi-250-ml',
|
||
inStock: true,
|
||
keywords: ['sport', 'wäsche', 'funktionskleidung', 'geruch'],
|
||
properties: ['frei-von-mikroplastik'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-spuelmittel-500-ml',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Spülmittel',
|
||
size: '500 ml',
|
||
category: 'Reinigung',
|
||
price: 3.9,
|
||
image: '/products/kaiser-natron-spuelmittel-500-ml.webp',
|
||
href: '/shop/kaiser-natron-spuelmittel-500-ml',
|
||
inStock: true,
|
||
keywords: ['geschirr', 'spülen', 'küche', 'reinigung'],
|
||
properties: ['mit-pampelmusenduft', 'hautvertraeglich-dermatologisch-bestaetigt', 'frei-von-mikroplastik'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-allzweck-reiniger-750-ml',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Allzweck-Reiniger',
|
||
size: '750 ml',
|
||
category: 'Reinigung',
|
||
price: 4.49,
|
||
image: '/products/kaiser-natron-allzweck-reiniger-750-ml.webp',
|
||
href: '/shop/kaiser-natron-allzweck-reiniger-750-ml',
|
||
inStock: true,
|
||
keywords: ['reinigung', 'allzweck', 'universal', 'haushalt'],
|
||
properties: ['hygienische-sauberkeit', 'fuer-boeden-und-oberflaechen', 'mit-pampelmusenduft', 'vegan', 'ohne-konservierungsstoffe', 'frei-von-mikroplastik'],
|
||
},
|
||
{
|
||
id: 'kaiser-natron-allzweck-spray-500-ml',
|
||
brand: 'Kaiser-Natron®',
|
||
title: 'Kaiser-Natron® Allzweck-Spray',
|
||
size: '500 ml',
|
||
category: 'Reinigung',
|
||
price: 4.9,
|
||
image: '/products/kaiser-natron-allzweck-spray-500-ml.webp',
|
||
href: '/shop/kaiser-natron-allzweck-spray-500-ml',
|
||
inStock: true,
|
||
keywords: ['reinigung', 'allzweck', 'spray', 'haushalt'],
|
||
properties: ['nachhaltig-durch-tenside-nachwachsenden-ursprungs', 'ohne-konservierungsstoffe', 'mit-natuerlicher-seife', 'frei-von-mikroplastik'],
|
||
},
|
||
{
|
||
id: 'holste-wasch-soda-500-g-beutel',
|
||
brand: 'Holste',
|
||
title: 'Holste Wasch-Soda',
|
||
size: '500 g Beutel',
|
||
category: 'Wäsche',
|
||
price: 2.9,
|
||
image: '/products/holste-wasch-soda-500-g-beutel.webp',
|
||
href: '/shop/holste-wasch-soda-500-g-beutel',
|
||
inStock: true,
|
||
keywords: ['soda', 'wasch-soda', 'waschen', 'natriumcarbonat'],
|
||
},
|
||
{
|
||
id: 'holste-handwaschpaste-500-ml',
|
||
brand: 'Holste',
|
||
title: 'Holste Handwaschpaste',
|
||
size: '500 ml',
|
||
category: 'Reinigung',
|
||
price: 6.9,
|
||
image: '/products/holste-handwaschpaste-500-ml.webp',
|
||
href: '/shop/holste-handwaschpaste-500-ml',
|
||
inStock: true,
|
||
keywords: ['hände', 'werkstatt', 'arbeit', 'grobe verschmutzung'],
|
||
},
|
||
{
|
||
id: 'holste-kalk-und-urinsteinloeser-750-ml',
|
||
brand: 'Holste',
|
||
title: 'Holste Kalk- und Urinsteinlöser',
|
||
size: '750 ml',
|
||
category: 'Reinigung',
|
||
price: 5.9,
|
||
image: '/products/holste-kalk--und-urinsteinloeser-750-ml.webp',
|
||
href: '/shop/holste-kalk-und-urinsteinloeser-750-ml',
|
||
inStock: true,
|
||
keywords: ['kalk', 'urinstein', 'bad', 'wc', 'toilette', 'entkalker'],
|
||
},
|
||
{
|
||
id: 'holste-reisstaerke-250-g-faltschachtel',
|
||
brand: 'Holste',
|
||
title: 'Holste Reisstärke',
|
||
size: '250 g Faltschachtel',
|
||
category: 'Wäsche',
|
||
price: 3.9,
|
||
image: '/products/holste-reisstaerke-250-g-faltschachtel.webp',
|
||
href: '/shop/holste-reisstaerke-250-g-faltschachtel',
|
||
inStock: true,
|
||
keywords: ['stärke', 'reisstärke', 'wäsche', 'bügeln'],
|
||
},
|
||
{
|
||
id: 'holste-schmierseife-fluessig-1-l-flasche',
|
||
brand: 'Holste',
|
||
title: 'Holste Schmierseife flüssig',
|
||
size: '1 l Flasche',
|
||
category: 'Reinigung',
|
||
price: 6.49,
|
||
image: '/products/holste-schmierseife-fluessig-1-l-flasche.webp',
|
||
href: '/shop/holste-schmierseife-fluessig-1-l-flasche',
|
||
inStock: true,
|
||
keywords: ['schmierseife', 'naturseife', 'boden', 'reinigung'],
|
||
},
|
||
{
|
||
id: 'holste-zitronensaeure-entkalker-fluessig-500-ml',
|
||
brand: 'Holste',
|
||
title: 'Holste Zitronensäure-Entkalker flüssig',
|
||
size: '500 ml',
|
||
category: 'Reinigung',
|
||
price: 4.9,
|
||
image: '/products/holste-zitronensaeure-entkalker-fluessig-500-ml.webp',
|
||
href: '/shop/holste-zitronensaeure-entkalker-fluessig-500-ml',
|
||
inStock: true,
|
||
keywords: ['zitronensäure', 'entkalker', 'kalk', 'haushalt', 'küche'],
|
||
},
|
||
{
|
||
id: 'gazelle-waeschestaerke-1000-ml-flasche',
|
||
brand: 'Gazelle',
|
||
title: 'Gazelle Wäschestärke',
|
||
size: '1.000 ml Flasche',
|
||
category: 'Wäsche',
|
||
price: 5.9,
|
||
image: '/products/gazelle-waeschestaerke-1000-ml-flasche.webp',
|
||
href: '/shop/gazelle-waeschestaerke-1000-ml-flasche',
|
||
inStock: true,
|
||
keywords: ['wäschestärke', 'stärke', 'bügeln', 'tischwäsche'],
|
||
},
|
||
{
|
||
id: 'gruene-tante-mit-quarzmehl-500-ml-dose',
|
||
brand: 'Grüne Tante',
|
||
title: 'Grüne Tante mit Quarzmehl',
|
||
size: '500 ml Dose',
|
||
category: 'Reinigung',
|
||
price: 7.9,
|
||
image: '/products/gruene-tante-mit-quarzmehl-500-ml-dose.webp',
|
||
href: '/shop/gruene-tante-mit-quarzmehl-500-ml-dose',
|
||
inStock: true,
|
||
keywords: ['handreiniger', 'werkstatt', 'quarzmehl', 'grobe verschmutzung'],
|
||
},
|
||
{
|
||
id: 'linda-fleckenweg-200-ml-tube',
|
||
brand: 'Linda',
|
||
title: 'Linda Fleckenweg',
|
||
size: '200 ml Tube',
|
||
category: 'Wäsche',
|
||
price: 4.9,
|
||
image: '/products/linda-fleckenweg-200-ml-tube.webp',
|
||
href: '/shop/linda-fleckenweg-200-ml-tube',
|
||
inStock: true,
|
||
keywords: ['fleck', 'fleckenentferner', 'vorbehandlung', 'wäsche'],
|
||
},
|
||
{
|
||
id: 'linda-handreiniger-der-kraftvolle-200-g-tube',
|
||
brand: 'Linda',
|
||
title: 'Linda Handreiniger – Der Kraftvolle',
|
||
size: '200 g Tube',
|
||
category: 'Reinigung',
|
||
price: 5.9,
|
||
image: '/products/linda-fleckenweg-200-ml-tube.webp',
|
||
href: '/shop/linda-handreiniger-der-kraftvolle-200-g-tube',
|
||
inStock: true,
|
||
keywords: ['handreiniger', 'werkstatt', 'öl', 'fett'],
|
||
},
|
||
{
|
||
id: 'linda-neutral-375-ml-dose',
|
||
brand: 'Linda',
|
||
title: 'Linda Neutral',
|
||
size: '375 ml Dose',
|
||
category: 'Reinigung',
|
||
price: 5.49,
|
||
image: '/products/linda-neutral-375-ml-dose.webp',
|
||
href: '/shop/linda-neutral-375-ml-dose',
|
||
inStock: true,
|
||
keywords: ['handreiniger', 'neutral', 'haut', 'empfindlich'],
|
||
},
|
||
]
|
||
|
||
// Normalize for search: lowercase + fold German diacritics so "Spülmittel"
|
||
// matches "spulmittel" and "großpackung" matches "grosspackung".
|
||
function normalize(input) {
|
||
if (!input) return ''
|
||
return String(input)
|
||
.toLowerCase()
|
||
.replace(/ß/g, 'ss')
|
||
.replace(/ä/g, 'ae')
|
||
.replace(/ö/g, 'oe')
|
||
.replace(/ü/g, 'ue')
|
||
.normalize('NFD')
|
||
.replace(/[̀-ͯ]/g, '')
|
||
}
|
||
|
||
function escapeRegExp(s) {
|
||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||
}
|
||
|
||
// Ranked match: every query term must hit at least one field; fields have
|
||
// weights so a title hit outranks a keyword hit. Exact > startsWith >
|
||
// word-boundary > substring. Terms AND together (all must match something).
|
||
export function searchProducts(query, list = products, limit = 8) {
|
||
const q = normalize(query).trim()
|
||
if (!q) return []
|
||
const terms = q.split(/\s+/).filter(Boolean)
|
||
|
||
const scored = []
|
||
for (const product of list) {
|
||
const haystacks = [
|
||
{ text: normalize(product.title), weight: 3 },
|
||
{ text: normalize(product.brand), weight: 2.2 },
|
||
{ text: normalize(product.category), weight: 1.4 },
|
||
{ text: normalize(product.size), weight: 1.2 },
|
||
{ text: normalize((product.keywords || []).join(' ')), weight: 1 },
|
||
]
|
||
|
||
let total = 0
|
||
let allTermsMatched = true
|
||
|
||
for (const term of terms) {
|
||
const boundary = new RegExp(`\\b${escapeRegExp(term)}`)
|
||
let best = 0
|
||
for (const { text, weight } of haystacks) {
|
||
if (!text) continue
|
||
let s = 0
|
||
if (text === term) s = 100
|
||
else if (text.startsWith(term)) s = 80
|
||
else if (boundary.test(text)) s = 55
|
||
else if (text.includes(term)) s = 25
|
||
if (s * weight > best) best = s * weight
|
||
}
|
||
if (best === 0) {
|
||
allTermsMatched = false
|
||
break
|
||
}
|
||
total += best
|
||
}
|
||
|
||
if (allTermsMatched) scored.push({ product, score: total })
|
||
}
|
||
|
||
scored.sort((a, b) => b.score - a.score)
|
||
return scored.slice(0, limit).map((s) => s.product)
|
||
}
|
||
|
||
export function formatPrice(amount, currency = '€') {
|
||
const num = typeof amount === 'number' ? amount : Number(amount)
|
||
if (!Number.isFinite(num)) return ''
|
||
return `${currency} ${num.toFixed(2).replace('.', ',')}`
|
||
}
|
||
|
||
/**
|
||
* Shop use-cases — the four top-level bands on the Shop page, each in
|
||
* its own brand colour (kaiser-natron.de palette). Product-level
|
||
* `category` strings are fine-grained (Pulver, Reinigung, Wäsche,
|
||
* Körperpflege, Sport, Tabletten); this table rolls them up into the
|
||
* four shop-facing groups so the mapping is declared in one place.
|
||
*
|
||
* cook — Küche: kitchen & digestion (baking, gastro) — lime
|
||
* clean — Reinigung: household cleaning — grapefruit
|
||
* wash — Wäsche: laundry & textile care — plum
|
||
* care — Pflege: bath, body and sport — orange
|
||
*/
|
||
export const USE_CASES = ['cook', 'clean', 'wash', 'care']
|
||
const USE_CASE_BY_CATEGORY = {
|
||
Pulver: 'cook',
|
||
Tabletten: 'cook',
|
||
Reinigung: 'clean',
|
||
Wäsche: 'wash',
|
||
Körperpflege: 'care',
|
||
Sport: 'care',
|
||
}
|
||
|
||
export function useCaseFor(product) {
|
||
return USE_CASE_BY_CATEGORY[product.category] || 'clean'
|
||
}
|
||
|
||
export function productsByUseCase(list = products) {
|
||
return list.reduce(
|
||
(acc, p) => {
|
||
acc[useCaseFor(p)].push(p)
|
||
return acc
|
||
},
|
||
{ cook: [], clean: [], wash: [], care: [] },
|
||
)
|
||
}
|