Add selectable package card color themes and refine the feature list styling
Bullet lists on package cards now use accent-colored checkmarks instead of plain browser dots. The card color system is now driven by a persistent site setting (Admin > Einstellungen > Website > "Paket-Kachel-Design") with three curated themes - Graphite Green, Deep Blue Luxury, and Warm Bronze - all converging on the same dark "Onyx + Gold" treatment for the priciest tier, matching the reference mockups. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d7fdd36662
commit
411ee8a241
7 changed files with 122 additions and 25 deletions
|
|
@ -25,6 +25,7 @@ const DEFAULT_SITE_SETTINGS = {
|
|||
contact_email: 'info@hifi-planet-amorbach.de',
|
||||
hero_image_path: null,
|
||||
ga_measurement_id: null,
|
||||
package_card_theme: 'graphite',
|
||||
};
|
||||
|
||||
// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG
|
||||
|
|
|
|||
|
|
@ -2,7 +2,20 @@ import { useEffect, useState } from 'react';
|
|||
import { api } from '../../../api/client.js';
|
||||
import ImageUploadField from '../../../components/ImageUploadField.jsx';
|
||||
|
||||
const emptyForm = { phone: '', whatsapp: '', contact_email: '', hero_image_path: '', ga_measurement_id: '' };
|
||||
const emptyForm = {
|
||||
phone: '',
|
||||
whatsapp: '',
|
||||
contact_email: '',
|
||||
hero_image_path: '',
|
||||
ga_measurement_id: '',
|
||||
package_card_theme: 'graphite',
|
||||
};
|
||||
|
||||
const PACKAGE_CARD_THEMES = [
|
||||
{ value: 'graphite', label: 'Graphite Green', description: 'Graphit über Bronze/Silber/Gold/Platin bis zur Onyx-Krönung.' },
|
||||
{ value: 'deep-blue', label: 'Deep Blue Luxury', description: 'Kühles Blau, das sich über Grün zu Gold erwärmt.' },
|
||||
{ value: 'warm-bronze', label: 'Warm Bronze', description: 'Durchgehend warmes Kupfer/Bronze/Amber bis zum Gold.' },
|
||||
];
|
||||
|
||||
export default function WebsiteSettings() {
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
|
|
@ -28,6 +41,7 @@ export default function WebsiteSettings() {
|
|||
contact_email: res.contact_email || '',
|
||||
hero_image_path: res.hero_image_path || '',
|
||||
ga_measurement_id: res.ga_measurement_id || '',
|
||||
package_card_theme: res.package_card_theme || 'graphite',
|
||||
})
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
|
|
@ -134,6 +148,37 @@ export default function WebsiteSettings() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Paket-Kachel-Design</h2>
|
||||
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
Bestimmt die Farbgebung der Paket-Stufen auf jeder Fahrzeug-Modell-Seite (von der günstigsten bis zur
|
||||
teuersten Option).
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{PACKAGE_CARD_THEMES.map((theme) => (
|
||||
<label
|
||||
key={theme.value}
|
||||
className={`cursor-pointer rounded-lg border p-3 text-sm transition ${
|
||||
form.package_card_theme === theme.value
|
||||
? 'border-brand-500 ring-2 ring-brand-500/40'
|
||||
: 'border-neutral-300 hover:border-neutral-400 dark:border-neutral-700 dark:hover:border-neutral-600'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="package_card_theme"
|
||||
value={theme.value}
|
||||
checked={form.package_card_theme === theme.value}
|
||||
onChange={(e) => update('package_card_theme', e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<p className="font-semibold text-neutral-900 dark:text-white">{theme.label}</p>
|
||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{theme.description}</p>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Google Analytics</h2>
|
||||
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
|
|
|
|||
|
|
@ -7,26 +7,46 @@ 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 { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
|
||||
|
||||
// Material-Stufen statt einfarbigem Verlauf: jede Preisstufe durchlaeuft eine eigene
|
||||
// "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 RGB linear interpoliert (siehe materialRgbAt),
|
||||
// sodass JEDES Paket (nicht nur die Materialien selbst) eine eigene Abstufung bekommt.
|
||||
const MATERIALS = [
|
||||
// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen. 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 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") - alle drei
|
||||
// laufen bewusst auf dieselbe Onyx+Gold-Kroenung bei der teuersten Stufe zu.
|
||||
const PACKAGE_THEMES = {
|
||||
graphite: [
|
||||
{ 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
|
||||
];
|
||||
],
|
||||
'deep-blue': [
|
||||
{ bg: [212, 30, 16], accent: [205, 45, 62] }, // Eisblau (Einstieg)
|
||||
{ bg: [212, 35, 20], accent: [205, 50, 64] }, // Tiefblau
|
||||
{ bg: [190, 30, 22], accent: [190, 45, 62] }, // Petrol
|
||||
{ bg: [160, 30, 20], accent: [150, 45, 58] }, // Seegruen
|
||||
{ bg: [95, 30, 20], accent: [95, 50, 58] }, // Markengruen
|
||||
{ bg: [225, 22, 5], accent: [45, 78, 68] }, // Onyx
|
||||
],
|
||||
'warm-bronze': [
|
||||
{ bg: [28, 22, 16], accent: [32, 40, 55] }, // Warmes Graphit (Einstieg)
|
||||
{ bg: [26, 38, 22], accent: [28, 58, 55] }, // Bronze
|
||||
{ bg: [32, 42, 24], accent: [34, 62, 56] }, // Kupfer
|
||||
{ bg: [38, 45, 24], accent: [38, 66, 58] }, // Amber
|
||||
{ bg: [42, 48, 24], accent: [42, 70, 60] }, // Reiches Gold
|
||||
{ bg: [225, 22, 5], accent: [45, 78, 68] }, // Onyx
|
||||
],
|
||||
};
|
||||
|
||||
const hslToRgb = (h, s, l) => {
|
||||
s /= 100;
|
||||
|
|
@ -67,6 +87,8 @@ export default function ModelPage() {
|
|||
const [error, setError] = useState('');
|
||||
const maintenance = useMaintenance();
|
||||
const { t, language } = useLanguage();
|
||||
const { package_card_theme: packageCardTheme } = useSiteSettings();
|
||||
const MATERIALS = PACKAGE_THEMES[packageCardTheme] || PACKAGE_THEMES.graphite;
|
||||
const formatPrice = (value) =>
|
||||
new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value);
|
||||
|
||||
|
|
@ -257,17 +279,27 @@ export default function ModelPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<ul style={{ color: mutedColor }} className="mb-6 flex-1 list-disc space-y-1 pl-5 text-sm">
|
||||
<ul className="mb-6 flex-1 space-y-2.5 text-sm">
|
||||
{pkg.products.map((product) => (
|
||||
<li key={product.id}>
|
||||
<li key={product.id} className="flex items-start gap-2.5">
|
||||
<DynamicIcon name="check" className="mt-0.5 h-4 w-4 shrink-0" style={{ color: accentColor }} />
|
||||
<span style={{ color: mutedColor }} className="leading-snug">
|
||||
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
{pkg.description
|
||||
?.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line, i) => <li key={`desc-${i}`}>{line}</li>)}
|
||||
.map((line, i) => (
|
||||
<li key={`desc-${i}`} className="flex items-start gap-2.5">
|
||||
<DynamicIcon name="check" className="mt-0.5 h-4 w-4 shrink-0" style={{ color: accentColor }} />
|
||||
<span style={{ color: mutedColor }} className="leading-snug">
|
||||
{line}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
|
|
|
|||
7
hifi/api/database/migration_package_card_theme.sql
Normal file
7
hifi/api/database/migration_package_card_theme.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Auswaehlbares Farbschema fuer die Paket-Kacheln (Graphite Green / Deep Blue / Warm
|
||||
-- Bronze). Additiv, nichts wird geloescht.
|
||||
-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren"
|
||||
-- unter Admin-Panel -> Einstellungen -> Datenbank.
|
||||
|
||||
ALTER TABLE app_settings
|
||||
ADD COLUMN IF NOT EXISTS package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite';
|
||||
|
|
@ -26,6 +26,7 @@ CREATE TABLE app_settings (
|
|||
hero_image_path VARCHAR(255) NULL,
|
||||
ga_measurement_id VARCHAR(20) NULL,
|
||||
ga_property_id VARCHAR(30) NULL,
|
||||
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
||||
mail_host VARCHAR(255) NULL,
|
||||
mail_port INT NULL,
|
||||
mail_username VARCHAR(255) NULL,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ class SiteSettingsController
|
|||
'contact_email' => 'info@hifi-planet-amorbach.de',
|
||||
'hero_image_path' => null,
|
||||
'ga_measurement_id' => null,
|
||||
'package_card_theme' => 'graphite',
|
||||
];
|
||||
|
||||
public static function show(): void
|
||||
{
|
||||
try {
|
||||
$stmt = Database::connection()->query(
|
||||
'SELECT phone, whatsapp, contact_email, hero_image_path, ga_measurement_id FROM app_settings WHERE id = 1'
|
||||
'SELECT phone, whatsapp, contact_email, hero_image_path, ga_measurement_id, package_card_theme FROM app_settings WHERE id = 1'
|
||||
);
|
||||
$row = $stmt->fetch();
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -37,14 +38,21 @@ class SiteSettingsController
|
|||
Http::send($result);
|
||||
}
|
||||
|
||||
private const PACKAGE_CARD_THEMES = ['graphite', 'deep-blue', 'warm-bronze'];
|
||||
|
||||
public static function update(): void
|
||||
{
|
||||
$body = Http::jsonBody();
|
||||
$db = Database::connection();
|
||||
|
||||
$packageCardTheme = trim($body['package_card_theme'] ?? '') ?: 'graphite';
|
||||
if (!in_array($packageCardTheme, self::PACKAGE_CARD_THEMES, true)) {
|
||||
$packageCardTheme = 'graphite';
|
||||
}
|
||||
|
||||
$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 = ? WHERE id = 1'
|
||||
'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ?, ga_measurement_id = ?, package_card_theme = ? WHERE id = 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
trim($body['phone'] ?? '') ?: null,
|
||||
|
|
@ -52,6 +60,7 @@ class SiteSettingsController
|
|||
trim($body['contact_email'] ?? '') ?: null,
|
||||
trim($body['hero_image_path'] ?? '') ?: null,
|
||||
trim($body['ga_measurement_id'] ?? '') ?: null,
|
||||
$packageCardTheme,
|
||||
]);
|
||||
|
||||
self::show();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class Schema
|
|||
hero_image_path VARCHAR(255) NULL,
|
||||
ga_measurement_id VARCHAR(20) NULL,
|
||||
ga_property_id VARCHAR(30) NULL,
|
||||
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
||||
mail_host VARCHAR(255) NULL,
|
||||
mail_port INT NULL,
|
||||
mail_username VARCHAR(255) NULL,
|
||||
|
|
@ -260,6 +261,7 @@ class Schema
|
|||
'hero_image_path' => 'VARCHAR(255) NULL',
|
||||
'ga_measurement_id' => 'VARCHAR(20) NULL',
|
||||
'ga_property_id' => 'VARCHAR(30) NULL',
|
||||
'package_card_theme' => "VARCHAR(30) NOT NULL DEFAULT 'graphite'",
|
||||
'mail_host' => 'VARCHAR(255) NULL',
|
||||
'mail_port' => 'INT NULL',
|
||||
'mail_username' => 'VARCHAR(255) NULL',
|
||||
|
|
|
|||
Loading…
Reference in a new issue