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:
Maaxxs 2026-07-06 16:21:48 +02:00
parent 95764039c7
commit b6909e756c
43 changed files with 609 additions and 566 deletions

View file

@ -5,7 +5,7 @@ export default function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState(null);
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) => {
const isOpen = openIndex === i;
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"
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
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
@ -24,7 +24,7 @@ export default function Accordion({ items }) {
fill="none"
stroke="currentColor"
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" />
</motion.svg>
@ -38,7 +38,7 @@ export default function Accordion({ items }) {
transition={{ duration: 0.25, ease: 'easeInOut' }}
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>
)}
</AnimatePresence>

View file

@ -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 ${
isActive
? '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 }) => (
@ -92,7 +92,7 @@ export default function AdminLayout() {
{navGroups.map((group, i) => (
<div key={i}>
{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}
</p>
)}
@ -115,25 +115,25 @@ export default function AdminLayout() {
);
return (
<div className="flex min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<aside className="hidden w-60 shrink-0 border-r border-slate-200 p-4 dark:border-slate-800 sm:block">
<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-neutral-200 p-4 dark:border-neutral-800 sm:block">
<div className="mb-6 flex items-center gap-2 px-3">
<span className="rounded-lg px-2 py-1 dark:bg-white">
<img src={logo} alt="HifiPlanet" className="h-7 w-auto" />
</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>
<NavLinks />
</aside>
<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 gap-2 sm:hidden">
<button
onClick={() => setMobileOpen((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"
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">
<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" />
</span>
</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' : ''}
</p>
<div className="flex items-center gap-2">
<ThemeToggle />
<button
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
</button>
</div>
</div>
{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)} />
</div>
)}

View file

@ -12,11 +12,11 @@ function SettingsModal({ onClose }) {
onClick={onClose}
>
<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()}
>
<h2 className="mb-1 text-lg font-bold text-slate-900 dark:text-white">Cookie-Einstellungen</h2>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
<h2 className="mb-1 text-lg font-bold text-neutral-900 dark:text-white">Cookie-Einstellungen</h2>
<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{' '}
<Link to="/datenschutz" className="underline hover:text-brand-500" onClick={onClose}>
Datenschutzerklärung
@ -24,20 +24,20 @@ function SettingsModal({ onClose }) {
.
</p>
<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>
<p className="text-sm font-semibold text-slate-800 dark:text-slate-100">Notwendig</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
<p className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">Notwendig</p>
<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).
Kann nicht deaktiviert werden.
</p>
</div>
<input type="checkbox" checked disabled className="mt-1 h-4 w-4 shrink-0" />
</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>
<p className="text-sm font-semibold text-slate-800 dark:text-slate-100">Externe Medien</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
<p className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">Externe Medien</p>
<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
übertragen.
</p>
@ -54,7 +54,7 @@ function SettingsModal({ onClose }) {
<button
type="button"
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
</button>
@ -77,9 +77,9 @@ export default function CookieConsentBanner() {
return (
<>
{!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">
<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
zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden. Mehr dazu in unserer{' '}
<Link to="/datenschutz" className="underline hover:text-brand-500">
@ -91,14 +91,14 @@ export default function CookieConsentBanner() {
<button
type="button"
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
</button>
<button
type="button"
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
</button>

View file

@ -9,9 +9,9 @@ export default function ExternalEmbed({ name, description, className = '', child
return (
<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.`}
</p>
<div className="flex flex-wrap justify-center gap-2">
@ -25,7 +25,7 @@ export default function ExternalEmbed({ name, description, className = '', child
<button
type="button"
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
</button>

View file

@ -34,15 +34,15 @@ export default function Footer() {
return (
<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' : ''
}`}
>
<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">
<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>
<p>Boxbrunner Str. 20a</p>
<p>63916 Amorbach</p>
@ -57,10 +57,10 @@ export default function Footer() {
</a>
</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">
<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>
<p>
<a href={`tel:${digitsOnly(phone)}`} className="hover:text-brand-500">{phone}</a>
@ -70,40 +70,40 @@ export default function Footer() {
</p>
<Link
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
</Link>
</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="flex items-center gap-2">
<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>
<span
className={`flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold ${
open
? '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'}
</span>
</div>
<dl className="space-y-1.5">
{HOURS.map((row) => (
<div key={row.days} className="flex items-center justify-between gap-3">
<dt className="text-slate-500 dark:text-slate-400">{row.days}</dt>
<dd className="font-medium text-slate-700 dark:text-slate-200">{row.label}</dd>
<dt className="text-neutral-500 dark:text-neutral-400">{row.days}</dt>
<dd className="font-medium text-neutral-700 dark:text-neutral-200">{row.label}</dd>
</div>
))}
</dl>
</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">
<span>© {new Date().getFullYear()} HifiPlanet. Alle Angaben ohne Gewähr.</span>
<span>·</span>

View file

@ -18,13 +18,13 @@ export default function IconPicker({ value, onChange }) {
return (
<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">
{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 className="text-sm">
<p className="font-medium text-slate-800 dark:text-slate-100">{value || 'Kein Icon gewählt'}</p>
<p className="text-xs text-slate-400">{ALL_ICON_NAMES.length} Icons verfügbar</p>
<p className="font-medium text-neutral-800 dark:text-neutral-100">{value || 'Kein Icon gewählt'}</p>
<p className="text-xs text-neutral-400">{ALL_ICON_NAMES.length} Icons verfügbar</p>
</div>
</div>
@ -32,10 +32,10 @@ export default function IconPicker({ value, onChange }) {
value={search}
onChange={(e) => setSearch(e.target.value)}
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) => (
<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 ${
value === name
? '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" />
</button>
))}
{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>
{!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>
);

View file

@ -25,11 +25,11 @@ export default function ImageUploadField({ value, onChange, label = 'Bild' }) {
return (
<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">
{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" />
{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>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
</div>

View file

@ -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">
<WrenchIcon />
</div>
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Wartungsarbeiten</h1>
<p className="text-slate-600 dark:text-slate-300">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Wartungsarbeiten</h1>
<p className="text-neutral-600 dark:text-neutral-300">
{message || 'Dieser Bereich wird gerade aktualisiert. Bitte schau in Kürze wieder vorbei.'}
</p>
</div>
@ -19,7 +19,7 @@ export default function MaintenanceNotice({ message, fullScreen = false }) {
if (fullScreen) {
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}
</div>
);

View file

@ -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 ThemeToggle from './ThemeToggle.jsx';
import DynamicIcon from './DynamicIcon.jsx';
@ -12,25 +13,41 @@ const digitsOnly = (value) => (value || '').replace(/[^\d+]/g, '');
export default function Navbar() {
const [open, setOpen] = useState(false);
const [menuVisible, setMenuVisible] = useState(false);
const { user } = useAuth();
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 && (
<Link
to="/admin"
aria-label="Zum Admin-Panel"
title="Zum Admin-Panel"
className="rounded-full p-2 text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
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" />
</Link>
);
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,
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">
{phone && (
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-1.5 hover:text-brand-500">
@ -57,68 +74,94 @@ export default function Navbar() {
</div>
<div className="mx-auto grid max-w-6xl grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-3 sm:px-6">
<div className="flex items-center justify-self-start">
<nav className="hidden items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300 md:flex">
<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((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 className="flex items-center justify-self-start gap-1">
{AdminIcon}
<ThemeToggle />
</div>
<Link to="/" className="flex items-center justify-self-center rounded-lg px-2 py-1 dark:bg-white">
<img src={logo} alt="HifiPlanet" className="h-11 w-auto sm:h-14" />
</Link>
<div className="flex items-center justify-self-end gap-1">
{AdminIcon}
<ThemeToggle />
<div className="flex items-center justify-self-end">
<nav className="hidden items-center gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300 md:flex">
<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>
{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">
<Link to="/fahrzeuge" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Fahrzeuge</Link>
<Link to="/leistungen" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Leistungen</Link>
<Link to="/kontakt" onClick={() => setOpen(false)} className="rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">Kontakt</Link>
<div className="my-1 border-t border-slate-200 dark:border-slate-800" />
{phone && (
<a href={`tel:${digitsOnly(phone)}`} className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800">
<DynamicIcon name="phone" className="h-4 w-4" />
{phone}
</a>
)}
{whatsapp && (
{open &&
createPortal(
<div
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"
style={{ background: 'linear-gradient(135deg, #f1f5f9 0%, #e3f0cf 50%, #f8fafc 100%)' }}
>
<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 && (
<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)}>
{phone}
<DynamicIcon name="phone" className="h-4 w-4" />
</a>
)}
{whatsapp && (
<a
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-neutral-600 hover:text-brand-600 ${flyIn()}`}
style={flyInStyle(5)}
>
WhatsApp
<DynamicIcon name="message-circle" className="h-4 w-4" />
</a>
)}
<a
href={`https://wa.me/${digitsOnly(whatsapp).replace(/^0/, '49').replace('+', '')}`}
href={SHOP_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
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="message-circle" className="h-4 w-4" />
WhatsApp
Zum Shop
<DynamicIcon name="shopping-bag" className="h-4 w-4" />
</a>
)}
<a
href={SHOP_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-md px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-800"
>
<DynamicIcon name="shopping-bag" className="h-4 w-4" />
Zum Shop
</a>
</nav>
)}
</div>,
document.body
)}
</header>
);
}

View file

@ -5,7 +5,7 @@ export default function ProtectedRoute() {
const { user, loading } = useAuth();
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) {

View file

@ -42,7 +42,7 @@ export default function PublicLayout() {
}, []);
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');
@ -55,7 +55,7 @@ export default function PublicLayout() {
return (
<MaintenanceProvider value={{ ...status, bypass }}>
<SiteSettingsProvider value={siteSettings}>
<div className="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<div className="flex min-h-screen flex-col bg-white text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100">
{bypass && status.global.enabled && <MaintenanceBypassBanner />}
<ScrollProgress />
<Navbar />

View file

@ -8,9 +8,9 @@ export default function RequirePermission({ permission, children }) {
if (!allowed) {
return (
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center dark:border-slate-800 dark:bg-slate-900">
<h1 className="mb-2 text-lg font-bold text-slate-900 dark:text-white">Keine Berechtigung</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
<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-neutral-900 dark:text-white">Keine Berechtigung</h1>
<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
Zugriff brauchst.
</p>

View file

@ -33,7 +33,7 @@ export default function SetupGate({ children }) {
}, [location.pathname]);
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') {

View file

@ -123,10 +123,10 @@ export default function TestimonialSlider({ testimonials }) {
<div ref={trackRef} className="marquee-track flex w-max gap-5">
{track.map((t, i) => (
<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" />
<p className="mb-4 flex-1 text-sm text-slate-600 dark:text-slate-300">{t.text}"</p>
<p className="text-sm font-semibold text-slate-900 dark:text-white">{t.name}</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-neutral-900 dark:text-white">{t.name}</p>
</div>
</div>
))}

View file

@ -7,7 +7,7 @@ export default function ThemeToggle() {
<button
onClick={toggleTheme}
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' ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">

View file

@ -119,20 +119,20 @@ export default function AccountSettings() {
return (
<div className="max-w-xl">
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Mein Konto</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Mein Konto</h1>
<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)' : ''}.
</p>
<div className="mb-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Passwort ändern</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<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-neutral-900 dark:text-white">Passwort ändern</h2>
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
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.'}
</p>
<form onSubmit={handleChangePassword}>
<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
</label>
<input
@ -140,11 +140,11 @@ export default function AccountSettings() {
value={passwordForm.current_password}
onChange={(e) => setPasswordForm({ ...passwordForm, current_password: e.target.value })}
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 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
</label>
<input
@ -153,11 +153,11 @@ export default function AccountSettings() {
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
required
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 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
</label>
<input
@ -166,12 +166,12 @@ export default function AccountSettings() {
onChange={(e) => setPasswordForm({ ...passwordForm, new_password_confirmation: e.target.value })}
required
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>
{user?.two_factor_enabled && (
<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
</label>
<input
@ -180,7 +180,7 @@ export default function AccountSettings() {
required
inputMode="numeric"
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>
)}
@ -196,9 +196,9 @@ export default function AccountSettings() {
</form>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Zwei-Faktor-Authentifizierung</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<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-neutral-900 dark:text-white">Zwei-Faktor-Authentifizierung</h2>
<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.
</p>
@ -209,14 +209,14 @@ export default function AccountSettings() {
{user?.two_factor_enabled ? (
<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>
{user?.two_factor_enabled ? (
<div className="flex flex-wrap gap-2">
<button
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
</button>
@ -241,19 +241,19 @@ export default function AccountSettings() {
{phase === 'setup' && (
<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>Scanne den folgenden QR-Code:</li>
</ol>
<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>
<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 />
<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>
<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
</label>
<input
@ -263,7 +263,7 @@ export default function AccountSettings() {
autoFocus
inputMode="numeric"
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>
{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">
Bestätigen & aktivieren
</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
</button>
</div>
@ -280,14 +280,14 @@ export default function AccountSettings() {
{phase === 'recovery-codes' && (
<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!
</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.
Diese Codes werden nur jetzt angezeigt.
</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>)}
</div>
<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' && (
<form onSubmit={handleDisable}>
<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
</label>
<input
@ -308,7 +308,7 @@ export default function AccountSettings() {
onChange={(e) => setPassword(e.target.value)}
required
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>
{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">
2FA jetzt deaktivieren
</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
</button>
</div>
@ -325,11 +325,11 @@ export default function AccountSettings() {
{phase === 'regenerate' && (
<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.
</p>
<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
</label>
<input
@ -338,7 +338,7 @@ export default function AccountSettings() {
onChange={(e) => setPassword(e.target.value)}
required
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>
{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">
Neue Codes erzeugen
</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
</button>
</div>

View file

@ -86,13 +86,13 @@ export default function AdminUsers() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Benutzer</h1>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Benutzer</h1>
<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.
</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">
<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>
<th className="px-4 py-2">Benutzername</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>
</tr>
</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) => (
<tr key={u.id} className="bg-white dark:bg-slate-950">
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">
<tr key={u.id} className="bg-white dark:bg-neutral-950">
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">
{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 className="px-4 py-2">
{u.is_super_admin ? (
@ -113,13 +113,13 @@ export default function AdminUsers() {
Super-Admin
</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 className="px-4 py-2">
<div className="flex flex-wrap gap-1">
{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}
</span>
))}
@ -135,26 +135,26 @@ export default function AdminUsers() {
</tr>
))}
{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>
</table>
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}</h2>
<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
required
value={form.username}
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>
<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)' : '*'}
</label>
<input
@ -162,17 +162,17 @@ export default function AdminUsers() {
required={!editingId}
value={form.password}
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>
{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
type="checkbox"
checked={form.is_super_admin}
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)
</label>
@ -181,15 +181,15 @@ export default function AdminUsers() {
{!form.is_super_admin && (
<>
<div>
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Einzelne Berechtigungen</p>
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
<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-neutral-200 p-3 dark:border-neutral-700">
{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
type="checkbox"
checked={form.permissions.includes(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}
</label>
@ -198,20 +198,20 @@ export default function AdminUsers() {
</div>
<div>
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Berechtigungsgruppen</p>
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
<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-neutral-200 p-3 dark:border-neutral-700">
{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
type="checkbox"
checked={form.group_ids.includes(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}
</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>
</>
@ -224,7 +224,7 @@ export default function AdminUsers() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -51,20 +51,20 @@ export default function Brands() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<h1 className="mb-4 text-xl font-bold text-slate-900 dark:text-white">Marken</h1>
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
<h1 className="mb-4 text-xl font-bold text-neutral-900 dark:text-white">Marken</h1>
<div className="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800">
<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>
<th className="px-4 py-2">Name</th>
<th className="px-4 py-2">Sortierung</th>
<th className="px-4 py-2 text-right">Aktionen</th>
</tr>
</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) => (
<tr key={brand.id} className="bg-white dark:bg-slate-950">
<td className="px-4 py-2 font-medium text-slate-800 dark:text-slate-100">
<tr key={brand.id} className="bg-white dark:bg-neutral-950">
<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">
{brand.name}
</Link>
@ -77,31 +77,31 @@ export default function Brands() {
</tr>
))}
{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>
</table>
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Marke bearbeiten' : 'Neue Marke'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Marke bearbeiten' : 'Neue Marke'}</h2>
<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
required
value={form.name}
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>
<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
type="number"
value={form.sort_order}
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>
@ -112,7 +112,7 @@ export default function Brands() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -61,8 +61,8 @@ export default function ContactRequests() {
return (
<div>
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Kontaktanfragen</h1>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Kontaktanfragen</h1>
<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.
</p>
@ -74,7 +74,7 @@ export default function ContactRequests() {
className={`rounded-full px-4 py-1.5 text-sm font-medium transition ${
filter === tab.value
? '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]})
@ -84,14 +84,14 @@ export default function ContactRequests() {
<div className="space-y-3">
{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>
<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 && (
<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 className="flex items-center gap-2">
<select
@ -108,7 +108,7 @@ export default function ContactRequests() {
onClick={() => handleDelete(r.id)}
aria-label="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 />
</button>
@ -117,7 +117,7 @@ export default function ContactRequests() {
</div>
{(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(' · ')}
</p>
)}
@ -133,12 +133,12 @@ export default function ContactRequests() {
</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>
))}
{filtered.length === 0 && (
<p className="text-slate-400">
<p className="text-neutral-400">
{filter === 'all' ? 'Noch keine Kontaktanfragen eingegangen.' : 'Keine Anfragen mit diesem Status.'}
</p>
)}

View file

@ -40,12 +40,12 @@ export default function Dashboard() {
return (
<div>
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Willkommen, {user?.username}!</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<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-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.
</p>
{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.
</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 ${
card.highlight && card.value > 0
? '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">
<Icon path={ICONS[card.icon]} />
</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-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>
))}
</div>

View file

@ -50,30 +50,30 @@ export default function Login() {
};
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" />
{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">
<h1 className="mb-6 text-xl font-bold text-slate-900 dark:text-white">Admin-Login</h1>
<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-neutral-900 dark:text-white">Admin-Login</h1>
<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
value={username}
onChange={(e) => setUsername(e.target.value)}
required
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 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
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
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>
@ -88,15 +88,15 @@ export default function Login() {
</button>
</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">
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Bestätigungscode</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<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-neutral-900 dark:text-white">Bestätigungscode</h1>
<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
einen deiner Wiederherstellungscodes verwenden.
</p>
<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
value={code}
onChange={(e) => setCode(e.target.value)}
@ -104,7 +104,7 @@ export default function Login() {
autoFocus
inputMode="numeric"
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>
@ -120,7 +120,7 @@ export default function Login() {
<button
type="button"
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
</button>

View file

@ -59,14 +59,14 @@ export default function Models() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<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">
<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
value={brandFilter}
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>
{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
</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 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">
<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>
<th className="px-4 py-2">Marke</th>
<th className="px-4 py-2">Modell</th>
<th className="px-4 py-2 text-right">Aktionen</th>
</tr>
</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) => (
<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 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">
<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>
@ -100,7 +100,7 @@ export default function Models() {
</tr>
))}
{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.'}
</td></tr>
)}
@ -109,36 +109,36 @@ export default function Models() {
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Modell bearbeiten' : 'Neues Modell'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Modell bearbeiten' : 'Neues Modell'}</h2>
<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
required
value={form.brand_id}
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>
{brands.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
</select>
</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
required
value={form.name}
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>
<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
type="number"
value={form.sort_order}
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>
@ -149,7 +149,7 @@ export default function Models() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -62,26 +62,26 @@ export default function PackageProducts() {
<p className="mb-2 text-sm">
<Link to="/admin/packages" className="text-brand-600 hover:underline"> Zurück zu den Paketen</Link>
</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]">
<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
required
type="url"
placeholder="https://www.audio4cars.de/produkt/..."
value={sourceUrl}
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 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
value={nameOverride}
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>
<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>}
<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">
<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>
<th className="px-4 py-2">Produkt</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>
</tr>
</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) => (
<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">
<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}
</a>
{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}
</span>
</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') : ''}
</td>
<td className="px-4 py-2 text-right">
@ -129,7 +129,7 @@ export default function PackageProducts() {
</tr>
))}
{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>
</table>

View file

@ -59,25 +59,25 @@ export default function PackageUpgrades() {
<p className="mb-2 text-sm">
<Link to="/admin/packages" className="text-brand-600 hover:underline"> Zurück zu den Paketen</Link>
</p>
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Optionale Upgrades</h1>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Optionale Upgrades</h1>
<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.
</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">
<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>
<th className="px-4 py-2">Name</th>
<th className="px-4 py-2">Preis</th>
<th className="px-4 py-2 text-right">Aktionen</th>
</tr>
</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) => (
<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">
<p className="font-medium text-slate-800 dark:text-slate-100">{u.name}</p>
{u.description && <p className="text-xs text-slate-500 dark:text-slate-400">{u.description}</p>}
<p className="font-medium text-neutral-800 dark:text-neutral-100">{u.name}</p>
{u.description && <p className="text-xs text-neutral-500 dark:text-neutral-400">{u.description}</p>}
</td>
<td className="px-4 py-2">{formatPrice(u.price)}</td>
<td className="px-4 py-2 text-right">
@ -87,35 +87,35 @@ export default function PackageUpgrades() {
</tr>
))}
{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>
</table>
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Upgrade bearbeiten' : 'Neues Upgrade'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Upgrade bearbeiten' : 'Neues Upgrade'}</h2>
<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
required
value={form.name}
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>
<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
rows={2}
value={form.description}
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>
<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
required
type="number"
@ -123,16 +123,16 @@ export default function PackageUpgrades() {
min="0"
value={form.price}
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>
<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
type="number"
value={form.sort_order}
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>
@ -143,7 +143,7 @@ export default function PackageUpgrades() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -97,14 +97,14 @@ export default function Packages() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<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">
<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
value={brandFilter}
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>
{brandOptions.map((b) => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
@ -112,7 +112,7 @@ export default function Packages() {
<select
value={modelFilter}
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>
{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
</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 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">
<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>
<th className="px-4 py-2">Marke / Modell</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>
</tr>
</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) => (
<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 font-medium text-slate-800 dark:text-slate-100">{pkg.name}</td>
<td className="px-4 py-2 text-slate-500 dark:text-slate-400">
<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-neutral-500 dark:text-neutral-400">
{pkg.markup_type === 'fixed' && `+${pkg.markup_value}`}
{pkg.markup_type === 'percent' && `+${pkg.markup_value} %`}
{(!pkg.markup_type || pkg.markup_type === 'none') && ''}
@ -154,7 +154,7 @@ export default function Packages() {
</tr>
))}
{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.'}
</td></tr>
)}
@ -163,46 +163,46 @@ export default function Packages() {
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Paket bearbeiten' : 'Neues Paket'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Paket bearbeiten' : 'Neues Paket'}</h2>
<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
required
value={form.car_model_id}
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>
{models.map((m) => <option key={m.id} value={m.id}>{m.brand_name} {m.name}</option>)}
</select>
</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
required
value={form.name}
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>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-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>
<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-neutral-500 dark:text-neutral-400">Jede Zeile erscheint als eigener Stichpunkt zusammen mit den Bauteilen.</p>
<textarea
rows={3}
value={form.description}
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>
<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">
<select
value={form.markup_type}
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="fixed">Fester Betrag ()</option>
@ -215,18 +215,18 @@ export default function Packages() {
disabled={form.markup_type === 'none'}
value={form.markup_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>
<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>
<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
type="number"
value={form.sort_order}
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>
@ -237,7 +237,7 @@ export default function Packages() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -59,28 +59,28 @@ export default function PermissionGroups() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Berechtigungsgruppen</h1>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Berechtigungsgruppen</h1>
<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
oder mehreren Benutzern zuweisen kannst.
</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">
<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>
<th className="px-4 py-2">Name</th>
<th className="px-4 py-2">Berechtigungen</th>
<th className="px-4 py-2 text-right">Aktionen</th>
</tr>
</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) => (
<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">
<p className="font-medium text-slate-800 dark:text-slate-100">{g.name}</p>
{g.description && <p className="text-xs text-slate-500 dark:text-slate-400">{g.description}</p>}
<p className="font-medium text-neutral-800 dark:text-neutral-100">{g.name}</p>
{g.description && <p className="text-xs text-neutral-500 dark:text-neutral-400">{g.description}</p>}
</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">
<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>
@ -88,43 +88,43 @@ export default function PermissionGroups() {
</tr>
))}
{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>
</table>
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Gruppe bearbeiten' : 'Neue Gruppe'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Gruppe bearbeiten' : 'Neue Gruppe'}</h2>
<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
required
value={form.name}
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>
<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
rows={2}
value={form.description}
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>
<p className="mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">Berechtigungen</p>
<div className="space-y-1.5 rounded-md border border-slate-200 p-3 dark:border-slate-700">
<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-neutral-200 p-3 dark:border-neutral-700">
{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
type="checkbox"
checked={form.permissions.includes(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}
</label>
@ -139,7 +139,7 @@ export default function PermissionGroups() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -72,14 +72,14 @@ export default function Services() {
return (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Leistungen</h1>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Leistungen</h1>
<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
Standard-Einstellung du kannst sie bearbeiten, löschen und beliebig neue hinzufügen.
</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">
<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>
<th className="px-4 py-2">Icon</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>
</tr>
</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) => (
<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">
<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" />
</div>
</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 text-right">
<button onClick={() => startEdit(s)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
@ -104,70 +104,70 @@ export default function Services() {
</tr>
))}
{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>
</table>
</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">
<h2 className="font-bold text-slate-900 dark:text-white">{editingId ? 'Leistung bearbeiten' : 'Neue Leistung'}</h2>
<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-neutral-900 dark:text-white">{editingId ? 'Leistung bearbeiten' : 'Neue Leistung'}</h2>
<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 })} />
</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
required
value={form.title}
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>
<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
required
rows={4}
value={form.description}
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>
<ImageUploadField value={form.image_path} onChange={(path) => setForm({ ...form, image_path: path })} label="Bild" />
<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
value={form.cta_label}
onChange={(e) => setForm({ ...form, cta_label: e.target.value })}
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>
<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
value={form.cta_url}
onChange={(e) => setForm({ ...form, cta_url: e.target.value })}
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>
<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
type="number"
value={form.sort_order}
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>
@ -178,7 +178,7 @@ export default function Services() {
{editingId ? 'Speichern' : 'Anlegen'}
</button>
{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
</button>
)}

View file

@ -70,46 +70,46 @@ export default function DatabaseSettings() {
return (
<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">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Datenbank-Zugangsdaten</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Datenbank-Zugangsdaten</h2>
<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
Datenbank ein. Die Verbindung wird vor dem Speichern getestet, damit du dich nicht aussperrst.
</p>
{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">
<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
required
value={form.host}
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>
<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
required
value={form.name}
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>
<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
required
value={form.user}
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>
<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)' : ''}
</label>
<div className="flex gap-2">
@ -118,23 +118,23 @@ export default function DatabaseSettings() {
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
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
type="button"
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'}
</button>
</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
value={form.charset}
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>
@ -152,9 +152,9 @@ export default function DatabaseSettings() {
)}
</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">
<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 && (
<span
className={`rounded-full px-3 py-1 text-xs font-semibold ${
@ -167,12 +167,12 @@ export default function DatabaseSettings() {
</span>
)}
</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,
wenn deine Live-Version auf einem anderen Server einen älteren Stand hat.
</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 && (
<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
onClick={checkSchema}
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
</button>
@ -208,7 +208,7 @@ export default function DatabaseSettings() {
</div>
{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>)}
</ul>
)}

View file

@ -31,9 +31,9 @@ export default function ExportImportSettings() {
return (
<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">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Daten exportieren</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Daten exportieren</h2>
<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)
herunter. Benutzerkonten und Kontaktanfragen sind bewusst <strong>nicht</strong> enthalten.
</p>
@ -45,9 +45,9 @@ export default function ExportImportSettings() {
</a>
</section>
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Daten importieren</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Daten importieren</h2>
<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
Marken, Modelle, Pakete, Produkte, Upgrades und Leistungen auf diesem Server das kann nicht
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)}
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
type="checkbox"
checked={importConfirmed}
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
Leistungen auf diesem Server unwiderruflich ersetzt.

View file

@ -68,19 +68,19 @@ export default function MaintenanceSettings() {
];
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 (
<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.
</p>
{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">
<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">
<input
type="checkbox"
@ -88,12 +88,12 @@ export default function MaintenanceSettings() {
onChange={(e) => update(section.key, 'enabled', e.target.checked)}
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" />
</label>
</div>
<p className="mb-3 text-sm text-slate-500 dark:text-slate-400">{section.hint}</p>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
<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-neutral-700 dark:text-neutral-300">
Eigene Nachricht (optional)
</label>
<textarea
@ -101,7 +101,7 @@ export default function MaintenanceSettings() {
value={status[section.key].message}
onChange={(e) => update(section.key, 'message', e.target.value)}
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>
))}

View file

@ -55,9 +55,9 @@ export default function ResetSettings() {
return (
<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">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Nur Leistungen zurücksetzen</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Nur Leistungen zurücksetzen</h2>
<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.
Marken, Modelle und Pakete bleiben unangetastet.
</p>
@ -68,12 +68,12 @@ export default function ResetSettings() {
>
{servicesBusy ? 'Setze zurück…' : 'Leistungen auf Standard zurücksetzen'}
</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 className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Nur Fahrzeugkatalog leeren</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Nur Fahrzeugkatalog leeren</h2>
<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
und Benutzerkonten bleiben erhalten.
</p>
@ -84,12 +84,12 @@ export default function ResetSettings() {
>
{catalogBusy ? 'Leere…' : 'Fahrzeugkatalog leeren'}
</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 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>
<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
Standard zurückgesetzt. Kontaktanfragen und Benutzerkonten bleiben erhalten. Nicht rückgängig zu machen.
</p>
@ -100,7 +100,7 @@ export default function ResetSettings() {
>
{allBusy ? 'Setze zurück…' : 'Wirklich alles zurücksetzen'}
</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>
</div>
);

View file

@ -13,13 +13,13 @@ export default function SettingsLayout() {
`rounded-md px-4 py-2 text-sm font-medium transition ${
isActive
? '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 (
<div>
<h1 className="mb-1 text-xl font-bold text-slate-900 dark:text-white">Einstellungen</h1>
<p className="mb-5 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-1 text-xl font-bold text-neutral-900 dark:text-white">Einstellungen</h1>
<p className="mb-5 text-sm text-neutral-500 dark:text-neutral-400">
Werkzeuge rund um Datenbank, Server-Umzug und Zurücksetzen dieser Seite.
</p>
<nav className="mb-6 flex flex-wrap gap-2">

View file

@ -46,14 +46,14 @@ export default function WebsiteSettings() {
};
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 (
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Startseiten-Bild</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Startseiten-Bild</h2>
<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.
</p>
<ImageUploadField
@ -63,41 +63,41 @@ export default function WebsiteSettings() {
/>
</section>
<section className="rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-1 font-semibold text-slate-900 dark:text-white">Kontaktdaten</h2>
<p className="mb-4 text-sm text-slate-500 dark:text-slate-400">
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Kontaktdaten</h2>
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
Wird im Header, Footer und auf der Kontaktseite angezeigt.
</p>
<div className="space-y-3">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">Telefon</label>
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Telefon</label>
<input
value={form.phone}
onChange={(e) => update('phone', e.target.value)}
placeholder="09373 20 62 390"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
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>
<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
</label>
<input
value={form.whatsapp}
onChange={(e) => update('whatsapp', e.target.value)}
placeholder="z. B. 09373 20 62 390"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
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>
<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
type="email"
value={form.contact_email}
onChange={(e) => update('contact_email', e.target.value)}
placeholder="info@hifi-planet-amorbach.de"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900"
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>

View file

@ -9,13 +9,13 @@ export default function AGB() {
return (
<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
</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>
<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>
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")
@ -24,7 +24,7 @@ export default function AGB() {
</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>
Ü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
@ -35,7 +35,7 @@ export default function AGB() {
</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>
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
@ -45,7 +45,7 @@ export default function AGB() {
</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>
Es gelten die im jeweiligen Einzelangebot genannten Preise inklusive der zum Zeitpunkt der
Rechnungsstellung gesetzlich geltenden Umsatzsteuer. Sofern nichts anderes vereinbart ist, ist die
@ -54,7 +54,7 @@ export default function AGB() {
</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>
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
@ -63,7 +63,7 @@ export default function AGB() {
</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>
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
@ -73,14 +73,14 @@ export default function AGB() {
</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>
Verbautes Material und gelieferte Ware bleiben bis zur vollständigen Bezahlung unser Eigentum.
</p>
</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>
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
@ -91,7 +91,7 @@ export default function AGB() {
</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>
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
@ -103,7 +103,7 @@ export default function AGB() {
</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>
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
@ -112,7 +112,7 @@ export default function AGB() {
</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>
Sollte eine Bestimmung dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen
Bestimmungen hiervon unberührt.

View file

@ -37,21 +37,21 @@ export default function BrandPage() {
}
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 (
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
{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}
</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
</h1>
{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">
@ -59,7 +59,7 @@ export default function BrandPage() {
<Link
key={model.id}
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}
</Link>

View file

@ -81,14 +81,14 @@ export default function ContactPage() {
return (
<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="md:col-span-3">
{status === 'sent' ? (
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-2 text-xl font-bold text-slate-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>
<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-neutral-900 dark:text-white">Danke für deine Anfrage!</h2>
<p className="text-neutral-600 dark:text-neutral-300">Wir melden uns so schnell wie möglich bei dir.</p>
</div>
) : (
<>
@ -111,20 +111,20 @@ export default function ContactPage() {
)}
{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">
<p className="mb-3 text-sm font-semibold text-slate-900 dark:text-white">Optionale Upgrades</p>
<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-neutral-900 dark:text-white">Optionale Upgrades</p>
<div className="space-y-2">
{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
type="checkbox"
checked={selectedUpgradeIds.includes(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="block font-medium text-slate-800 dark:text-slate-100">{u.name}</span>
{u.description && <span className="block text-slate-500 dark:text-slate-400">{u.description}</span>}
<span className="block font-medium text-neutral-800 dark:text-neutral-100">{u.name}</span>
{u.description && <span className="block text-neutral-500 dark:text-neutral-400">{u.description}</span>}
</span>
<span className="font-semibold text-brand-600 dark:text-brand-400">+{formatPrice(u.price)}</span>
</label>
@ -135,37 +135,37 @@ export default function ContactPage() {
<form onSubmit={handleSubmit} className="space-y-4">
<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
name="name"
required
value={form.name}
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>
<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
type="email"
name="email"
required
value={form.email}
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>
<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
name="phone"
value={form.phone}
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>
<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)
</label>
<input
@ -173,17 +173,17 @@ export default function ContactPage() {
value={form.vin}
onChange={handleChange}
placeholder="Optional hilft uns bei der genauen Einschätzung deines Fahrzeugs"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900"
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>
<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
name="message"
rows={4}
value={form.message}
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>
@ -202,32 +202,32 @@ export default function ContactPage() {
</div>
<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">
<h2 className="mb-4 font-semibold text-slate-900 dark:text-white">HifiPlanet Amorbach</h2>
<dl className="space-y-3 text-sm text-slate-600 dark:text-slate-300">
<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-neutral-900 dark:text-white">HifiPlanet Amorbach</h2>
<dl className="space-y-3 text-sm text-neutral-600 dark:text-neutral-300">
<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>
</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>
</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>
</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>MoFr: 9:0018:00 Uhr</dd>
<dd>Sa: 10:0013:00 Uhr</dd>
</div>
</dl>
</div>
<div className="mt-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<h2 className="mb-4 font-semibold text-slate-900 dark:text-white">Anfahrt</h2>
<div className="overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700">
<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-neutral-900 dark:text-white">Anfahrt</h2>
<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">
<iframe
title="HifiPlanet Amorbach Standort"

View file

@ -10,11 +10,11 @@ export default function Datenschutz() {
return (
<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>
<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>
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
<br />
@ -28,7 +28,7 @@ export default function Datenschutz() {
</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>
<ul className="list-disc space-y-1 pl-5">
<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>
<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>
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,
@ -60,7 +60,7 @@ export default function Datenschutz() {
</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>
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
@ -75,7 +75,7 @@ export default function Datenschutz() {
</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">
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:
@ -100,7 +100,7 @@ export default function Datenschutz() {
</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>
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
@ -122,7 +122,7 @@ export default function Datenschutz() {
</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>
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
@ -145,7 +145,7 @@ export default function Datenschutz() {
</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>
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
@ -155,7 +155,7 @@ export default function Datenschutz() {
</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>
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
@ -165,7 +165,7 @@ export default function Datenschutz() {
</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>
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.
@ -173,7 +173,7 @@ export default function Datenschutz() {
</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>
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

View file

@ -106,10 +106,10 @@ export default function Home() {
<div>
<motion.div
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">
<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?
</p>
<div className="flex w-full gap-3 sm:w-auto">
@ -121,7 +121,7 @@ export default function Home() {
</Link>
<Link
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
</Link>
@ -136,7 +136,7 @@ export default function Home() {
alt="Individueller Subwoofer-Einbau mit LED-Beleuchtung im Kofferraum"
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
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">
Audiophil aus Prinzip
</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,
entdecke passende Pakete und frag unverbindlich an wir kümmern uns um den Rest.
</p>
@ -175,25 +175,25 @@ export default function Home() {
</motion.div>
</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">
{stats.map((stat, i) => (
<Reveal key={stat.label} index={i}>
<p className="text-2xl font-extrabold text-white sm:text-3xl">{stat.value}</p>
<p className="text-sm text-brand-50">{stat.label}</p>
<p className="text-2xl font-extrabold text-brand-400 sm:text-3xl">{stat.value}</p>
<p className="text-sm text-neutral-400">{stat.label}</p>
</Reveal>
))}
</div>
</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">
<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
</p>
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
{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}
</Reveal>
))}
@ -203,24 +203,24 @@ export default function Home() {
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<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>
<div className="grid gap-6 sm:grid-cols-3">
{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">
{i + 1}
</div>
<h3 className="mb-1 font-semibold text-slate-900 dark:text-white">{step.title}</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">{step.text}</p>
<h3 className="mb-1 font-semibold text-neutral-900 dark:text-white">{step.title}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-300">{step.text}</p>
</Reveal>
))}
</div>
</section>
<section className="relative overflow-hidden bg-gradient-to-br from-red-700 via-red-800 to-slate-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 -bottom-24 -left-24 h-72 w-72 rounded-full bg-red-500/20 blur-3xl" />
<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-brand-500/25 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">
<Reveal direction="left" className="text-center lg:text-left">
@ -231,10 +231,10 @@ export default function Home() {
HifiPlanet auf YouTube
</div>
<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!"
</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) => (
<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">
@ -248,7 +248,7 @@ export default function Home() {
href={YOUTUBE_URL}
target="_blank"
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
<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>
</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="grid items-center gap-8 md:grid-cols-2">
<Reveal direction="left" shine className="rounded-xl shadow-lg">
@ -282,8 +282,8 @@ export default function Home() {
</Reveal>
<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>
<h2 className="mb-3 text-2xl font-bold text-slate-900 dark:text-white">Präzisionsarbeit in jedem Detail</h2>
<p className="text-slate-600 dark:text-slate-300">
<h2 className="mb-3 text-2xl font-bold text-neutral-900 dark:text-white">Präzisionsarbeit in jedem Detail</h2>
<p className="text-neutral-600 dark:text-neutral-300">
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.
</p>
@ -293,8 +293,8 @@ export default function Home() {
<div className="grid items-center gap-8 md:grid-cols-2">
<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>
<h2 className="mb-3 text-2xl font-bold text-slate-900 dark:text-white">Von der ersten Idee bis zum letzten Schliff</h2>
<p className="text-slate-600 dark:text-slate-300">
<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-neutral-600 dark:text-neutral-300">
Jedes Fahrzeug und jeder Anspruch ist anders. Deshalb beraten wir dich persönlich und unverbindlich
vom dezenten Upgrade bis zum kompromisslosen High-End-System.
</p>
@ -308,28 +308,28 @@ export default function Home() {
<section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<Reveal className="mb-8 text-center">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Einblicke in unsere Umbauten</h2>
<p className="mt-2 text-slate-600 dark:text-slate-300">
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">Einblicke in unsere Umbauten</h2>
<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.
</p>
</Reveal>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{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" />
</Reveal>
))}
</div>
</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">
<Reveal className="mb-10 text-center">
<div className="mb-2 flex items-center justify-center gap-2">
<StarRating className="h-6 w-6" />
</div>
<h2 className="text-2xl font-bold text-slate-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>
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">5,0 von 181 Kunden bewertet</h2>
<p className="mt-1 text-neutral-600 dark:text-neutral-300">Das sagen unsere Kunden über uns.</p>
<a
href="https://www.google.com/maps/search/?api=1&query=Hifi+Planet+Amorbach+Boxbrunner+Stra%C3%9Fe+20a"
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" />
</Reveal>
<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>
<p className="mx-auto mb-8 max-w-2xl text-slate-600 dark:text-slate-300 md:mx-0">
<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-neutral-600 dark:text-neutral-300 md:mx-0">
Neben individuellen Sound-Umbauten bieten wir CNC-Zerspanung, Lasertechnik, 3D-Druck und mehr
alles aus einer Hand in unserer eigenen Werkstatt.
</p>
<Link
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
</Link>
@ -366,10 +366,10 @@ export default function Home() {
</div>
</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">
<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 index={1}>
<Accordion items={faqs} />

View file

@ -9,11 +9,11 @@ export default function Impressum() {
return (
<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>
<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>
Hifi Planet e.K.
<br />
@ -26,7 +26,7 @@ export default function Impressum() {
</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>
Telefon: <a href="tel:+4993732062390" className="hover:text-brand-500">09373 20 62 390</a>
<br />
@ -35,19 +35,19 @@ export default function Impressum() {
</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>
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz: DE 275084766
</p>
</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>
</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>
Christopher Horndasch
<br />
@ -56,7 +56,7 @@ export default function Impressum() {
</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>
Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle im Sinne des Verbraucherstreitbeilegungsgesetzes (VSBG) teilzunehmen.
@ -64,7 +64,7 @@ export default function Impressum() {
</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>
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
@ -78,7 +78,7 @@ export default function Impressum() {
</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>
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
@ -90,7 +90,7 @@ export default function Impressum() {
</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>
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

View file

@ -30,8 +30,8 @@ export default function Leistungen() {
return (
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
{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>
<p className="mb-10 max-w-2xl text-slate-600 dark:text-slate-300">
<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-neutral-600 dark:text-neutral-300">
Von individuellen Sound-Umbauten bis zur eigenen CNC- und 3D-Druck-Fertigung alles aus einer Hand
in unserer Werkstatt in Amorbach.
</p>
@ -42,19 +42,19 @@ export default function Leistungen() {
key={service.id}
index={i % 3}
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]">
{service.image_path && (
<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" />
</div>
</div>
<div className="p-6 pt-8">
<h2 className="mb-2 font-semibold text-slate-900 dark:text-white">{service.title}</h2>
<p className="mb-3 text-sm text-slate-600 dark:text-slate-300">{service.description}</p>
<h2 className="mb-2 font-semibold text-neutral-900 dark:text-white">{service.title}</h2>
<p className="mb-3 text-sm text-neutral-600 dark:text-neutral-300">{service.description}</p>
{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">
{service.cta_label}
@ -65,9 +65,9 @@ export default function Leistungen() {
))}
</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">
<h2 className="mb-2 text-xl font-bold text-slate-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>
<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-neutral-900 dark:text-white">Dein Projekt ist nicht dabei?</h2>
<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">
Kontakt aufnehmen
</Link>

View file

@ -40,7 +40,7 @@ export default function ModelPage() {
}
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;
@ -59,30 +59,30 @@ export default function ModelPage() {
return (
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
{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/${brandSlug}`} className="hover:text-brand-500">{model.brand_name}</Link> / {model.name}
</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
</h1>
{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">
{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">
<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">
<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>
</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) => (
<li key={product.id}>
{product.name_override || product.scraped_name || 'Produkt wird geladen…'}

View file

@ -107,8 +107,8 @@ export default function SetupWizard() {
};
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';
const labelClass = 'mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300';
'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-neutral-700 dark:text-neutral-300';
const steps = [
{ key: 'database', label: 'Datenbank' },
@ -118,16 +118,16 @@ export default function SetupWizard() {
const currentStepIndex = steps.findIndex((s) => s.key === phase);
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" />
<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">
{phase === 'loading' && <p className="text-center text-sm text-slate-400">Lädt</p>}
<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-neutral-400">Lädt</p>}
{phase === 'locked' && (
<div className="text-center">
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Bereits eingerichtet</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Bereits eingerichtet</h1>
<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.
</p>
<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'
: i === currentStepIndex
? '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}
</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>
@ -159,8 +159,8 @@ export default function SetupWizard() {
{phase === 'password' && (
<form onSubmit={handlePassword}>
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Ersteinrichtung</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Ersteinrichtung</h1>
<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.
</p>
<div className="mb-4">
@ -187,8 +187,8 @@ export default function SetupWizard() {
{phase === 'database' && (
<form onSubmit={handleDatabase}>
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Datenbank-Zugangsdaten</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Datenbank-Zugangsdaten</h1>
<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
getestet.
</p>
@ -249,12 +249,12 @@ export default function SetupWizard() {
{phase === 'schema' && (
<div>
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Datenbankstruktur</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Datenbankstruktur</h1>
<p className="mb-6 text-sm text-neutral-500 dark:text-neutral-400">
Die Datenbankverbindung funktioniert. Jetzt legen wir die benötigten Tabellen an.
</p>
{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) => (
<li key={i}>{a}</li>
))}
@ -274,8 +274,8 @@ export default function SetupWizard() {
{phase === 'admin' && (
<form onSubmit={handleAdmin}>
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Erster Admin-Account</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<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-neutral-500 dark:text-neutral-400">
Lege den ersten Super-Admin-Account an, mit dem du dich anschließend einloggen kannst.
</p>
<div className="mb-3">
@ -323,8 +323,8 @@ export default function SetupWizard() {
{phase === 'done' && (
<div className="text-center">
<h1 className="mb-2 text-xl font-bold text-slate-900 dark:text-white">Fertig!</h1>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<h1 className="mb-2 text-xl font-bold text-neutral-900 dark:text-white">Fertig!</h1>
<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.
</p>
<Link

View file

@ -29,15 +29,15 @@ export default function VehicleSelect() {
return (
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
{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>
<p className="mb-8 text-slate-600 dark:text-slate-300">
<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-neutral-600 dark:text-neutral-300">
Wähle zuerst deine Marke, dann dein Modell wir zeigen dir direkt die passenden Sound-Pakete.
</p>
{error && <p className="text-red-600">{error}</p>}
{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">
@ -45,7 +45,7 @@ export default function VehicleSelect() {
<Link
key={brand.id}
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}
</Link>