Add admin-configurable website settings and VIN field to contact form
Adds a Website settings tab (hero image, phone, WhatsApp, email) that feeds the navbar, footer and contact page via a new SiteSettingsContext, redesigns the navbar with a bigger centered logo plus phone/WhatsApp/shop links, and adds a VIN/chassis-number field to the contact form, admin inbox and notification email. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
febe294129
commit
95764039c7
16 changed files with 376 additions and 54 deletions
|
|
@ -30,6 +30,7 @@ import AccountSettings from './pages/admin/AccountSettings.jsx';
|
||||||
import AdminUsers from './pages/admin/AdminUsers.jsx';
|
import AdminUsers from './pages/admin/AdminUsers.jsx';
|
||||||
import PermissionGroups from './pages/admin/PermissionGroups.jsx';
|
import PermissionGroups from './pages/admin/PermissionGroups.jsx';
|
||||||
import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx';
|
import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx';
|
||||||
|
import WebsiteSettings from './pages/admin/settings/WebsiteSettings.jsx';
|
||||||
import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx';
|
import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx';
|
||||||
import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx';
|
import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx';
|
||||||
import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx';
|
import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx';
|
||||||
|
|
@ -74,7 +75,8 @@ export default function App() {
|
||||||
path="settings"
|
path="settings"
|
||||||
element={<RequirePermission permission="settings.manage"><SettingsLayout /></RequirePermission>}
|
element={<RequirePermission permission="settings.manage"><SettingsLayout /></RequirePermission>}
|
||||||
>
|
>
|
||||||
<Route index element={<Navigate to="database" replace />} />
|
<Route index element={<Navigate to="website" replace />} />
|
||||||
|
<Route path="website" element={<WebsiteSettings />} />
|
||||||
<Route path="database" element={<DatabaseSettings />} />
|
<Route path="database" element={<DatabaseSettings />} />
|
||||||
<Route path="export-import" element={<ExportImportSettings />} />
|
<Route path="export-import" element={<ExportImportSettings />} />
|
||||||
<Route path="maintenance" element={<MaintenanceSettings />} />
|
<Route path="maintenance" element={<MaintenanceSettings />} />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import DynamicIcon from './DynamicIcon.jsx';
|
import DynamicIcon from './DynamicIcon.jsx';
|
||||||
import { useCookieConsent } from '../context/CookieConsentContext.jsx';
|
import { useCookieConsent } from '../context/CookieConsentContext.jsx';
|
||||||
|
import { useSiteSettings } from '../context/SiteSettingsContext.jsx';
|
||||||
|
|
||||||
|
const digitsOnly = (value) => (value || '').replace(/[^\d+]/g, '');
|
||||||
|
|
||||||
const SHOP_ADDRESS_ENCODED = encodeURIComponent('Boxbrunner Str. 20a, 63916 Amorbach');
|
const SHOP_ADDRESS_ENCODED = encodeURIComponent('Boxbrunner Str. 20a, 63916 Amorbach');
|
||||||
const DIRECTIONS_URL = `https://www.google.com/maps/dir/?api=1&destination=${SHOP_ADDRESS_ENCODED}`;
|
const DIRECTIONS_URL = `https://www.google.com/maps/dir/?api=1&destination=${SHOP_ADDRESS_ENCODED}`;
|
||||||
|
|
@ -27,6 +30,7 @@ export default function Footer() {
|
||||||
// zusätzlichen Platz für sie freihalten.
|
// zusätzlichen Platz für sie freihalten.
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const isHome = pathname === '/';
|
const isHome = pathname === '/';
|
||||||
|
const { phone, contact_email: contactEmail } = useSiteSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
|
|
@ -59,10 +63,10 @@ export default function Footer() {
|
||||||
<p className="font-semibold text-slate-900 dark:text-white">Kontakt</p>
|
<p className="font-semibold text-slate-900 dark:text-white">Kontakt</p>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<a href="tel:+4993732062390" className="hover:text-brand-500">09373 20 62 390</a>
|
<a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="mailto:info@hifi-planet-amorbach.de" className="hover:text-brand-500">info@hifi-planet-amorbach.de</a>
|
<a href={`mailto:${contactEmail}`} className="hover:text-brand-500">{contactEmail}</a>
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/kontakt"
|
to="/kontakt"
|
||||||
|
|
|
||||||
|
|
@ -3,58 +3,86 @@ import { Link } from 'react-router-dom';
|
||||||
import ThemeToggle from './ThemeToggle.jsx';
|
import ThemeToggle from './ThemeToggle.jsx';
|
||||||
import DynamicIcon from './DynamicIcon.jsx';
|
import DynamicIcon from './DynamicIcon.jsx';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
|
import { useSiteSettings } from '../context/SiteSettingsContext.jsx';
|
||||||
import logo from '../assets/logo.png';
|
import logo from '../assets/logo.png';
|
||||||
|
|
||||||
|
const SHOP_URL = 'https://www.audio4cars.de/';
|
||||||
|
|
||||||
|
const digitsOnly = (value) => (value || '').replace(/[^\d+]/g, '');
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { phone, whatsapp } = useSiteSettings();
|
||||||
|
|
||||||
|
const AdminIcon = user && (
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
aria-label="Zum Admin-Panel"
|
||||||
|
title="Zum Admin-Panel"
|
||||||
|
className="rounded-full p-2 text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<DynamicIcon name="layout-dashboard" className="h-5 w-5" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/90 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90">
|
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/90 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6">
|
{/* Schmale Kontakt-Leiste (Telefon/WhatsApp/Shop) - auf kleinen Screens ausgeblendet,
|
||||||
<Link to="/" className="flex items-center rounded-lg px-2 py-1 dark:bg-white">
|
steht dort stattdessen im aufklappbaren mobilen Menü. */}
|
||||||
<img src={logo} alt="HifiPlanet" className="h-8 w-auto sm:h-10" />
|
<div className="hidden border-b border-slate-100 bg-slate-50 px-4 py-1.5 text-xs text-slate-600 dark:border-slate-900 dark:bg-slate-900/40 dark:text-slate-400 sm:px-6 md:block">
|
||||||
</Link>
|
<div className="mx-auto flex max-w-6xl items-center justify-end gap-5">
|
||||||
|
{phone && (
|
||||||
|
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-1.5 hover:text-brand-500">
|
||||||
|
<DynamicIcon name="phone" className="h-3.5 w-3.5" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{whatsapp && (
|
||||||
|
<a
|
||||||
|
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1.5 hover:text-brand-500"
|
||||||
|
>
|
||||||
|
<DynamicIcon name="message-circle" className="h-3.5 w-3.5" />
|
||||||
|
WhatsApp
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a href={SHOP_URL} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5 hover:text-brand-500">
|
||||||
|
<DynamicIcon name="shopping-bag" className="h-3.5 w-3.5" />
|
||||||
|
Zum Shop
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto grid max-w-6xl grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-3 sm:px-6">
|
||||||
|
<div className="flex items-center justify-self-start">
|
||||||
<nav className="hidden items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300 md:flex">
|
<nav className="hidden items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300 md:flex">
|
||||||
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link>
|
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link>
|
||||||
<Link to="/leistungen" className="hover:text-brand-500">Leistungen</Link>
|
<Link to="/leistungen" className="hover:text-brand-500">Leistungen</Link>
|
||||||
<Link to="/kontakt" className="hover:text-brand-500">Kontakt</Link>
|
<Link to="/kontakt" className="hover:text-brand-500">Kontakt</Link>
|
||||||
{user && (
|
|
||||||
<Link
|
|
||||||
to="/admin"
|
|
||||||
aria-label="Zum Admin-Panel"
|
|
||||||
title="Zum Admin-Panel"
|
|
||||||
className="rounded-full p-2 text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
|
||||||
>
|
|
||||||
<DynamicIcon name="layout-dashboard" className="h-5 w-5" />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<ThemeToggle />
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 md:hidden">
|
|
||||||
{user && (
|
|
||||||
<Link
|
|
||||||
to="/admin"
|
|
||||||
aria-label="Zum Admin-Panel"
|
|
||||||
title="Zum Admin-Panel"
|
|
||||||
className="rounded-full p-2 text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
|
||||||
>
|
|
||||||
<DynamicIcon name="layout-dashboard" className="h-5 w-5" />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<ThemeToggle />
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
aria-label="Menü öffnen"
|
aria-label="Menü öffnen"
|
||||||
className="rounded-md p-2 text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
|
className="rounded-md p-2 text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 md:hidden"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-6 w-6">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-6 w-6">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Link to="/" className="flex items-center justify-self-center rounded-lg px-2 py-1 dark:bg-white">
|
||||||
|
<img src={logo} alt="HifiPlanet" className="h-11 w-auto sm:h-14" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-self-end gap-1">
|
||||||
|
{AdminIcon}
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
|
|
@ -62,6 +90,33 @@ export default function Navbar() {
|
||||||
<Link to="/fahrzeuge" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Fahrzeuge</Link>
|
<Link to="/fahrzeuge" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Fahrzeuge</Link>
|
||||||
<Link to="/leistungen" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Leistungen</Link>
|
<Link to="/leistungen" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Leistungen</Link>
|
||||||
<Link to="/kontakt" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Kontakt</Link>
|
<Link to="/kontakt" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Kontakt</Link>
|
||||||
|
<div className="my-1 border-t border-slate-200 dark:border-slate-800" />
|
||||||
|
{phone && (
|
||||||
|
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">
|
||||||
|
<DynamicIcon name="phone" className="h-4 w-4" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{whatsapp && (
|
||||||
|
<a
|
||||||
|
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<DynamicIcon name="message-circle" className="h-4 w-4" />
|
||||||
|
WhatsApp
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={SHOP_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<DynamicIcon name="shopping-bag" className="h-4 w-4" />
|
||||||
|
Zum Shop
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import MaintenanceNotice from './MaintenanceNotice.jsx';
|
||||||
import MaintenanceBypassBanner from './MaintenanceBypassBanner.jsx';
|
import MaintenanceBypassBanner from './MaintenanceBypassBanner.jsx';
|
||||||
import CookieConsentBanner from './CookieConsentBanner.jsx';
|
import CookieConsentBanner from './CookieConsentBanner.jsx';
|
||||||
import { MaintenanceProvider } from '../context/MaintenanceContext.jsx';
|
import { MaintenanceProvider } from '../context/MaintenanceContext.jsx';
|
||||||
|
import { SiteSettingsProvider } from '../context/SiteSettingsContext.jsx';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { api } from '../api/client.js';
|
import { api } from '../api/client.js';
|
||||||
|
|
||||||
|
|
@ -16,6 +17,13 @@ const FALLBACK_STATUS = {
|
||||||
vehicles: { enabled: false, message: null },
|
vehicles: { enabled: false, message: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SITE_SETTINGS = {
|
||||||
|
phone: '09373 20 62 390',
|
||||||
|
whatsapp: null,
|
||||||
|
contact_email: 'info@hifi-planet-amorbach.de',
|
||||||
|
hero_image_path: null,
|
||||||
|
};
|
||||||
|
|
||||||
// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG
|
// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG
|
||||||
// "leicht erkennbar und unmittelbar erreichbar" bleiben – auch während der
|
// "leicht erkennbar und unmittelbar erreichbar" bleiben – auch während der
|
||||||
// generellen Wartung darf diese Erreichbarkeit nicht durch die Wartungsseite
|
// generellen Wartung darf diese Erreichbarkeit nicht durch die Wartungsseite
|
||||||
|
|
@ -24,11 +32,13 @@ const LEGAL_PATHS = ['/impressum', '/datenschutz', '/agb'];
|
||||||
|
|
||||||
export default function PublicLayout() {
|
export default function PublicLayout() {
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
|
const [siteSettings, setSiteSettings] = useState(DEFAULT_SITE_SETTINGS);
|
||||||
const { loading: authLoading, hasPermission } = useAuth();
|
const { loading: authLoading, hasPermission } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/maintenance').then(setStatus).catch(() => setStatus(FALLBACK_STATUS));
|
api.get('/maintenance').then(setStatus).catch(() => setStatus(FALLBACK_STATUS));
|
||||||
|
api.get('/site-settings').then(setSiteSettings).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status === null || authLoading) {
|
if (status === null || authLoading) {
|
||||||
|
|
@ -44,6 +54,7 @@ export default function PublicLayout() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaintenanceProvider value={{ ...status, bypass }}>
|
<MaintenanceProvider value={{ ...status, bypass }}>
|
||||||
|
<SiteSettingsProvider value={siteSettings}>
|
||||||
<div className="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
<div className="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
||||||
{bypass && status.global.enabled && <MaintenanceBypassBanner />}
|
{bypass && status.global.enabled && <MaintenanceBypassBanner />}
|
||||||
<ScrollProgress />
|
<ScrollProgress />
|
||||||
|
|
@ -54,6 +65,7 @@ export default function PublicLayout() {
|
||||||
<Footer />
|
<Footer />
|
||||||
<CookieConsentBanner />
|
<CookieConsentBanner />
|
||||||
</div>
|
</div>
|
||||||
|
</SiteSettingsProvider>
|
||||||
</MaintenanceProvider>
|
</MaintenanceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
hifi-src/src/context/SiteSettingsContext.jsx
Normal file
16
hifi-src/src/context/SiteSettingsContext.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// Defaults entsprechen dem, was früher fest im Code stand - greifen, solange
|
||||||
|
// der eigentliche /site-settings-Request noch lädt oder fehlschlägt.
|
||||||
|
const SiteSettingsContext = createContext({
|
||||||
|
phone: '09373 20 62 390',
|
||||||
|
whatsapp: null,
|
||||||
|
contact_email: 'info@hifi-planet-amorbach.de',
|
||||||
|
hero_image_path: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SiteSettingsProvider = SiteSettingsContext.Provider;
|
||||||
|
|
||||||
|
export function useSiteSettings() {
|
||||||
|
return useContext(SiteSettingsContext);
|
||||||
|
}
|
||||||
|
|
@ -88,6 +88,9 @@ export default function ContactRequests() {
|
||||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2">
|
<div className="mb-2 flex flex-wrap items-start justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-slate-800 dark:text-slate-100">{r.name} · {r.email}{r.phone ? ` · ${r.phone}` : ''}</p>
|
<p className="font-semibold text-slate-800 dark:text-slate-100">{r.name} · {r.email}{r.phone ? ` · ${r.phone}` : ''}</p>
|
||||||
|
{r.vin && (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">FIN: <span className="font-mono">{r.vin}</span></p>
|
||||||
|
)}
|
||||||
<p className="text-xs text-slate-400">{new Date(r.created_at.replace(' ', 'T')).toLocaleString('de-DE')}</p>
|
<p className="text-xs text-slate-400">{new Date(r.created_at.replace(' ', 'T')).toLocaleString('de-DE')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { NavLink, Outlet } from 'react-router-dom';
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
{ to: '/admin/settings/website', label: 'Website' },
|
||||||
{ to: '/admin/settings/database', label: 'Datenbank' },
|
{ to: '/admin/settings/database', label: 'Datenbank' },
|
||||||
{ to: '/admin/settings/export-import', label: 'Export & Import' },
|
{ to: '/admin/settings/export-import', label: 'Export & Import' },
|
||||||
{ to: '/admin/settings/maintenance', label: 'Wartungsmodus' },
|
{ to: '/admin/settings/maintenance', label: 'Wartungsmodus' },
|
||||||
|
|
|
||||||
118
hifi-src/src/pages/admin/settings/WebsiteSettings.jsx
Normal file
118
hifi-src/src/pages/admin/settings/WebsiteSettings.jsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
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: '' };
|
||||||
|
|
||||||
|
export default function WebsiteSettings() {
|
||||||
|
const [form, setForm] = useState(emptyForm);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.get('/site-settings')
|
||||||
|
.then((res) =>
|
||||||
|
setForm({
|
||||||
|
phone: res.phone || '',
|
||||||
|
whatsapp: res.whatsapp || '',
|
||||||
|
contact_email: res.contact_email || '',
|
||||||
|
hero_image_path: res.hero_image_path || '',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const update = (field, value) => {
|
||||||
|
setForm((f) => ({ ...f, [field]: value }));
|
||||||
|
setSaved(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
setError('');
|
||||||
|
setSaved(false);
|
||||||
|
try {
|
||||||
|
await api.post('/site-settings', form);
|
||||||
|
setSaved(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p className="text-sm text-slate-400">Lädt…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
|
||||||
|
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Startseiten-Bild</h2>
|
||||||
|
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Das große Hero-Bild ganz oben auf der Startseite. Ohne eigenes Bild wird das Standardbild verwendet.
|
||||||
|
</p>
|
||||||
|
<ImageUploadField
|
||||||
|
value={form.hero_image_path}
|
||||||
|
onChange={(path) => update('hero_image_path', path)}
|
||||||
|
label="Bild"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Kontaktdaten</h2>
|
||||||
|
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Wird im Header, Footer und auf der Kontaktseite angezeigt.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Telefon</label>
|
||||||
|
<input
|
||||||
|
value={form.phone}
|
||||||
|
onChange={(e) => update('phone', e.target.value)}
|
||||||
|
placeholder="09373 20 62 390"
|
||||||
|
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
WhatsApp Business Nummer
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={form.whatsapp}
|
||||||
|
onChange={(e) => update('whatsapp', e.target.value)}
|
||||||
|
placeholder="z. B. 09373 20 62 390"
|
||||||
|
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Leer lassen, um den WhatsApp-Link im Header auszublenden.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">E-Mail</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={form.contact_email}
|
||||||
|
onChange={(e) => update('contact_email', e.target.value)}
|
||||||
|
placeholder="info@hifi-planet-amorbach.de"
|
||||||
|
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||||
|
{saved && <p className="text-sm text-green-600 dark:text-green-400">Gespeichert.</p>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{busy ? 'Speichere…' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,19 @@ import { useSearchParams } from 'react-router-dom';
|
||||||
import { api } from '../../api/client.js';
|
import { api } from '../../api/client.js';
|
||||||
import usePageMeta from '../../hooks/usePageMeta.js';
|
import usePageMeta from '../../hooks/usePageMeta.js';
|
||||||
import ExternalEmbed from '../../components/ExternalEmbed.jsx';
|
import ExternalEmbed from '../../components/ExternalEmbed.jsx';
|
||||||
|
import { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
|
||||||
|
|
||||||
const formatPrice = (value) =>
|
const formatPrice = (value) =>
|
||||||
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value);
|
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value);
|
||||||
|
|
||||||
|
const digitsOnly = (value) => (value || '').replace(/[^\d+]/g, '');
|
||||||
|
|
||||||
const SHOP_ADDRESS_ENCODED = encodeURIComponent('Boxbrunner Str. 20a, 63916 Amorbach');
|
const SHOP_ADDRESS_ENCODED = encodeURIComponent('Boxbrunner Str. 20a, 63916 Amorbach');
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const [form, setForm] = useState({ name: '', email: '', phone: '', message: '' });
|
const { phone, contact_email: contactEmail } = useSiteSettings();
|
||||||
|
const [form, setForm] = useState({ name: '', email: '', phone: '', vin: '', message: '' });
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [upgrades, setUpgrades] = useState([]);
|
const [upgrades, setUpgrades] = useState([]);
|
||||||
|
|
@ -160,6 +164,18 @@ export default function ContactPage() {
|
||||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900"
|
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
Fahrgestellnummer (FIN)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="vin"
|
||||||
|
value={form.vin}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Optional – hilft uns bei der genauen Einschätzung deines Fahrzeugs"
|
||||||
|
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Nachricht</label>
|
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Nachricht</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -195,11 +211,11 @@ export default function ContactPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="font-medium text-slate-800 dark:text-slate-100">Telefon</dt>
|
<dt className="font-medium text-slate-800 dark:text-slate-100">Telefon</dt>
|
||||||
<dd><a href="tel:+4993732062390" className="hover:text-brand-500">09373 20 62 390</a></dd>
|
<dd><a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</a></dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="font-medium text-slate-800 dark:text-slate-100">E-Mail</dt>
|
<dt className="font-medium text-slate-800 dark:text-slate-100">E-Mail</dt>
|
||||||
<dd><a href="mailto:info@hifi-planet-amorbach.de" className="hover:text-brand-500">info@hifi-planet-amorbach.de</a></dd>
|
<dd><a href={`mailto:${contactEmail}`} className="hover:text-brand-500">{contactEmail}</a></dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="font-medium text-slate-800 dark:text-slate-100">Öffnungszeiten</dt>
|
<dt className="font-medium text-slate-800 dark:text-slate-100">Öffnungszeiten</dt>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import Accordion from '../../components/Accordion.jsx';
|
||||||
import StarRating from '../../components/StarRating.jsx';
|
import StarRating from '../../components/StarRating.jsx';
|
||||||
import TestimonialSlider from '../../components/TestimonialSlider.jsx';
|
import TestimonialSlider from '../../components/TestimonialSlider.jsx';
|
||||||
import ExternalEmbed from '../../components/ExternalEmbed.jsx';
|
import ExternalEmbed from '../../components/ExternalEmbed.jsx';
|
||||||
import heroImage from '../../assets/photos/hero-trunk.jpg';
|
import { useSiteSettings } from '../../context/SiteSettingsContext.jsx';
|
||||||
|
import defaultHeroImage from '../../assets/photos/hero-trunk.jpg';
|
||||||
import galleryAmp from '../../assets/photos/gallery-amp-purple.jpg';
|
import galleryAmp from '../../assets/photos/gallery-amp-purple.jpg';
|
||||||
import gallerySpeaker from '../../assets/photos/gallery-speaker.jpg';
|
import gallerySpeaker from '../../assets/photos/gallery-speaker.jpg';
|
||||||
import gallerySubs from '../../assets/photos/gallery-subs.jpg';
|
import gallerySubs from '../../assets/photos/gallery-subs.jpg';
|
||||||
|
|
@ -66,6 +67,9 @@ const faqs = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { hero_image_path: heroImagePath } = useSiteSettings();
|
||||||
|
const heroImage = heroImagePath || defaultHeroImage;
|
||||||
|
|
||||||
usePageMeta({
|
usePageMeta({
|
||||||
title: 'Car-Hifi Umbauten nach Maß',
|
title: 'Car-Hifi Umbauten nach Maß',
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
13
hifi/api/database/migration_site_settings.sql
Normal file
13
hifi/api/database/migration_site_settings.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Kontaktdaten (Telefon/WhatsApp/E-Mail) und Startseiten-Hero-Bild admin-konfigurierbar machen,
|
||||||
|
-- sowie ein Fahrgestellnummer/FIN-Feld fuer Kontaktanfragen. 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 phone VARCHAR(50) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS whatsapp VARCHAR(50) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS contact_email VARCHAR(150) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS hero_image_path VARCHAR(255) NULL;
|
||||||
|
|
||||||
|
ALTER TABLE contact_requests
|
||||||
|
ADD COLUMN IF NOT EXISTS vin VARCHAR(50) NULL AFTER phone;
|
||||||
|
|
@ -20,6 +20,10 @@ CREATE TABLE app_settings (
|
||||||
maintenance_services_message VARCHAR(500) NULL,
|
maintenance_services_message VARCHAR(500) NULL,
|
||||||
maintenance_vehicles TINYINT(1) NOT NULL DEFAULT 0,
|
maintenance_vehicles TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
maintenance_vehicles_message VARCHAR(500) NULL,
|
maintenance_vehicles_message VARCHAR(500) NULL,
|
||||||
|
phone VARCHAR(50) NULL,
|
||||||
|
whatsapp VARCHAR(50) NULL,
|
||||||
|
contact_email VARCHAR(150) NULL,
|
||||||
|
hero_image_path VARCHAR(255) NULL,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
|
@ -137,6 +141,7 @@ CREATE TABLE contact_requests (
|
||||||
name VARCHAR(150) NOT NULL,
|
name VARCHAR(150) NOT NULL,
|
||||||
email VARCHAR(150) NOT NULL,
|
email VARCHAR(150) NOT NULL,
|
||||||
phone VARCHAR(50) NULL,
|
phone VARCHAR(50) NULL,
|
||||||
|
vin VARCHAR(50) NULL,
|
||||||
message TEXT NULL,
|
message TEXT NULL,
|
||||||
brand_name VARCHAR(100) NULL,
|
brand_name VARCHAR(100) NULL,
|
||||||
model_name VARCHAR(100) NULL,
|
model_name VARCHAR(100) NULL,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use App\Controllers\SchemaController;
|
||||||
use App\Controllers\ServiceController;
|
use App\Controllers\ServiceController;
|
||||||
use App\Controllers\SettingsController;
|
use App\Controllers\SettingsController;
|
||||||
use App\Controllers\SetupController;
|
use App\Controllers\SetupController;
|
||||||
|
use App\Controllers\SiteSettingsController;
|
||||||
use App\Controllers\TwoFactorController;
|
use App\Controllers\TwoFactorController;
|
||||||
use App\Controllers\UploadController;
|
use App\Controllers\UploadController;
|
||||||
use App\Middleware\AuthMiddleware;
|
use App\Middleware\AuthMiddleware;
|
||||||
|
|
@ -124,7 +125,7 @@ $router->get('/contact', $perm('contact.manage', fn($p) => ContactController::in
|
||||||
$router->patch('/contact/{id}', $perm('contact.manage', fn($p) => ContactController::updateStatus($p)));
|
$router->patch('/contact/{id}', $perm('contact.manage', fn($p) => ContactController::updateStatus($p)));
|
||||||
$router->delete('/contact/{id}', $perm('contact.delete', fn($p) => ContactController::destroy($p)));
|
$router->delete('/contact/{id}', $perm('contact.delete', fn($p) => ContactController::destroy($p)));
|
||||||
|
|
||||||
$router->post('/uploads', $anyPerm(['brands.manage', 'models.manage', 'services.manage'], fn($p) => UploadController::store()));
|
$router->post('/uploads', $anyPerm(['brands.manage', 'models.manage', 'services.manage', 'settings.manage'], fn($p) => UploadController::store()));
|
||||||
|
|
||||||
$router->get('/services', $maint('services', fn($p) => ServiceController::index()));
|
$router->get('/services', $maint('services', fn($p) => ServiceController::index()));
|
||||||
$router->post('/services', $perm('services.manage', fn($p) => ServiceController::store()));
|
$router->post('/services', $perm('services.manage', fn($p) => ServiceController::store()));
|
||||||
|
|
@ -156,6 +157,9 @@ $router->post('/settings/database', $perm('settings.manage', fn($p) => DatabaseC
|
||||||
$router->get('/maintenance', fn($p) => MaintenanceController::status());
|
$router->get('/maintenance', fn($p) => MaintenanceController::status());
|
||||||
$router->post('/maintenance', $perm('settings.manage', fn($p) => MaintenanceController::update()));
|
$router->post('/maintenance', $perm('settings.manage', fn($p) => MaintenanceController::update()));
|
||||||
|
|
||||||
|
$router->get('/site-settings', fn($p) => SiteSettingsController::show());
|
||||||
|
$router->post('/site-settings', $perm('settings.manage', fn($p) => SiteSettingsController::update()));
|
||||||
|
|
||||||
// Ersteinrichtungs-Assistent (/setup) - bewusst ohne Login, da es ja noch keinen
|
// Ersteinrichtungs-Assistent (/setup) - bewusst ohne Login, da es ja noch keinen
|
||||||
// Admin gibt. Jeder schreibende Endpunkt prüft selbst, ob wirklich noch kein
|
// Admin gibt. Jeder schreibende Endpunkt prüft selbst, ob wirklich noch kein
|
||||||
// Admin-Account existiert (siehe SetupController::requireUnlocked).
|
// Admin-Account existiert (siehe SetupController::requireUnlocked).
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,14 @@ class ContactController
|
||||||
|
|
||||||
$stmt = $db->prepare(
|
$stmt = $db->prepare(
|
||||||
'INSERT INTO contact_requests
|
'INSERT INTO contact_requests
|
||||||
(name, email, phone, message, brand_name, model_name, package_name, product_name, package_id, package_product_id, selected_upgrades)
|
(name, email, phone, vin, message, brand_name, model_name, package_name, product_name, package_id, package_product_id, selected_upgrades)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$name,
|
$name,
|
||||||
$email,
|
$email,
|
||||||
$body['phone'] ?? null,
|
$body['phone'] ?? null,
|
||||||
|
$body['vin'] ?? null,
|
||||||
$body['message'] ?? null,
|
$body['message'] ?? null,
|
||||||
$body['brand_name'] ?? null,
|
$body['brand_name'] ?? null,
|
||||||
$body['model_name'] ?? null,
|
$body['model_name'] ?? null,
|
||||||
|
|
@ -71,7 +72,7 @@ class ContactController
|
||||||
public static function index(): void
|
public static function index(): void
|
||||||
{
|
{
|
||||||
$stmt = Database::connection()->query(
|
$stmt = Database::connection()->query(
|
||||||
'SELECT id, name, email, phone, message, brand_name, model_name, package_name, product_name,
|
'SELECT id, name, email, phone, vin, message, brand_name, model_name, package_name, product_name,
|
||||||
selected_upgrades, status, created_at
|
selected_upgrades, status, created_at
|
||||||
FROM contact_requests ORDER BY created_at DESC'
|
FROM contact_requests ORDER BY created_at DESC'
|
||||||
);
|
);
|
||||||
|
|
@ -145,7 +146,8 @@ class ContactController
|
||||||
}
|
}
|
||||||
|
|
||||||
$mail->Subject = 'Neue Kontaktanfrage von ' . $name;
|
$mail->Subject = 'Neue Kontaktanfrage von ' . $name;
|
||||||
$mail->Body = "Name: {$name}\nE-Mail: {$email}\nTelefon: " . ($body['phone'] ?? '-') . "\n\n"
|
$mail->Body = "Name: {$name}\nE-Mail: {$email}\nTelefon: " . ($body['phone'] ?? '-')
|
||||||
|
. "\nFahrgestellnummer (FIN): " . ($body['vin'] ?? '-') . "\n\n"
|
||||||
. $context . "\n\nGewünschte Upgrades:\n" . $upgradesText . "\n\nNachricht:\n" . ($body['message'] ?? '-');
|
. $context . "\n\nGewünschte Upgrades:\n" . $upgradesText . "\n\nNachricht:\n" . ($body['message'] ?? '-');
|
||||||
|
|
||||||
$mail->send();
|
$mail->send();
|
||||||
|
|
|
||||||
57
hifi/api/src/Controllers/SiteSettingsController.php
Normal file
57
hifi/api/src/Controllers/SiteSettingsController.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Config\Database;
|
||||||
|
use App\Support\Http;
|
||||||
|
|
||||||
|
class SiteSettingsController
|
||||||
|
{
|
||||||
|
// Greifen, solange in den Einstellungen noch nichts eingetragen wurde -
|
||||||
|
// entsprechen dem, was bisher fest im Code stand.
|
||||||
|
private const DEFAULTS = [
|
||||||
|
'phone' => '09373 20 62 390',
|
||||||
|
'whatsapp' => null,
|
||||||
|
'contact_email' => 'info@hifi-planet-amorbach.de',
|
||||||
|
'hero_image_path' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function show(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = Database::connection()->query(
|
||||||
|
'SELECT phone, whatsapp, contact_email, hero_image_path FROM app_settings WHERE id = 1'
|
||||||
|
);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$row = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $row ?: [];
|
||||||
|
$result = [];
|
||||||
|
foreach (self::DEFAULTS as $key => $default) {
|
||||||
|
$result[$key] = ($row[$key] ?? null) !== null && $row[$key] !== '' ? $row[$key] : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
Http::send($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function update(): void
|
||||||
|
{
|
||||||
|
$body = Http::jsonBody();
|
||||||
|
$db = Database::connection();
|
||||||
|
|
||||||
|
$db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)');
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ? WHERE id = 1'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
trim($body['phone'] ?? '') ?: null,
|
||||||
|
trim($body['whatsapp'] ?? '') ?: null,
|
||||||
|
trim($body['contact_email'] ?? '') ?: null,
|
||||||
|
trim($body['hero_image_path'] ?? '') ?: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,10 @@ class Schema
|
||||||
maintenance_services_message VARCHAR(500) NULL,
|
maintenance_services_message VARCHAR(500) NULL,
|
||||||
maintenance_vehicles TINYINT(1) NOT NULL DEFAULT 0,
|
maintenance_vehicles TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
maintenance_vehicles_message VARCHAR(500) NULL,
|
maintenance_vehicles_message VARCHAR(500) NULL,
|
||||||
|
phone VARCHAR(50) NULL,
|
||||||
|
whatsapp VARCHAR(50) NULL,
|
||||||
|
contact_email VARCHAR(150) NULL,
|
||||||
|
hero_image_path VARCHAR(255) NULL,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB",
|
) ENGINE=InnoDB",
|
||||||
|
|
||||||
|
|
@ -148,6 +152,7 @@ class Schema
|
||||||
name VARCHAR(150) NOT NULL,
|
name VARCHAR(150) NOT NULL,
|
||||||
email VARCHAR(150) NOT NULL,
|
email VARCHAR(150) NOT NULL,
|
||||||
phone VARCHAR(50) NULL,
|
phone VARCHAR(50) NULL,
|
||||||
|
vin VARCHAR(50) NULL,
|
||||||
message TEXT NULL,
|
message TEXT NULL,
|
||||||
brand_name VARCHAR(100) NULL,
|
brand_name VARCHAR(100) NULL,
|
||||||
model_name VARCHAR(100) NULL,
|
model_name VARCHAR(100) NULL,
|
||||||
|
|
@ -187,6 +192,10 @@ class Schema
|
||||||
'maintenance_services_message' => 'VARCHAR(500) NULL',
|
'maintenance_services_message' => 'VARCHAR(500) NULL',
|
||||||
'maintenance_vehicles' => 'TINYINT(1) NOT NULL DEFAULT 0',
|
'maintenance_vehicles' => 'TINYINT(1) NOT NULL DEFAULT 0',
|
||||||
'maintenance_vehicles_message' => 'VARCHAR(500) NULL',
|
'maintenance_vehicles_message' => 'VARCHAR(500) NULL',
|
||||||
|
'phone' => 'VARCHAR(50) NULL',
|
||||||
|
'whatsapp' => 'VARCHAR(50) NULL',
|
||||||
|
'contact_email' => 'VARCHAR(150) NULL',
|
||||||
|
'hero_image_path' => 'VARCHAR(255) NULL',
|
||||||
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
|
|
@ -280,6 +289,7 @@ class Schema
|
||||||
'name' => 'VARCHAR(150) NOT NULL',
|
'name' => 'VARCHAR(150) NOT NULL',
|
||||||
'email' => 'VARCHAR(150) NOT NULL',
|
'email' => 'VARCHAR(150) NOT NULL',
|
||||||
'phone' => 'VARCHAR(50) NULL',
|
'phone' => 'VARCHAR(50) NULL',
|
||||||
|
'vin' => 'VARCHAR(50) NULL',
|
||||||
'message' => 'TEXT NULL',
|
'message' => 'TEXT NULL',
|
||||||
'brand_name' => 'VARCHAR(100) NULL',
|
'brand_name' => 'VARCHAR(100) NULL',
|
||||||
'model_name' => 'VARCHAR(100) NULL',
|
'model_name' => 'VARCHAR(100) NULL',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue