Redesign package tiles as a dark, credit-card-style scroll strip with hue-sweeping tiers
Package cards go back to a permanently dark stage (all price tiers, not just the priciest), with each theme sweeping through its own colour family across the tiers instead of just lightening/darkening. Adds an admin toggle between the previous wrapping grid and a new horizontal scroll strip of narrow, tall cards. Card content is restructured to match the client's reference mockup: no icon chip, centered uppercase name with a short divider, an empty middle area that lets the glowing road motif show through, then the feature list and price stacked at the bottom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
55284d0421
commit
3e237421c6
7 changed files with 130 additions and 64 deletions
|
|
@ -26,6 +26,7 @@ const DEFAULT_SITE_SETTINGS = {
|
||||||
hero_image_path: null,
|
hero_image_path: null,
|
||||||
ga_measurement_id: null,
|
ga_measurement_id: null,
|
||||||
package_card_theme: 'graphite',
|
package_card_theme: 'graphite',
|
||||||
|
package_card_layout: 'strip',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG
|
// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const emptyForm = {
|
||||||
hero_image_path: '',
|
hero_image_path: '',
|
||||||
ga_measurement_id: '',
|
ga_measurement_id: '',
|
||||||
package_card_theme: 'graphite',
|
package_card_theme: 'graphite',
|
||||||
|
package_card_layout: 'strip',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PACKAGE_CARD_THEMES = [
|
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.' },
|
{ 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() {
|
export default function WebsiteSettings() {
|
||||||
const [form, setForm] = useState(emptyForm);
|
const [form, setForm] = useState(emptyForm);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -42,6 +48,7 @@ export default function WebsiteSettings() {
|
||||||
hero_image_path: res.hero_image_path || '',
|
hero_image_path: res.hero_image_path || '',
|
||||||
ga_measurement_id: res.ga_measurement_id || '',
|
ga_measurement_id: res.ga_measurement_id || '',
|
||||||
package_card_theme: res.package_card_theme || 'graphite',
|
package_card_theme: res.package_card_theme || 'graphite',
|
||||||
|
package_card_layout: res.package_card_layout || 'strip',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
|
@ -179,6 +186,36 @@ export default function WebsiteSettings() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">Kachel-Layout</h2>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Bestimmt, wie die Paket-Stufen auf jeder Fahrzeug-Modell-Seite angeordnet werden.
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{PACKAGE_CARD_LAYOUTS.map((layout) => (
|
||||||
|
<label
|
||||||
|
key={layout.value}
|
||||||
|
className={`cursor-pointer rounded-lg border p-3 text-sm transition ${
|
||||||
|
form.package_card_layout === layout.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_layout"
|
||||||
|
value={layout.value}
|
||||||
|
checked={form.package_card_layout === layout.value}
|
||||||
|
onChange={(e) => update('package_card_layout', e.target.value)}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<p className="font-semibold text-neutral-900 dark:text-white">{layout.label}</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{layout.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">
|
<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>
|
<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">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
|
|
||||||
|
|
@ -23,41 +23,42 @@ const PACKAGE_TEXTURES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Material-Stufen statt einfarbigem Verlauf: jede Preisstufe durchlaeuft eine eigene
|
// Material-Stufen statt einfarbigem Verlauf: jede Preisstufe durchlaeuft eine eigene
|
||||||
// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen - hell/weiss beim Einstieg (passt zum
|
// "Wertigkeit" wie bei Kreditkarten-/Loyalty-Stufen. Die Karten bleiben durchgehend dunkel
|
||||||
// hellen Seitenstil), zunehmend gesaettigter und dunkler zur teuersten Stufe hin, aber
|
// (wie eine Fahrzeugkonfigurator-Buehne) - die Preis-Staffelung zeigt sich nicht in
|
||||||
// NIE bis zu Grau/Schwarz entsaettigt - die oberste Stufe bleibt eine satte, tiefe
|
// Hell/Dunkel, sondern im Farbton: jedes Theme wandert ueber die 6 Stufen durch eine
|
||||||
// Farbe (tiefes Gruen/Saphirblau/Bordeaux-Bronze je nach Theme). Jede Stufe hat eine
|
// eigene Farbfamilie (z.B. Graphit: kuehles Grau -> Petrol -> Gruen -> Gold), angelehnt
|
||||||
// Flaechenfarbe (bg) und eine dazu passende Akzentfarbe (Rahmen/Leucht-Schatten/Preis/
|
// an das vom Kunden gezeigte Referenzbild. Jede Stufe hat eine Flaechenfarbe (bg) und
|
||||||
// Icon) - nur der "Kontakt anfragen"-Button bleibt ueberall einheitlich markengruen,
|
// eine dazu passende, hellere Akzentfarbe (Rahmen/Leucht-Schatten/Preis/Icon) - nur der
|
||||||
// damit die Kernaktion auf jeder Karte wiedererkennbar bleibt. Zwischen zwei Nachbar-
|
// "Kontakt anfragen"-Button bleibt ueberall einheitlich markengruen, damit die
|
||||||
// Materialien wird in RGB linear interpoliert (siehe materialRgbAt), sodass JEDES Paket
|
// Kernaktion auf jeder Karte wiedererkennbar bleibt. Zwischen zwei Nachbar-Materialien
|
||||||
// (nicht nur die Materialien selbst) eine eigene Abstufung bekommt. Welches Farbschema
|
// wird in RGB linear interpoliert (siehe materialRgbAt), sodass JEDES Paket (nicht nur
|
||||||
// verwendet wird, waehlt der Kunde selbst unter Admin -> Einstellungen -> Website
|
// die Materialien selbst) eine eigene Abstufung bekommt. Welches Farbschema verwendet
|
||||||
|
// wird, waehlt der Kunde selbst unter Admin -> Einstellungen -> Website
|
||||||
// ("Paket-Kachel-Design").
|
// ("Paket-Kachel-Design").
|
||||||
const PACKAGE_THEMES = {
|
const PACKAGE_THEMES = {
|
||||||
graphite: [
|
graphite: [
|
||||||
{ bg: [215, 6, 98], accent: [214, 20, 45] },
|
{ bg: [220, 8, 22], accent: [220, 10, 60] },
|
||||||
{ bg: [214, 9, 85], accent: [213, 24, 46] },
|
{ bg: [195, 18, 20], accent: [195, 35, 58] },
|
||||||
{ bg: [213, 11, 65], accent: [211, 28, 48] },
|
{ bg: [175, 35, 19], accent: [175, 55, 58] },
|
||||||
{ bg: [212, 13, 45], accent: [209, 32, 52] },
|
{ bg: [150, 40, 17], accent: [150, 55, 56] },
|
||||||
{ bg: [211, 15, 28], accent: [207, 36, 58] },
|
{ bg: [120, 38, 15], accent: [120, 55, 55] },
|
||||||
{ bg: [210, 17, 15], accent: [205, 40, 64] },
|
{ bg: [42, 55, 18], accent: [42, 75, 60] },
|
||||||
],
|
],
|
||||||
'deep-blue': [
|
'deep-blue': [
|
||||||
{ bg: [210, 10, 98], accent: [205, 45, 50] },
|
{ bg: [210, 8, 22], accent: [210, 12, 58] },
|
||||||
{ bg: [212, 30, 85], accent: [205, 48, 52] },
|
{ bg: [195, 30, 19], accent: [195, 45, 58] },
|
||||||
{ bg: [212, 40, 65], accent: [203, 52, 55] },
|
{ bg: [210, 45, 17], accent: [210, 60, 58] },
|
||||||
{ bg: [210, 50, 45], accent: [200, 58, 58] },
|
{ bg: [220, 55, 15], accent: [220, 65, 60] },
|
||||||
{ bg: [208, 58, 28], accent: [198, 62, 62] },
|
{ bg: [230, 58, 12], accent: [230, 68, 62] },
|
||||||
{ bg: [205, 65, 15], accent: [195, 68, 68] },
|
{ bg: [240, 62, 10], accent: [240, 72, 65] },
|
||||||
],
|
],
|
||||||
'warm-bronze': [
|
'warm-bronze': [
|
||||||
{ bg: [35, 12, 98], accent: [32, 55, 42] },
|
{ bg: [30, 8, 22], accent: [30, 12, 58] },
|
||||||
{ bg: [36, 32, 85], accent: [30, 58, 44] },
|
{ bg: [38, 35, 20], accent: [38, 55, 60] },
|
||||||
{ bg: [34, 42, 65], accent: [28, 60, 46] },
|
{ bg: [30, 50, 18], accent: [30, 65, 60] },
|
||||||
{ bg: [30, 48, 45], accent: [26, 64, 50] },
|
{ bg: [20, 50, 15], accent: [20, 65, 60] },
|
||||||
{ bg: [26, 54, 28], accent: [24, 68, 54] },
|
{ bg: [10, 48, 13], accent: [10, 62, 60] },
|
||||||
{ bg: [20, 60, 15], accent: [22, 72, 58] },
|
{ bg: [0, 45, 11], accent: [0, 58, 62] },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -100,9 +101,10 @@ export default function ModelPage() {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const maintenance = useMaintenance();
|
const maintenance = useMaintenance();
|
||||||
const { t, language } = useLanguage();
|
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 MATERIALS = PACKAGE_THEMES[packageCardTheme] || PACKAGE_THEMES.graphite;
|
||||||
const cardTexture = PACKAGE_TEXTURES[packageCardTheme] || PACKAGE_TEXTURES.graphite;
|
const cardTexture = PACKAGE_TEXTURES[packageCardTheme] || PACKAGE_TEXTURES.graphite;
|
||||||
|
const isStripLayout = packageCardLayout !== 'grid';
|
||||||
const formatPrice = (value) =>
|
const formatPrice = (value) =>
|
||||||
new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(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',
|
mixBlendMode: 'overlay',
|
||||||
opacity: textureOpacity,
|
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
|
// Duenne, gleichbleibende Haarlinie statt dickerem Rahmen - die zunehmende Wertigkeit
|
||||||
// zeigt sich in Farbe/Leucht-Schatten, nicht in der Strichstaerke. Wirkt ruhiger/edler.
|
// zeigt sich in Farbe/Leucht-Schatten, nicht in der Strichstaerke. Wirkt ruhiger/edler.
|
||||||
glowLineStyle: {
|
glowLineStyle: {
|
||||||
|
|
@ -250,22 +247,28 @@ export default function ModelPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{packages.length > 0 && (
|
{packages.length > 0 && (
|
||||||
<section className="py-14 sm:py-20">
|
// 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.
|
||||||
|
<section className="bg-neutral-950 py-14 sm:py-20">
|
||||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||||
<div className="mb-10 text-center sm:mb-14">
|
<div className="mb-10 text-center sm:mb-14">
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">
|
<h2 className="text-2xl font-bold text-white sm:text-3xl">{t('modelPage.tiersHeading')(packages.length)}</h2>
|
||||||
{t('modelPage.tiersHeading')(packages.length)}
|
<p className="mx-auto mt-2 max-w-xl text-sm text-neutral-400">{t('modelPage.tiersSubheading')}</p>
|
||||||
</h2>
|
|
||||||
<p className="mx-auto mt-2 max-w-xl text-sm text-neutral-500 dark:text-neutral-400">{t('modelPage.tiersSubheading')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
<div
|
||||||
|
className={
|
||||||
|
isStripLayout
|
||||||
|
? 'flex items-stretch gap-5 overflow-x-auto pb-2 snap-x snap-mandatory'
|
||||||
|
: 'grid gap-6 sm:grid-cols-2 xl:grid-cols-3'
|
||||||
|
}
|
||||||
|
>
|
||||||
{packages.map((pkg) => {
|
{packages.map((pkg) => {
|
||||||
const {
|
const {
|
||||||
style,
|
style,
|
||||||
priceColor,
|
priceColor,
|
||||||
mutedColor,
|
mutedColor,
|
||||||
iconChipStyle,
|
|
||||||
accentColor,
|
accentColor,
|
||||||
glowLineStyle,
|
glowLineStyle,
|
||||||
roadOpacity,
|
roadOpacity,
|
||||||
|
|
@ -275,7 +278,15 @@ export default function ModelPage() {
|
||||||
} = styleOf(pkg);
|
} = styleOf(pkg);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={pkg.id} style={style} className="relative flex flex-col overflow-hidden rounded-xl border p-8">
|
<div
|
||||||
|
key={pkg.id}
|
||||||
|
style={style}
|
||||||
|
className={
|
||||||
|
isStripLayout
|
||||||
|
? 'relative flex w-[190px] shrink-0 snap-start flex-col overflow-hidden rounded-xl border p-6 sm:w-[210px]'
|
||||||
|
: 'relative flex flex-col overflow-hidden rounded-xl border p-8'
|
||||||
|
}
|
||||||
|
>
|
||||||
{/* Abstrakter "Lichtweg" im Hintergrund - je teurer die Stufe, desto praesenter/leuchtender.
|
{/* Abstrakter "Lichtweg" im Hintergrund - je teurer die Stufe, desto praesenter/leuchtender.
|
||||||
Fixe Aspect-Ratio + "slice" statt "none", damit Strich/Kurve bei jeder Kartenhoehe
|
Fixe Aspect-Ratio + "slice" statt "none", damit Strich/Kurve bei jeder Kartenhoehe
|
||||||
gleichmaessig aussieht statt verzerrt gestreckt zu werden. */}
|
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
|
{/* Eigener Stapelkontext ueber dem Lichtweg-SVG, damit der Text IMMER lesbar
|
||||||
oben liegt statt vom absolut positionierten Hintergrund verdeckt zu werden. */}
|
oben liegt statt vom absolut positionierten Hintergrund verdeckt zu werden. */}
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col text-center">
|
||||||
{pkg.icon_name && (
|
<h3 className={`text-lg font-bold uppercase tracking-wide ${isDarkCard ? 'text-white' : 'text-neutral-900'}`}>
|
||||||
<div style={iconChipStyle} className="mb-4 flex h-11 w-11 items-center justify-center rounded-full">
|
{pkg.name}
|
||||||
<DynamicIcon name={pkg.icon_name} className="h-6 w-6" />
|
</h3>
|
||||||
</div>
|
<div style={glowLineStyle} className="mx-auto mt-3 h-px w-10" />
|
||||||
)}
|
|
||||||
|
|
||||||
<h3 className={`text-lg font-bold ${isDarkCard ? 'text-white' : 'text-neutral-900'}`}>{pkg.name}</h3>
|
|
||||||
{pkg.tagline && (
|
{pkg.tagline && (
|
||||||
<p style={{ color: mutedColor }} className="mt-1 text-sm">
|
<p style={{ color: mutedColor }} className="mx-auto mt-3 max-w-[16ch] text-sm">
|
||||||
{pkg.tagline}
|
{pkg.tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={glowLineStyle} className="my-5 h-px w-full" />
|
{/* Leerraum, der den Lichtweg im Hintergrund zur Geltung bringt - erst
|
||||||
|
darunter folgen Stichpunkte und Preis, ganz wie im Referenzbild. */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
<div className="mb-4">
|
<ul className="mb-4 space-y-2.5 text-left text-sm">
|
||||||
<p style={{ color: mutedColor }} className="text-xs uppercase tracking-wide">
|
|
||||||
{t('modelPage.totalPrice')}
|
|
||||||
</p>
|
|
||||||
<p style={{ color: priceColor }} className="text-2xl font-extrabold">
|
|
||||||
{formatPrice(pkg.total_price)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="mb-6 flex-1 space-y-2.5 text-sm">
|
|
||||||
{pkg.products.map((product) => (
|
{pkg.products.map((product) => (
|
||||||
<li key={product.id} className="flex items-start gap-2.5">
|
<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: priceColor }} />
|
<DynamicIcon name="check" className="mt-0.5 h-4 w-4 shrink-0" style={{ color: priceColor }} />
|
||||||
|
|
@ -364,6 +365,15 @@ export default function ModelPage() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p style={{ color: mutedColor }} className="text-xs uppercase tracking-wide">
|
||||||
|
{t('modelPage.totalPrice')}
|
||||||
|
</p>
|
||||||
|
<p style={{ color: priceColor }} className="text-2xl font-extrabold">
|
||||||
|
{formatPrice(pkg.total_price)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={contactUrl(pkg)}
|
to={contactUrl(pkg)}
|
||||||
className="inline-block rounded-md bg-brand-500 px-4 py-2 text-center text-sm font-semibold text-white shadow-lg shadow-brand-500/30 hover:bg-brand-400"
|
className="inline-block rounded-md bg-brand-500 px-4 py-2 text-center text-sm font-semibold text-white shadow-lg shadow-brand-500/30 hover:bg-brand-400"
|
||||||
|
|
|
||||||
7
hifi/api/database/migration_package_card_layout.sql
Normal file
7
hifi/api/database/migration_package_card_layout.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Auswaehlbares Layout fuer die Paket-Kacheln (responsives Raster / horizontales
|
||||||
|
-- Scroll-Band aus schmalen Kacheln). 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_layout VARCHAR(20) NOT NULL DEFAULT 'strip';
|
||||||
|
|
@ -27,6 +27,7 @@ CREATE TABLE app_settings (
|
||||||
ga_measurement_id VARCHAR(20) NULL,
|
ga_measurement_id VARCHAR(20) NULL,
|
||||||
ga_property_id VARCHAR(30) NULL,
|
ga_property_id VARCHAR(30) NULL,
|
||||||
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
||||||
|
package_card_layout VARCHAR(20) NOT NULL DEFAULT 'strip',
|
||||||
mail_host VARCHAR(255) NULL,
|
mail_host VARCHAR(255) NULL,
|
||||||
mail_port INT NULL,
|
mail_port INT NULL,
|
||||||
mail_username VARCHAR(255) NULL,
|
mail_username VARCHAR(255) NULL,
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,14 @@ class SiteSettingsController
|
||||||
'hero_image_path' => null,
|
'hero_image_path' => null,
|
||||||
'ga_measurement_id' => null,
|
'ga_measurement_id' => null,
|
||||||
'package_card_theme' => 'graphite',
|
'package_card_theme' => 'graphite',
|
||||||
|
'package_card_layout' => 'strip',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function show(): void
|
public static function show(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$stmt = Database::connection()->query(
|
$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();
|
$row = $stmt->fetch();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -39,6 +40,7 @@ class SiteSettingsController
|
||||||
}
|
}
|
||||||
|
|
||||||
private const PACKAGE_CARD_THEMES = ['graphite', 'deep-blue', 'warm-bronze'];
|
private const PACKAGE_CARD_THEMES = ['graphite', 'deep-blue', 'warm-bronze'];
|
||||||
|
private const PACKAGE_CARD_LAYOUTS = ['grid', 'strip'];
|
||||||
|
|
||||||
public static function update(): void
|
public static function update(): void
|
||||||
{
|
{
|
||||||
|
|
@ -50,9 +52,14 @@ class SiteSettingsController
|
||||||
$packageCardTheme = 'graphite';
|
$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)');
|
$db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)');
|
||||||
$stmt = $db->prepare(
|
$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([
|
$stmt->execute([
|
||||||
trim($body['phone'] ?? '') ?: null,
|
trim($body['phone'] ?? '') ?: null,
|
||||||
|
|
@ -61,6 +68,7 @@ class SiteSettingsController
|
||||||
trim($body['hero_image_path'] ?? '') ?: null,
|
trim($body['hero_image_path'] ?? '') ?: null,
|
||||||
trim($body['ga_measurement_id'] ?? '') ?: null,
|
trim($body['ga_measurement_id'] ?? '') ?: null,
|
||||||
$packageCardTheme,
|
$packageCardTheme,
|
||||||
|
$packageCardLayout,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
self::show();
|
self::show();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class Schema
|
||||||
ga_measurement_id VARCHAR(20) NULL,
|
ga_measurement_id VARCHAR(20) NULL,
|
||||||
ga_property_id VARCHAR(30) NULL,
|
ga_property_id VARCHAR(30) NULL,
|
||||||
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
package_card_theme VARCHAR(30) NOT NULL DEFAULT 'graphite',
|
||||||
|
package_card_layout VARCHAR(20) NOT NULL DEFAULT 'strip',
|
||||||
mail_host VARCHAR(255) NULL,
|
mail_host VARCHAR(255) NULL,
|
||||||
mail_port INT NULL,
|
mail_port INT NULL,
|
||||||
mail_username VARCHAR(255) NULL,
|
mail_username VARCHAR(255) NULL,
|
||||||
|
|
@ -262,6 +263,7 @@ class Schema
|
||||||
'ga_measurement_id' => 'VARCHAR(20) NULL',
|
'ga_measurement_id' => 'VARCHAR(20) NULL',
|
||||||
'ga_property_id' => 'VARCHAR(30) NULL',
|
'ga_property_id' => 'VARCHAR(30) NULL',
|
||||||
'package_card_theme' => "VARCHAR(30) NOT NULL DEFAULT 'graphite'",
|
'package_card_theme' => "VARCHAR(30) NOT NULL DEFAULT 'graphite'",
|
||||||
|
'package_card_layout' => "VARCHAR(20) NOT NULL DEFAULT 'strip'",
|
||||||
'mail_host' => 'VARCHAR(255) NULL',
|
'mail_host' => 'VARCHAR(255) NULL',
|
||||||
'mail_port' => 'INT NULL',
|
'mail_port' => 'INT NULL',
|
||||||
'mail_username' => 'VARCHAR(255) NULL',
|
'mail_username' => 'VARCHAR(255) NULL',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue