Redesign navbar mobile menu and rework site color palette
Swaps navbar layout (theme toggle left, nav links right), replaces the mobile dropdown with a fullscreen overlay whose items fly in top-to-bottom, and renames the site-wide "slate" gray to "neutral" to remove a blue undertone that clashed with the brand green. Also converts the flat saturated-green YouTube and stats sections into dark backgrounds with green used only as an accent, matching typical premium car-audio branding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
95764039c7
commit
b6909e756c
43 changed files with 609 additions and 566 deletions
|
|
@ -5,7 +5,7 @@ export default function Accordion({ items }) {
|
|||
const [openIndex, setOpenIndex] = useState(null);
|
||||
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>Mo–Fr: 9:00–18:00 Uhr</dd>
|
||||
<dd>Sa: 10:00–13: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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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…'}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue