import { useEffect, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { api } from '../../api/client.js'; import usePageMeta from '../../hooks/usePageMeta.js'; import MaintenanceNotice from '../../components/MaintenanceNotice.jsx'; import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx'; import DynamicIcon from '../../components/DynamicIcon.jsx'; import { useMaintenance } from '../../context/MaintenanceContext.jsx'; import { useLanguage } from '../../context/LanguageContext.jsx'; import { useSiteSettings } from '../../context/SiteSettingsContext.jsx'; import roadGlowTexture from '../../assets/textures/road-glow.webp'; // Die 8 fertigen "Straßen"-Glow-Bilder aus dem Kunden-Referenzdesign (Kacheln.html) - // je Preisstufe ein eigenes Bild (Grau -> Petrol -> Gruen -> Gold). Wurden 1:1 aus der // Referenzdatei extrahiert und nur PNG->WebP optimiert (Motiv unveraendert). import roadBase from '../../assets/roads/base.webp'; import roadClear from '../../assets/roads/clear.webp'; import roadDrive from '../../assets/roads/drive.webp'; import roadPrime from '../../assets/roads/prime.webp'; import roadEliteR from '../../assets/roads/elite_r.webp'; import roadApex from '../../assets/roads/apex.webp'; import roadStatement from '../../assets/roads/statement.webp'; import roadLimitless from '../../assets/roads/limitless.webp'; const hslToRgb = (h, s, l) => { s /= 100; l /= 100; const k = (n) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)]; }; const hslCss = (h, s, l) => `rgb(${hslToRgb(h, s, l).join(', ')})`; // Baut aus [bgH,bgS,bgL, acH,acS,acL]-Zeilen eine Stufenliste ohne eigenes Straßenbild // (fuer die beiden nicht im Referenzdesign enthaltenen Themes) - Rahmenfarbe = Flaeche // etwas heller, wie im Referenzdesign. const gen = (rows) => rows.map(([bh, bs, bl, ah, as, al]) => ({ bgTop: hslCss(bh, bs, bl), border: hslCss(bh, bs, bl + 8), accent: hslCss(ah, as, al), img: null, })); // "graphite" = exakt die 8 Referenz-Kacheln (Farben + Straßenbild) aus Kacheln.html: // Base / Clear / Drive / Prime / Elite R / Apex / The Statement / Limitless. Jede Stufe // hat eine Flaechenfarbe oben im Verlauf (bgTop), eine Rahmenfarbe (border) und eine // Akzentfarbe (Name + Trennlinie). Welche Stufe ein Paket bekommt, ergibt sich aus seinem // Preis-Rang (siehe tierOf). Die beiden anderen Themes sind eigene Farbfamilien im selben // Aufbau, nutzen aber (mangels eigener Referenzbilder) das neutrale, umfaerbbare Glow-Motiv. const PACKAGE_THEMES = { graphite: [ { accent: '#b6b6b6', border: '#2a2a2a', bgTop: '#1c1c1c', img: roadBase }, { accent: '#d3d3d3', border: '#2e2e2e', bgTop: '#212121', img: roadClear }, { accent: '#8fb8bd', border: '#26383b', bgTop: '#16282b', img: roadDrive }, { accent: '#6fbf73', border: '#1f3a24', bgTop: '#122916', img: roadPrime }, { accent: '#63d34f', border: '#204a1c', bgTop: '#123312', img: roadEliteR }, { accent: '#8bea3c', border: '#2b5518', bgTop: '#173a0f', img: roadApex }, { accent: '#d9a83c', border: '#4a3a16', bgTop: '#33270d', img: roadStatement }, { accent: '#f2c14e', border: '#5a4718', bgTop: '#3d2f0c', img: roadLimitless }, ], 'deep-blue': gen([ [215, 18, 11, 210, 15, 60], [212, 25, 11, 208, 32, 60], [210, 38, 12, 206, 46, 60], [212, 46, 12, 205, 56, 61], [214, 52, 12, 205, 63, 62], [216, 58, 12, 205, 68, 63], [220, 62, 12, 212, 73, 65], [226, 66, 12, 216, 78, 68], ]), 'warm-bronze': gen([ [30, 15, 11, 34, 22, 60], [34, 32, 11, 34, 46, 60], [32, 46, 12, 30, 58, 60], [26, 52, 12, 24, 65, 60], [20, 55, 12, 18, 68, 60], [14, 55, 12, 12, 68, 61], [8, 52, 12, 6, 64, 62], [2, 50, 12, 358, 60, 63], ]), }; // Referenz-Werte fuer die Preis-Sektion (unabhaengig von der Preisstufe, wie im Design): const PRICE_COLOR = '#f2f2f2'; const LABEL_COLOR = '#8a8a8a'; const BULLET_COLOR = '#c4c4c4'; // Mehr sichtbare Stichpunkte wuerden das Straßenbild verdraengen - gerade bei den teuren // Paketen (die laut Kunde die laengsten Listen haben) soll die Kachel ruhig bleiben. Der // Rest steckt hinter einem "+X weitere Leistungen"-Toggle, dessen Zaehler selbst als // Wertigkeitssignal wirkt. const VISIBLE_BULLETS = 5; function PackageCard({ pkg, tier, tierT, isStripLayout, bullets, formatPrice, contactUrl, t }) { const [expanded, setExpanded] = useState(false); const hiddenCount = bullets.length - VISIBLE_BULLETS; const shownBullets = expanded ? bullets : bullets.slice(0, VISIBLE_BULLETS); // Fuer die Themes ohne eigenes Straßenbild: neutrales Glow-Motiv, per Maske // in die Akzentfarbe eingefaerbt (zwei Ebenen: weicher Halo + scharfe Kontur). const glowMask = tier.img ? null : { backgroundColor: tier.accent, WebkitMaskImage: `url(${roadGlowTexture})`, maskImage: `url(${roadGlowTexture})`, maskMode: 'luminance', WebkitMaskSize: 'cover', maskSize: 'cover', WebkitMaskPosition: 'center', maskPosition: 'center', WebkitMaskRepeat: 'no-repeat', maskRepeat: 'no-repeat', }; return (
{error}
; } if (!data) { return{t('modelPage.loading')}
; } const { model, packages } = data; // Preis-Rang innerhalb dieses Modells waehlt die Referenz-Stufe: die guenstigste Option // bekommt die erste Kachel (Base), die teuerste die letzte (Limitless), der Rest wird // gleichmaessig auf die dazwischenliegenden Stufen verteilt. Da jede Stufe ihr eigenes // Straßenbild hat (und Bilder sich nicht mischen lassen), wird die naechstgelegene Stufe // gewaehlt statt zwischen zwei Stufen zu interpolieren - so passen Bild und Farbe immer // exakt zur Referenz, egal ob ein Modell 2, 8 oder 12 Pakete hat. const rankById = new Map( [...packages].sort((a, b) => a.total_price - b.total_price).map((p, i) => [p.id, i]) ); const tierOf = (pkg) => { const n = packages.length; const rank = rankById.get(pkg.id) ?? 0; const tierT = n <= 1 ? 0 : rank / (n - 1); const idx = Math.round(tierT * (TIERS.length - 1)); return { tier: TIERS[idx], tierT }; }; // Stichpunkte einer Kachel = die Produkte des Pakets plus die Freitext-Zeilen der // Beschreibung (je Zeile ein Punkt). Das ist der einzige Zusatz zum Referenzdesign. const bulletsOf = (pkg) => [ ...pkg.products.map((p) => p.name_override || p.scraped_name || t('modelPage.productLoading')), ...(pkg.description || '') .split('\n') .map((line) => line.trim()) .filter(Boolean), ]; const contactUrl = (pkg) => { const params = new URLSearchParams({ brand: model.brand_name, model: model.name, package: pkg.name, packageId: pkg.id, total: pkg.total_price, }); return `/kontakt?${params.toString()}`; }; return ( <>{t('modelPage.breadcrumbVehicles')} /{' '} {model.brand_name} / {model.name}
{t('modelPage.empty')}
)}