From d7fdd3666200b7696877d1b3c5bf34b2849d9229 Mon Sep 17 00:00:00 2001 From: Maaxxs <61059039+MaaxxsDev@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:02:01 +0200 Subject: [PATCH] Give the package tier section a dedicated dark configurator-style stage Flat cards on the regular page background couldn't deliver the elegance/luxury feel from the reference mockups. The tier grid now sits on its own always-dark full-bleed section (independent of site light/dark mode, like a vehicle configurator), with a section headline, larger elegant tier numbering, glowing per-card divider lines, and thinner hairline borders instead of thick ones - letting color and glow carry the escalation instead of stroke weight. Co-Authored-By: Claude Sonnet 5 --- hifi-src/src/i18n/de.js | 2 + hifi-src/src/i18n/en.js | 2 + hifi-src/src/pages/public/ModelPage.jsx | 233 +++++++++++++----------- 3 files changed, 129 insertions(+), 108 deletions(-) diff --git a/hifi-src/src/i18n/de.js b/hifi-src/src/i18n/de.js index 1b7ee48..ca1660d 100644 --- a/hifi-src/src/i18n/de.js +++ b/hifi-src/src/i18n/de.js @@ -156,6 +156,8 @@ export default { requestContact: 'Kontakt anfragen', featuredBadge: 'Empfohlen', tierLabels: ['Einstieg', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Referenz'], + tiersHeading: (count) => `${count} Stufen. Ein Ziel: perfekter Klang.`, + tiersSubheading: 'Von der soliden Basis bis zum kompromisslosen Meisterwerk – finde dein Paket.', }, leistungen: { metaTitle: 'Leistungen', diff --git a/hifi-src/src/i18n/en.js b/hifi-src/src/i18n/en.js index 9506557..08eaf48 100644 --- a/hifi-src/src/i18n/en.js +++ b/hifi-src/src/i18n/en.js @@ -156,6 +156,8 @@ export default { requestContact: 'Request contact', featuredBadge: 'Recommended', tierLabels: ['Entry', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Reference'], + tiersHeading: (count) => `${count} tiers. One goal: perfect sound.`, + tiersSubheading: 'From a solid foundation to an uncompromising masterpiece – find your package.', }, leistungen: { metaTitle: 'Services', diff --git a/hifi-src/src/pages/public/ModelPage.jsx b/hifi-src/src/pages/public/ModelPage.jsx index cac910a..862435d 100644 --- a/hifi-src/src/pages/public/ModelPage.jsx +++ b/hifi-src/src/pages/public/ModelPage.jsx @@ -7,31 +7,25 @@ import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.js import DynamicIcon from '../../components/DynamicIcon.jsx'; import { useMaintenance } from '../../context/MaintenanceContext.jsx'; import { useLanguage } from '../../context/LanguageContext.jsx'; -import { useTheme } from '../../context/ThemeContext.jsx'; // Material-Stufen statt einfarbigem Verlauf: jede Preisstufe durchlaeuft eine eigene -// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen - Basis (weiss) -> Bronze -> Silber -// -> Gold -> Platin -> Onyx (fast schwarz, mit Gold-Glanz als Kroenung ganz oben). Jede -// Stufe hat sowohl eine Flaechenfarbe (bg) als auch eine dazu passende Akzentfarbe -// (Rahmen/Leucht-Schatten/Preis/Icon) - nur der "Kontakt anfragen"-Button bleibt ueberall +// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen - Graphit (Einstieg) -> Bronze -> +// Silber -> Gold -> Platin -> Onyx (fast schwarz, mit Gold-Glanz als Kroenung ganz oben). +// Die Paket-Sektion sitzt bewusst IMMER auf einer dunklen Buehne (unabhaengig vom +// Hell/Dunkel-Modus der Seite, wie ein Fahrzeugkonfigurator) - das ist der groesste Hebel +// fuer den edlen/luxurioesen Eindruck, den flache helle Kacheln nicht liefern konnten. +// Jede Stufe hat eine Flaechenfarbe (bg) und eine dazu passende Akzentfarbe (Rahmen/ +// Leucht-Schatten/Preis/Icon) - nur der "Kontakt anfragen"-Button bleibt ueberall // einheitlich markengruen, damit die Kernaktion auf jeder Karte wiedererkennbar bleibt. -// Zwischen zwei Nachbar-Materialien wird in HSL linear interpoliert, sodass JEDES Paket -// (nicht nur die Materialien selbst) eine eigene Abstufung bekommt. -const MATERIALS_LIGHT = [ - { bg: [0, 0, 100], accent: [88, 63, 40] }, // Basis - { bg: [24, 42, 55], accent: [24, 58, 42] }, // Bronze - { bg: [212, 10, 80], accent: [212, 15, 48] }, // Silber - { bg: [45, 60, 60], accent: [42, 75, 42] }, // Gold - { bg: [196, 16, 85], accent: [200, 22, 52] }, // Platin - { bg: [225, 20, 6], accent: [45, 70, 55] }, // Onyx -]; -const MATERIALS_DARK = [ - { bg: [0, 0, 9], accent: [88, 55, 48] }, - { bg: [22, 35, 20], accent: [25, 55, 58] }, - { bg: [212, 10, 20], accent: [212, 18, 68] }, - { bg: [45, 42, 22], accent: [42, 68, 58] }, - { bg: [196, 14, 24], accent: [200, 22, 72] }, - { bg: [225, 22, 4], accent: [45, 75, 68] }, +// Zwischen zwei Nachbar-Materialien wird in RGB linear interpoliert (siehe materialRgbAt), +// sodass JEDES Paket (nicht nur die Materialien selbst) eine eigene Abstufung bekommt. +const MATERIALS = [ + { bg: [0, 0, 14], accent: [88, 45, 55] }, // Graphit (Einstieg) + { bg: [24, 32, 22], accent: [24, 55, 58] }, // Bronze + { bg: [212, 10, 24], accent: [212, 18, 68] }, // Silber + { bg: [45, 38, 24], accent: [42, 68, 58] }, // Gold + { bg: [196, 12, 27], accent: [200, 20, 72] }, // Platin + { bg: [225, 22, 5], accent: [45, 78, 68] }, // Onyx ]; const hslToRgb = (h, s, l) => { @@ -73,7 +67,6 @@ export default function ModelPage() { const [error, setError] = useState(''); const maintenance = useMaintenance(); const { t, language } = useLanguage(); - const { theme } = useTheme(); const formatPrice = (value) => new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value); @@ -105,12 +98,11 @@ export default function ModelPage() { const { model, packages } = data; - // Preis-Rang innerhalb dieses Modells bestimmt die optische Wucht der Kachel: - // die guenstigste Option bleibt schlicht/weiss, jede weitere Preisstufe durchlaeuft + // Preis-Rang innerhalb dieses Modells bestimmt die optische Wucht der Kachel: die + // guenstigste Option bleibt schlichtes Graphit, jede weitere Preisstufe durchlaeuft // eine eigene Materialstufe (Bronze/Silber/Gold/Platin) bis zur Onyx-Kroenung ganz // oben. Das braucht keine zusaetzliche Admin-Einstellung und passt sich automatisch // an jede Paketanzahl an. - const materials = theme === 'dark' ? MATERIALS_DARK : MATERIALS_LIGHT; const tierLabels = t('modelPage.tierLabels'); const rankById = new Map( [...packages].sort((a, b) => a.total_price - b.total_price).map((p, i) => [p.id, i]) @@ -122,8 +114,8 @@ export default function ModelPage() { const tierNumber = String(rank + 1).padStart(2, '0'); const tierLabel = tierLabels[Math.min(Math.floor(tierT * tierLabels.length), tierLabels.length - 1)]; - const bgRgb = materialRgbAt(materials, tierT, 'bg').map(Math.round); - const accentRgbFloat = materialRgbAt(materials, tierT, 'accent'); + const bgRgb = materialRgbAt(MATERIALS, tierT, 'bg').map(Math.round); + const accentRgbFloat = materialRgbAt(MATERIALS, tierT, 'accent'); const accentRgb = accentRgbFloat.map(Math.round); const bgHsl = rgbToHsl(...bgRgb); const accentHsl = rgbToHsl(...accentRgbFloat); @@ -152,21 +144,26 @@ export default function ModelPage() { const highlight = hslToRgb(bgHsl[0], Math.max(bgHsl[1] - 8, 0), Math.min(bgL + 16, 99)); return { - isDarkCard, mutedColor, tierNumber, tierLabel, + accentColor: `rgb(${accentRgb.join(', ')})`, priceColor: `rgb(${priceRgb.join(', ')})`, iconChipStyle: { backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`, color: `rgb(${priceRgb.join(', ')})`, boxShadow: `inset 0 0 0 1px rgba(${accentRgb.join(', ')}, 0.45)`, }, + // Duenne, gleichbleibende Haarlinie statt dickerem Rahmen - die zunehmende Wertigkeit + // zeigt sich in Farbe/Leucht-Schatten, nicht in der Strichstaerke. Wirkt ruhiger/edler. + glowLineStyle: { + background: `linear-gradient(90deg, transparent, rgba(${accentRgb.join(', ')}, 0.9), transparent)`, + boxShadow: `0 0 ${Math.round(6 + tierT * 14)}px rgba(${accentRgb.join(', ')}, ${(0.25 + tierT * 0.45).toFixed(2)})`, + }, style: { backgroundColor: `rgb(${bgRgb.join(', ')})`, backgroundImage: `radial-gradient(135% 160% at 88% -20%, rgb(${highlight.join(', ')}) 0%, rgb(${bgRgb.join(', ')}) 55%)`, borderColor: `rgb(${accentRgb.join(', ')})`, - borderWidth: tierT > 0.08 ? 2 : 1, boxShadow: tierT > 0.04 ? `0 ${Math.round(6 + tierT * 14)}px ${Math.round(20 + tierT * 45)}px -10px rgba(${accentRgb.join(', ')}, ${glowAlpha})` @@ -187,85 +184,105 @@ export default function ModelPage() { }; return ( -
- {maintenance.vehicles.enabled && maintenance.bypass && } -

- {t('modelPage.breadcrumbVehicles')} /{' '} - {model.brand_name} / {model.name} -

-

- {model.brand_name} {model.name} – {t('modelPage.titleSuffix')} -

+ <> +
+ {maintenance.vehicles.enabled && maintenance.bypass && } +

+ {t('modelPage.breadcrumbVehicles')} /{' '} + {model.brand_name} / {model.name} +

+

+ {model.brand_name} {model.name} – {t('modelPage.titleSuffix')} +

- {packages.length === 0 && ( -

{t('modelPage.empty')}

- )} - -
- {packages.map((pkg) => { - const { isDarkCard, style, priceColor, mutedColor, iconChipStyle, tierNumber, tierLabel } = styleOf(pkg); - - return ( -
- {pkg.is_featured && ( - - {t('modelPage.featuredBadge')} - - )} - -

- {tierNumber} · {tierLabel} -

- - {pkg.icon_name && ( -
- -
- )} - -

- {pkg.name} -

- {pkg.tagline && ( -

- {pkg.tagline} -

- )} - -
-

- {t('modelPage.totalPrice')} -

-

- {formatPrice(pkg.total_price)} -

-
- -
    - {pkg.products.map((product) => ( -
  • - {product.name_override || product.scraped_name || t('modelPage.productLoading')} -
  • - ))} - {pkg.description - ?.split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line, i) =>
  • {line}
  • )} -
- - - {t('modelPage.requestContact')} - -
- ); - })} + {packages.length === 0 && ( +

{t('modelPage.empty')}

+ )}
-
+ + {packages.length > 0 && ( + // Die Paket-Sektion bekommt bewusst eine eigene, immer dunkle Buehne (wie ein + // Fahrzeugkonfigurator) statt der hellen/dunklen Seiten-Textur zu folgen - das + // ist der Haupthebel fuer den edlen Eindruck, den flache Kacheln nicht liefern. +
+
+
+

{t('modelPage.tiersHeading')(packages.length)}

+

{t('modelPage.tiersSubheading')}

+
+ +
+ {packages.map((pkg) => { + const { style, priceColor, mutedColor, iconChipStyle, tierNumber, tierLabel, accentColor, glowLineStyle } = + styleOf(pkg); + + return ( +
+ {pkg.is_featured && ( + + {t('modelPage.featuredBadge')} + + )} + +
+ + {tierNumber} + + + {tierLabel} + +
+ + {pkg.icon_name && ( +
+ +
+ )} + +

{pkg.name}

+ {pkg.tagline && ( +

+ {pkg.tagline} +

+ )} + +
+ +
+

+ {t('modelPage.totalPrice')} +

+

+ {formatPrice(pkg.total_price)} +

+
+ +
    + {pkg.products.map((product) => ( +
  • + {product.name_override || product.scraped_name || t('modelPage.productLoading')} +
  • + ))} + {pkg.description + ?.split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line, i) =>
  • {line}
  • )} +
+ + + {t('modelPage.requestContact')} + +
+ ); + })} +
+
+
+ )} + ); }