diff --git a/hifi-src/src/components/PublicLayout.jsx b/hifi-src/src/components/PublicLayout.jsx index 99b0dea..db0b3ab 100644 --- a/hifi-src/src/components/PublicLayout.jsx +++ b/hifi-src/src/components/PublicLayout.jsx @@ -26,6 +26,7 @@ const DEFAULT_SITE_SETTINGS = { hero_image_path: null, ga_measurement_id: null, package_card_theme: 'graphite', + package_card_layout: 'strip', }; // Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG diff --git a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx index 61d16b7..2f41e3f 100644 --- a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx +++ b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx @@ -9,6 +9,7 @@ const emptyForm = { hero_image_path: '', ga_measurement_id: '', package_card_theme: 'graphite', + package_card_layout: 'strip', }; const PACKAGE_CARD_THEMES = [ @@ -17,6 +18,11 @@ const PACKAGE_CARD_THEMES = [ { value: 'warm-bronze', label: 'Warm Bronze', description: 'Durchgehend warmes Kupfer/Bronze/Amber bis zum Gold.' }, ]; +const PACKAGE_CARD_LAYOUTS = [ + { value: 'grid', label: 'Raster', description: 'Breite Kacheln, die responsiv in mehrere Zeilen umbrechen.' }, + { value: 'strip', label: 'Scroll-Band', description: 'Schmale, hochkant Kacheln in einer horizontal scrollbaren Reihe.' }, +]; + export default function WebsiteSettings() { const [form, setForm] = useState(emptyForm); const [loading, setLoading] = useState(true); @@ -42,6 +48,7 @@ export default function WebsiteSettings() { hero_image_path: res.hero_image_path || '', ga_measurement_id: res.ga_measurement_id || '', package_card_theme: res.package_card_theme || 'graphite', + package_card_layout: res.package_card_layout || 'strip', }) ) .finally(() => setLoading(false)); @@ -179,6 +186,36 @@ export default function WebsiteSettings() { +
+

Kachel-Layout

+

+ Bestimmt, wie die Paket-Stufen auf jeder Fahrzeug-Modell-Seite angeordnet werden. +

+
+ {PACKAGE_CARD_LAYOUTS.map((layout) => ( + + ))} +
+
+

Google Analytics

diff --git a/hifi-src/src/pages/public/ModelPage.jsx b/hifi-src/src/pages/public/ModelPage.jsx index 5607c31..749e25c 100644 --- a/hifi-src/src/pages/public/ModelPage.jsx +++ b/hifi-src/src/pages/public/ModelPage.jsx @@ -23,41 +23,42 @@ const PACKAGE_TEXTURES = { }; // Material-Stufen statt einfarbigem Verlauf: jede Preisstufe durchlaeuft eine eigene -// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen - hell/weiss beim Einstieg (passt zum -// hellen Seitenstil), zunehmend gesaettigter und dunkler zur teuersten Stufe hin, aber -// NIE bis zu Grau/Schwarz entsaettigt - die oberste Stufe bleibt eine satte, tiefe -// Farbe (tiefes Gruen/Saphirblau/Bordeaux-Bronze je nach Theme). 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 RGB linear interpoliert (siehe materialRgbAt), sodass JEDES Paket -// (nicht nur die Materialien selbst) eine eigene Abstufung bekommt. Welches Farbschema -// verwendet wird, waehlt der Kunde selbst unter Admin -> Einstellungen -> Website +// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen. Die Karten bleiben durchgehend dunkel +// (wie eine Fahrzeugkonfigurator-Buehne) - die Preis-Staffelung zeigt sich nicht in +// Hell/Dunkel, sondern im Farbton: jedes Theme wandert ueber die 6 Stufen durch eine +// eigene Farbfamilie (z.B. Graphit: kuehles Grau -> Petrol -> Gruen -> Gold), angelehnt +// an das vom Kunden gezeigte Referenzbild. Jede Stufe hat eine Flaechenfarbe (bg) und +// eine dazu passende, hellere 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 RGB linear interpoliert (siehe materialRgbAt), sodass JEDES Paket (nicht nur +// die Materialien selbst) eine eigene Abstufung bekommt. Welches Farbschema verwendet +// wird, waehlt der Kunde selbst unter Admin -> Einstellungen -> Website // ("Paket-Kachel-Design"). const PACKAGE_THEMES = { graphite: [ - { bg: [215, 6, 98], accent: [214, 20, 45] }, - { bg: [214, 9, 85], accent: [213, 24, 46] }, - { bg: [213, 11, 65], accent: [211, 28, 48] }, - { bg: [212, 13, 45], accent: [209, 32, 52] }, - { bg: [211, 15, 28], accent: [207, 36, 58] }, - { bg: [210, 17, 15], accent: [205, 40, 64] }, + { bg: [220, 8, 22], accent: [220, 10, 60] }, + { bg: [195, 18, 20], accent: [195, 35, 58] }, + { bg: [175, 35, 19], accent: [175, 55, 58] }, + { bg: [150, 40, 17], accent: [150, 55, 56] }, + { bg: [120, 38, 15], accent: [120, 55, 55] }, + { bg: [42, 55, 18], accent: [42, 75, 60] }, ], 'deep-blue': [ - { bg: [210, 10, 98], accent: [205, 45, 50] }, - { bg: [212, 30, 85], accent: [205, 48, 52] }, - { bg: [212, 40, 65], accent: [203, 52, 55] }, - { bg: [210, 50, 45], accent: [200, 58, 58] }, - { bg: [208, 58, 28], accent: [198, 62, 62] }, - { bg: [205, 65, 15], accent: [195, 68, 68] }, + { bg: [210, 8, 22], accent: [210, 12, 58] }, + { bg: [195, 30, 19], accent: [195, 45, 58] }, + { bg: [210, 45, 17], accent: [210, 60, 58] }, + { bg: [220, 55, 15], accent: [220, 65, 60] }, + { bg: [230, 58, 12], accent: [230, 68, 62] }, + { bg: [240, 62, 10], accent: [240, 72, 65] }, ], 'warm-bronze': [ - { bg: [35, 12, 98], accent: [32, 55, 42] }, - { bg: [36, 32, 85], accent: [30, 58, 44] }, - { bg: [34, 42, 65], accent: [28, 60, 46] }, - { bg: [30, 48, 45], accent: [26, 64, 50] }, - { bg: [26, 54, 28], accent: [24, 68, 54] }, - { bg: [20, 60, 15], accent: [22, 72, 58] }, + { bg: [30, 8, 22], accent: [30, 12, 58] }, + { bg: [38, 35, 20], accent: [38, 55, 60] }, + { bg: [30, 50, 18], accent: [30, 65, 60] }, + { bg: [20, 50, 15], accent: [20, 65, 60] }, + { bg: [10, 48, 13], accent: [10, 62, 60] }, + { bg: [0, 45, 11], accent: [0, 58, 62] }, ], }; @@ -100,9 +101,10 @@ export default function ModelPage() { const [error, setError] = useState(''); const maintenance = useMaintenance(); const { t, language } = useLanguage(); - const { package_card_theme: packageCardTheme } = useSiteSettings(); + const { package_card_theme: packageCardTheme, package_card_layout: packageCardLayout } = useSiteSettings(); const MATERIALS = PACKAGE_THEMES[packageCardTheme] || PACKAGE_THEMES.graphite; const cardTexture = PACKAGE_TEXTURES[packageCardTheme] || PACKAGE_TEXTURES.graphite; + const isStripLayout = packageCardLayout !== 'grid'; const formatPrice = (value) => new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value); @@ -198,11 +200,6 @@ export default function ModelPage() { mixBlendMode: 'overlay', opacity: textureOpacity, }, - 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: { @@ -250,22 +247,28 @@ export default function ModelPage() { {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')}

+

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

+

{t('modelPage.tiersSubheading')}

-
+
{packages.map((pkg) => { const { style, priceColor, mutedColor, - iconChipStyle, accentColor, glowLineStyle, roadOpacity, @@ -275,7 +278,15 @@ export default function ModelPage() { } = styleOf(pkg); return ( -
+
{/* Abstrakter "Lichtweg" im Hintergrund - je teurer die Stufe, desto praesenter/leuchtender. Fixe Aspect-Ratio + "slice" statt "none", damit Strich/Kurve bei jeder Kartenhoehe gleichmaessig aussieht statt verzerrt gestreckt zu werden. */} @@ -316,32 +327,22 @@ export default function ModelPage() { {/* Eigener Stapelkontext ueber dem Lichtweg-SVG, damit der Text IMMER lesbar oben liegt statt vom absolut positionierten Hintergrund verdeckt zu werden. */} -
- {pkg.icon_name && ( -
- -
- )} - -

{pkg.name}

+
+

+ {pkg.name} +

+
{pkg.tagline && ( -

+

{pkg.tagline}

)} -
+ {/* Leerraum, der den Lichtweg im Hintergrund zur Geltung bringt - erst + darunter folgen Stichpunkte und Preis, ganz wie im Referenzbild. */} +
-
-

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

-

- {formatPrice(pkg.total_price)} -

-
- -
    +
      {pkg.products.map((product) => (
    • @@ -364,6 +365,15 @@ export default function ModelPage() { ))}
    +
    +

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

    +

    + {formatPrice(pkg.total_price)} +

    +
    + Einstellungen -> Datenbank. + +ALTER TABLE app_settings + ADD COLUMN IF NOT EXISTS package_card_layout VARCHAR(20) NOT NULL DEFAULT 'strip'; diff --git a/hifi/api/database/schema.sql b/hifi/api/database/schema.sql index 5a2e287..a738f21 100644 --- a/hifi/api/database/schema.sql +++ b/hifi/api/database/schema.sql @@ -27,6 +27,7 @@ CREATE TABLE app_settings ( ga_measurement_id VARCHAR(20) NULL, ga_property_id VARCHAR(30) NULL, package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite', + package_card_layout VARCHAR(20) NOT NULL DEFAULT 'strip', mail_host VARCHAR(255) NULL, mail_port INT NULL, mail_username VARCHAR(255) NULL, diff --git a/hifi/api/src/Controllers/SiteSettingsController.php b/hifi/api/src/Controllers/SiteSettingsController.php index fa4daa8..1b92c7a 100644 --- a/hifi/api/src/Controllers/SiteSettingsController.php +++ b/hifi/api/src/Controllers/SiteSettingsController.php @@ -16,13 +16,14 @@ class SiteSettingsController 'hero_image_path' => null, 'ga_measurement_id' => null, 'package_card_theme' => 'graphite', + 'package_card_layout' => 'strip', ]; public static function show(): void { try { $stmt = Database::connection()->query( - 'SELECT phone, whatsapp, contact_email, hero_image_path, ga_measurement_id, package_card_theme FROM app_settings WHERE id = 1' + 'SELECT phone, whatsapp, contact_email, hero_image_path, ga_measurement_id, package_card_theme, package_card_layout FROM app_settings WHERE id = 1' ); $row = $stmt->fetch(); } catch (\Throwable $e) { @@ -39,6 +40,7 @@ class SiteSettingsController } private const PACKAGE_CARD_THEMES = ['graphite', 'deep-blue', 'warm-bronze']; + private const PACKAGE_CARD_LAYOUTS = ['grid', 'strip']; public static function update(): void { @@ -50,9 +52,14 @@ class SiteSettingsController $packageCardTheme = 'graphite'; } + $packageCardLayout = trim($body['package_card_layout'] ?? '') ?: 'strip'; + if (!in_array($packageCardLayout, self::PACKAGE_CARD_LAYOUTS, true)) { + $packageCardLayout = 'strip'; + } + $db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)'); $stmt = $db->prepare( - 'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ?, ga_measurement_id = ?, package_card_theme = ? WHERE id = 1' + 'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ?, ga_measurement_id = ?, package_card_theme = ?, package_card_layout = ? WHERE id = 1' ); $stmt->execute([ trim($body['phone'] ?? '') ?: null, @@ -61,6 +68,7 @@ class SiteSettingsController trim($body['hero_image_path'] ?? '') ?: null, trim($body['ga_measurement_id'] ?? '') ?: null, $packageCardTheme, + $packageCardLayout, ]); self::show(); diff --git a/hifi/api/src/Support/Schema.php b/hifi/api/src/Support/Schema.php index 2d05b95..bd99f0d 100644 --- a/hifi/api/src/Support/Schema.php +++ b/hifi/api/src/Support/Schema.php @@ -38,6 +38,7 @@ class Schema ga_measurement_id VARCHAR(20) NULL, ga_property_id VARCHAR(30) NULL, package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite', + package_card_layout VARCHAR(20) NOT NULL DEFAULT 'strip', mail_host VARCHAR(255) NULL, mail_port INT NULL, mail_username VARCHAR(255) NULL, @@ -262,6 +263,7 @@ class Schema 'ga_measurement_id' => 'VARCHAR(20) NULL', 'ga_property_id' => 'VARCHAR(30) NULL', 'package_card_theme' => "VARCHAR(30) NOT NULL DEFAULT 'graphite'", + 'package_card_layout' => "VARCHAR(20) NOT NULL DEFAULT 'strip'", 'mail_host' => 'VARCHAR(255) NULL', 'mail_port' => 'INT NULL', 'mail_username' => 'VARCHAR(255) NULL',