Redesign navbar mobile menu and rework site color palette
Swaps navbar layout (theme toggle left, nav links right), replaces the mobile dropdown with a fullscreen overlay whose items fly in top-to-bottom, and renames the site-wide "slate" gray to "neutral" to remove a blue undertone that clashed with the brand green. Also converts the flat saturated-green YouTube and stats sections into dark backgrounds with green used only as an accent, matching typical premium car-audio branding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
95764039c7
commit
b6909e756c
43 changed files with 609 additions and 566 deletions
|
|
@ -5,7 +5,7 @@ export default function Accordion({ items }) {
|
||||||
const [openIndex, setOpenIndex] = useState(null);
|
const [openIndex, setOpenIndex] = useState(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white dark:divide-slate-800 dark:border-slate-800 dark:bg-slate-900">
|
<div className="divide-y divide-neutral-200 rounded-xl border border-neutral-200 bg-white dark:divide-neutral-800 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
{items.map((item, i) => {
|
{items.map((item, i) => {
|
||||||
const isOpen = openIndex === i;
|
const isOpen = openIndex === i;
|
||||||
return (
|
return (
|
||||||
|
|
@ -15,7 +15,7 @@ export default function Accordion({ items }) {
|
||||||
className="flex w-full items-center justify-between gap-4 px-5 py-4 text-left"
|
className="flex w-full items-center justify-between gap-4 px-5 py-4 text-left"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<span className="font-medium text-slate-900 dark:text-white">{item.question}</span>
|
<span className="font-medium text-neutral-900 dark:text-white">{item.question}</span>
|
||||||
<motion.svg
|
<motion.svg
|
||||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
|
|
@ -24,7 +24,7 @@ export default function Accordion({ items }) {
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-5 w-5 shrink-0 text-slate-500"
|
className="h-5 w-5 shrink-0 text-neutral-500"
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="m6 9 6 6 6-6" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="m6 9 6 6 6-6" />
|
||||||
</motion.svg>
|
</motion.svg>
|
||||||
|
|
@ -38,7 +38,7 @@ export default function Accordion({ items }) {
|
||||||
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<p className="px-5 pb-4 text-sm text-slate-600 dark:text-slate-300">{item.answer}</p>
|
<p className="px-5 pb-4 text-sm text-neutral-600 dark:text-neutral-300">{item.answer}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export default function AdminLayout() {
|
||||||
`flex items-center justify-between gap-2 rounded-md px-3 py-2 text-sm font-medium transition ${
|
`flex items-center justify-between gap-2 rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-brand-500 text-white'
|
? 'bg-brand-500 text-white'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
: 'text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const NavLinks = ({ onNavigate }) => (
|
const NavLinks = ({ onNavigate }) => (
|
||||||
|
|
@ -92,7 +92,7 @@ export default function AdminLayout() {
|
||||||
{navGroups.map((group, i) => (
|
{navGroups.map((group, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{group.title && (
|
{group.title && (
|
||||||
<p className="mb-1.5 px-3 text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">
|
<p className="mb-1.5 px-3 text-xs font-semibold uppercase tracking-wide text-neutral-400 dark:text-neutral-500">
|
||||||
{group.title}
|
{group.title}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -115,25 +115,25 @@ export default function AdminLayout() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
<div className="flex min-h-screen bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100">
|
||||||
<aside className="hidden w-60 shrink-0 border-r border-slate-200 p-4 dark:border-slate-800 sm:block">
|
<aside className="hidden w-60 shrink-0 border-r border-neutral-200 p-4 dark:border-neutral-800 sm:block">
|
||||||
<div className="mb-6 flex items-center gap-2 px-3">
|
<div className="mb-6 flex items-center gap-2 px-3">
|
||||||
<span className="rounded-lg px-2 py-1 dark:bg-white">
|
<span className="rounded-lg px-2 py-1 dark:bg-white">
|
||||||
<img src={logo} alt="HifiPlanet" className="h-7 w-auto" />
|
<img src={logo} alt="HifiPlanet" className="h-7 w-auto" />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold text-slate-500 dark:text-slate-400">Admin</span>
|
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">Admin</span>
|
||||||
</div>
|
</div>
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<header className="border-b border-slate-200 dark:border-slate-800">
|
<header className="border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between px-4 py-3 sm:px-6">
|
<div className="flex items-center justify-between px-4 py-3 sm:px-6">
|
||||||
<div className="flex items-center gap-2 sm:hidden">
|
<div className="flex items-center gap-2 sm:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen((o) => !o)}
|
onClick={() => setMobileOpen((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-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<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" />
|
||||||
|
|
@ -143,21 +143,21 @@ export default function AdminLayout() {
|
||||||
<img src={logo} alt="HifiPlanet" className="h-6 w-auto" />
|
<img src={logo} alt="HifiPlanet" className="h-6 w-auto" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden text-sm text-slate-500 dark:text-slate-400 sm:block">
|
<p className="hidden text-sm text-neutral-500 dark:text-neutral-400 sm:block">
|
||||||
Angemeldet als {user?.username}{user?.is_super_admin ? ' · Super-Admin' : ''}
|
Angemeldet als {user?.username}{user?.is_super_admin ? ' · Super-Admin' : ''}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="rounded-md border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
className="rounded-md border border-neutral-300 px-3 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
Abmelden
|
Abmelden
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<div className="border-t border-slate-200 px-4 py-3 dark:border-slate-800 sm:hidden">
|
<div className="border-t border-neutral-200 px-4 py-3 dark:border-neutral-800 sm:hidden">
|
||||||
<NavLinks onNavigate={() => setMobileOpen(false)} />
|
<NavLinks onNavigate={() => setMobileOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ function SettingsModal({ onClose }) {
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-md rounded-xl bg-white p-6 dark:bg-slate-900"
|
className="w-full max-w-md rounded-xl bg-white p-6 dark:bg-neutral-900"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<h2 className="mb-1 text-lg font-bold text-slate-900 dark:text-white">Cookie-Einstellungen</h2>
|
<h2 className="mb-1 text-lg font-bold text-neutral-900 dark:text-white">Cookie-Einstellungen</h2>
|
||||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
<p className="mb-4 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
Lege fest, welche Kategorien du zulassen möchtest. Mehr dazu in unserer{' '}
|
Lege fest, welche Kategorien du zulassen möchtest. Mehr dazu in unserer{' '}
|
||||||
<Link to="/datenschutz" className="underline hover:text-brand-500" onClick={onClose}>
|
<Link to="/datenschutz" className="underline hover:text-brand-500" onClick={onClose}>
|
||||||
Datenschutzerklärung
|
Datenschutzerklärung
|
||||||
|
|
@ -24,20 +24,20 @@ function SettingsModal({ onClose }) {
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-start justify-between gap-3 rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
<div className="flex items-start justify-between gap-3 rounded-lg border border-neutral-200 p-3 dark:border-neutral-700">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-800 dark:text-slate-100">Notwendig</p>
|
<p className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">Notwendig</p>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
Für den Betrieb der Website erforderlich (z. B. Login-Session, Theme- und Cookie-Einstellung).
|
Für den Betrieb der Website erforderlich (z. B. Login-Session, Theme- und Cookie-Einstellung).
|
||||||
Kann nicht deaktiviert werden.
|
Kann nicht deaktiviert werden.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input type="checkbox" checked disabled className="mt-1 h-4 w-4 shrink-0" />
|
<input type="checkbox" checked disabled className="mt-1 h-4 w-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-between gap-3 rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
<div className="flex items-start justify-between gap-3 rounded-lg border border-neutral-200 p-3 dark:border-neutral-700">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-800 dark:text-slate-100">Externe Medien</p>
|
<p className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">Externe Medien</p>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
Google Maps und YouTube-Videos. Beim Laden werden Daten (u. a. deine IP-Adresse) an Google
|
Google Maps und YouTube-Videos. Beim Laden werden Daten (u. a. deine IP-Adresse) an Google
|
||||||
übertragen.
|
übertragen.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -54,7 +54,7 @@ function SettingsModal({ onClose }) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700"
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -77,9 +77,9 @@ export default function CookieConsentBanner() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!decided && (
|
{!decided && (
|
||||||
<div className="fixed inset-x-0 bottom-0 z-50 border-t border-slate-200 bg-white/95 p-4 shadow-lg backdrop-blur dark:border-slate-800 dark:bg-slate-900/95 sm:p-5">
|
<div className="fixed inset-x-0 bottom-0 z-50 border-t border-neutral-200 bg-white/95 p-4 shadow-lg backdrop-blur dark:border-neutral-800 dark:bg-neutral-900/95 sm:p-5">
|
||||||
<div className="mx-auto flex max-w-6xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mx-auto flex max-w-6xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
Wir verwenden nur technisch notwendige Cookies. Für Google Maps und YouTube-Videos benötigen wir
|
Wir verwenden nur technisch notwendige Cookies. Für Google Maps und YouTube-Videos benötigen wir
|
||||||
zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden. Mehr dazu in unserer{' '}
|
zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden. Mehr dazu in unserer{' '}
|
||||||
<Link to="/datenschutz" className="underline hover:text-brand-500">
|
<Link to="/datenschutz" className="underline hover:text-brand-500">
|
||||||
|
|
@ -91,14 +91,14 @@ export default function CookieConsentBanner() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
className="rounded-md border border-slate-300 px-3 py-2 text-sm dark:border-slate-700"
|
className="rounded-md border border-neutral-300 px-3 py-2 text-sm dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
Einstellungen
|
Einstellungen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={rejectNonEssential}
|
onClick={rejectNonEssential}
|
||||||
className="rounded-md border border-slate-300 px-3 py-2 text-sm dark:border-slate-700"
|
className="rounded-md border border-neutral-300 px-3 py-2 text-sm dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
Nur notwendige
|
Nur notwendige
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ export default function ExternalEmbed({ name, description, className = '', child
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center justify-center gap-3 rounded-xl border border-slate-200 bg-slate-50 p-6 text-center dark:border-slate-800 dark:bg-slate-900/40 ${className}`}
|
className={`flex flex-col items-center justify-center gap-3 rounded-xl border border-neutral-200 bg-neutral-50 p-6 text-center dark:border-neutral-800 dark:bg-neutral-900/40 ${className}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
{description || `${name} wird erst nach deiner Zustimmung geladen, da dabei Daten an Google übertragen werden.`}
|
{description || `${name} wird erst nach deiner Zustimmung geladen, da dabei Daten an Google übertragen werden.`}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
|
|
@ -25,7 +25,7 @@ export default function ExternalEmbed({ name, description, className = '', child
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700"
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
Cookie-Einstellungen
|
Cookie-Einstellungen
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ export default function Footer() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={`border-t border-slate-200 bg-slate-50 py-10 text-sm text-slate-600 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-300 ${
|
className={`border-t border-neutral-200 bg-neutral-50 py-10 text-sm text-neutral-600 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-300 ${
|
||||||
isHome ? 'pb-24' : ''
|
isHome ? 'pb-24' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto grid max-w-6xl gap-4 px-4 sm:grid-cols-3 sm:px-6">
|
<div className="mx-auto grid max-w-6xl gap-4 px-4 sm:grid-cols-3 sm:px-6">
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<DynamicIcon name="map-pin" className="h-4 w-4 text-brand-500" />
|
<DynamicIcon name="map-pin" className="h-4 w-4 text-brand-500" />
|
||||||
<p className="font-semibold text-slate-900 dark:text-white">HifiPlanet</p>
|
<p className="font-semibold text-neutral-900 dark:text-white">HifiPlanet</p>
|
||||||
</div>
|
</div>
|
||||||
<p>Boxbrunner Str. 20a</p>
|
<p>Boxbrunner Str. 20a</p>
|
||||||
<p>63916 Amorbach</p>
|
<p>63916 Amorbach</p>
|
||||||
|
|
@ -57,10 +57,10 @@ export default function Footer() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<DynamicIcon name="phone" className="h-4 w-4 text-brand-500" />
|
<DynamicIcon name="phone" className="h-4 w-4 text-brand-500" />
|
||||||
<p className="font-semibold text-slate-900 dark:text-white">Kontakt</p>
|
<p className="font-semibold text-neutral-900 dark:text-white">Kontakt</p>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</a>
|
<a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</a>
|
||||||
|
|
@ -70,40 +70,40 @@ export default function Footer() {
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/kontakt"
|
to="/kontakt"
|
||||||
className="mt-4 flex items-center justify-center gap-1.5 rounded-md border border-slate-300 px-3 py-2 text-xs font-semibold text-slate-700 hover:border-brand-500 hover:text-brand-600 dark:border-slate-700 dark:text-slate-200"
|
className="mt-4 flex items-center justify-center gap-1.5 rounded-md border border-neutral-300 px-3 py-2 text-xs font-semibold text-neutral-700 hover:border-brand-500 hover:text-brand-600 dark:border-neutral-700 dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
Kontaktformular
|
Kontaktformular
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-3 flex items-center justify-between gap-2">
|
<div className="mb-3 flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DynamicIcon name="clock" className="h-4 w-4 text-brand-500" />
|
<DynamicIcon name="clock" className="h-4 w-4 text-brand-500" />
|
||||||
<p className="font-semibold text-slate-900 dark:text-white">Öffnungszeiten</p>
|
<p className="font-semibold text-neutral-900 dark:text-white">Öffnungszeiten</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold ${
|
className={`flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold ${
|
||||||
open
|
open
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
: 'bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400'
|
: 'bg-neutral-100 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${open ? 'bg-green-500' : 'bg-slate-400'}`} />
|
<span className={`h-1.5 w-1.5 rounded-full ${open ? 'bg-green-500' : 'bg-neutral-400'}`} />
|
||||||
{open ? 'Geöffnet' : 'Geschlossen'}
|
{open ? 'Geöffnet' : 'Geschlossen'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<dl className="space-y-1.5">
|
<dl className="space-y-1.5">
|
||||||
{HOURS.map((row) => (
|
{HOURS.map((row) => (
|
||||||
<div key={row.days} className="flex items-center justify-between gap-3">
|
<div key={row.days} className="flex items-center justify-between gap-3">
|
||||||
<dt className="text-slate-500 dark:text-slate-400">{row.days}</dt>
|
<dt className="text-neutral-500 dark:text-neutral-400">{row.days}</dt>
|
||||||
<dd className="font-medium text-slate-700 dark:text-slate-200">{row.label}</dd>
|
<dd className="font-medium text-neutral-700 dark:text-neutral-200">{row.label}</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-8 max-w-6xl border-t border-slate-200 px-4 pt-6 text-center text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400 sm:px-6">
|
<div className="mx-auto mt-8 max-w-6xl border-t border-neutral-200 px-4 pt-6 text-center text-xs text-neutral-500 dark:border-neutral-800 dark:text-neutral-400 sm:px-6">
|
||||||
<p className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
|
<p className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
|
||||||
<span>© {new Date().getFullYear()} HifiPlanet. Alle Angaben ohne Gewähr.</span>
|
<span>© {new Date().getFullYear()} HifiPlanet. Alle Angaben ohne Gewähr.</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ export default function IconPicker({ value, onChange }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-3 rounded-md border border-slate-300 p-2 dark:border-slate-700">
|
<div className="mb-2 flex items-center gap-3 rounded-md border border-neutral-300 p-2 dark:border-neutral-700">
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
{value ? <DynamicIcon name={value} className="h-6 w-6" /> : <span className="text-xs text-slate-400">?</span>}
|
{value ? <DynamicIcon name={value} className="h-6 w-6" /> : <span className="text-xs text-neutral-400">?</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-100">{value || 'Kein Icon gewählt'}</p>
|
<p className="font-medium text-neutral-800 dark:text-neutral-100">{value || 'Kein Icon gewählt'}</p>
|
||||||
<p className="text-xs text-slate-400">{ALL_ICON_NAMES.length} Icons verfügbar</p>
|
<p className="text-xs text-neutral-400">{ALL_ICON_NAMES.length} Icons verfügbar</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -32,10 +32,10 @@ export default function IconPicker({ value, onChange }) {
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Icon suchen (z. B. car, shield, zap)…"
|
placeholder="Icon suchen (z. B. car, shield, zap)…"
|
||||||
className="mb-2 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
|
className="mb-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid max-h-64 grid-cols-6 gap-1 overflow-y-auto rounded-md border border-slate-200 p-2 sm:grid-cols-8 dark:border-slate-700">
|
<div className="grid max-h-64 grid-cols-6 gap-1 overflow-y-auto rounded-md border border-neutral-200 p-2 sm:grid-cols-8 dark:border-neutral-700">
|
||||||
{results.map((name) => (
|
{results.map((name) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -45,18 +45,18 @@ export default function IconPicker({ value, onChange }) {
|
||||||
className={`flex h-10 w-10 items-center justify-center rounded-md transition ${
|
className={`flex h-10 w-10 items-center justify-center rounded-md transition ${
|
||||||
value === name
|
value === name
|
||||||
? 'bg-brand-500 text-white'
|
? 'bg-brand-500 text-white'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
: 'text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<DynamicIcon name={name} className="h-5 w-5" />
|
<DynamicIcon name={name} className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{results.length === 0 && (
|
{results.length === 0 && (
|
||||||
<p className="col-span-full py-4 text-center text-sm text-slate-400">Keine Icons gefunden.</p>
|
<p className="col-span-full py-4 text-center text-sm text-neutral-400">Keine Icons gefunden.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!search && (
|
{!search && (
|
||||||
<p className="mt-1 text-xs text-slate-400">Tipp: Tippe einen Suchbegriff ein, um alle {ALL_ICON_NAMES.length} Icons zu durchsuchen.</p>
|
<p className="mt-1 text-xs text-neutral-400">Tipp: Tippe einen Suchbegriff ein, um alle {ALL_ICON_NAMES.length} Icons zu durchsuchen.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ export default function ImageUploadField({ value, onChange, label = 'Bild' }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">{label}</label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{value && <img src={value} alt="" className="h-12 w-12 rounded-md object-cover" />}
|
{value && <img src={value} alt="" className="h-12 w-12 rounded-md object-cover" />}
|
||||||
<input type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFile} className="text-sm" />
|
<input type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFile} className="text-sm" />
|
||||||
{uploading && <span className="text-sm text-slate-500">Lädt hoch…</span>}
|
{uploading && <span className="text-sm text-neutral-500">Lädt hoch…</span>}
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
|
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ export default function MaintenanceNotice({ message, fullScreen = false }) {
|
||||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
<WrenchIcon />
|
<WrenchIcon />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Wartungsarbeiten</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Wartungsarbeiten</h1>
|
||||||
<p className="text-slate-600 dark:text-slate-300">
|
<p className="text-neutral-600 dark:text-neutral-300">
|
||||||
{message || 'Dieser Bereich wird gerade aktualisiert. Bitte schau in Kürze wieder vorbei.'}
|
{message || 'Dieser Bereich wird gerade aktualisiert. Bitte schau in Kürze wieder vorbei.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,7 +19,7 @@ export default function MaintenanceNotice({ message, fullScreen = false }) {
|
||||||
|
|
||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4 dark:bg-slate-950">
|
<div className="flex min-h-screen items-center justify-center bg-white px-4 dark:bg-neutral-950">
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Link } from 'react-router-dom';
|
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';
|
||||||
|
|
@ -12,25 +13,41 @@ 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 [menuVisible, setMenuVisible] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { phone, whatsapp } = useSiteSettings();
|
const { phone, whatsapp } = useSiteSettings();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setMenuVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = requestAnimationFrame(() => setMenuVisible(true));
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const flyIn = () => `transition-all duration-500 ease-out ${menuVisible ? 'opacity-100' : 'opacity-0'}`;
|
||||||
|
const flyInStyle = (index) => ({
|
||||||
|
transitionDelay: `${index * 70}ms`,
|
||||||
|
transform: menuVisible ? 'translateY(0)' : 'translateY(-140px)',
|
||||||
|
});
|
||||||
|
|
||||||
const AdminIcon = user && (
|
const AdminIcon = user && (
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
aria-label="Zum Admin-Panel"
|
aria-label="Zum Admin-Panel"
|
||||||
title="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"
|
className="rounded-full p-2 text-neutral-600 transition hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<DynamicIcon name="layout-dashboard" className="h-5 w-5" />
|
<DynamicIcon name="layout-dashboard" className="h-5 w-5" />
|
||||||
</Link>
|
</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-neutral-200 bg-white/90 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/90">
|
||||||
{/* Schmale Kontakt-Leiste (Telefon/WhatsApp/Shop) - auf kleinen Screens ausgeblendet,
|
{/* Schmale Kontakt-Leiste (Telefon/WhatsApp/Shop) - auf kleinen Screens ausgeblendet,
|
||||||
steht dort stattdessen im aufklappbaren mobilen Menü. */}
|
steht dort stattdessen im aufklappbaren mobilen Menü. */}
|
||||||
<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">
|
<div className="hidden border-b border-neutral-100 bg-neutral-50 px-4 py-1.5 text-xs text-neutral-600 dark:border-neutral-900 dark:bg-neutral-900/40 dark:text-neutral-400 sm:px-6 md:block">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-end gap-5">
|
<div className="mx-auto flex max-w-6xl items-center justify-end gap-5">
|
||||||
{phone && (
|
{phone && (
|
||||||
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-1.5 hover:text-brand-500">
|
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-1.5 hover:text-brand-500">
|
||||||
|
|
@ -57,44 +74,67 @@ export default function Navbar() {
|
||||||
</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="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">
|
<div className="flex items-center justify-self-start gap-1">
|
||||||
<nav className="hidden items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300 md:flex">
|
{AdminIcon}
|
||||||
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link>
|
<ThemeToggle />
|
||||||
<Link to="/leistungen" className="hover:text-brand-500">Leistungen</Link>
|
|
||||||
<Link to="/kontakt" className="hover:text-brand-500">Kontakt</Link>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setOpen((o) => !o)}
|
|
||||||
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 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">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to="/" className="flex items-center justify-self-center rounded-lg px-2 py-1 dark:bg-white">
|
<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" />
|
<img src={logo} alt="HifiPlanet" className="h-11 w-auto sm:h-14" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center justify-self-end gap-1">
|
<div className="flex items-center justify-self-end">
|
||||||
{AdminIcon}
|
<nav className="hidden items-center gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300 md:flex">
|
||||||
<ThemeToggle />
|
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link>
|
||||||
|
<Link to="/leistungen" className="hover:text-brand-500">Leistungen</Link>
|
||||||
|
<Link to="/kontakt" className="hover:text-brand-500">Kontakt</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label="Menü öffnen"
|
||||||
|
className="rounded-md p-2 text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-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">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
{open &&
|
||||||
<nav className="flex flex-col gap-1 border-t border-slate-200 px-4 py-3 text-sm font-medium text-slate-600 dark:border-slate-800 dark:text-slate-300 md:hidden">
|
createPortal(
|
||||||
<Link to="/fahrzeuge" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Fahrzeuge</Link>
|
<div
|
||||||
<Link to="/leistungen" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Leistungen</Link>
|
className="fixed inset-0 z-[100] flex flex-col items-end justify-center gap-5 overflow-y-auto px-8 py-24 text-right backdrop-blur-xl md:hidden"
|
||||||
<Link to="/kontakt" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Kontakt</Link>
|
style={{ background: 'linear-gradient(135deg, #f1f5f9 0%, #e3f0cf 50%, #f8fafc 100%)' }}
|
||||||
<div className="my-1 border-t border-slate-200 dark:border-slate-800" />
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-label="Menü schließen"
|
||||||
|
className="absolute right-6 top-6 flex items-center gap-2 text-neutral-600 hover:text-brand-600"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wide">Schließen</span>
|
||||||
|
<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="M6 6l12 12M18 6 6 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link to="/fahrzeuge" onClick={() => setOpen(false)} className={`text-3xl font-extrabold tracking-tight text-neutral-900 hover:text-brand-600 ${flyIn()}`} style={flyInStyle(0)}>
|
||||||
|
Fahrzeuge
|
||||||
|
</Link>
|
||||||
|
<Link to="/leistungen" onClick={() => setOpen(false)} className={`text-3xl font-extrabold tracking-tight text-neutral-900 hover:text-brand-600 ${flyIn()}`} style={flyInStyle(1)}>
|
||||||
|
Leistungen
|
||||||
|
</Link>
|
||||||
|
<Link to="/kontakt" onClick={() => setOpen(false)} className={`text-3xl font-extrabold tracking-tight text-neutral-900 hover:text-brand-600 ${flyIn()}`} style={flyInStyle(2)}>
|
||||||
|
Kontakt
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className={`h-px w-16 bg-brand-500/60 ${flyIn()}`} style={flyInStyle(3)} />
|
||||||
|
|
||||||
{phone && (
|
{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">
|
<a href={`tel:${digitsOnly(phone)}`} className={`flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-neutral-600 hover:text-brand-600 ${flyIn()}`} style={flyInStyle(4)}>
|
||||||
<DynamicIcon name="phone" className="h-4 w-4" />
|
|
||||||
{phone}
|
{phone}
|
||||||
|
<DynamicIcon name="phone" className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{whatsapp && (
|
{whatsapp && (
|
||||||
|
|
@ -102,22 +142,25 @@ export default function Navbar() {
|
||||||
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
|
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
|
className={`flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-neutral-600 hover:text-brand-600 ${flyIn()}`}
|
||||||
|
style={flyInStyle(5)}
|
||||||
>
|
>
|
||||||
<DynamicIcon name="message-circle" className="h-4 w-4" />
|
|
||||||
WhatsApp
|
WhatsApp
|
||||||
|
<DynamicIcon name="message-circle" className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
href={SHOP_URL}
|
href={SHOP_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
|
className={`flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-neutral-600 hover:text-brand-600 ${flyIn()}`}
|
||||||
|
style={flyInStyle(whatsapp ? 6 : 5)}
|
||||||
>
|
>
|
||||||
<DynamicIcon name="shopping-bag" className="h-4 w-4" />
|
|
||||||
Zum Shop
|
Zum Shop
|
||||||
|
<DynamicIcon name="shopping-bag" className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default function ProtectedRoute() {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex min-h-screen items-center justify-center text-slate-500">Lädt…</div>;
|
return <div className="flex min-h-screen items-center justify-center text-neutral-500">Lädt…</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export default function PublicLayout() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status === null || authLoading) {
|
if (status === null || authLoading) {
|
||||||
return <div className="min-h-screen bg-white dark:bg-slate-950" />;
|
return <div className="min-h-screen bg-white dark:bg-neutral-950" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bypass = hasPermission('maintenance.bypass');
|
const bypass = hasPermission('maintenance.bypass');
|
||||||
|
|
@ -55,7 +55,7 @@ export default function PublicLayout() {
|
||||||
return (
|
return (
|
||||||
<MaintenanceProvider value={{ ...status, bypass }}>
|
<MaintenanceProvider value={{ ...status, bypass }}>
|
||||||
<SiteSettingsProvider value={siteSettings}>
|
<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-neutral-900 dark:bg-neutral-950 dark:text-neutral-100">
|
||||||
{bypass && status.global.enabled && <MaintenanceBypassBanner />}
|
{bypass && status.global.enabled && <MaintenanceBypassBanner />}
|
||||||
<ScrollProgress />
|
<ScrollProgress />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export default function RequirePermission({ permission, children }) {
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-8 text-center dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h1 className="mb-2 text-lg font-bold text-slate-900 dark:text-white">Keine Berechtigung</h1>
|
<h1 className="mb-2 text-lg font-bold text-neutral-900 dark:text-white">Keine Berechtigung</h1>
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Du hast keinen Zugriff auf diesen Bereich. Bitte wende dich an einen Administrator, falls du hier
|
Du hast keinen Zugriff auf diesen Bereich. Bitte wende dich an einen Administrator, falls du hier
|
||||||
Zugriff brauchst.
|
Zugriff brauchst.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function SetupGate({ children }) {
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
if (state.pathname !== location.pathname || state.needsSetup === null) {
|
if (state.pathname !== location.pathname || state.needsSetup === null) {
|
||||||
return <div className="min-h-screen bg-white dark:bg-slate-950" />;
|
return <div className="min-h-screen bg-white dark:bg-neutral-950" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.needsSetup && location.pathname !== '/setup') {
|
if (state.needsSetup && location.pathname !== '/setup') {
|
||||||
|
|
|
||||||
|
|
@ -123,10 +123,10 @@ export default function TestimonialSlider({ testimonials }) {
|
||||||
<div ref={trackRef} className="marquee-track flex w-max gap-5">
|
<div ref={trackRef} className="marquee-track flex w-max gap-5">
|
||||||
{track.map((t, i) => (
|
{track.map((t, i) => (
|
||||||
<div key={`${t.name}-${i}`} className="w-64 shrink-0 sm:w-72 lg:w-80">
|
<div key={`${t.name}-${i}`} className="w-64 shrink-0 sm:w-72 lg:w-80">
|
||||||
<div className="flex h-full flex-col rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<div className="flex h-full flex-col rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<StarRating className="mb-3 h-4 w-4" />
|
<StarRating className="mb-3 h-4 w-4" />
|
||||||
<p className="mb-4 flex-1 text-sm text-slate-600 dark:text-slate-300">„{t.text}"</p>
|
<p className="mb-4 flex-1 text-sm text-neutral-600 dark:text-neutral-300">„{t.text}"</p>
|
||||||
<p className="text-sm font-semibold text-slate-900 dark:text-white">{t.name}</p>
|
<p className="text-sm font-semibold text-neutral-900 dark:text-white">{t.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default function ThemeToggle() {
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
aria-label="Theme umschalten"
|
aria-label="Theme umschalten"
|
||||||
className="rounded-full p-2 text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 transition"
|
className="rounded-full p-2 text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800 transition"
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? (
|
{theme === 'dark' ? (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
|
||||||
|
|
|
||||||
|
|
@ -119,20 +119,20 @@ export default function AccountSettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Mein Konto</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Mein Konto</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Angemeldet als <strong>{user?.username}</strong>{user?.is_super_admin ? ' (Super-Admin)' : ''}.
|
Angemeldet als <strong>{user?.username}</strong>{user?.is_super_admin ? ' (Super-Admin)' : ''}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mb-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<div className="mb-6 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-slate-900 dark:text-white">Passwort ändern</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Passwort ändern</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Vergib ein neues Passwort für dein Konto.
|
Vergib ein neues Passwort für dein Konto.
|
||||||
{user?.two_factor_enabled && ' Da 2FA aktiv ist, musst du die Änderung zusätzlich mit deinem Code bestätigen.'}
|
{user?.two_factor_enabled && ' Da 2FA aktiv ist, musst du die Änderung zusätzlich mit deinem Code bestätigen.'}
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={handleChangePassword}>
|
<form onSubmit={handleChangePassword}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Aktuelles Passwort
|
Aktuelles Passwort
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -140,11 +140,11 @@ export default function AccountSettings() {
|
||||||
value={passwordForm.current_password}
|
value={passwordForm.current_password}
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, current_password: e.target.value })}
|
onChange={(e) => setPasswordForm({ ...passwordForm, current_password: e.target.value })}
|
||||||
required
|
required
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Neues Passwort
|
Neues Passwort
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -153,11 +153,11 @@ export default function AccountSettings() {
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
|
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Neues Passwort wiederholen
|
Neues Passwort wiederholen
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -166,12 +166,12 @@ export default function AccountSettings() {
|
||||||
onChange={(e) => setPasswordForm({ ...passwordForm, new_password_confirmation: e.target.value })}
|
onChange={(e) => setPasswordForm({ ...passwordForm, new_password_confirmation: e.target.value })}
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{user?.two_factor_enabled && (
|
{user?.two_factor_enabled && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
2FA-Code zur Bestätigung
|
2FA-Code zur Bestätigung
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -180,7 +180,7 @@ export default function AccountSettings() {
|
||||||
required
|
required
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
placeholder="123456 oder Wiederherstellungscode"
|
placeholder="123456 oder Wiederherstellungscode"
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -196,9 +196,9 @@ export default function AccountSettings() {
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<div 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-slate-900 dark:text-white">Zwei-Faktor-Authentifizierung</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Zwei-Faktor-Authentifizierung</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Schützt dein Konto zusätzlich mit einem Code aus der Google Authenticator App.
|
Schützt dein Konto zusätzlich mit einem Code aus der Google Authenticator App.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -209,14 +209,14 @@ export default function AccountSettings() {
|
||||||
{user?.two_factor_enabled ? (
|
{user?.two_factor_enabled ? (
|
||||||
<span className="font-semibold text-green-600 dark:text-green-400">Aktiv</span>
|
<span className="font-semibold text-green-600 dark:text-green-400">Aktiv</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="font-semibold text-slate-500 dark:text-slate-400">Nicht aktiv</span>
|
<span className="font-semibold text-neutral-500 dark:text-neutral-400">Nicht aktiv</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{user?.two_factor_enabled ? (
|
{user?.two_factor_enabled ? (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setPhase('regenerate'); setError(''); }}
|
onClick={() => { setPhase('regenerate'); setError(''); }}
|
||||||
className="rounded-md border border-slate-300 px-4 py-2 text-sm font-medium hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800"
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
Neue Wiederherstellungscodes erzeugen
|
Neue Wiederherstellungscodes erzeugen
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -241,19 +241,19 @@ export default function AccountSettings() {
|
||||||
|
|
||||||
{phase === 'setup' && (
|
{phase === 'setup' && (
|
||||||
<form onSubmit={confirmSetup}>
|
<form onSubmit={confirmSetup}>
|
||||||
<ol className="mb-4 list-decimal space-y-2 pl-5 text-sm text-slate-700 dark:text-slate-300">
|
<ol className="mb-4 list-decimal space-y-2 pl-5 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<li>Öffne die Google Authenticator App (oder eine kompatible App).</li>
|
<li>Öffne die Google Authenticator App (oder eine kompatible App).</li>
|
||||||
<li>Scanne den folgenden QR-Code:</li>
|
<li>Scanne den folgenden QR-Code:</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div className="mb-4 flex justify-center">
|
<div className="mb-4 flex justify-center">
|
||||||
{qrDataUrl && <img src={qrDataUrl} alt="QR-Code für Zwei-Faktor-Authentifizierung" className="h-48 w-48 rounded-lg border border-slate-200 dark:border-slate-700" />}
|
{qrDataUrl && <img src={qrDataUrl} alt="QR-Code für Zwei-Faktor-Authentifizierung" className="h-48 w-48 rounded-lg border border-neutral-200 dark:border-neutral-700" />}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-center text-xs text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-center text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
Geht das Scannen nicht? Gib diesen Code manuell ein: <br />
|
Geht das Scannen nicht? Gib diesen Code manuell ein: <br />
|
||||||
<code className="mt-1 inline-block rounded bg-slate-100 px-2 py-1 font-mono tracking-wider dark:bg-slate-800">{secret}</code>
|
<code className="mt-1 inline-block rounded bg-neutral-100 px-2 py-1 font-mono tracking-wider dark:bg-neutral-800">{secret}</code>
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Bestätigungscode aus der App
|
Bestätigungscode aus der App
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -263,7 +263,7 @@ export default function AccountSettings() {
|
||||||
autoFocus
|
autoFocus
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-center text-lg tracking-widest dark:border-slate-700 dark:bg-slate-900"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-center text-lg tracking-widest dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
||||||
|
|
@ -271,7 +271,7 @@ export default function AccountSettings() {
|
||||||
<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-60">
|
<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-60">
|
||||||
Bestätigen & aktivieren
|
Bestätigen & aktivieren
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setPhase('idle')} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={() => setPhase('idle')} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -280,14 +280,14 @@ export default function AccountSettings() {
|
||||||
|
|
||||||
{phase === 'recovery-codes' && (
|
{phase === 'recovery-codes' && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-3 text-sm font-semibold text-slate-900 dark:text-white">
|
<p className="mb-3 text-sm font-semibold text-neutral-900 dark:text-white">
|
||||||
Wiederherstellungscodes – jetzt sicher speichern!
|
Wiederherstellungscodes – jetzt sicher speichern!
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-3 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-3 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Jeder Code funktioniert einmalig, falls du keinen Zugriff mehr auf die Authenticator-App hast.
|
Jeder Code funktioniert einmalig, falls du keinen Zugriff mehr auf die Authenticator-App hast.
|
||||||
Diese Codes werden nur jetzt angezeigt.
|
Diese Codes werden nur jetzt angezeigt.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4 grid grid-cols-2 gap-2 rounded-md bg-slate-100 p-4 font-mono text-sm dark:bg-slate-800">
|
<div className="mb-4 grid grid-cols-2 gap-2 rounded-md bg-neutral-100 p-4 font-mono text-sm dark:bg-neutral-800">
|
||||||
{recoveryCodes.map((c) => <span key={c}>{c}</span>)}
|
{recoveryCodes.map((c) => <span key={c}>{c}</span>)}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={finishRecoveryCodes} className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600">
|
<button onClick={finishRecoveryCodes} className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600">
|
||||||
|
|
@ -299,7 +299,7 @@ export default function AccountSettings() {
|
||||||
{phase === 'disable' && (
|
{phase === 'disable' && (
|
||||||
<form onSubmit={handleDisable}>
|
<form onSubmit={handleDisable}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Passwort zur Bestätigung
|
Passwort zur Bestätigung
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -308,7 +308,7 @@ export default function AccountSettings() {
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
||||||
|
|
@ -316,7 +316,7 @@ export default function AccountSettings() {
|
||||||
<button type="submit" disabled={busy} className="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-60">
|
<button type="submit" disabled={busy} className="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-60">
|
||||||
2FA jetzt deaktivieren
|
2FA jetzt deaktivieren
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => { setPhase('idle'); setPassword(''); }} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={() => { setPhase('idle'); setPassword(''); }} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -325,11 +325,11 @@ export default function AccountSettings() {
|
||||||
|
|
||||||
{phase === 'regenerate' && (
|
{phase === 'regenerate' && (
|
||||||
<form onSubmit={handleRegenerate}>
|
<form onSubmit={handleRegenerate}>
|
||||||
<p className="mb-3 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-3 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Die bisherigen Wiederherstellungscodes werden ungültig.
|
Die bisherigen Wiederherstellungscodes werden ungültig.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Passwort zur Bestätigung
|
Passwort zur Bestätigung
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -338,7 +338,7 @@ export default function AccountSettings() {
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
||||||
|
|
@ -346,7 +346,7 @@ export default function AccountSettings() {
|
||||||
<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-60">
|
<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-60">
|
||||||
Neue Codes erzeugen
|
Neue Codes erzeugen
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => { setPhase('idle'); setPassword(''); }} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={() => { setPhase('idle'); setPassword(''); }} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,13 @@ export default function AdminUsers() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Benutzer</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Benutzer</h1>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Jeder Mitarbeiter bekommt ein eigenes Konto mit genau den Rechten, die er braucht.
|
Jeder Mitarbeiter bekommt ein eigenes Konto mit genau den Rechten, die er braucht.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Benutzername</th>
|
<th className="px-4 py-2">Benutzername</th>
|
||||||
<th className="px-4 py-2">Rolle</th>
|
<th className="px-4 py-2">Rolle</th>
|
||||||
|
|
@ -100,12 +100,12 @@ export default function AdminUsers() {
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{users.map((u) => (
|
{users.map((u) => (
|
||||||
<tr key={u.id} className="bg-white dark:bg-slate-950">
|
<tr key={u.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">
|
||||||
{u.username}
|
{u.username}
|
||||||
{u.id === currentUser?.id && <span className="ml-1 text-xs text-slate-400">(du)</span>}
|
{u.id === currentUser?.id && <span className="ml-1 text-xs text-neutral-400">(du)</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
{u.is_super_admin ? (
|
{u.is_super_admin ? (
|
||||||
|
|
@ -113,13 +113,13 @@ export default function AdminUsers() {
|
||||||
Super-Admin
|
Super-Admin
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-500 dark:text-slate-400">{u.permissions.length} Einzelrechte</span>
|
<span className="text-neutral-500 dark:text-neutral-400">{u.permissions.length} Einzelrechte</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{u.groups.map((g) => (
|
{u.groups.map((g) => (
|
||||||
<span key={g.id} className="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">
|
<span key={g.id} className="rounded-full bg-neutral-100 px-2 py-0.5 text-xs text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
|
||||||
{g.name}
|
{g.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
@ -135,26 +135,26 @@ export default function AdminUsers() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{users.length === 0 && (
|
{users.length === 0 && (
|
||||||
<tr><td colSpan={4} className="px-4 py-6 text-center text-slate-400">Noch keine Benutzer angelegt.</td></tr>
|
<tr><td colSpan={4} className="px-4 py-6 text-center text-neutral-400">Noch keine Benutzer angelegt.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Benutzername *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Benutzername *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Passwort {editingId ? '(leer lassen = unverändert)' : '*'}
|
Passwort {editingId ? '(leer lassen = unverändert)' : '*'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -162,17 +162,17 @@ export default function AdminUsers() {
|
||||||
required={!editingId}
|
required={!editingId}
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentUser?.is_super_admin && (
|
{currentUser?.is_super_admin && (
|
||||||
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={form.is_super_admin}
|
checked={form.is_super_admin}
|
||||||
onChange={(e) => setForm({ ...form, is_super_admin: e.target.checked })}
|
onChange={(e) => setForm({ ...form, is_super_admin: e.target.checked })}
|
||||||
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
Super-Admin (hat automatisch alle Rechte)
|
Super-Admin (hat automatisch alle Rechte)
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -181,15 +181,15 @@ export default function AdminUsers() {
|
||||||
{!form.is_super_admin && (
|
{!form.is_super_admin && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Einzelne Berechtigungen</p>
|
<p className="mb-1 text-sm font-medium text-neutral-700 dark:text-neutral-300">Einzelne Berechtigungen</p>
|
||||||
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
|
<div className="space-y-1.5 rounded-md border border-neutral-200 p-3 dark:border-neutral-700">
|
||||||
{catalog.map((p) => (
|
{catalog.map((p) => (
|
||||||
<label key={p.key} className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label key={p.key} className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={form.permissions.includes(p.key)}
|
checked={form.permissions.includes(p.key)}
|
||||||
onChange={() => togglePermission(p.key)}
|
onChange={() => togglePermission(p.key)}
|
||||||
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
{p.label}
|
{p.label}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -198,20 +198,20 @@ export default function AdminUsers() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Berechtigungsgruppen</p>
|
<p className="mb-1 text-sm font-medium text-neutral-700 dark:text-neutral-300">Berechtigungsgruppen</p>
|
||||||
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
|
<div className="space-y-1.5 rounded-md border border-neutral-200 p-3 dark:border-neutral-700">
|
||||||
{groups.map((g) => (
|
{groups.map((g) => (
|
||||||
<label key={g.id} className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label key={g.id} className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={form.group_ids.includes(g.id)}
|
checked={form.group_ids.includes(g.id)}
|
||||||
onChange={() => toggleGroup(g.id)}
|
onChange={() => toggleGroup(g.id)}
|
||||||
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
{g.name}
|
{g.name}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{groups.length === 0 && <p className="text-sm text-slate-400">Noch keine Berechtigungsgruppen angelegt.</p>}
|
{groups.length === 0 && <p className="text-sm text-neutral-400">Noch keine Berechtigungsgruppen angelegt.</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -224,7 +224,7 @@ export default function AdminUsers() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -51,20 +51,20 @@ export default function Brands() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">Marken</h1>
|
<h1 className="mb-4 text-xl font-bold text-neutral-900 dark:text-white">Marken</h1>
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Name</th>
|
<th className="px-4 py-2">Name</th>
|
||||||
<th className="px-4 py-2">Sortierung</th>
|
<th className="px-4 py-2">Sortierung</th>
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{brands.map((brand) => (
|
{brands.map((brand) => (
|
||||||
<tr key={brand.id} className="bg-white dark:bg-slate-950">
|
<tr key={brand.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">
|
||||||
<Link to={`/marken/${brand.slug}`} target="_blank" className="hover:text-brand-500">
|
<Link to={`/marken/${brand.slug}`} target="_blank" className="hover:text-brand-500">
|
||||||
{brand.name}
|
{brand.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -77,31 +77,31 @@ export default function Brands() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{brands.length === 0 && (
|
{brands.length === 0 && (
|
||||||
<tr><td colSpan={3} className="px-4 py-6 text-center text-slate-400">Noch keine Marken angelegt.</td></tr>
|
<tr><td colSpan={3} className="px-4 py-6 text-center text-neutral-400">Noch keine Marken angelegt.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Marke bearbeiten' : 'Neue Marke'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Marke bearbeiten' : 'Neue Marke'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Sortierung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Sortierung</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sort_order}
|
value={form.sort_order}
|
||||||
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ export default function Brands() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,8 @@ export default function ContactRequests() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Kontaktanfragen</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Kontaktanfragen</h1>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Alle Anfragen deiner Kunden. Wähle oben einen Reiter, um nur bestimmte Anfragen zu sehen.
|
Alle Anfragen deiner Kunden. Wähle oben einen Reiter, um nur bestimmte Anfragen zu sehen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ export default function ContactRequests() {
|
||||||
className={`rounded-full px-4 py-1.5 text-sm font-medium transition ${
|
className={`rounded-full px-4 py-1.5 text-sm font-medium transition ${
|
||||||
filter === tab.value
|
filter === tab.value
|
||||||
? 'bg-brand-500 text-white'
|
? 'bg-brand-500 text-white'
|
||||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'
|
: 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label} ({counts[tab.value]})
|
{tab.label} ({counts[tab.value]})
|
||||||
|
|
@ -84,14 +84,14 @@ export default function ContactRequests() {
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{filtered.map((r) => (
|
{filtered.map((r) => (
|
||||||
<div key={r.id} className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
<div key={r.id} className="rounded-xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<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-neutral-800 dark:text-neutral-100">{r.name} · {r.email}{r.phone ? ` · ${r.phone}` : ''}</p>
|
||||||
{r.vin && (
|
{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-neutral-500 dark:text-neutral-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-neutral-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">
|
||||||
<select
|
<select
|
||||||
|
|
@ -108,7 +108,7 @@ export default function ContactRequests() {
|
||||||
onClick={() => handleDelete(r.id)}
|
onClick={() => handleDelete(r.id)}
|
||||||
aria-label="Anfrage löschen"
|
aria-label="Anfrage löschen"
|
||||||
title="Anfrage löschen"
|
title="Anfrage löschen"
|
||||||
className="rounded-md p-1.5 text-slate-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
className="rounded-md p-1.5 text-neutral-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -117,7 +117,7 @@ export default function ContactRequests() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(r.brand_name || r.model_name || r.package_name || r.product_name) && (
|
{(r.brand_name || r.model_name || r.package_name || r.product_name) && (
|
||||||
<p className="mb-2 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
{[r.brand_name, r.model_name, r.package_name, r.product_name].filter(Boolean).join(' · ')}
|
{[r.brand_name, r.model_name, r.package_name, r.product_name].filter(Boolean).join(' · ')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -133,12 +133,12 @@ export default function ContactRequests() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{r.message && <p className="text-sm text-slate-700 dark:text-slate-300">{r.message}</p>}
|
{r.message && <p className="text-sm text-neutral-700 dark:text-neutral-300">{r.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<p className="text-slate-400">
|
<p className="text-neutral-400">
|
||||||
{filter === 'all' ? 'Noch keine Kontaktanfragen eingegangen.' : 'Keine Anfragen mit diesem Status.'}
|
{filter === 'all' ? 'Noch keine Kontaktanfragen eingegangen.' : 'Keine Anfragen mit diesem Status.'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@ export default function Dashboard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Willkommen, {user?.username}!</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Willkommen, {user?.username}!</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Hier siehst du auf einen Blick, wie viel in deinem Shop hinterlegt ist. Klicke auf eine Kachel, um dorthin zu gelangen.
|
Hier siehst du auf einen Blick, wie viel in deinem Shop hinterlegt ist. Klicke auf eine Kachel, um dorthin zu gelangen.
|
||||||
</p>
|
</p>
|
||||||
{cards.length === 0 ? (
|
{cards.length === 0 ? (
|
||||||
<p className="text-slate-500 dark:text-slate-400">
|
<p className="text-neutral-500 dark:text-neutral-400">
|
||||||
Für deinen Account sind noch keine Bereiche freigeschaltet. Bitte wende dich an einen Administrator.
|
Für deinen Account sind noch keine Bereiche freigeschaltet. Bitte wende dich an einen Administrator.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -57,15 +57,15 @@ export default function Dashboard() {
|
||||||
className={`rounded-xl border p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md ${
|
className={`rounded-xl border p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md ${
|
||||||
card.highlight && card.value > 0
|
card.highlight && card.value > 0
|
||||||
? 'border-brand-300 bg-brand-50 dark:border-brand-900 dark:bg-brand-900/20'
|
? 'border-brand-300 bg-brand-50 dark:border-brand-900 dark:bg-brand-900/20'
|
||||||
: 'border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'
|
: 'border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
<Icon path={ICONS[card.icon]} />
|
<Icon path={ICONS[card.icon]} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-200">{card.label}</p>
|
<p className="text-sm font-semibold text-neutral-700 dark:text-neutral-200">{card.label}</p>
|
||||||
<p className="mt-1 text-3xl font-extrabold text-brand-600 dark:text-brand-400">{card.value ?? '…'}</p>
|
<p className="mt-1 text-3xl font-extrabold text-brand-600 dark:text-brand-400">{card.value ?? '…'}</p>
|
||||||
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">{card.hint}</p>
|
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">{card.hint}</p>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,30 +50,30 @@ export default function Login() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-slate-50 px-4 dark:bg-slate-950">
|
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-neutral-50 px-4 dark:bg-neutral-950">
|
||||||
<img src={logo} alt="HifiPlanet" className="h-16 w-auto sm:h-20" />
|
<img src={logo} alt="HifiPlanet" className="h-16 w-auto sm:h-20" />
|
||||||
{step === 'credentials' ? (
|
{step === 'credentials' ? (
|
||||||
<form onSubmit={handleCredentials} className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleCredentials} className="w-full max-w-sm rounded-xl border border-neutral-200 bg-white p-8 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h1 className="mb-6 text-xl font-bold text-slate-900 dark:text-white">Admin-Login</h1>
|
<h1 className="mb-6 text-xl font-bold text-neutral-900 dark:text-white">Admin-Login</h1>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Benutzername</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Benutzername</label>
|
||||||
<input
|
<input
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Passwort</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Passwort</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,15 +88,15 @@ export default function Login() {
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleCode} className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleCode} className="w-full max-w-sm rounded-xl border border-neutral-200 bg-white p-8 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Bestätigungscode</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Bestätigungscode</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Öffne die Google Authenticator App und gib den 6-stelligen Code ein. Alternativ kannst du auch
|
Öffne die Google Authenticator App und gib den 6-stelligen Code ein. Alternativ kannst du auch
|
||||||
einen deiner Wiederherstellungscodes verwenden.
|
einen deiner Wiederherstellungscodes verwenden.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Code</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Code</label>
|
||||||
<input
|
<input
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
|
@ -104,7 +104,7 @@ export default function Login() {
|
||||||
autoFocus
|
autoFocus
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-center text-lg tracking-widest focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-center text-lg tracking-widest focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ export default function Login() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setStep('credentials'); setCode(''); setError(''); }}
|
onClick={() => { setStep('credentials'); setCode(''); setError(''); }}
|
||||||
className="mt-3 w-full text-sm text-slate-500 hover:underline dark:text-slate-400"
|
className="mt-3 w-full text-sm text-neutral-500 hover:underline dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
Zurück zum Login
|
Zurück zum Login
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,14 @@ export default function Models() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">Modelle</h1>
|
<h1 className="mb-4 text-xl font-bold text-neutral-900 dark:text-white">Modelle</h1>
|
||||||
|
|
||||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||||
<label className="text-sm font-medium text-slate-600 dark:text-slate-300">Nach Marke filtern:</label>
|
<label className="text-sm font-medium text-neutral-600 dark:text-neutral-300">Nach Marke filtern:</label>
|
||||||
<select
|
<select
|
||||||
value={brandFilter}
|
value={brandFilter}
|
||||||
onChange={(e) => setBrandFilter(e.target.value)}
|
onChange={(e) => setBrandFilter(e.target.value)}
|
||||||
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-900"
|
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="all">Alle Marken</option>
|
<option value="all">Alle Marken</option>
|
||||||
{brands.map((b) => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
{brands.map((b) => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
||||||
|
|
@ -76,23 +76,23 @@ export default function Models() {
|
||||||
Filter zurücksetzen
|
Filter zurücksetzen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-slate-400">{filteredModels.length} von {models.length}</span>
|
<span className="text-sm text-neutral-400">{filteredModels.length} von {models.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Marke</th>
|
<th className="px-4 py-2">Marke</th>
|
||||||
<th className="px-4 py-2">Modell</th>
|
<th className="px-4 py-2">Modell</th>
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) => (
|
||||||
<tr key={model.id} className="bg-white dark:bg-slate-950">
|
<tr key={model.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">{model.brand_name}</td>
|
<td className="px-4 py-2">{model.brand_name}</td>
|
||||||
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">{model.name}</td>
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">{model.name}</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
<button onClick={() => startEdit(model)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
<button onClick={() => startEdit(model)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
||||||
<button onClick={() => handleDelete(model.id)} className="text-red-600 hover:underline">Löschen</button>
|
<button onClick={() => handleDelete(model.id)} className="text-red-600 hover:underline">Löschen</button>
|
||||||
|
|
@ -100,7 +100,7 @@ export default function Models() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{filteredModels.length === 0 && (
|
{filteredModels.length === 0 && (
|
||||||
<tr><td colSpan={3} className="px-4 py-6 text-center text-slate-400">
|
<tr><td colSpan={3} className="px-4 py-6 text-center text-neutral-400">
|
||||||
{models.length === 0 ? 'Noch keine Modelle angelegt.' : 'Keine Modelle für diese Marke.'}
|
{models.length === 0 ? 'Noch keine Modelle angelegt.' : 'Keine Modelle für diese Marke.'}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
|
|
@ -109,36 +109,36 @@ export default function Models() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Modell bearbeiten' : 'Neues Modell'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Modell bearbeiten' : 'Neues Modell'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Marke *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Marke *</label>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
value={form.brand_id}
|
value={form.brand_id}
|
||||||
onChange={(e) => setForm({ ...form, brand_id: e.target.value })}
|
onChange={(e) => setForm({ ...form, brand_id: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="">Bitte wählen…</option>
|
<option value="">Bitte wählen…</option>
|
||||||
{brands.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
|
{brands.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Modellname *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Modellname *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Sortierung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Sortierung</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sort_order}
|
value={form.sort_order}
|
||||||
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ export default function Models() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -62,26 +62,26 @@ export default function PackageProducts() {
|
||||||
<p className="mb-2 text-sm">
|
<p className="mb-2 text-sm">
|
||||||
<Link to="/admin/packages" className="text-brand-600 hover:underline">← Zurück zu den Paketen</Link>
|
<Link to="/admin/packages" className="text-brand-600 hover:underline">← Zurück zu den Paketen</Link>
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">Produkte im Paket</h1>
|
<h1 className="mb-4 text-xl font-bold text-neutral-900 dark:text-white">Produkte im Paket</h1>
|
||||||
|
|
||||||
<form onSubmit={handleAdd} className="mb-6 flex flex-wrap items-end gap-3 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleAdd} className="mb-6 flex flex-wrap items-end gap-3 rounded-xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="flex-1 min-w-[240px]">
|
<div className="flex-1 min-w-[240px]">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">audio4cars.de Produkt-URL *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">audio4cars.de Produkt-URL *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="https://www.audio4cars.de/produkt/..."
|
placeholder="https://www.audio4cars.de/produkt/..."
|
||||||
value={sourceUrl}
|
value={sourceUrl}
|
||||||
onChange={(e) => setSourceUrl(e.target.value)}
|
onChange={(e) => setSourceUrl(e.target.value)}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-56">
|
<div className="w-56">
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name überschreiben (optional)</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name überschreiben (optional)</label>
|
||||||
<input
|
<input
|
||||||
value={nameOverride}
|
value={nameOverride}
|
||||||
onChange={(e) => setNameOverride(e.target.value)}
|
onChange={(e) => setNameOverride(e.target.value)}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button disabled={busy} type="submit" className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600 disabled:opacity-60">
|
<button disabled={busy} type="submit" className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600 disabled:opacity-60">
|
||||||
|
|
@ -91,9 +91,9 @@ export default function PackageProducts() {
|
||||||
|
|
||||||
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Produkt</th>
|
<th className="px-4 py-2">Produkt</th>
|
||||||
<th className="px-4 py-2">Preis</th>
|
<th className="px-4 py-2">Preis</th>
|
||||||
|
|
@ -102,11 +102,11 @@ export default function PackageProducts() {
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{products.map((p) => (
|
{products.map((p) => (
|
||||||
<tr key={p.id} className="bg-white dark:bg-slate-950">
|
<tr key={p.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<a href={p.source_url} target="_blank" rel="noreferrer" className="font-medium text-slate-800 hover:underline dark:text-slate-100">
|
<a href={p.source_url} target="_blank" rel="noreferrer" className="font-medium text-neutral-800 hover:underline dark:text-neutral-100">
|
||||||
{p.name_override || p.scraped_name || p.source_url}
|
{p.name_override || p.scraped_name || p.source_url}
|
||||||
</a>
|
</a>
|
||||||
{p.scrape_status === 'error' && <p className="text-xs text-red-500">{p.scrape_error}</p>}
|
{p.scrape_status === 'error' && <p className="text-xs text-red-500">{p.scrape_error}</p>}
|
||||||
|
|
@ -117,7 +117,7 @@ export default function PackageProducts() {
|
||||||
{p.scrape_status}
|
{p.scrape_status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-500 dark:text-slate-400">
|
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">
|
||||||
{p.price_updated_at ? new Date(p.price_updated_at.replace(' ', 'T')).toLocaleString('de-DE') : '–'}
|
{p.price_updated_at ? new Date(p.price_updated_at.replace(' ', 'T')).toLocaleString('de-DE') : '–'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
|
|
@ -129,7 +129,7 @@ export default function PackageProducts() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{products.length === 0 && (
|
{products.length === 0 && (
|
||||||
<tr><td colSpan={5} className="px-4 py-6 text-center text-slate-400">Noch keine Produkte im Paket.</td></tr>
|
<tr><td colSpan={5} className="px-4 py-6 text-center text-neutral-400">Noch keine Produkte im Paket.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -59,25 +59,25 @@ export default function PackageUpgrades() {
|
||||||
<p className="mb-2 text-sm">
|
<p className="mb-2 text-sm">
|
||||||
<Link to="/admin/packages" className="text-brand-600 hover:underline">← Zurück zu den Paketen</Link>
|
<Link to="/admin/packages" className="text-brand-600 hover:underline">← Zurück zu den Paketen</Link>
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Optionale Upgrades</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Optionale Upgrades</h1>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Diese Upgrades erscheinen nicht in der öffentlichen Paketübersicht, sondern erst als Auswahl im Kontaktformular.
|
Diese Upgrades erscheinen nicht in der öffentlichen Paketübersicht, sondern erst als Auswahl im Kontaktformular.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Name</th>
|
<th className="px-4 py-2">Name</th>
|
||||||
<th className="px-4 py-2">Preis</th>
|
<th className="px-4 py-2">Preis</th>
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{upgrades.map((u) => (
|
{upgrades.map((u) => (
|
||||||
<tr key={u.id} className="bg-white dark:bg-slate-950">
|
<tr key={u.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-100">{u.name}</p>
|
<p className="font-medium text-neutral-800 dark:text-neutral-100">{u.name}</p>
|
||||||
{u.description && <p className="text-xs text-slate-500 dark:text-slate-400">{u.description}</p>}
|
{u.description && <p className="text-xs text-neutral-500 dark:text-neutral-400">{u.description}</p>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">{formatPrice(u.price)}</td>
|
<td className="px-4 py-2">{formatPrice(u.price)}</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
|
|
@ -87,35 +87,35 @@ export default function PackageUpgrades() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{upgrades.length === 0 && (
|
{upgrades.length === 0 && (
|
||||||
<tr><td colSpan={3} className="px-4 py-6 text-center text-slate-400">Noch keine Upgrades hinterlegt.</td></tr>
|
<tr><td colSpan={3} className="px-4 py-6 text-center text-neutral-400">Noch keine Upgrades hinterlegt.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Upgrade bearbeiten' : 'Neues Upgrade'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Upgrade bearbeiten' : 'Neues Upgrade'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Beschreibung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Beschreibung</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Preis (€) *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Preis (€) *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -123,16 +123,16 @@ export default function PackageUpgrades() {
|
||||||
min="0"
|
min="0"
|
||||||
value={form.price}
|
value={form.price}
|
||||||
onChange={(e) => setForm({ ...form, price: e.target.value })}
|
onChange={(e) => setForm({ ...form, price: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Sortierung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Sortierung</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sort_order}
|
value={form.sort_order}
|
||||||
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ export default function PackageUpgrades() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -97,14 +97,14 @@ export default function Packages() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">Pakete</h1>
|
<h1 className="mb-4 text-xl font-bold text-neutral-900 dark:text-white">Pakete</h1>
|
||||||
|
|
||||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||||
<label className="text-sm font-medium text-slate-600 dark:text-slate-300">Filtern:</label>
|
<label className="text-sm font-medium text-neutral-600 dark:text-neutral-300">Filtern:</label>
|
||||||
<select
|
<select
|
||||||
value={brandFilter}
|
value={brandFilter}
|
||||||
onChange={(e) => handleBrandFilterChange(e.target.value)}
|
onChange={(e) => handleBrandFilterChange(e.target.value)}
|
||||||
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-900"
|
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="all">Alle Marken</option>
|
<option value="all">Alle Marken</option>
|
||||||
{brandOptions.map((b) => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
{brandOptions.map((b) => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
||||||
|
|
@ -112,7 +112,7 @@ export default function Packages() {
|
||||||
<select
|
<select
|
||||||
value={modelFilter}
|
value={modelFilter}
|
||||||
onChange={(e) => setModelFilter(e.target.value)}
|
onChange={(e) => setModelFilter(e.target.value)}
|
||||||
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-900"
|
className="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="all">Alle Modelle</option>
|
<option value="all">Alle Modelle</option>
|
||||||
{modelOptions.map((m) => <option key={m.id} value={String(m.id)}>{m.brand_name} {m.name}</option>)}
|
{modelOptions.map((m) => <option key={m.id} value={String(m.id)}>{m.brand_name} {m.name}</option>)}
|
||||||
|
|
@ -122,12 +122,12 @@ export default function Packages() {
|
||||||
Filter zurücksetzen
|
Filter zurücksetzen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-slate-400">{filteredPackages.length} von {packages.length}</span>
|
<span className="text-sm text-neutral-400">{filteredPackages.length} von {packages.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Marke / Modell</th>
|
<th className="px-4 py-2">Marke / Modell</th>
|
||||||
<th className="px-4 py-2">Paket</th>
|
<th className="px-4 py-2">Paket</th>
|
||||||
|
|
@ -135,12 +135,12 @@ export default function Packages() {
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{filteredPackages.map((pkg) => (
|
{filteredPackages.map((pkg) => (
|
||||||
<tr key={pkg.id} className="bg-white dark:bg-slate-950">
|
<tr key={pkg.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">{pkg.brand_name} {pkg.model_name}</td>
|
<td className="px-4 py-2">{pkg.brand_name} {pkg.model_name}</td>
|
||||||
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">{pkg.name}</td>
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">{pkg.name}</td>
|
||||||
<td className="px-4 py-2 text-slate-500 dark:text-slate-400">
|
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">
|
||||||
{pkg.markup_type === 'fixed' && `+${pkg.markup_value} €`}
|
{pkg.markup_type === 'fixed' && `+${pkg.markup_value} €`}
|
||||||
{pkg.markup_type === 'percent' && `+${pkg.markup_value} %`}
|
{pkg.markup_type === 'percent' && `+${pkg.markup_value} %`}
|
||||||
{(!pkg.markup_type || pkg.markup_type === 'none') && '–'}
|
{(!pkg.markup_type || pkg.markup_type === 'none') && '–'}
|
||||||
|
|
@ -154,7 +154,7 @@ export default function Packages() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{filteredPackages.length === 0 && (
|
{filteredPackages.length === 0 && (
|
||||||
<tr><td colSpan={4} className="px-4 py-6 text-center text-slate-400">
|
<tr><td colSpan={4} className="px-4 py-6 text-center text-neutral-400">
|
||||||
{packages.length === 0 ? 'Noch keine Pakete angelegt.' : 'Keine Pakete für diesen Filter.'}
|
{packages.length === 0 ? 'Noch keine Pakete angelegt.' : 'Keine Pakete für diesen Filter.'}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
|
|
@ -163,46 +163,46 @@ export default function Packages() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Paket bearbeiten' : 'Neues Paket'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Paket bearbeiten' : 'Neues Paket'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Modell *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Modell *</label>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
value={form.car_model_id}
|
value={form.car_model_id}
|
||||||
onChange={(e) => setForm({ ...form, car_model_id: e.target.value })}
|
onChange={(e) => setForm({ ...form, car_model_id: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="">Bitte wählen…</option>
|
<option value="">Bitte wählen…</option>
|
||||||
{models.map((m) => <option key={m.id} value={m.id}>{m.brand_name} {m.name}</option>)}
|
{models.map((m) => <option key={m.id} value={m.id}>{m.brand_name} {m.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Paketname *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Paketname *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Beschreibung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Beschreibung</label>
|
||||||
<p className="mb-1 text-xs text-slate-500 dark:text-slate-400">Jede Zeile erscheint als eigener Stichpunkt zusammen mit den Bauteilen.</p>
|
<p className="mb-1 text-xs text-neutral-500 dark:text-neutral-400">Jede Zeile erscheint als eigener Stichpunkt zusammen mit den Bauteilen.</p>
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Aufschlag</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Aufschlag</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<select
|
<select
|
||||||
value={form.markup_type}
|
value={form.markup_type}
|
||||||
onChange={(e) => setForm({ ...form, markup_type: e.target.value })}
|
onChange={(e) => setForm({ ...form, markup_type: e.target.value })}
|
||||||
className="w-40 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
|
className="w-40 rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<option value="none">Kein Aufschlag</option>
|
<option value="none">Kein Aufschlag</option>
|
||||||
<option value="fixed">Fester Betrag (€)</option>
|
<option value="fixed">Fester Betrag (€)</option>
|
||||||
|
|
@ -215,18 +215,18 @@ export default function Packages() {
|
||||||
disabled={form.markup_type === 'none'}
|
disabled={form.markup_type === 'none'}
|
||||||
value={form.markup_value}
|
value={form.markup_value}
|
||||||
onChange={(e) => setForm({ ...form, markup_value: e.target.value })}
|
onChange={(e) => setForm({ ...form, markup_value: e.target.value })}
|
||||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">Wird auf die Summe der Bauteilpreise aufgeschlagen und ergibt den Gesamtpreis für den Kunden.</p>
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">Wird auf die Summe der Bauteilpreise aufgeschlagen und ergibt den Gesamtpreis für den Kunden.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Sortierung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Sortierung</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sort_order}
|
value={form.sort_order}
|
||||||
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -237,7 +237,7 @@ export default function Packages() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -59,28 +59,28 @@ export default function PermissionGroups() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Berechtigungsgruppen</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Berechtigungsgruppen</h1>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Fasse mehrere Berechtigungen zu einer Gruppe zusammen (z. B. „Werkstatt"), die du dann bequem einem
|
Fasse mehrere Berechtigungen zu einer Gruppe zusammen (z. B. „Werkstatt"), die du dann bequem einem
|
||||||
oder mehreren Benutzern zuweisen kannst.
|
oder mehreren Benutzern zuweisen kannst.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Name</th>
|
<th className="px-4 py-2">Name</th>
|
||||||
<th className="px-4 py-2">Berechtigungen</th>
|
<th className="px-4 py-2">Berechtigungen</th>
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{groups.map((g) => (
|
{groups.map((g) => (
|
||||||
<tr key={g.id} className="bg-white dark:bg-slate-950">
|
<tr key={g.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-100">{g.name}</p>
|
<p className="font-medium text-neutral-800 dark:text-neutral-100">{g.name}</p>
|
||||||
{g.description && <p className="text-xs text-slate-500 dark:text-slate-400">{g.description}</p>}
|
{g.description && <p className="text-xs text-neutral-500 dark:text-neutral-400">{g.description}</p>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-500 dark:text-slate-400">{g.permissions.length} von {catalog.length}</td>
|
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">{g.permissions.length} von {catalog.length}</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
<button onClick={() => startEdit(g)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
<button onClick={() => startEdit(g)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
||||||
<button onClick={() => handleDelete(g.id)} className="text-red-600 hover:underline">Löschen</button>
|
<button onClick={() => handleDelete(g.id)} className="text-red-600 hover:underline">Löschen</button>
|
||||||
|
|
@ -88,43 +88,43 @@ export default function PermissionGroups() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{groups.length === 0 && (
|
{groups.length === 0 && (
|
||||||
<tr><td colSpan={3} className="px-4 py-6 text-center text-slate-400">Noch keine Berechtigungsgruppen angelegt.</td></tr>
|
<tr><td colSpan={3} className="px-4 py-6 text-center text-neutral-400">Noch keine Berechtigungsgruppen angelegt.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Gruppe bearbeiten' : 'Neue Gruppe'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Gruppe bearbeiten' : 'Neue Gruppe'}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Beschreibung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Beschreibung</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Berechtigungen</p>
|
<p className="mb-1 text-sm font-medium text-neutral-700 dark:text-neutral-300">Berechtigungen</p>
|
||||||
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
|
<div className="space-y-1.5 rounded-md border border-neutral-200 p-3 dark:border-neutral-700">
|
||||||
{catalog.map((p) => (
|
{catalog.map((p) => (
|
||||||
<label key={p.key} className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label key={p.key} className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={form.permissions.includes(p.key)}
|
checked={form.permissions.includes(p.key)}
|
||||||
onChange={() => togglePermission(p.key)}
|
onChange={() => togglePermission(p.key)}
|
||||||
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
{p.label}
|
{p.label}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -139,7 +139,7 @@ export default function PermissionGroups() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,14 @@ export default function Services() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Leistungen</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Leistungen</h1>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Diese Karten erscheinen auf der öffentlichen Leistungen-Seite. Die 8 mitgelieferten Einträge sind die
|
Diese Karten erscheinen auf der öffentlichen Leistungen-Seite. Die 8 mitgelieferten Einträge sind die
|
||||||
Standard-Einstellung – du kannst sie bearbeiten, löschen und beliebig neue hinzufügen.
|
Standard-Einstellung – du kannst sie bearbeiten, löschen und beliebig neue hinzufügen.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-slate-50 text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
<thead className="bg-neutral-50 text-neutral-500 dark:bg-neutral-900 dark:text-neutral-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Icon</th>
|
<th className="px-4 py-2">Icon</th>
|
||||||
<th className="px-4 py-2">Titel</th>
|
<th className="px-4 py-2">Titel</th>
|
||||||
|
|
@ -87,15 +87,15 @@ export default function Services() {
|
||||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
{services.map((s) => (
|
{services.map((s) => (
|
||||||
<tr key={s.id} className="bg-white dark:bg-slate-950">
|
<tr key={s.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
<DynamicIcon name={s.icon_name} className="h-5 w-5" />
|
<DynamicIcon name={s.icon_name} className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">{s.title}</td>
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">{s.title}</td>
|
||||||
<td className="px-4 py-2">{s.sort_order}</td>
|
<td className="px-4 py-2">{s.sort_order}</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
<button onClick={() => startEdit(s)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
<button onClick={() => startEdit(s)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
||||||
|
|
@ -104,70 +104,70 @@ export default function Services() {
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{services.length === 0 && (
|
{services.length === 0 && (
|
||||||
<tr><td colSpan={4} className="px-4 py-6 text-center text-slate-400">Noch keine Leistungen angelegt.</td></tr>
|
<tr><td colSpan={4} className="px-4 py-6 text-center text-neutral-400">Noch keine Leistungen angelegt.</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
<form onSubmit={handleSubmit} className="h-fit space-y-4 rounded-xl border border-neutral-200 bg-white p-5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Leistung bearbeiten' : 'Neue Leistung'}</h2>
|
<h2 className="font-bold text-neutral-900 dark:text-white">{editingId ? 'Leistung bearbeiten' : 'Neue Leistung'}</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Icon *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Icon *</label>
|
||||||
<IconPicker value={form.icon_name} onChange={(name) => setForm({ ...form, icon_name: name })} />
|
<IconPicker value={form.icon_name} onChange={(name) => setForm({ ...form, icon_name: name })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.title}
|
value={form.title}
|
||||||
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Beschreibung *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Beschreibung *</label>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
rows={4}
|
rows={4}
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImageUploadField value={form.image_path} onChange={(path) => setForm({ ...form, image_path: path })} label="Bild" />
|
<ImageUploadField value={form.image_path} onChange={(path) => setForm({ ...form, image_path: path })} label="Bild" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Button-Text (optional)</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Button-Text (optional)</label>
|
||||||
<input
|
<input
|
||||||
value={form.cta_label}
|
value={form.cta_label}
|
||||||
onChange={(e) => setForm({ ...form, cta_label: e.target.value })}
|
onChange={(e) => setForm({ ...form, cta_label: e.target.value })}
|
||||||
placeholder="z. B. Fahrzeug konfigurieren"
|
placeholder="z. B. Fahrzeug konfigurieren"
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Button-Link (optional)</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Button-Link (optional)</label>
|
||||||
<input
|
<input
|
||||||
value={form.cta_url}
|
value={form.cta_url}
|
||||||
onChange={(e) => setForm({ ...form, cta_url: e.target.value })}
|
onChange={(e) => setForm({ ...form, cta_url: e.target.value })}
|
||||||
placeholder="z. B. /fahrzeuge"
|
placeholder="z. B. /fahrzeuge"
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Sortierung</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Sortierung</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sort_order}
|
value={form.sort_order}
|
||||||
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
onChange={(e) => setForm({ ...form, sort_order: Number(e.target.value) })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -178,7 +178,7 @@ export default function Services() {
|
||||||
{editingId ? 'Speichern' : 'Anlegen'}
|
{editingId ? 'Speichern' : 'Anlegen'}
|
||||||
</button>
|
</button>
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<button type="button" onClick={resetForm} className="rounded-md border border-slate-300 px-4 py-2 text-sm dark:border-slate-700">
|
<button type="button" onClick={resetForm} className="rounded-md border border-neutral-300 px-4 py-2 text-sm dark:border-neutral-700">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -70,46 +70,46 @@ export default function DatabaseSettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl space-y-6">
|
<div 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">
|
<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-slate-900 dark:text-white">Datenbank-Zugangsdaten</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Datenbank-Zugangsdaten</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Wichtig für einen Umzug auf einen anderen Server/Webspace: Trage hier die Zugangsdaten der dortigen
|
Wichtig für einen Umzug auf einen anderen Server/Webspace: Trage hier die Zugangsdaten der dortigen
|
||||||
Datenbank ein. Die Verbindung wird vor dem Speichern getestet, damit du dich nicht aussperrst.
|
Datenbank ein. Die Verbindung wird vor dem Speichern getestet, damit du dich nicht aussperrst.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-sm text-slate-400">Lädt…</p>
|
<p className="text-sm text-neutral-400">Lädt…</p>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSave} className="space-y-3">
|
<form onSubmit={handleSave} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Host *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Host *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.host}
|
value={form.host}
|
||||||
onChange={(e) => setForm({ ...form, host: e.target.value })}
|
onChange={(e) => setForm({ ...form, host: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Datenbankname *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Datenbankname *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Benutzer *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Benutzer *</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={form.user}
|
value={form.user}
|
||||||
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Passwort {hasPassword ? '(leer lassen = unverändert)' : ''}
|
Passwort {hasPassword ? '(leer lassen = unverändert)' : ''}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
@ -118,23 +118,23 @@ export default function DatabaseSettings() {
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
placeholder={hasPassword ? '••••••••' : ''}
|
placeholder={hasPassword ? '••••••••' : ''}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword((s) => !s)}
|
onClick={() => setShowPassword((s) => !s)}
|
||||||
className="shrink-0 rounded-md border border-slate-300 px-3 text-sm dark:border-slate-700"
|
className="shrink-0 rounded-md border border-neutral-300 px-3 text-sm dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
{showPassword ? 'Verbergen' : 'Anzeigen'}
|
{showPassword ? 'Verbergen' : 'Anzeigen'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Zeichensatz</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Zeichensatz</label>
|
||||||
<input
|
<input
|
||||||
value={form.charset}
|
value={form.charset}
|
||||||
onChange={(e) => setForm({ ...form, charset: e.target.value })}
|
onChange={(e) => setForm({ ...form, charset: e.target.value })}
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -152,9 +152,9 @@ export default function DatabaseSettings() {
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-1 flex items-center justify-between gap-3">
|
<div className="mb-1 flex items-center justify-between gap-3">
|
||||||
<h2 className="font-semibold text-slate-900 dark:text-white">Datenbankstruktur</h2>
|
<h2 className="font-semibold text-neutral-900 dark:text-white">Datenbankstruktur</h2>
|
||||||
{!schemaChecking && schemaReport && (
|
{!schemaChecking && schemaReport && (
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-3 py-1 text-xs font-semibold ${
|
className={`rounded-full px-3 py-1 text-xs font-semibold ${
|
||||||
|
|
@ -167,12 +167,12 @@ export default function DatabaseSettings() {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Prüft, ob diese Datenbank alle Tabellen und Spalten hat, die der aktuelle Code-Stand braucht – wichtig,
|
Prüft, ob diese Datenbank alle Tabellen und Spalten hat, die der aktuelle Code-Stand braucht – wichtig,
|
||||||
wenn deine Live-Version auf einem anderen Server einen älteren Stand hat.
|
wenn deine Live-Version auf einem anderen Server einen älteren Stand hat.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{schemaChecking && <p className="text-sm text-slate-400">Prüfe…</p>}
|
{schemaChecking && <p className="text-sm text-neutral-400">Prüfe…</p>}
|
||||||
|
|
||||||
{!schemaChecking && schemaReport && !schemaReport.ok && (
|
{!schemaChecking && schemaReport && !schemaReport.ok && (
|
||||||
<ul className="mb-4 space-y-1 rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-300">
|
<ul className="mb-4 space-y-1 rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-300">
|
||||||
|
|
@ -192,7 +192,7 @@ export default function DatabaseSettings() {
|
||||||
<button
|
<button
|
||||||
onClick={checkSchema}
|
onClick={checkSchema}
|
||||||
disabled={schemaChecking}
|
disabled={schemaChecking}
|
||||||
className="rounded-md border border-slate-300 px-4 py-2 text-sm font-medium hover:bg-slate-50 disabled:opacity-50 dark:border-slate-700 dark:hover:bg-slate-800"
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium hover:bg-neutral-50 disabled:opacity-50 dark:border-neutral-700 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
Erneut prüfen
|
Erneut prüfen
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -208,7 +208,7 @@ export default function DatabaseSettings() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{schemaActions && (
|
{schemaActions && (
|
||||||
<ul className="mt-3 space-y-1 rounded-md bg-slate-50 p-3 text-sm text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
<ul className="mt-3 space-y-1 rounded-md bg-neutral-50 p-3 text-sm text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
|
||||||
{schemaActions.map((a, i) => <li key={i}>{a}</li>)}
|
{schemaActions.map((a, i) => <li key={i}>{a}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ export default function ExportImportSettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl space-y-6">
|
<div 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">
|
<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-slate-900 dark:text-white">Daten exportieren</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Daten exportieren</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Lädt eine Datei mit allen Marken, Modellen, Paketen, Produkten, Upgrades und Leistungen (inkl. Bilder)
|
Lädt eine Datei mit allen Marken, Modellen, Paketen, Produkten, Upgrades und Leistungen (inkl. Bilder)
|
||||||
herunter. Benutzerkonten und Kontaktanfragen sind bewusst <strong>nicht</strong> enthalten.
|
herunter. Benutzerkonten und Kontaktanfragen sind bewusst <strong>nicht</strong> enthalten.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -45,9 +45,9 @@ export default function ExportImportSettings() {
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-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-slate-900 dark:text-white">Daten importieren</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Daten importieren</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Lädt eine zuvor exportierte Datei auf diesen Server. <strong>Ersetzt dabei alle</strong> aktuellen
|
Lädt eine zuvor exportierte Datei auf diesen Server. <strong>Ersetzt dabei alle</strong> aktuellen
|
||||||
Marken, Modelle, Pakete, Produkte, Upgrades und Leistungen auf diesem Server – das kann nicht
|
Marken, Modelle, Pakete, Produkte, Upgrades und Leistungen auf diesem Server – das kann nicht
|
||||||
rückgängig gemacht werden. Kontaktanfragen und Benutzerkonten bleiben unangetastet.
|
rückgängig gemacht werden. Kontaktanfragen und Benutzerkonten bleiben unangetastet.
|
||||||
|
|
@ -59,12 +59,12 @@ export default function ExportImportSettings() {
|
||||||
onChange={(e) => setImportFile(e.target.files?.[0] || null)}
|
onChange={(e) => setImportFile(e.target.files?.[0] || null)}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
/>
|
/>
|
||||||
<label className="flex items-start gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label className="flex items-start gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={importConfirmed}
|
checked={importConfirmed}
|
||||||
onChange={(e) => setImportConfirmed(e.target.checked)}
|
onChange={(e) => setImportConfirmed(e.target.checked)}
|
||||||
className="mt-0.5 h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="mt-0.5 h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
Mir ist bewusst, dass dies alle aktuellen Marken, Modelle, Pakete, Produkte, Upgrades und
|
Mir ist bewusst, dass dies alle aktuellen Marken, Modelle, Pakete, Produkte, Upgrades und
|
||||||
Leistungen auf diesem Server unwiderruflich ersetzt.
|
Leistungen auf diesem Server unwiderruflich ersetzt.
|
||||||
|
|
|
||||||
|
|
@ -68,19 +68,19 @@ export default function MaintenanceSettings() {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <p className="text-sm text-slate-400">Lädt…</p>;
|
return <p className="text-sm text-neutral-400">Lädt…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
|
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Ist "Gesamte Seite" aktiv, gewinnt das immer – die beiden anderen Schalter spielen dann keine Rolle mehr.
|
Ist "Gesamte Seite" aktiv, gewinnt das immer – die beiden anderen Schalter spielen dann keine Rolle mehr.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{sections.map((section) => (
|
{sections.map((section) => (
|
||||||
<section key={section.key} className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<section key={section.key} className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-1 flex items-center justify-between gap-3">
|
<div className="mb-1 flex items-center justify-between gap-3">
|
||||||
<h2 className="font-semibold text-slate-900 dark:text-white">{section.title}</h2>
|
<h2 className="font-semibold text-neutral-900 dark:text-white">{section.title}</h2>
|
||||||
<label className="relative inline-flex cursor-pointer items-center">
|
<label className="relative inline-flex cursor-pointer items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -88,12 +88,12 @@ export default function MaintenanceSettings() {
|
||||||
onChange={(e) => update(section.key, 'enabled', e.target.checked)}
|
onChange={(e) => update(section.key, 'enabled', e.target.checked)}
|
||||||
className="peer sr-only"
|
className="peer sr-only"
|
||||||
/>
|
/>
|
||||||
<div className="h-6 w-11 rounded-full bg-slate-200 transition peer-checked:bg-brand-500 dark:bg-slate-700" />
|
<div className="h-6 w-11 rounded-full bg-neutral-200 transition peer-checked:bg-brand-500 dark:bg-neutral-700" />
|
||||||
<div className="absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition peer-checked:translate-x-5" />
|
<div className="absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition peer-checked:translate-x-5" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-3 text-sm text-slate-500 dark:text-slate-400">{section.hint}</p>
|
<p className="mb-3 text-sm text-neutral-500 dark:text-neutral-400">{section.hint}</p>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Eigene Nachricht (optional)
|
Eigene Nachricht (optional)
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -101,7 +101,7 @@ export default function MaintenanceSettings() {
|
||||||
value={status[section.key].message}
|
value={status[section.key].message}
|
||||||
onChange={(e) => update(section.key, 'message', e.target.value)}
|
onChange={(e) => update(section.key, 'message', e.target.value)}
|
||||||
placeholder="z. B. Wir aktualisieren gerade unser Angebot. Schau bald wieder vorbei!"
|
placeholder="z. B. Wir aktualisieren gerade unser Angebot. Schau bald wieder vorbei!"
|
||||||
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@ export default function ResetSettings() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl space-y-6">
|
<div 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">
|
<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-slate-900 dark:text-white">Nur Leistungen zurücksetzen</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Nur Leistungen zurücksetzen</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Löscht alle aktuellen Leistungen-Karten und stellt die 8 mitgelieferten Standard-Leistungen wieder her.
|
Löscht alle aktuellen Leistungen-Karten und stellt die 8 mitgelieferten Standard-Leistungen wieder her.
|
||||||
Marken, Modelle und Pakete bleiben unangetastet.
|
Marken, Modelle und Pakete bleiben unangetastet.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -68,12 +68,12 @@ export default function ResetSettings() {
|
||||||
>
|
>
|
||||||
{servicesBusy ? 'Setze zurück…' : 'Leistungen auf Standard zurücksetzen'}
|
{servicesBusy ? 'Setze zurück…' : 'Leistungen auf Standard zurücksetzen'}
|
||||||
</button>
|
</button>
|
||||||
{servicesMessage && <p className="mt-3 text-sm text-slate-600 dark:text-slate-300">{servicesMessage}</p>}
|
{servicesMessage && <p className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">{servicesMessage}</p>}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-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-slate-900 dark:text-white">Nur Fahrzeugkatalog leeren</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Nur Fahrzeugkatalog leeren</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Löscht alle Marken, Modelle, Pakete, Produkte und Upgrades unwiderruflich. Leistungen, Kontaktanfragen
|
Löscht alle Marken, Modelle, Pakete, Produkte und Upgrades unwiderruflich. Leistungen, Kontaktanfragen
|
||||||
und Benutzerkonten bleiben erhalten.
|
und Benutzerkonten bleiben erhalten.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -84,12 +84,12 @@ export default function ResetSettings() {
|
||||||
>
|
>
|
||||||
{catalogBusy ? 'Leere…' : 'Fahrzeugkatalog leeren'}
|
{catalogBusy ? 'Leere…' : 'Fahrzeugkatalog leeren'}
|
||||||
</button>
|
</button>
|
||||||
{catalogMessage && <p className="mt-3 text-sm text-slate-600 dark:text-slate-300">{catalogMessage}</p>}
|
{catalogMessage && <p className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">{catalogMessage}</p>}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-red-300 bg-red-50/40 p-6 dark:border-red-900 dark:bg-red-900/10">
|
<section className="rounded-xl border border-red-300 bg-red-50/40 p-6 dark:border-red-900 dark:bg-red-900/10">
|
||||||
<h2 className="mb-1 font-semibold text-red-700 dark:text-red-300">Alles zurücksetzen</h2>
|
<h2 className="mb-1 font-semibold text-red-700 dark:text-red-300">Alles zurücksetzen</h2>
|
||||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
<p className="mb-4 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
Kombiniert beides: Fahrzeugkatalog wird komplett geleert <strong>und</strong> Leistungen werden auf
|
Kombiniert beides: Fahrzeugkatalog wird komplett geleert <strong>und</strong> Leistungen werden auf
|
||||||
Standard zurückgesetzt. Kontaktanfragen und Benutzerkonten bleiben erhalten. Nicht rückgängig zu machen.
|
Standard zurückgesetzt. Kontaktanfragen und Benutzerkonten bleiben erhalten. Nicht rückgängig zu machen.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -100,7 +100,7 @@ export default function ResetSettings() {
|
||||||
>
|
>
|
||||||
{allBusy ? 'Setze zurück…' : 'Wirklich alles zurücksetzen'}
|
{allBusy ? 'Setze zurück…' : 'Wirklich alles zurücksetzen'}
|
||||||
</button>
|
</button>
|
||||||
{allMessage && <p className="mt-3 text-sm text-slate-700 dark:text-slate-300">{allMessage}</p>}
|
{allMessage && <p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{allMessage}</p>}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ export default function SettingsLayout() {
|
||||||
`rounded-md px-4 py-2 text-sm font-medium transition ${
|
`rounded-md px-4 py-2 text-sm font-medium transition ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-brand-500 text-white'
|
? 'bg-brand-500 text-white'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
: 'text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Einstellungen</h1>
|
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Einstellungen</h1>
|
||||||
<p className="mb-5 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-5 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Werkzeuge rund um Datenbank, Server-Umzug und Zurücksetzen dieser Seite.
|
Werkzeuge rund um Datenbank, Server-Umzug und Zurücksetzen dieser Seite.
|
||||||
</p>
|
</p>
|
||||||
<nav className="mb-6 flex flex-wrap gap-2">
|
<nav className="mb-6 flex flex-wrap gap-2">
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,14 @@ export default function WebsiteSettings() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <p className="text-sm text-slate-400">Lädt…</p>;
|
return <p className="text-sm text-neutral-400">Lädt…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
|
<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">
|
<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-slate-900 dark:text-white">Startseiten-Bild</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Startseiten-Bild</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Das große Hero-Bild ganz oben auf der Startseite. Ohne eigenes Bild wird das Standardbild verwendet.
|
Das große Hero-Bild ganz oben auf der Startseite. Ohne eigenes Bild wird das Standardbild verwendet.
|
||||||
</p>
|
</p>
|
||||||
<ImageUploadField
|
<ImageUploadField
|
||||||
|
|
@ -63,41 +63,41 @@ export default function WebsiteSettings() {
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-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-slate-900 dark:text-white">Kontaktdaten</h2>
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Kontaktdaten</h2>
|
||||||
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Wird im Header, Footer und auf der Kontaktseite angezeigt.
|
Wird im Header, Footer und auf der Kontaktseite angezeigt.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Telefon</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Telefon</label>
|
||||||
<input
|
<input
|
||||||
value={form.phone}
|
value={form.phone}
|
||||||
onChange={(e) => update('phone', e.target.value)}
|
onChange={(e) => update('phone', e.target.value)}
|
||||||
placeholder="09373 20 62 390"
|
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
WhatsApp Business Nummer
|
WhatsApp Business Nummer
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={form.whatsapp}
|
value={form.whatsapp}
|
||||||
onChange={(e) => update('whatsapp', e.target.value)}
|
onChange={(e) => update('whatsapp', e.target.value)}
|
||||||
placeholder="z. B. 09373 20 62 390"
|
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">Leer lassen, um den WhatsApp-Link im Header auszublenden.</p>
|
<p className="mt-1 text-xs text-neutral-400">Leer lassen, um den WhatsApp-Link im Header auszublenden.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">E-Mail</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">E-Mail</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={form.contact_email}
|
value={form.contact_email}
|
||||||
onChange={(e) => update('contact_email', e.target.value)}
|
onChange={(e) => update('contact_email', e.target.value)}
|
||||||
placeholder="info@hifi-planet-amorbach.de"
|
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ export default function AGB() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">
|
||||||
Allgemeine Geschäftsbedingungen
|
Allgemeine Geschäftsbedingungen
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="space-y-8 text-sm text-slate-600 dark:text-slate-300">
|
<div className="space-y-8 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">1. Geltungsbereich</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">1. Geltungsbereich</h2>
|
||||||
<p>
|
<p>
|
||||||
Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für alle Beratungs-, Werkstatt- und
|
Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für alle Beratungs-, Werkstatt- und
|
||||||
Umbauleistungen, die die Hifi Planet e.K., Boxbrunner Str. 20a, 63916 Amorbach („wir", „HifiPlanet")
|
Umbauleistungen, die die Hifi Planet e.K., Boxbrunner Str. 20a, 63916 Amorbach („wir", „HifiPlanet")
|
||||||
|
|
@ -24,7 +24,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">2. Anfragen über die Website</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">2. Anfragen über die Website</h2>
|
||||||
<p>
|
<p>
|
||||||
Über das Kontaktformular können Sie unverbindlich eine Beratung oder ein Angebot zu einem einzelnen
|
Über das Kontaktformular können Sie unverbindlich eine Beratung oder ein Angebot zu einem einzelnen
|
||||||
Produkt oder einem ganzen Paket anfragen. Die auf der Website angezeigten Preise sind, soweit sie von
|
Produkt oder einem ganzen Paket anfragen. Die auf der Website angezeigten Preise sind, soweit sie von
|
||||||
|
|
@ -35,7 +35,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">3. Vertragsschluss</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">3. Vertragsschluss</h2>
|
||||||
<p>
|
<p>
|
||||||
Ein verbindlicher Vertrag über Beratung, Material und Einbauleistungen kommt erst zustande, wenn wir
|
Ein verbindlicher Vertrag über Beratung, Material und Einbauleistungen kommt erst zustande, wenn wir
|
||||||
Ihnen nach persönlicher oder schriftlicher Abstimmung ein konkretes Angebot unterbreitet haben und Sie
|
Ihnen nach persönlicher oder schriftlicher Abstimmung ein konkretes Angebot unterbreitet haben und Sie
|
||||||
|
|
@ -45,7 +45,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">4. Preise und Zahlung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">4. Preise und Zahlung</h2>
|
||||||
<p>
|
<p>
|
||||||
Es gelten die im jeweiligen Einzelangebot genannten Preise inklusive der zum Zeitpunkt der
|
Es gelten die im jeweiligen Einzelangebot genannten Preise inklusive der zum Zeitpunkt der
|
||||||
Rechnungsstellung gesetzlich geltenden Umsatzsteuer. Sofern nichts anderes vereinbart ist, ist die
|
Rechnungsstellung gesetzlich geltenden Umsatzsteuer. Sofern nichts anderes vereinbart ist, ist die
|
||||||
|
|
@ -54,7 +54,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">5. Termine und Fahrzeugübergabe</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">5. Termine und Fahrzeugübergabe</h2>
|
||||||
<p>
|
<p>
|
||||||
Vereinbarte Termine zur Fahrzeugübergabe sind von beiden Seiten einzuhalten. Verschiebungen sind
|
Vereinbarte Termine zur Fahrzeugübergabe sind von beiden Seiten einzuhalten. Verschiebungen sind
|
||||||
möglichst frühzeitig mitzuteilen. Für Gegenstände, die sich zum Zeitpunkt der Übergabe im Fahrzeug
|
möglichst frühzeitig mitzuteilen. Für Gegenstände, die sich zum Zeitpunkt der Übergabe im Fahrzeug
|
||||||
|
|
@ -63,7 +63,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">6. Gewährleistung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">6. Gewährleistung</h2>
|
||||||
<p>
|
<p>
|
||||||
Es gelten die gesetzlichen Gewährleistungsrechte. Mängel sind uns unverzüglich nach Feststellung
|
Es gelten die gesetzlichen Gewährleistungsrechte. Mängel sind uns unverzüglich nach Feststellung
|
||||||
anzuzeigen, damit wir Ihnen Nacherfüllung anbieten können. Für Verschleißteile sowie für Schäden, die
|
anzuzeigen, damit wir Ihnen Nacherfüllung anbieten können. Für Verschleißteile sowie für Schäden, die
|
||||||
|
|
@ -73,14 +73,14 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">7. Eigentumsvorbehalt</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">7. Eigentumsvorbehalt</h2>
|
||||||
<p>
|
<p>
|
||||||
Verbautes Material und gelieferte Ware bleiben bis zur vollständigen Bezahlung unser Eigentum.
|
Verbautes Material und gelieferte Ware bleiben bis zur vollständigen Bezahlung unser Eigentum.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">8. Haftung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">8. Haftung</h2>
|
||||||
<p>
|
<p>
|
||||||
Wir haften unbeschränkt bei Vorsatz und grober Fahrlässigkeit sowie nach Maßgabe des
|
Wir haften unbeschränkt bei Vorsatz und grober Fahrlässigkeit sowie nach Maßgabe des
|
||||||
Produkthaftungsgesetzes. Bei leichter Fahrlässigkeit haften wir nur bei Verletzung einer wesentlichen
|
Produkthaftungsgesetzes. Bei leichter Fahrlässigkeit haften wir nur bei Verletzung einer wesentlichen
|
||||||
|
|
@ -91,7 +91,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">9. Widerrufsrecht</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">9. Widerrufsrecht</h2>
|
||||||
<p>
|
<p>
|
||||||
Ein gesetzliches Widerrufsrecht für Verbraucher besteht bei Verträgen, die außerhalb von
|
Ein gesetzliches Widerrufsrecht für Verbraucher besteht bei Verträgen, die außerhalb von
|
||||||
Geschäftsräumen oder im Fernabsatz geschlossen werden. Da der eigentliche Werkvertrag über Beratung und
|
Geschäftsräumen oder im Fernabsatz geschlossen werden. Da der eigentliche Werkvertrag über Beratung und
|
||||||
|
|
@ -103,7 +103,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">10. Anwendbares Recht und Gerichtsstand</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">10. Anwendbares Recht und Gerichtsstand</h2>
|
||||||
<p>
|
<p>
|
||||||
Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts. Ist der Kunde
|
Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts. Ist der Kunde
|
||||||
Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen, ist
|
Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen, ist
|
||||||
|
|
@ -112,7 +112,7 @@ export default function AGB() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">11. Schlussbestimmungen</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">11. Schlussbestimmungen</h2>
|
||||||
<p>
|
<p>
|
||||||
Sollte eine Bestimmung dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen
|
Sollte eine Bestimmung dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen
|
||||||
Bestimmungen hiervon unberührt.
|
Bestimmungen hiervon unberührt.
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,21 @@ export default function BrandPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <p className="mx-auto max-w-6xl px-4 py-12 text-slate-500 sm:px-6">Lädt…</p>;
|
return <p className="mx-auto max-w-6xl px-4 py-12 text-neutral-500 sm:px-6">Lädt…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
||||||
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
||||||
<p className="mb-1 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link> / {data.brand.name}
|
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link> / {data.brand.name}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">
|
||||||
{data.brand.name} – Modell wählen
|
{data.brand.name} – Modell wählen
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{data.models.length === 0 && (
|
{data.models.length === 0 && (
|
||||||
<p className="text-slate-500 dark:text-slate-400">Für diese Marke sind noch keine Modelle hinterlegt.</p>
|
<p className="text-neutral-500 dark:text-neutral-400">Für diese Marke sind noch keine Modelle hinterlegt.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
|
@ -59,7 +59,7 @@ export default function BrandPage() {
|
||||||
<Link
|
<Link
|
||||||
key={model.id}
|
key={model.id}
|
||||||
to={`/fahrzeuge/${brandSlug}/${model.slug}`}
|
to={`/fahrzeuge/${brandSlug}/${model.slug}`}
|
||||||
className="flex items-center justify-center rounded-xl border border-slate-200 bg-white px-6 py-8 text-center font-semibold text-slate-800 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100"
|
className="flex items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 py-8 text-center font-semibold text-neutral-800 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
>
|
>
|
||||||
{model.name}
|
{model.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -81,14 +81,14 @@ export default function ContactPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-5xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-5xl px-4 py-12 sm:px-6">
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">Kontakt aufnehmen</h1>
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">Kontakt aufnehmen</h1>
|
||||||
|
|
||||||
<div className="grid gap-10 md:grid-cols-5">
|
<div className="grid gap-10 md:grid-cols-5">
|
||||||
<div className="md:col-span-3">
|
<div className="md:col-span-3">
|
||||||
{status === 'sent' ? (
|
{status === 'sent' ? (
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-8 text-center dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Danke für deine Anfrage!</h2>
|
<h2 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Danke für deine Anfrage!</h2>
|
||||||
<p className="text-slate-600 dark:text-slate-300">Wir melden uns so schnell wie möglich bei dir.</p>
|
<p className="text-neutral-600 dark:text-neutral-300">Wir melden uns so schnell wie möglich bei dir.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -111,20 +111,20 @@ export default function ContactPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{upgrades.length > 0 && (
|
{upgrades.length > 0 && (
|
||||||
<div className="mb-6 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
<div className="mb-6 rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<p className="mb-3 text-sm font-semibold text-slate-900 dark:text-white">Optionale Upgrades</p>
|
<p className="mb-3 text-sm font-semibold text-neutral-900 dark:text-white">Optionale Upgrades</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{upgrades.map((u) => (
|
{upgrades.map((u) => (
|
||||||
<label key={u.id} className="flex cursor-pointer items-start gap-3 rounded-md border border-slate-200 p-3 text-sm hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">
|
<label key={u.id} className="flex cursor-pointer items-start gap-3 rounded-md border border-neutral-200 p-3 text-sm hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedUpgradeIds.includes(u.id)}
|
checked={selectedUpgradeIds.includes(u.id)}
|
||||||
onChange={() => toggleUpgrade(u.id)}
|
onChange={() => toggleUpgrade(u.id)}
|
||||||
className="mt-0.5 h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"
|
className="mt-0.5 h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
<span className="block font-medium text-slate-800 dark:text-slate-100">{u.name}</span>
|
<span className="block font-medium text-neutral-800 dark:text-neutral-100">{u.name}</span>
|
||||||
{u.description && <span className="block text-slate-500 dark:text-slate-400">{u.description}</span>}
|
{u.description && <span className="block text-neutral-500 dark:text-neutral-400">{u.description}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold text-brand-600 dark:text-brand-400">+{formatPrice(u.price)}</span>
|
<span className="font-semibold text-brand-600 dark:text-brand-400">+{formatPrice(u.price)}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -135,37 +135,37 @@ export default function ContactPage() {
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Name *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name *</label>
|
||||||
<input
|
<input
|
||||||
name="name"
|
name="name"
|
||||||
required
|
required
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">E-Mail *</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">E-Mail *</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Telefon</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Telefon</label>
|
||||||
<input
|
<input
|
||||||
name="phone"
|
name="phone"
|
||||||
value={form.phone}
|
value={form.phone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Fahrgestellnummer (FIN)
|
Fahrgestellnummer (FIN)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -173,17 +173,17 @@ export default function ContactPage() {
|
||||||
value={form.vin}
|
value={form.vin}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Optional – hilft uns bei der genauen Einschätzung deines Fahrzeugs"
|
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"
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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-neutral-700 dark:text-neutral-300">Nachricht</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="message"
|
name="message"
|
||||||
rows={4}
|
rows={4}
|
||||||
value={form.message}
|
value={form.message}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
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-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -202,32 +202,32 @@ export default function ContactPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="mb-4 font-semibold text-slate-900 dark:text-white">HifiPlanet Amorbach</h2>
|
<h2 className="mb-4 font-semibold text-neutral-900 dark:text-white">HifiPlanet Amorbach</h2>
|
||||||
<dl className="space-y-3 text-sm text-slate-600 dark:text-slate-300">
|
<dl className="space-y-3 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
<div>
|
<div>
|
||||||
<dt className="font-medium text-slate-800 dark:text-slate-100">Adresse</dt>
|
<dt className="font-medium text-neutral-800 dark:text-neutral-100">Adresse</dt>
|
||||||
<dd>Boxbrunner Str. 20a, 63916 Amorbach</dd>
|
<dd>Boxbrunner Str. 20a, 63916 Amorbach</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="font-medium text-slate-800 dark:text-slate-100">Telefon</dt>
|
<dt className="font-medium text-neutral-800 dark:text-neutral-100">Telefon</dt>
|
||||||
<dd><a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</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-neutral-800 dark:text-neutral-100">E-Mail</dt>
|
||||||
<dd><a href={`mailto:${contactEmail}`} className="hover:text-brand-500">{contactEmail}</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-neutral-800 dark:text-neutral-100">Öffnungszeiten</dt>
|
||||||
<dd>Mo–Fr: 9:00–18:00 Uhr</dd>
|
<dd>Mo–Fr: 9:00–18:00 Uhr</dd>
|
||||||
<dd>Sa: 10:00–13:00 Uhr</dd>
|
<dd>Sa: 10:00–13:00 Uhr</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
|
<div className="mt-6 rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<h2 className="mb-4 font-semibold text-slate-900 dark:text-white">Anfahrt</h2>
|
<h2 className="mb-4 font-semibold text-neutral-900 dark:text-white">Anfahrt</h2>
|
||||||
<div className="overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700">
|
<div className="overflow-hidden rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<ExternalEmbed name="Die Google-Maps-Karte" className="h-[220px] w-full">
|
<ExternalEmbed name="Die Google-Maps-Karte" className="h-[220px] w-full">
|
||||||
<iframe
|
<iframe
|
||||||
title="HifiPlanet Amorbach – Standort"
|
title="HifiPlanet Amorbach – Standort"
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ export default function Datenschutz() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">Datenschutzerklärung</h1>
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">Datenschutzerklärung</h1>
|
||||||
|
|
||||||
<div className="space-y-8 text-sm text-slate-600 dark:text-slate-300">
|
<div className="space-y-8 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">1. Verantwortlicher</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">1. Verantwortlicher</h2>
|
||||||
<p>
|
<p>
|
||||||
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
|
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -28,7 +28,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">2. Ihre Rechte als betroffene Person</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">2. Ihre Rechte als betroffene Person</h2>
|
||||||
<p className="mb-2">Sie haben jederzeit das Recht,</p>
|
<p className="mb-2">Sie haben jederzeit das Recht,</p>
|
||||||
<ul className="list-disc space-y-1 pl-5">
|
<ul className="list-disc space-y-1 pl-5">
|
||||||
<li>Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten zu erhalten (Art. 15 DSGVO),</li>
|
<li>Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten zu erhalten (Art. 15 DSGVO),</li>
|
||||||
|
|
@ -47,7 +47,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">3. Hosting und Server-Logfiles</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">3. Hosting und Server-Logfiles</h2>
|
||||||
<p>
|
<p>
|
||||||
Beim Aufruf dieser Website erhebt der Webserver automatisch Daten in sogenannten Server-Logfiles, die
|
Beim Aufruf dieser Website erhebt der Webserver automatisch Daten in sogenannten Server-Logfiles, die
|
||||||
Ihr Browser übermittelt. Dazu gehören: IP-Adresse, Datum und Uhrzeit der Anfrage, aufgerufene Seite,
|
Ihr Browser übermittelt. Dazu gehören: IP-Adresse, Datum und Uhrzeit der Anfrage, aufgerufene Seite,
|
||||||
|
|
@ -60,7 +60,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">4. Kontaktformular</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">4. Kontaktformular</h2>
|
||||||
<p>
|
<p>
|
||||||
Wenn Sie uns über das Kontaktformular eine Anfrage zukommen lassen, werden Ihre Angaben aus dem
|
Wenn Sie uns über das Kontaktformular eine Anfrage zukommen lassen, werden Ihre Angaben aus dem
|
||||||
Formular (Name, E-Mail-Adresse, optional Telefonnummer, Nachricht sowie ggf. der Kontext Ihrer Anfrage
|
Formular (Name, E-Mail-Adresse, optional Telefonnummer, Nachricht sowie ggf. der Kontext Ihrer Anfrage
|
||||||
|
|
@ -75,7 +75,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">5. Cookies und lokale Speicherung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">5. Cookies und lokale Speicherung</h2>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
Diese Website verwendet technisch notwendige Cookies bzw. Speichertechnologien, die für den Betrieb der
|
Diese Website verwendet technisch notwendige Cookies bzw. Speichertechnologien, die für den Betrieb der
|
||||||
Seite erforderlich sind und ohne die einzelne Funktionen nicht funktionieren würden. Dazu gehören:
|
Seite erforderlich sind und ohne die einzelne Funktionen nicht funktionieren würden. Dazu gehören:
|
||||||
|
|
@ -100,7 +100,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">6. Google Maps</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">6. Google Maps</h2>
|
||||||
<p>
|
<p>
|
||||||
Auf unserer Kontaktseite und im Footer binden wir Kartenmaterial des Dienstes „Google Maps" ein, um
|
Auf unserer Kontaktseite und im Footer binden wir Kartenmaterial des Dienstes „Google Maps" ein, um
|
||||||
Ihnen die Anfahrt zu unserem Standort zu erleichtern. Anbieter ist Google Ireland Limited, Gordon
|
Ihnen die Anfahrt zu unserem Standort zu erleichtern. Anbieter ist Google Ireland Limited, Gordon
|
||||||
|
|
@ -122,7 +122,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">7. YouTube</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">7. YouTube</h2>
|
||||||
<p>
|
<p>
|
||||||
Auf unserer Startseite binden wir ein Video des Anbieters YouTube (Google Ireland Limited, Gordon
|
Auf unserer Startseite binden wir ein Video des Anbieters YouTube (Google Ireland Limited, Gordon
|
||||||
House, Barrow Street, Dublin 4, Irland) im „erweiterten Datenschutzmodus" ein (Domain
|
House, Barrow Street, Dublin 4, Irland) im „erweiterten Datenschutzmodus" ein (Domain
|
||||||
|
|
@ -145,7 +145,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">8. Empfänger der Daten</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">8. Empfänger der Daten</h2>
|
||||||
<p>
|
<p>
|
||||||
Ihre Daten werden grundsätzlich nur innerhalb unseres Unternehmens verarbeitet. Eine Weitergabe an
|
Ihre Daten werden grundsätzlich nur innerhalb unseres Unternehmens verarbeitet. Eine Weitergabe an
|
||||||
Dritte erfolgt nur, soweit dies zur Bearbeitung Ihrer Anfrage erforderlich ist (z. B. an unseren
|
Dritte erfolgt nur, soweit dies zur Bearbeitung Ihrer Anfrage erforderlich ist (z. B. an unseren
|
||||||
|
|
@ -155,7 +155,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">9. Speicherdauer</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">9. Speicherdauer</h2>
|
||||||
<p>
|
<p>
|
||||||
Sofern in dieser Erklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre
|
Sofern in dieser Erklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre
|
||||||
personenbezogenen Daten bei uns, bis der Zweck für die Datenspeicherung entfällt. Gesetzliche
|
personenbezogenen Daten bei uns, bis der Zweck für die Datenspeicherung entfällt. Gesetzliche
|
||||||
|
|
@ -165,7 +165,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">10. SSL-/TLS-Verschlüsselung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">10. SSL-/TLS-Verschlüsselung</h2>
|
||||||
<p>
|
<p>
|
||||||
Diese Seite nutzt aus Sicherheitsgründen eine SSL-/TLS-Verschlüsselung zur Übertragung Ihrer Daten. Eine
|
Diese Seite nutzt aus Sicherheitsgründen eine SSL-/TLS-Verschlüsselung zur Übertragung Ihrer Daten. Eine
|
||||||
verschlüsselte Verbindung erkennen Sie an dem Schloss-Symbol in der Adresszeile Ihres Browsers.
|
verschlüsselte Verbindung erkennen Sie an dem Schloss-Symbol in der Adresszeile Ihres Browsers.
|
||||||
|
|
@ -173,7 +173,7 @@ export default function Datenschutz() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">11. Änderung dieser Datenschutzerklärung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">11. Änderung dieser Datenschutzerklärung</h2>
|
||||||
<p>
|
<p>
|
||||||
Wir passen diese Datenschutzerklärung an, sobald sich die von uns eingesetzten Datenverarbeitungen
|
Wir passen diese Datenschutzerklärung an, sobald sich die von uns eingesetzten Datenverarbeitungen
|
||||||
ändern. Es gilt jeweils die zum Zeitpunkt Ihres Besuchs aktuelle, auf dieser Seite veröffentlichte
|
ändern. Es gilt jeweils die zum Zeitpunkt Ihres Besuchs aktuelle, auf dieser Seite veröffentlichte
|
||||||
|
|
|
||||||
|
|
@ -106,10 +106,10 @@ export default function Home() {
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{ opacity: ctaOpacity, pointerEvents: ctaPointerEvents, y: ctaY }}
|
style={{ opacity: ctaOpacity, pointerEvents: ctaPointerEvents, y: ctaY }}
|
||||||
className="fixed inset-x-0 bottom-0 z-20 border-t border-slate-200 bg-white/95 px-4 py-3 shadow-[0_-8px_24px_rgba(0,0,0,0.08)] backdrop-blur dark:border-slate-800 dark:bg-slate-950/95 sm:px-6"
|
className="fixed inset-x-0 bottom-0 z-20 border-t border-neutral-200 bg-white/95 px-4 py-3 shadow-[0_-8px_24px_rgba(0,0,0,0.08)] backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/95 sm:px-6"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3">
|
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3">
|
||||||
<p className="hidden text-sm font-medium text-slate-600 dark:text-slate-300 sm:block">
|
<p className="hidden text-sm font-medium text-neutral-600 dark:text-neutral-300 sm:block">
|
||||||
Bereit für deinen Sound-Umbau?
|
Bereit für deinen Sound-Umbau?
|
||||||
</p>
|
</p>
|
||||||
<div className="flex w-full gap-3 sm:w-auto">
|
<div className="flex w-full gap-3 sm:w-auto">
|
||||||
|
|
@ -121,7 +121,7 @@ export default function Home() {
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/kontakt"
|
to="/kontakt"
|
||||||
className="flex-1 rounded-md border border-slate-300 px-5 py-2.5 text-center text-sm font-semibold text-slate-700 transition hover:-translate-y-0.5 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 sm:flex-none"
|
className="flex-1 rounded-md border border-neutral-300 px-5 py-2.5 text-center text-sm font-semibold text-neutral-700 transition hover:-translate-y-0.5 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800 sm:flex-none"
|
||||||
>
|
>
|
||||||
Kontakt aufnehmen
|
Kontakt aufnehmen
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -136,7 +136,7 @@ export default function Home() {
|
||||||
alt="Individueller Subwoofer-Einbau mit LED-Beleuchtung im Kofferraum"
|
alt="Individueller Subwoofer-Einbau mit LED-Beleuchtung im Kofferraum"
|
||||||
className="absolute inset-0 h-full w-full scale-110 object-cover"
|
className="absolute inset-0 h-full w-full scale-110 object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/80 to-slate-950/40" />
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-950 via-neutral-950/80 to-neutral-950/40" />
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{ y: heroTextY, opacity: heroOpacity }}
|
style={{ y: heroTextY, opacity: heroOpacity }}
|
||||||
|
|
@ -153,7 +153,7 @@ export default function Home() {
|
||||||
<h1 className="text-3xl font-extrabold tracking-tight text-white sm:text-5xl">
|
<h1 className="text-3xl font-extrabold tracking-tight text-white sm:text-5xl">
|
||||||
Audiophil aus Prinzip
|
Audiophil aus Prinzip
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-4 max-w-2xl text-base text-slate-200 sm:text-lg">
|
<p className="mx-auto mt-4 max-w-2xl text-base text-neutral-200 sm:text-lg">
|
||||||
Dein Car-Hifi Spezialist für individuelle Sound-Umbauten. Wähle dein Fahrzeug,
|
Dein Car-Hifi Spezialist für individuelle Sound-Umbauten. Wähle dein Fahrzeug,
|
||||||
entdecke passende Pakete und frag unverbindlich an – wir kümmern uns um den Rest.
|
entdecke passende Pakete und frag unverbindlich an – wir kümmern uns um den Rest.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -175,25 +175,25 @@ export default function Home() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-b border-slate-200 bg-brand-500 py-8 dark:border-slate-800">
|
<section className="border-b border-neutral-200 bg-neutral-950 py-8 dark:border-neutral-800">
|
||||||
<div className="mx-auto grid max-w-6xl grid-cols-2 gap-6 px-4 text-center sm:grid-cols-4 sm:px-6">
|
<div className="mx-auto grid max-w-6xl grid-cols-2 gap-6 px-4 text-center sm:grid-cols-4 sm:px-6">
|
||||||
{stats.map((stat, i) => (
|
{stats.map((stat, i) => (
|
||||||
<Reveal key={stat.label} index={i}>
|
<Reveal key={stat.label} index={i}>
|
||||||
<p className="text-2xl font-extrabold text-white sm:text-3xl">{stat.value}</p>
|
<p className="text-2xl font-extrabold text-brand-400 sm:text-3xl">{stat.value}</p>
|
||||||
<p className="text-sm text-brand-50">{stat.label}</p>
|
<p className="text-sm text-neutral-400">{stat.label}</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-b border-slate-200 py-8 dark:border-slate-800">
|
<section className="border-b border-neutral-200 py-8 dark:border-neutral-800">
|
||||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||||
<p className="mb-4 text-center text-xs font-semibold uppercase tracking-wide text-slate-400">
|
<p className="mb-4 text-center text-xs font-semibold uppercase tracking-wide text-neutral-400">
|
||||||
Marken, mit denen wir arbeiten
|
Marken, mit denen wir arbeiten
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
|
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
|
||||||
{brands.map((brand, i) => (
|
{brands.map((brand, i) => (
|
||||||
<Reveal key={brand} index={i} className="text-lg font-bold text-slate-400 dark:text-slate-600">
|
<Reveal key={brand} index={i} className="text-lg font-bold text-neutral-400 dark:text-neutral-600">
|
||||||
{brand}
|
{brand}
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
|
|
@ -203,24 +203,24 @@ export default function Home() {
|
||||||
|
|
||||||
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
|
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<h2 className="mb-8 text-center text-2xl font-bold text-slate-900 dark:text-white">So funktioniert's</h2>
|
<h2 className="mb-8 text-center text-2xl font-bold text-neutral-900 dark:text-white">So funktioniert's</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div className="grid gap-6 sm:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-3">
|
||||||
{steps.map((step, i) => (
|
{steps.map((step, i) => (
|
||||||
<Reveal key={step.title} index={i} className="rounded-xl border border-slate-200 bg-white p-6 text-center dark:border-slate-800 dark:bg-slate-900">
|
<Reveal key={step.title} index={i} className="rounded-xl border border-neutral-200 bg-white p-6 text-center dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mx-auto mb-4 flex h-10 w-10 items-center justify-center rounded-full bg-brand-500 text-lg font-bold text-white">
|
<div className="mx-auto mb-4 flex h-10 w-10 items-center justify-center rounded-full bg-brand-500 text-lg font-bold text-white">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-1 font-semibold text-slate-900 dark:text-white">{step.title}</h3>
|
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-white">{step.title}</h3>
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">{step.text}</p>
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">{step.text}</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="relative overflow-hidden bg-gradient-to-br from-red-700 via-red-800 to-slate-950 py-20 sm:py-28">
|
<section className="relative overflow-hidden bg-neutral-950 py-20 sm:py-28">
|
||||||
<div className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full bg-red-500/30 blur-3xl" />
|
<div className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full bg-brand-500/25 blur-3xl" />
|
||||||
<div className="pointer-events-none absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-red-500/20 blur-3xl" />
|
<div className="pointer-events-none absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-brand-500/15 blur-3xl" />
|
||||||
|
|
||||||
<div className="relative mx-auto grid max-w-6xl gap-12 px-4 sm:px-6 lg:grid-cols-2 lg:items-center">
|
<div className="relative mx-auto grid max-w-6xl gap-12 px-4 sm:px-6 lg:grid-cols-2 lg:items-center">
|
||||||
<Reveal direction="left" className="text-center lg:text-left">
|
<Reveal direction="left" className="text-center lg:text-left">
|
||||||
|
|
@ -231,10 +231,10 @@ export default function Home() {
|
||||||
HifiPlanet auf YouTube
|
HifiPlanet auf YouTube
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">Sieh dir unsere Umbauten in Aktion an</h2>
|
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">Sieh dir unsere Umbauten in Aktion an</h2>
|
||||||
<p className="mx-auto mt-4 max-w-xl text-lg text-red-50/90 lg:mx-0">
|
<p className="mx-auto mt-4 max-w-xl text-lg text-neutral-300 lg:mx-0">
|
||||||
Umbauten, Soundchecks und Verstärker-Tests aus unserer Werkstatt – „Den besten Sound gibt es nicht ab Werk!"
|
Umbauten, Soundchecks und Verstärker-Tests aus unserer Werkstatt – „Den besten Sound gibt es nicht ab Werk!"
|
||||||
</p>
|
</p>
|
||||||
<ul className="mx-auto mt-6 flex max-w-xl flex-col items-center gap-2 text-sm text-red-50/90 sm:flex-row sm:flex-wrap sm:justify-center lg:mx-0 lg:justify-start">
|
<ul className="mx-auto mt-6 flex max-w-xl flex-col items-center gap-2 text-sm text-neutral-300 sm:flex-row sm:flex-wrap sm:justify-center lg:mx-0 lg:justify-start">
|
||||||
{['Umbauten Schritt für Schritt', 'Soundchecks & Vergleiche', 'Verstärker- & Komponenten-Tests'].map((item) => (
|
{['Umbauten Schritt für Schritt', 'Soundchecks & Vergleiche', 'Verstärker- & Komponenten-Tests'].map((item) => (
|
||||||
<li key={item} className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
|
<li key={item} className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-3.5 w-3.5 flex-none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-3.5 w-3.5 flex-none">
|
||||||
|
|
@ -248,7 +248,7 @@ export default function Home() {
|
||||||
href={YOUTUBE_URL}
|
href={YOUTUBE_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="mt-8 inline-flex items-center gap-2 rounded-md bg-white px-6 py-3 text-sm font-bold text-red-700 shadow-lg shadow-black/20 transition hover:-translate-y-0.5 hover:bg-red-50"
|
className="mt-8 inline-flex items-center gap-2 rounded-md bg-brand-500 px-6 py-3 text-sm font-bold text-white shadow-lg shadow-black/40 transition hover:-translate-y-0.5 hover:bg-brand-400"
|
||||||
>
|
>
|
||||||
Zum YouTube-Kanal
|
Zum YouTube-Kanal
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-4 w-4">
|
||||||
|
|
@ -274,7 +274,7 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-y border-slate-200 bg-slate-50 py-16 dark:border-slate-800 dark:bg-slate-900/40">
|
<section className="border-y border-neutral-200 bg-neutral-50 py-16 dark:border-neutral-800 dark:bg-neutral-900/40">
|
||||||
<div className="mx-auto max-w-6xl space-y-16 overflow-hidden px-4 sm:px-6">
|
<div className="mx-auto max-w-6xl space-y-16 overflow-hidden px-4 sm:px-6">
|
||||||
<div className="grid items-center gap-8 md:grid-cols-2">
|
<div className="grid items-center gap-8 md:grid-cols-2">
|
||||||
<Reveal direction="left" shine className="rounded-xl shadow-lg">
|
<Reveal direction="left" shine className="rounded-xl shadow-lg">
|
||||||
|
|
@ -282,8 +282,8 @@ export default function Home() {
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal direction="right" index={1}>
|
<Reveal direction="right" index={1}>
|
||||||
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-brand-600 dark:text-brand-400">Handwerk</p>
|
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-brand-600 dark:text-brand-400">Handwerk</p>
|
||||||
<h2 className="mb-3 text-2xl font-bold text-slate-900 dark:text-white">Präzisionsarbeit in jedem Detail</h2>
|
<h2 className="mb-3 text-2xl font-bold text-neutral-900 dark:text-white">Präzisionsarbeit in jedem Detail</h2>
|
||||||
<p className="text-slate-600 dark:text-slate-300">
|
<p className="text-neutral-600 dark:text-neutral-300">
|
||||||
Ob eigens gefertigte Subwoofer-Gehäuse, integrierte Beleuchtung oder unsichtbar verlegte Kabelwege –
|
Ob eigens gefertigte Subwoofer-Gehäuse, integrierte Beleuchtung oder unsichtbar verlegte Kabelwege –
|
||||||
dank eigener CNC-, Laser- und 3D-Druck-Fertigung entstehen bei uns Lösungen, die es von der Stange nicht gibt.
|
dank eigener CNC-, Laser- und 3D-Druck-Fertigung entstehen bei uns Lösungen, die es von der Stange nicht gibt.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -293,8 +293,8 @@ export default function Home() {
|
||||||
<div className="grid items-center gap-8 md:grid-cols-2">
|
<div className="grid items-center gap-8 md:grid-cols-2">
|
||||||
<Reveal direction="left" className="order-2 md:order-1">
|
<Reveal direction="left" className="order-2 md:order-1">
|
||||||
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-brand-600 dark:text-brand-400">Beratung</p>
|
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-brand-600 dark:text-brand-400">Beratung</p>
|
||||||
<h2 className="mb-3 text-2xl font-bold text-slate-900 dark:text-white">Von der ersten Idee bis zum letzten Schliff</h2>
|
<h2 className="mb-3 text-2xl font-bold text-neutral-900 dark:text-white">Von der ersten Idee bis zum letzten Schliff</h2>
|
||||||
<p className="text-slate-600 dark:text-slate-300">
|
<p className="text-neutral-600 dark:text-neutral-300">
|
||||||
Jedes Fahrzeug und jeder Anspruch ist anders. Deshalb beraten wir dich persönlich und unverbindlich –
|
Jedes Fahrzeug und jeder Anspruch ist anders. Deshalb beraten wir dich persönlich und unverbindlich –
|
||||||
vom dezenten Upgrade bis zum kompromisslosen High-End-System.
|
vom dezenten Upgrade bis zum kompromisslosen High-End-System.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -308,28 +308,28 @@ export default function Home() {
|
||||||
|
|
||||||
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
|
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
|
||||||
<Reveal className="mb-8 text-center">
|
<Reveal className="mb-8 text-center">
|
||||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Einblicke in unsere Umbauten</h2>
|
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">Einblicke in unsere Umbauten</h2>
|
||||||
<p className="mt-2 text-slate-600 dark:text-slate-300">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-300">
|
||||||
Vom unauffälligen Sound-Upgrade bis zum aufwendigen Komplettumbau – Handarbeit aus unserer Werkstatt in Amorbach.
|
Vom unauffälligen Sound-Upgrade bis zum aufwendigen Komplettumbau – Handarbeit aus unserer Werkstatt in Amorbach.
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||||
{gallery.map((img, i) => (
|
{gallery.map((img, i) => (
|
||||||
<Reveal key={img.src} index={i % 3} shine className="aspect-square overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
<Reveal key={img.src} index={i % 3} shine className="aspect-square overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<img src={img.src} alt={img.alt} loading="lazy" className="h-full w-full object-cover transition duration-300 hover:scale-105" />
|
<img src={img.src} alt={img.alt} loading="lazy" className="h-full w-full object-cover transition duration-300 hover:scale-105" />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-y border-slate-200 bg-slate-50 py-16 dark:border-slate-800 dark:bg-slate-900/40">
|
<section className="border-y border-neutral-200 bg-neutral-50 py-16 dark:border-neutral-800 dark:bg-neutral-900/40">
|
||||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||||
<Reveal className="mb-10 text-center">
|
<Reveal className="mb-10 text-center">
|
||||||
<div className="mb-2 flex items-center justify-center gap-2">
|
<div className="mb-2 flex items-center justify-center gap-2">
|
||||||
<StarRating className="h-6 w-6" />
|
<StarRating className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">5,0 von 181 Kunden bewertet</h2>
|
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">5,0 von 181 Kunden bewertet</h2>
|
||||||
<p className="mt-1 text-slate-600 dark:text-slate-300">Das sagen unsere Kunden über uns.</p>
|
<p className="mt-1 text-neutral-600 dark:text-neutral-300">Das sagen unsere Kunden über uns.</p>
|
||||||
<a
|
<a
|
||||||
href="https://www.google.com/maps/search/?api=1&query=Hifi+Planet+Amorbach+Boxbrunner+Stra%C3%9Fe+20a"
|
href="https://www.google.com/maps/search/?api=1&query=Hifi+Planet+Amorbach+Boxbrunner+Stra%C3%9Fe+20a"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -351,14 +351,14 @@ export default function Home() {
|
||||||
<img src={heroHighlight} alt="Hochwertige Endstufe und Subwoofer, Studioaufnahme" className="h-full w-full object-cover" loading="lazy" />
|
<img src={heroHighlight} alt="Hochwertige Endstufe und Subwoofer, Studioaufnahme" className="h-full w-full object-cover" loading="lazy" />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal direction="right" index={1} className="text-center md:text-left">
|
<Reveal direction="right" index={1} className="text-center md:text-left">
|
||||||
<h2 className="mb-3 text-2xl font-bold text-slate-900 dark:text-white">Mehr als nur Car-Hifi</h2>
|
<h2 className="mb-3 text-2xl font-bold text-neutral-900 dark:text-white">Mehr als nur Car-Hifi</h2>
|
||||||
<p className="mx-auto mb-8 max-w-2xl text-slate-600 dark:text-slate-300 md:mx-0">
|
<p className="mx-auto mb-8 max-w-2xl text-neutral-600 dark:text-neutral-300 md:mx-0">
|
||||||
Neben individuellen Sound-Umbauten bieten wir CNC-Zerspanung, Lasertechnik, 3D-Druck und mehr –
|
Neben individuellen Sound-Umbauten bieten wir CNC-Zerspanung, Lasertechnik, 3D-Druck und mehr –
|
||||||
alles aus einer Hand in unserer eigenen Werkstatt.
|
alles aus einer Hand in unserer eigenen Werkstatt.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/leistungen"
|
to="/leistungen"
|
||||||
className="inline-block rounded-md border border-slate-300 px-6 py-3 text-sm font-semibold text-slate-700 transition hover:-translate-y-0.5 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
className="inline-block rounded-md border border-neutral-300 px-6 py-3 text-sm font-semibold text-neutral-700 transition hover:-translate-y-0.5 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
Alle Leistungen entdecken
|
Alle Leistungen entdecken
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -366,10 +366,10 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-t border-slate-200 py-16 dark:border-slate-800">
|
<section className="border-t border-neutral-200 py-16 dark:border-neutral-800">
|
||||||
<div className="mx-auto max-w-3xl px-4 sm:px-6">
|
<div className="mx-auto max-w-3xl px-4 sm:px-6">
|
||||||
<Reveal className="mb-8 text-center">
|
<Reveal className="mb-8 text-center">
|
||||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Häufig gestellte Fragen</h2>
|
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">Häufig gestellte Fragen</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal index={1}>
|
<Reveal index={1}>
|
||||||
<Accordion items={faqs} />
|
<Accordion items={faqs} />
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ export default function Impressum() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6">
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">Impressum</h1>
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">Impressum</h1>
|
||||||
|
|
||||||
<div className="space-y-8 text-sm text-slate-600 dark:text-slate-300">
|
<div className="space-y-8 text-sm text-neutral-600 dark:text-neutral-300">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Angaben gemäß § 5 DDG</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Angaben gemäß § 5 DDG</h2>
|
||||||
<p>
|
<p>
|
||||||
Hifi Planet e.K.
|
Hifi Planet e.K.
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -26,7 +26,7 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Kontakt</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Kontakt</h2>
|
||||||
<p>
|
<p>
|
||||||
Telefon: <a href="tel:+4993732062390" className="hover:text-brand-500">09373 20 62 390</a>
|
Telefon: <a href="tel:+4993732062390" className="hover:text-brand-500">09373 20 62 390</a>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -35,19 +35,19 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Umsatzsteuer-ID</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Umsatzsteuer-ID</h2>
|
||||||
<p>
|
<p>
|
||||||
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz: DE 275084766
|
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz: DE 275084766
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Zuständige Kammer</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Zuständige Kammer</h2>
|
||||||
<p>IHK Aschaffenburg</p>
|
<p>IHK Aschaffenburg</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
|
||||||
<p>
|
<p>
|
||||||
Christopher Horndasch
|
Christopher Horndasch
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -56,7 +56,7 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Verbraucherstreitbeilegung</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Verbraucherstreitbeilegung</h2>
|
||||||
<p>
|
<p>
|
||||||
Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer
|
Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||||
Verbraucherschlichtungsstelle im Sinne des Verbraucherstreitbeilegungsgesetzes (VSBG) teilzunehmen.
|
Verbraucherschlichtungsstelle im Sinne des Verbraucherstreitbeilegungsgesetzes (VSBG) teilzunehmen.
|
||||||
|
|
@ -64,7 +64,7 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Haftung für Inhalte</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Haftung für Inhalte</h2>
|
||||||
<p>
|
<p>
|
||||||
Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den
|
Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den
|
||||||
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht
|
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht
|
||||||
|
|
@ -78,7 +78,7 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Haftung für Links</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Haftung für Links</h2>
|
||||||
<p>
|
<p>
|
||||||
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben.
|
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben.
|
||||||
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
|
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
|
||||||
|
|
@ -90,7 +90,7 @@ export default function Impressum() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">Urheberrecht</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">Urheberrecht</h2>
|
||||||
<p>
|
<p>
|
||||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
|
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
|
||||||
Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
|
Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ export default function Leistungen() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
||||||
{maintenance.services.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
{maintenance.services.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
||||||
<h1 className="mb-2 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">Unsere Leistungen</h1>
|
<h1 className="mb-2 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">Unsere Leistungen</h1>
|
||||||
<p className="mb-10 max-w-2xl text-slate-600 dark:text-slate-300">
|
<p className="mb-10 max-w-2xl text-neutral-600 dark:text-neutral-300">
|
||||||
Von individuellen Sound-Umbauten bis zur eigenen CNC- und 3D-Druck-Fertigung – alles aus einer Hand
|
Von individuellen Sound-Umbauten bis zur eigenen CNC- und 3D-Druck-Fertigung – alles aus einer Hand
|
||||||
in unserer Werkstatt in Amorbach.
|
in unserer Werkstatt in Amorbach.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -42,19 +42,19 @@ export default function Leistungen() {
|
||||||
key={service.id}
|
key={service.id}
|
||||||
index={i % 3}
|
index={i % 3}
|
||||||
shine
|
shine
|
||||||
className="overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
|
className="overflow-hidden rounded-xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[4/3]">
|
<div className="relative aspect-[4/3]">
|
||||||
{service.image_path && (
|
{service.image_path && (
|
||||||
<img src={service.image_path} alt={service.title} className="h-full w-full object-cover" loading="lazy" />
|
<img src={service.image_path} alt={service.title} className="h-full w-full object-cover" loading="lazy" />
|
||||||
)}
|
)}
|
||||||
<div className="absolute -bottom-5 left-5 flex h-12 w-12 items-center justify-center rounded-full bg-brand-500 text-white shadow-lg ring-4 ring-white dark:ring-slate-900">
|
<div className="absolute -bottom-5 left-5 flex h-12 w-12 items-center justify-center rounded-full bg-brand-500 text-white shadow-lg ring-4 ring-white dark:ring-neutral-900">
|
||||||
<DynamicIcon name={service.icon_name} className="h-6 w-6" />
|
<DynamicIcon name={service.icon_name} className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 pt-8">
|
<div className="p-6 pt-8">
|
||||||
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">{service.title}</h2>
|
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">{service.title}</h2>
|
||||||
<p className="mb-3 text-sm text-slate-600 dark:text-slate-300">{service.description}</p>
|
<p className="mb-3 text-sm text-neutral-600 dark:text-neutral-300">{service.description}</p>
|
||||||
{service.cta_label && service.cta_url && (
|
{service.cta_label && service.cta_url && (
|
||||||
<Link to={service.cta_url} className="text-sm font-semibold text-brand-600 hover:underline dark:text-brand-400">
|
<Link to={service.cta_url} className="text-sm font-semibold text-brand-600 hover:underline dark:text-brand-400">
|
||||||
{service.cta_label} →
|
{service.cta_label} →
|
||||||
|
|
@ -65,9 +65,9 @@ export default function Leistungen() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 rounded-xl border border-slate-200 bg-slate-50 p-8 text-center dark:border-slate-800 dark:bg-slate-900/40">
|
<div className="mt-12 rounded-xl border border-neutral-200 bg-neutral-50 p-8 text-center dark:border-neutral-800 dark:bg-neutral-900/40">
|
||||||
<h2 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Dein Projekt ist nicht dabei?</h2>
|
<h2 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Dein Projekt ist nicht dabei?</h2>
|
||||||
<p className="mb-5 text-slate-600 dark:text-slate-300">Sprich uns einfach an – wir finden gemeinsam die passende Lösung.</p>
|
<p className="mb-5 text-neutral-600 dark:text-neutral-300">Sprich uns einfach an – wir finden gemeinsam die passende Lösung.</p>
|
||||||
<Link to="/kontakt" className="inline-block rounded-md bg-brand-500 px-6 py-3 text-sm font-semibold text-white hover:bg-brand-600">
|
<Link to="/kontakt" className="inline-block rounded-md bg-brand-500 px-6 py-3 text-sm font-semibold text-white hover:bg-brand-600">
|
||||||
Kontakt aufnehmen
|
Kontakt aufnehmen
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function ModelPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <p className="mx-auto max-w-6xl px-4 py-12 text-slate-500 sm:px-6">Lädt…</p>;
|
return <p className="mx-auto max-w-6xl px-4 py-12 text-neutral-500 sm:px-6">Lädt…</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { model, packages } = data;
|
const { model, packages } = data;
|
||||||
|
|
@ -59,30 +59,30 @@ export default function ModelPage() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
||||||
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
||||||
<p className="mb-1 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link> /{' '}
|
<Link to="/fahrzeuge" className="hover:text-brand-500">Fahrzeuge</Link> /{' '}
|
||||||
<Link to={`/fahrzeuge/${brandSlug}`} className="hover:text-brand-500">{model.brand_name}</Link> / {model.name}
|
<Link to={`/fahrzeuge/${brandSlug}`} className="hover:text-brand-500">{model.brand_name}</Link> / {model.name}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">
|
<h1 className="mb-8 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">
|
||||||
{model.brand_name} {model.name} – Sound-Pakete
|
{model.brand_name} {model.name} – Sound-Pakete
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{packages.length === 0 && (
|
{packages.length === 0 && (
|
||||||
<p className="text-slate-500 dark:text-slate-400">Für dieses Modell sind noch keine Pakete hinterlegt.</p>
|
<p className="text-neutral-500 dark:text-neutral-400">Für dieses Modell sind noch keine Pakete hinterlegt.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{packages.map((pkg) => (
|
{packages.map((pkg) => (
|
||||||
<div key={pkg.id} className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<div key={pkg.id} className="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
|
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
|
||||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">{pkg.name}</h2>
|
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">{pkg.name}</h2>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs uppercase tracking-wide text-slate-400">Gesamtpreis (ca.)</p>
|
<p className="text-xs uppercase tracking-wide text-neutral-400">Gesamtpreis (ca.)</p>
|
||||||
<p className="text-xl font-extrabold text-brand-600 dark:text-brand-400">{formatPrice(pkg.total_price)}</p>
|
<p className="text-xl font-extrabold text-brand-600 dark:text-brand-400">{formatPrice(pkg.total_price)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mb-5 list-disc space-y-1 pl-5 text-sm text-slate-700 dark:text-slate-300">
|
<ul className="mb-5 list-disc space-y-1 pl-5 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
{pkg.products.map((product) => (
|
{pkg.products.map((product) => (
|
||||||
<li key={product.id}>
|
<li key={product.id}>
|
||||||
{product.name_override || product.scraped_name || 'Produkt wird geladen…'}
|
{product.name_override || product.scraped_name || 'Produkt wird geladen…'}
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@ export default function SetupWizard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
'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';
|
'w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900';
|
||||||
const labelClass = 'mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300';
|
const labelClass = 'mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300';
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ key: 'database', label: 'Datenbank' },
|
{ key: 'database', label: 'Datenbank' },
|
||||||
|
|
@ -118,16 +118,16 @@ export default function SetupWizard() {
|
||||||
const currentStepIndex = steps.findIndex((s) => s.key === phase);
|
const currentStepIndex = steps.findIndex((s) => s.key === phase);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-slate-50 px-4 py-12 dark:bg-slate-950">
|
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-neutral-50 px-4 py-12 dark:bg-neutral-950">
|
||||||
<img src={logo} alt="HifiPlanet" className="h-16 w-auto sm:h-20" />
|
<img src={logo} alt="HifiPlanet" className="h-16 w-auto sm:h-20" />
|
||||||
|
|
||||||
<div className="w-full max-w-md rounded-xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<div className="w-full max-w-md rounded-xl border border-neutral-200 bg-white p-8 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
{phase === 'loading' && <p className="text-center text-sm text-slate-400">Lädt…</p>}
|
{phase === 'loading' && <p className="text-center text-sm text-neutral-400">Lädt…</p>}
|
||||||
|
|
||||||
{phase === 'locked' && (
|
{phase === 'locked' && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Bereits eingerichtet</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Bereits eingerichtet</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Diese Seite wurde bereits vollständig eingerichtet. Der Assistent ist deshalb gesperrt.
|
Diese Seite wurde bereits vollständig eingerichtet. Der Assistent ist deshalb gesperrt.
|
||||||
</p>
|
</p>
|
||||||
<Link to="/admin/login" className="text-sm font-medium text-brand-600 hover:underline dark:text-brand-400">
|
<Link to="/admin/login" className="text-sm font-medium text-brand-600 hover:underline dark:text-brand-400">
|
||||||
|
|
@ -146,12 +146,12 @@ export default function SetupWizard() {
|
||||||
? 'bg-green-500 text-white'
|
? 'bg-green-500 text-white'
|
||||||
: i === currentStepIndex
|
: i === currentStepIndex
|
||||||
? 'bg-brand-500 text-white'
|
? 'bg-brand-500 text-white'
|
||||||
: 'bg-slate-200 text-slate-500 dark:bg-slate-700 dark:text-slate-400'
|
: 'bg-neutral-200 text-neutral-500 dark:bg-neutral-700 dark:text-neutral-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i < currentStepIndex ? '✓' : i + 1}
|
{i < currentStepIndex ? '✓' : i + 1}
|
||||||
</span>
|
</span>
|
||||||
{i < steps.length - 1 && <span className="h-px w-6 bg-slate-300 dark:bg-slate-700" />}
|
{i < steps.length - 1 && <span className="h-px w-6 bg-neutral-300 dark:bg-neutral-700" />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -159,8 +159,8 @@ export default function SetupWizard() {
|
||||||
|
|
||||||
{phase === 'password' && (
|
{phase === 'password' && (
|
||||||
<form onSubmit={handlePassword}>
|
<form onSubmit={handlePassword}>
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Ersteinrichtung</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Ersteinrichtung</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Gib das Einmalpasswort ein, das in <code>config/setup.php</code> hinterlegt ist.
|
Gib das Einmalpasswort ein, das in <code>config/setup.php</code> hinterlegt ist.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
@ -187,8 +187,8 @@ export default function SetupWizard() {
|
||||||
|
|
||||||
{phase === 'database' && (
|
{phase === 'database' && (
|
||||||
<form onSubmit={handleDatabase}>
|
<form onSubmit={handleDatabase}>
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Datenbank-Zugangsdaten</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Datenbank-Zugangsdaten</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Trage die Zugangsdaten der Datenbank auf diesem Server ein. Die Verbindung wird vor dem Speichern
|
Trage die Zugangsdaten der Datenbank auf diesem Server ein. Die Verbindung wird vor dem Speichern
|
||||||
getestet.
|
getestet.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -249,12 +249,12 @@ export default function SetupWizard() {
|
||||||
|
|
||||||
{phase === 'schema' && (
|
{phase === 'schema' && (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Datenbankstruktur</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Datenbankstruktur</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Die Datenbankverbindung funktioniert. Jetzt legen wir die benötigten Tabellen an.
|
Die Datenbankverbindung funktioniert. Jetzt legen wir die benötigten Tabellen an.
|
||||||
</p>
|
</p>
|
||||||
{migrateActions && (
|
{migrateActions && (
|
||||||
<ul className="mb-4 space-y-1 rounded-md bg-slate-50 p-3 text-sm text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
<ul className="mb-4 space-y-1 rounded-md bg-neutral-50 p-3 text-sm text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
|
||||||
{migrateActions.map((a, i) => (
|
{migrateActions.map((a, i) => (
|
||||||
<li key={i}>{a}</li>
|
<li key={i}>{a}</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -274,8 +274,8 @@ export default function SetupWizard() {
|
||||||
|
|
||||||
{phase === 'admin' && (
|
{phase === 'admin' && (
|
||||||
<form onSubmit={handleAdmin}>
|
<form onSubmit={handleAdmin}>
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Erster Admin-Account</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Erster Admin-Account</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Lege den ersten Super-Admin-Account an, mit dem du dich anschließend einloggen kannst.
|
Lege den ersten Super-Admin-Account an, mit dem du dich anschließend einloggen kannst.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|
@ -323,8 +323,8 @@ export default function SetupWizard() {
|
||||||
|
|
||||||
{phase === 'done' && (
|
{phase === 'done' && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Fertig!</h1>
|
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Fertig!</h1>
|
||||||
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
Die Ersteinrichtung ist abgeschlossen. Du kannst dich jetzt mit deinem neuen Admin-Account anmelden.
|
Die Ersteinrichtung ist abgeschlossen. Du kannst dich jetzt mit deinem neuen Admin-Account anmelden.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,15 @@ export default function VehicleSelect() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
||||||
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
{maintenance.vehicles.enabled && maintenance.bypass && <MaintenanceBypassBanner inline />}
|
||||||
<h1 className="mb-2 text-2xl font-bold text-slate-900 dark:text-white sm:text-3xl">Fahrzeug auswählen</h1>
|
<h1 className="mb-2 text-2xl font-bold text-neutral-900 dark:text-white sm:text-3xl">Fahrzeug auswählen</h1>
|
||||||
<p className="mb-8 text-slate-600 dark:text-slate-300">
|
<p className="mb-8 text-neutral-600 dark:text-neutral-300">
|
||||||
Wähle zuerst deine Marke, dann dein Modell – wir zeigen dir direkt die passenden Sound-Pakete.
|
Wähle zuerst deine Marke, dann dein Modell – wir zeigen dir direkt die passenden Sound-Pakete.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && <p className="text-red-600">{error}</p>}
|
{error && <p className="text-red-600">{error}</p>}
|
||||||
|
|
||||||
{brands.length === 0 && !error && (
|
{brands.length === 0 && !error && (
|
||||||
<p className="text-slate-500 dark:text-slate-400">Es sind noch keine Marken hinterlegt.</p>
|
<p className="text-neutral-500 dark:text-neutral-400">Es sind noch keine Marken hinterlegt.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||||
|
|
@ -45,7 +45,7 @@ export default function VehicleSelect() {
|
||||||
<Link
|
<Link
|
||||||
key={brand.id}
|
key={brand.id}
|
||||||
to={`/fahrzeuge/${brand.slug}`}
|
to={`/fahrzeuge/${brand.slug}`}
|
||||||
className="flex items-center justify-center rounded-xl border border-slate-200 bg-white px-6 py-8 text-center font-semibold text-slate-800 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100"
|
className="flex items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 py-8 text-center font-semibold text-neutral-800 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-500 hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100"
|
||||||
>
|
>
|
||||||
{brand.name}
|
{brand.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue