fix: 1:1 prototype port – exact hms-* CSS, real logo, all components from app.js

This commit is contained in:
Implementation Engineer
2026-07-10 22:28:26 +02:00
parent 786cb0c040
commit 90a7d7f2e0
38 changed files with 2733 additions and 3250 deletions
+761
View File
@@ -0,0 +1,761 @@
/* ============================================
HMS Licht & Ton Vue 3 SPA Prototype v4
Dark Theme + Subtle Orange + Images + Speaker Grid
============================================ */
const { createApp, ref, reactive, computed, onMounted, defineComponent, h } = Vue;
// ===== Logo Component =====
const HmsLogo = defineComponent({
name: 'HmsLogo',
props: { size: { type: Number, default: 40 } },
setup(props) {
return () => h('svg', {
class: 'hms-logo-svg', viewBox: '0 0 200 200',
style: `height:${props.size}px;width:auto`,
'aria-label': 'HMS Licht & Ton Logo', role: 'img'
}, [
h('path', { fill: 'currentColor', d: 'M166.266,172.872h-37.687v-60.718H74.226v60.718H36.539V26.956h37.687v56.335h54.354V26.956h37.687V172.872z' }),
h('rect', { x: '23.598', y: '15.343', fill: 'none', stroke: '#EC6925', 'stroke-width': '15.1525', 'stroke-miterlimit': '10', width: '155.612', height: '168.874' })
]);
}
});
// ===== Speaker SVG Icon Component =====
const SpeakerIcon = defineComponent({
name: 'SpeakerIcon',
props: { type: { type: String, default: 'linearray' } },
setup(props) {
const icons = {
linearray: '<rect x="8" y="2" width="16" height="40" rx="2"/><circle cx="16" cy="10" r="3"/><circle cx="16" cy="22" r="4"/><circle cx="16" cy="34" r="3"/>',
subwoofer: '<rect x="4" y="8" width="24" height="20" rx="3"/><circle cx="16" cy="18" r="7"/><circle cx="16" cy="18" r="3"/>',
monitor: '<path d="M4 6h20v14H4z"/><path d="M8 24h12"/><circle cx="14" cy="13" r="3"/>',
pointsource: '<circle cx="16" cy="16" r="12"/><circle cx="16" cy="16" r="6"/><circle cx="16" cy="16" r="2"/>',
column: '<rect x="10" y="2" width="12" height="44" rx="2"/><circle cx="16" cy="8" r="2"/><circle cx="16" cy="16" r="2"/><circle cx="16" cy="24" r="2"/><circle cx="16" cy="32" r="2"/><circle cx="16" cy="40" r="2"/>',
movinghead: '<rect x="10" y="4" width="12" height="16" rx="2"/><path d="M16 20v8"/><rect x="8" y="28" width="16" height="4" rx="1"/>'
};
const svgStr = computed(() => '<svg class="hms-speaker-icon" viewBox="0 0 32 48" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">' + (icons[props.type] || icons.linearray) + '</svg>');
return { svgStr };
},
template: '<span class="hms-speaker-icon-wrap" v-html="svgStr"></span>'
});
// ===== Header =====
const AppHeader = defineComponent({
name: 'AppHeader',
setup() {
const mobileMenuOpen = ref(false);
const currentRoute = ref(window.location.hash || '#/');
const navItems = [
{ label: 'Home', route: '#/' },
{ label: 'Referenzen', route: '#/referenzen' },
{ label: 'Mietkatalog', route: '#/mietkatalog' },
{ label: 'Kontakt', route: '#/kontakt' }
];
function navigate(route) { window.location.hash = route; mobileMenuOpen.value = false; window.scrollTo(0, 0); }
function isActive(route) { return currentRoute.value === route || (route !== '#/' && currentRoute.value.startsWith(route)); }
window.addEventListener('hashchange', () => { currentRoute.value = window.location.hash || '#/'; mobileMenuOpen.value = false; });
return { mobileMenuOpen, currentRoute, navItems, navigate, isActive };
},
template: `
<header class="hms-header" role="banner">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between" :style="{ height: 'var(--header-height)' }">
<a href="#/" @click.prevent="navigate('#/')" class="flex items-center gap-2" style="color: var(--text)" aria-label="HMS Licht & Ton Startseite">
<hms-logo :size="40" />
<div class="hidden sm:block leading-tight">
<div class="font-bold text-base" style="color: var(--text)">HMS Licht & Ton</div>
<div class="text-xs" style="color: var(--secondary)">Veranstaltungstechnik</div>
</div>
</a>
<nav class="hidden md:flex items-center gap-1" role="navigation" aria-label="Hauptnavigation">
<button v-for="item in navItems" :key="item.route" @click="navigate(item.route)"
class="hms-nav-btn px-4 py-2 rounded-lg text-sm font-medium"
:style="isActive(item.route) ? { color: 'var(--color-accent)', background: 'var(--color-accent-light)' } : { color: 'var(--text-muted)' }"
:aria-current="isActive(item.route) ? 'page' : undefined">
{{ item.label }}
</button>
<button @click="navigate('#/warenkorb')" class="hms-btn hms-btn-primary text-sm ml-2 px-4 py-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Warenkorb
</button>
</nav>
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden p-2 rounded-lg" :style="{ color: 'var(--text-muted)' }" :aria-expanded="mobileMenuOpen" aria-label="Menü öffnen/schließen">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<transition name="mobile-menu">
<nav v-if="mobileMenuOpen" id="mobile-menu" class="md:hidden border-t" :style="{ borderColor: 'var(--border)', background: 'var(--panel)' }" role="navigation" aria-label="Mobile Navigation">
<div class="px-4 py-3 space-y-1">
<button v-for="item in navItems" :key="item.route" @click="navigate(item.route)"
class="hms-nav-btn block w-full text-left px-4 py-3 rounded-lg text-sm font-medium"
:style="isActive(item.route) ? { color: 'var(--color-accent)', background: 'var(--color-accent-light)' } : { color: 'var(--text-muted)' }">
{{ item.label }}
</button>
<button @click="navigate('#/warenkorb')" class="block w-full text-left px-4 py-3 rounded-lg text-sm font-medium text-white" style="background: var(--color-accent)">🛒 Warenkorb</button>
</div>
</nav>
</transition>
</header>
`
});
// ===== Footer =====
const AppFooter = defineComponent({
name: 'AppFooter',
setup() {
function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
const year = new Date().getFullYear();
return { navigate, year };
},
template: `
<footer class="hms-footer mt-16" role="contentinfo">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div>
<div class="flex items-center gap-2 mb-4" style="color: var(--text)">
<hms-logo :size="36" />
<div><div class="font-bold text-sm">HMS Licht & Ton</div><div class="text-xs" style="color: var(--secondary)">Veranstaltungstechnik</div></div>
</div>
<p class="text-sm leading-relaxed" style="color: var(--secondary)">Hammerschmidt u. Mössle GbR seit über 20 Jahren Ihr Partner für professionelle Veranstaltungstechnik in der Region Leipheim / Ellzee.</p>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Navigation</h3>
<ul class="space-y-2 text-sm">
<li><a href="#/" @click.prevent="navigate('#/')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Home</a></li>
<li><a href="#/referenzen" @click.prevent="navigate('#/referenzen')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Referenzen</a></li>
<li><a href="#/mietkatalog" @click.prevent="navigate('#/mietkatalog')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Mietkatalog</a></li>
<li><a href="#/kontakt" @click.prevent="navigate('#/kontakt')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Kontakt</a></li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Kontakt</h3>
<ul class="space-y-2 text-sm" style="color: var(--secondary)">
<li>Grockelhofen 10, 89340 Leipheim</li>
<li><a href="tel:+498221204433" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">+49 (0) 8221 / 204433</a></li>
<li><a href="mailto:info@hms-licht-ton.de" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">info@hms-licht-ton.de</a></li>
<li class="flex gap-3 pt-2">
<a href="https://facebook.com" target="_blank" rel="noopener" aria-label="Facebook" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></a>
<a href="https://instagram.com" target="_blank" rel="noopener" aria-label="Instagram" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg></a>
</li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Rechtliches</h3>
<ul class="space-y-2 text-sm">
<li><a href="#/impressum" @click.prevent="navigate('#/kontakt')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Impressum</a></li>
<li><a href="#/dsgvo" @click.prevent="navigate('#/kontakt')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">DSGVO</a></li>
<li><a href="#/agb" @click.prevent="navigate('#/kontakt')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">AGB Vermietung</a></li>
<li><a href="#/admin" @click.prevent="navigate('#/admin')" style="color: var(--secondary)" class="hover:text-[var(--color-accent)] text-xs">Admin-Login</a></li>
</ul>
</div>
</div>
<div class="border-t mt-8 pt-6 text-center text-xs" :style="{ borderColor: 'var(--border)', color: 'var(--secondary)' }">
© {{ year }} Hammerschmidt u. Mössle GbR · Veranstaltungstechnik · Leipheim / Ellzee
</div>
</div>
</footer>
`
});
// ===== Service Card =====
const ServiceCard = defineComponent({
name: 'ServiceCard',
props: { icon: String, title: String, description: String },
template: `
<div class="hms-card p-6">
<div class="hms-icon-circle mb-4"><span aria-hidden="true">{{ icon }}</span></div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="text-sm leading-relaxed" style="color: var(--text-muted)">{{ description }}</p>
</div>`
});
// ===== Equipment Card =====
const EquipmentCard = defineComponent({
name: 'EquipmentCard',
props: { item: Object },
emits: ['click', 'add-to-cart'],
template: `
<div class="hms-eq-card" @click="$emit('click', item)" role="article" tabindex="0" @keydown.enter="$emit('click', item)">
<div class="aspect-[4/3] flex items-center justify-center relative overflow-hidden" style="background: var(--surface)">
<img v-if="item.image" :src="item.image" :alt="item.name" loading="lazy" class="absolute inset-0 w-full h-full object-cover" />
<div v-else class="text-4xl" style="color: var(--secondary)">📦</div>
<span v-if="item.category" class="hms-badge hms-badge-primary absolute top-3 left-3">{{ item.category }}</span>
</div>
<div class="p-4">
<h3 class="font-semibold text-sm mb-1 truncate" style="color: var(--text)">{{ item.name }}</h3>
<p class="text-xs mb-3 line-clamp-2" style="color: var(--secondary)">{{ item.description }}</p>
<div class="flex items-center justify-between">
<span class="text-xs" style="color: var(--secondary)">{{ item.code }}</span>
<button @click.stop="$emit('add-to-cart', item)" class="hms-btn hms-btn-ghost text-xs px-3 py-1.5" style="color: var(--color-accent)" :aria-label="item.name + ' zum Warenkorb hinzufügen'">+ Hinzufügen</button>
</div>
</div>
</div>`
});
// ===== UX State Components =====
const LoadingSkeleton = defineComponent({
name: 'LoadingSkeleton',
props: { count: { type: Number, default: 6 } },
template: `
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" aria-live="polite" aria-busy="true">
<div v-for="i in count" :key="i" class="hms-card overflow-hidden">
<div class="hms-skeleton aspect-[4/3]" style="border-radius:0"></div>
<div class="p-4 space-y-3"><div class="hms-skeleton h-4 w-3/4"></div><div class="hms-skeleton h-3 w-full"></div><div class="hms-skeleton h-3 w-1/2"></div><div class="flex justify-between items-center pt-2"><div class="hms-skeleton h-3 w-20"></div><div class="hms-skeleton h-6 w-24" style="border-radius:var(--radius-md)"></div></div></div>
</div>
</div>`
});
const EmptyState = defineComponent({
name: 'EmptyState',
props: { icon: { type: String, default: '🔍' }, title: String, message: String, actionLabel: String },
emits: ['action'],
template: `
<div class="text-center py-16 px-4" role="status">
<div class="text-6xl mb-4" aria-hidden="true">{{ icon }}</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="max-w-md mx-auto mb-6" style="color: var(--secondary)">{{ message }}</p>
<button v-if="actionLabel" @click="$emit('action')" class="hms-btn hms-btn-primary">{{ actionLabel }}</button>
</div>`
});
const ErrorState = defineComponent({
name: 'ErrorState',
props: { title: { type: String, default: 'Ein Fehler ist aufgetreten' }, message: String },
emits: ['retry'],
template: `
<div class="text-center py-16 px-4" role="alert">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-error-bg)', color: 'var(--color-error)' }">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="max-w-md mx-auto mb-6" style="color: var(--secondary)">{{ message }}</p>
<button @click="$emit('retry')" class="hms-btn hms-btn-primary">Erneut versuchen</button>
</div>`
});
// ===== HOME PAGE with Hero Image + Speaker Grid =====
const HomePage = defineComponent({
name: 'HomePage',
emits: ['navigate', 'add-to-cart'],
setup(_, { emit }) {
function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
const services = [
{ icon: '🔊', title: 'Vermietung', description: 'Lautsprecher, Mischpulte, Lichtanlagen, Rigging und Komplett-Setups aus unserem 1.000+ Geräte umfassenden Mietlager in Ellzee. Geprüftes Equipment, sofort einsatzbereit.' },
{ icon: '🛒', title: 'Verkauf', description: 'Neu- und Gebrauchtgeräte führender Hersteller mit Beratung, Inbetriebnahme und Service. Wir liefern nicht nur wir konfigurieren Ihre Anlage einsatzfertig.' },
{ icon: '👷', title: 'Personal', description: 'Qualifizierte Tontechniker, Lichttechniker und Rigger für Aufbau, Betrieb und Abbau. Mit TÜV-geprüfter Ausbildung und umfangreicher Praxiserfahrung.' },
{ icon: '🚚', title: 'Transport', description: 'Eigene Transportfahrzeuge für sichere An- und Ablieferung. Inklusive Verladekonzept, Positionierung und Rückholung kundengerecht terminiert.' },
{ icon: '📦', title: 'Lagerung', description: 'Klimatisierte und trockene Lagerung für Equipment und Veranstaltungsbestände. Mit Bestandsmanagement und regelmäßiger Funktionsprüfung.' },
{ icon: '🔧', title: 'Werkstatt', description: 'Herstellerübergreifende Reparatur und Wartung in unserer eigenen Werkstatt. Regelmäßige DGUV-Prüfungen und Kalibrierung für Ihren sicheren Betrieb.' },
{ icon: '⚡', title: 'Installation', description: 'Festinstallationen in Schaufenstern, Lokalen und Veranstaltungsstätten. Von der Planung über die Elektroinstallation bis zur Abnahme alles aus einer Hand.' },
{ icon: '📅', title: 'Booking', description: 'Vermittlung von Künstlern und Technik-Personal für Ihre Veranstaltung. Mit unserem branchenweiten Netzwerk finden wir die passenden Professionals für Ihr Event.' }
];
const speakers = [
{ type: 'linearray', name: 'L-Acoustics K2', category: 'Line Array' },
{ type: 'subwoofer', name: 'L-Acoustics KS28', category: 'Subwoofer' },
{ type: 'pointsource', name: 'd&b T10', category: 'Point Source' },
{ type: 'column', name: 'd&b KSL8', category: 'Column Array' },
{ type: 'monitor', name: 'd&b M4', category: 'Monitor' },
{ type: 'movinghead', name: 'Robe Pointe', category: 'Moving Head' }
];
return { services, speakers, navigate };
},
template: `
<div>
<!-- Hero with background image -->
<section class="hms-hero py-20 sm:py-32" aria-labelledby="hero-title">
<div class="hms-hero-bg" style="background-image: url('https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?w=1920&q=80')"></div>
<div class="hms-hero-overlay"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="max-w-2xl">
<span class="hms-badge hms-badge-primary mb-4">Seit 2003 · Über 20 Jahre Veranstaltungstechnik</span>
<h1 id="hero-title" class="text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight mb-6" style="color: var(--text)">
Professionelle Technik<br><span style="color: var(--color-accent)">für Ihre Veranstaltung</span>
</h1>
<p class="text-lg sm:text-xl mb-8 max-w-xl" style="color: var(--text-muted)">
Von der Firmenevent-Beschallung bis zur Open-Air-Großproduktion: HMS Licht & Ton liefert Tontechnik, Lichttechnik, Rigging und Komplettlösungen aus Leipheim und Ellzee.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<button @click="navigate('#/mietkatalog')" class="hms-btn hms-btn-primary text-base px-8 py-4">
Mietkatalog öffnen
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
</button>
<button @click="navigate('#/kontakt')" class="hms-btn hms-btn-secondary text-base px-8 py-4">Beratung anfragen</button>
</div>
</div>
</div>
</section>
<!-- Speaker Grid -->
<section class="py-12 sm:py-16" :style="{ background: 'var(--bg)' }" aria-labelledby="speaker-title">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<span class="text-sm font-semibold uppercase tracking-wider" style="color: var(--color-accent)">Equipment-Highlights</span>
<h2 id="speaker-title" class="text-2xl sm:text-3xl font-bold mt-1" style="color: var(--text)">Lautsprecher & Strahler aus unserem Mietlager</h2>
</div>
<div class="hms-speaker-grid">
<div v-for="sp in speakers" :key="sp.name" class="hms-speaker-item" @click="navigate('#/mietkatalog')" role="button" tabindex="0" @keydown.enter="navigate('#/mietkatalog')">
<speaker-icon :type="sp.type" />
<div class="hms-speaker-name">{{ sp.name }}</div>
<div class="hms-speaker-type">{{ sp.category }}</div>
</div>
</div>
</div>
</section>
<!-- Über uns -->
<section class="py-16 sm:py-20" :style="{ background: 'var(--panel)' }" aria-labelledby="about-title">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div>
<span class="text-sm font-semibold uppercase tracking-wider mb-2" style="color: var(--color-accent)">Unternehmen</span>
<h2 id="about-title" class="text-3xl sm:text-4xl font-bold mb-6" style="color: var(--text)">Ihr Technik-Partner mit Erfahrung</h2>
<p class="leading-relaxed mb-4" style="color: var(--text-muted)">Die <strong style="color: var(--text)">Hammerschmidt u. Mössle GbR</strong> ist seit 2003 Ihr zuverlässiger Partner für Veranstaltungstechnik in der Region Leipheim / Ellzee und im gesamten süddeutschen Raum.</p>
<p class="leading-relaxed mb-4" style="color: var(--text-muted)">Mit einem Mietlager von über 1.000 Geräten, einer eigenen Werkstatt und erfahrenem Personal realisieren wir Veranstaltungen jeder Größenordnung.</p>
<div class="grid grid-cols-3 gap-4 mt-8">
<div class="text-center"><div class="text-3xl font-bold" style="color: var(--color-accent)">20+</div><div class="text-sm" style="color: var(--secondary)">Jahre Erfahrung</div></div>
<div class="text-center"><div class="text-3xl font-bold" style="color: var(--color-accent)">1.000+</div><div class="text-sm" style="color: var(--secondary)">Geräte im Lager</div></div>
<div class="text-center"><div class="text-3xl font-bold" style="color: var(--color-accent)">2</div><div class="text-sm" style="color: var(--secondary)">Standorte</div></div>
</div>
</div>
<div class="relative">
<div class="aspect-[4/3] rounded-lg overflow-hidden shadow-xl">
<img src="https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=800&q=80" alt="Veranstaltungstechnik Setup" loading="lazy" class="w-full h-full object-cover" />
</div>
</div>
</div>
</div>
</section>
<!-- Leistungen -->
<section class="py-16 sm:py-20" :style="{ background: 'var(--bg)' }" aria-labelledby="services-title">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<span class="text-sm font-semibold uppercase tracking-wider mb-2" style="color: var(--color-accent)">Leistungen</span>
<h2 id="services-title" class="text-3xl sm:text-4xl font-bold" style="color: var(--text)">Von der Vermietung bis zur Komplettbetreuung</h2>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<service-card v-for="s in services" :key="s.title" :icon="s.icon" :title="s.title" :description="s.description" />
</div>
</div>
</section>
<!-- CTA -->
<section class="py-16 sm:py-20" :style="{ background: 'var(--panel)' }" aria-labelledby="rental-title">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="rounded-lg p-8 sm:p-12 lg:p-16 text-center" style="background: var(--bg); border: 1px solid var(--border)">
<h2 id="rental-title" class="text-3xl sm:text-4xl font-bold mb-4" style="color: var(--text)">Online-Mietkatalog</h2>
<p class="max-w-2xl mx-auto mb-8" style="color: var(--text-muted)">Durchsuchen Sie unser Equipment-Sortiment und stellen Sie Ihre Mietanfrage direkt online zusammen.</p>
<button @click="navigate('#/mietkatalog')" class="hms-btn hms-btn-primary text-base px-8 py-4">
Katalog durchsuchen
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
</button>
</div>
</div>
</section>
</div>`
});
// ===== REFERENZEN PAGE with real HMS images =====
const ReferenzenPage = defineComponent({
name: 'ReferenzenPage',
setup() {
const loading = ref(true);
const error = ref(false);
const filter = ref('alle');
const categories = ['alle', 'Open-Air', 'Indoor', 'Rigging', 'Event'];
const allImages = [
{ id: 1, title: 'Open-Air Veranstaltung', category: 'Open-Air', date: '2017', img: 'img/ref1.jpg' },
{ id: 2, title: 'Bühnenaufbau', category: 'Rigging', date: '2016', img: 'img/ref2.jpg' },
{ id: 3, title: 'Donautal Radelspass', category: 'Event', date: '2019', img: 'img/ref3.jpg' },
{ id: 4, title: 'Traversenaufbau', category: 'Rigging', date: '2019', img: 'img/ref4.jpg' },
{ id: 5, title: 'Veranstaltungstechnik vor Ort', category: 'Event', date: '2019', img: 'img/ref5.jpg' },
{ id: 6, title: 'Indoor Beschallung', category: 'Indoor', date: '2016', img: 'img/ref6.jpg' },
{ id: 7, title: 'Bühnentechnik Setup', category: 'Rigging', date: '2019', img: 'img/ref7.jpg' },
{ id: 8, title: 'Event-Aufbau', category: 'Event', date: '2019', img: 'img/ref8.jpg' },
{ id: 9, title: 'Großbühne Aufbau', category: 'Open-Air', date: '2016', img: 'img/ref9.jpg' }
];
const filteredImages = computed(() => {
if (filter.value === 'alle') return allImages;
return allImages.filter(i => i.category === filter.value);
});
onMounted(() => { setTimeout(() => { loading.value = false; }, 1000); });
function retry() { loading.value = true; error.value = false; setTimeout(() => { loading.value = false; }, 800); }
return { loading, error, filter, categories, filteredImages, retry };
},
template: `
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Referenzen</h1>
<p class="mb-8" style="color: var(--secondary)">Ausgewählte Projekte aus unserer Veranstaltungstechnik-Praxis.</p>
<div class="flex flex-wrap gap-2 mb-8" role="tablist" aria-label="Referenz-Filter">
<button v-for="cat in categories" :key="cat" @click="filter = cat" :class="['hms-chip', filter === cat ? 'active' : '']" role="tab" :aria-selected="filter === cat">{{ cat === 'alle' ? 'Alle' : cat }}</button>
</div>
<div v-if="loading">
<div class="hms-gallery">
<div v-for="i in 6" :key="i" class="hms-card overflow-hidden">
<div class="hms-skeleton aspect-[4/3]" style="border-radius:0"></div>
<div class="p-4 space-y-2"><div class="hms-skeleton h-4 w-3/4"></div><div class="hms-skeleton h-3 w-1/3"></div></div>
</div>
</div>
</div>
<error-state v-else-if="error" title="Referenzen konnten nicht geladen werden" message="Bitte versuchen Sie es in Kürze erneut." @retry="retry" />
<div v-else class="hms-gallery">
<article v-for="img in filteredImages" :key="img.id" class="hms-card overflow-hidden group cursor-pointer">
<div class="aspect-[4/3] relative overflow-hidden">
<img :src="img.img" :alt="img.title" loading="lazy" class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span class="hms-badge hms-badge-primary absolute top-3 left-3">{{ img.category }}</span>
</div>
<div class="p-4"><h3 class="font-semibold text-sm" style="color: var(--text)">{{ img.title }}</h3><p class="text-xs mt-1" style="color: var(--secondary)">{{ img.date }}</p></div>
</article>
</div>
<empty-state v-if="!loading && !error && filteredImages.length === 0" icon="📭" title="Keine Referenzen" message="Für diese Kategorie liegen aktuell keine Referenzen vor." action-label="Alle anzeigen" @action="filter = 'alle'" />
</div>`
});
// ===== KONTAKT PAGE =====
const KontaktPage = defineComponent({
name: 'KontaktPage',
setup() {
const form = reactive({ name: '', email: '', phone: '', subject: '', message: '', privacy: false });
const errors = reactive({});
const submitted = ref(false);
const submitting = ref(false);
function validate() {
const e = {};
if (!form.name.trim()) e.name = 'Name ist erforderlich';
if (!form.email.trim()) e.email = 'E-Mail ist erforderlich';
else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail-Adresse';
if (!form.message.trim()) e.message = 'Nachricht ist erforderlich';
if (!form.privacy) e.privacy = 'Bitte stimmen Sie der Datenschutzerklärung zu';
Object.keys(errors).forEach(k => delete errors[k]); Object.assign(errors, e);
return Object.keys(e).length === 0;
}
function submit() { if (!validate()) return; submitting.value = true; setTimeout(() => { submitting.value = false; submitted.value = true; }, 1500); }
function resetForm() { Object.assign(form, { name: '', email: '', phone: '', subject: '', message: '', privacy: false }); submitted.value = false; }
return { form, errors, submitted, submitting, validate, submit, resetForm };
},
template: `
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Kontakt</h1>
<p class="mb-8" style="color: var(--secondary)">Wir beraten Sie persönlich telefonisch, per E-Mail oder über das Kontaktformular.</p>
<div class="grid lg:grid-cols-2 gap-8">
<div class="space-y-6">
<div class="hms-card p-6"><h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text)"><span style="color: var(--color-accent)">🏢</span> Büro Leipheim</h2><div class="space-y-1 text-sm" style="color: var(--text-muted)"><p>Grockelhofen 10<br>89340 Leipheim</p><a href="https://maps.google.com/?q=Grockelhofen+10+89340+Leipheim" target="_blank" rel="noopener" class="inline-flex items-center gap-1 mt-2" style="color: var(--color-accent)">Route planen <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg></a></div></div>
<div class="hms-card p-6"><h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text)"><span style="color: var(--color-accent)">📦</span> Mietlager Ellzee</h2><div class="space-y-1 text-sm" style="color: var(--text-muted)"><p>Zur Schönhalde 8<br>89352 Ellzee</p><a href="https://maps.google.com/?q=Zur+Schönhalde+8+89352+Ellzee" target="_blank" rel="noopener" class="inline-flex items-center gap-1 mt-2" style="color: var(--color-accent)">Route planen <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg></a></div></div>
<div class="hms-card p-6"><h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text)"><span style="color: var(--color-accent)">🕐</span> Öffnungszeiten</h2><div class="text-sm" style="color: var(--text-muted)"><div class="flex justify-between py-2 border-b" :style="{ borderColor: 'var(--border)' }"><span>Montag Freitag</span><span class="font-medium" style="color: var(--text)">10:00 18:00</span></div><div class="flex justify-between py-2"><span>Samstag & Sonntag</span><span style="color: var(--secondary)">Geschlossen</span></div></div></div>
<div class="grid sm:grid-cols-2 gap-4">
<div class="hms-card p-6 text-center"><div class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center text-2xl" style="background: var(--surface); color: var(--secondary)">👤</div><h3 class="font-semibold" style="color: var(--text)">Leopold Hammerschmidt</h3><div class="text-xs mb-3" style="color: var(--secondary)">Geschäftsführung</div><a href="tel:+491726264796" class="block text-sm mb-1" style="color: var(--text-muted)">+49 (0) 172 6264796</a><a href="mailto:leopold.hammerschmidt@hms-licht-ton.de" class="block text-sm break-all" style="color: var(--text-muted)">leopold.hammerschmidt@hms-licht-ton.de</a></div>
<div class="hms-card p-6 text-center"><div class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center text-2xl" style="background: var(--surface); color: var(--secondary)">👤</div><h3 class="font-semibold" style="color: var(--text)">Andreas Mössle</h3><div class="text-xs mb-3" style="color: var(--secondary)">Geschäftsführung</div><a href="tel:+491739014604" class="block text-sm mb-1" style="color: var(--text-muted)">+49 (0) 173 / 9014604</a><a href="mailto:andreas.moessle@hms-licht-ton.de" class="block text-sm break-all" style="color: var(--text-muted)">andreas.moessle@hms-licht-ton.de</a></div>
</div>
</div>
<div>
<div class="hms-card p-6 sm:p-8">
<h2 class="text-xl font-semibold mb-6" style="color: var(--text)">Nachricht senden</h2>
<div v-if="submitted" class="text-center py-8" role="status">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-success-bg)', color: 'var(--color-success)' }"><svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg></div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text)">Nachricht übermittelt</h3>
<p class="text-sm mb-6" style="color: var(--secondary)">Vielen Dank für Ihre Anfrage. Wir melden uns innerhalb von 24 Stunden bei Ihnen.</p>
<button @click="resetForm" class="hms-btn hms-btn-secondary">Neue Nachricht</button>
</div>
<form v-else @submit.prevent="submit" novalidate>
<div class="space-y-4">
<div><label for="name" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Name <span style="color: var(--color-error)" aria-label="Pflichtfeld">*</span></label><input id="name" v-model="form.name" type="text" :class="['hms-input', errors.name ? 'hms-input-error' : '']" placeholder="Ihr Name" :aria-invalid="!!errors.name" /><p v-if="errors.name" class="text-xs mt-1" style="color: var(--color-error)" role="alert">{{ errors.name }}</p></div>
<div><label for="email" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">E-Mail <span style="color: var(--color-error)" aria-label="Pflichtfeld">*</span></label><input id="email" v-model="form.email" type="email" :class="['hms-input', errors.email ? 'hms-input-error' : '']" placeholder="ihre@email.de" :aria-invalid="!!errors.email" /><p v-if="errors.email" class="text-xs mt-1" style="color: var(--color-error)" role="alert">{{ errors.email }}</p></div>
<div><label for="phone" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Telefon</label><input id="phone" v-model="form.phone" type="tel" class="hms-input" placeholder="+49 ..." /></div>
<div><label for="subject" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Anliegen</label><select id="subject" v-model="form.subject" class="hms-input"><option value="">Bitte wählen</option><option value="vermietung">Vermietungsanfrage</option><option value="verkauf">Verkaufsberatung</option><option value="personal">Personalanfrage</option><option value="installation">Installationsanfrage</option><option value="sonstiges">Sonstiges</option></select></div>
<div><label for="message" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Nachricht <span style="color: var(--color-error)" aria-label="Pflichtfeld">*</span></label><textarea id="message" v-model="form.message" rows="5" :class="['hms-input', errors.message ? 'hms-input-error' : '']" placeholder="Beschreiben Sie Ihr Anliegen Veranstaltungstyp, Ort, erwartete Besucherzahl, benötigtes Equipment..." :aria-invalid="!!errors.message"></textarea><p v-if="errors.message" class="text-xs mt-1" style="color: var(--color-error)" role="alert">{{ errors.message }}</p></div>
<div><label class="flex items-start gap-3 cursor-pointer"><input type="checkbox" v-model="form.privacy" class="mt-1 w-5 h-5 rounded" :style="{ accentColor: 'var(--color-accent)' }" :aria-invalid="!!errors.privacy" /><span class="text-xs" style="color: var(--text-muted)">Ich habe die <a href="#/dsgvo" style="color: var(--color-accent)">Datenschutzerklärung</a> gelesen und stimme zu, dass meine Angaben zur Bearbeitung meiner Anfrage gespeichert werden. <span style="color: var(--color-error)">*</span></span></label><p v-if="errors.privacy" class="text-xs mt-1" style="color: var(--color-error)" role="alert">{{ errors.privacy }}</p></div>
<button type="submit" :disabled="submitting" class="hms-btn hms-btn-primary w-full text-base py-3"><span v-if="submitting" class="hms-spinner" style="width:18px;height:18px;border-width:2px"></span><span v-else>Nachricht senden</span></button>
</div>
</form>
</div>
</div>
</div>
</div>`
});
// ===== MIETKATALOG PAGE =====
const MietkatalogPage = defineComponent({
name: 'MietkatalogPage',
emits: ['navigate', 'add-to-cart'],
setup(_, { emit }) {
const loading = ref(true); const error = ref(false);
const searchQuery = ref(''); const activeCategory = ref('alle'); const sortBy = ref('name');
const categories = ['alle', 'Tontechnik', 'Lichttechnik', 'Rigging', 'Video', 'Strom', 'Zubehör'];
const allEquipment = [
{ id: 1, code: 'LT-001', name: 'L-Acoustics K2', category: 'Tontechnik', description: 'High-Performance Line-Array Element mit variabler Krümmung für große Open-Air-Produktionen und Hallenbeschallung', brand: 'L-Acoustics', specs: { weight: '56 kg', power: '1440 W', freq_range: '35 Hz - 20 kHz' }, image: '' },
{ id: 2, code: 'LT-002', name: 'd&b audiotechnik KSL8', category: 'Tontechnik', description: 'Bi-amplified Column Array Loudspeaker, 80° horizontal, für mittlere bis große Events', brand: 'd&b audiotechnik', specs: { weight: '32 kg', power: '1200 W', freq_range: '55 Hz - 18 kHz' }, image: '' },
{ id: 3, code: 'LT-003', name: 'Shure SM58', category: 'Tontechnik', description: 'Dynamisches Gesangsmikrofon, Kardioid-Richtcharakteristik, Industrie-Standard seit Jahrzehnten', brand: 'Shure', specs: { weight: '330 g', type: 'dynamisch', polar: 'Kardioid' }, image: '' },
{ id: 4, code: 'LT-004', name: 'Allen & Heath dLive S5000', category: 'Tontechnik', description: 'Digitales Mischpult mit 96 Input Fadern, 64 Mix Buses, Dante-kompatibel für große Live-Produktionen', brand: 'Allen & Heath', specs: { channels: '96', buses: '64', fx: '16 Effekt-Engines' }, image: '' },
{ id: 5, code: 'LL-001', name: 'Robe Pointe', category: 'Lichttechnik', description: '230W Moving Beam mit 5°-20° Zoom, 16-bit Dimming, vielseitig für Beam- und Wash-Anwendungen', brand: 'Robe', specs: { power: '230 W', source: 'MSD 230', zoom: '5°-20°' }, image: '' },
{ id: 6, code: 'LL-002', name: 'Mac Aura PXL', category: 'Lichttechnik', description: 'RGBW Wash-Light mit 18 pixel-mappable Zellen, Zoom 4°-60°, Art-Net steuerbar', brand: 'Martin', specs: { power: '700 W', cells: '18', zoom: '4°-60°' }, image: '' },
{ id: 7, code: 'LL-003', name: 'Showtec Phantom 140', category: 'Lichttechnik', description: '140W LED Spot Moving Head, 9 rotierende + 9 feste Gobos, 8-fach Prisma', brand: 'Showtec', specs: { power: '140 W', gobos: '9 rotierend + 9 fest', zoom: '12°-18°' }, image: '' },
{ id: 8, code: 'LL-004', name: 'Astera Titan Tube', category: 'Lichttechnik', description: 'Pixel Tube 1m, batteriebetrieben mit 20 Std. Laufzeit, DMX/CRMX-Steuerung, RGBW', brand: 'Astera', specs: { length: '1015 mm', battery: '20 Std', cells: '16 Pixel' }, image: '' },
{ id: 9, code: 'RG-001', name: 'Tomcat Truss 290', category: 'Rigging', description: 'Aluminium Traverse 4-Punkt, 290mm Profil, 2m Länge, belastbar bis 450kg freitragend', brand: 'Tomcat', specs: { profile: '290x290 mm', length: '2 m', load: '450 kg' }, image: '' },
{ id: 10, code: 'RG-002', name: 'Chain Motor 1T', category: 'Rigging', description: 'Bühnen-Motor 1000kg, D8+ Plus geprüft, inklusive Steuerkabel und Lasthaken', brand: 'Verlinde', specs: { capacity: '1000 kg', speed: '4-8 m/min', control: 'Direct Control' }, image: '' },
{ id: 11, code: 'VD-001', name: 'LED Video Wall P3.9', category: 'Video', description: 'Indoor LED Panel P3.9, 500x500mm, 3840Hz Refresh Rate, 1500 nits für Bühnenhintergründe', brand: 'Absen', specs: { pixel_pitch: '3.9 mm', size: '500x500 mm', refresh: '3840 Hz' }, image: '' },
{ id: 12, code: 'VD-002', name: 'Blackmagic ATEM Mini Extreme', category: 'Video', description: '8-Input HDMI Switcher mit ISO Recording, Multiview und Direct Streaming', brand: 'Blackmagic', specs: { inputs: '8x HDMI', outputs: '2x HDMI', recording: 'ISO Recording' }, image: '' },
{ id: 13, code: 'ST-001', name: 'WAGO 24V 40A Netzteilkasten', category: 'Strom', description: '24V DC 40A Netzteil in Flightcase, 4x Schuko Ausgang, für LED- und Steuerungstechnik', brand: 'WAGO', specs: { output: '24V DC 40A', inputs: '4x Schuko', protection: 'IP54' }, image: '' },
{ id: 14, code: 'ST-002', name: 'Duraplex 32A Stromverteiler', category: 'Strom', description: '32A 5-polig auf 3x 16A + 3x Schuko, mit FI-Schutzschalter, professionelle Bühnenstromversorgung', brand: 'Duraplex', specs: { input: '32A 5-pol CEE', outputs: '3x 16A + 3x Schuko', protection: 'FI/A' }, image: '' },
{ id: 15, code: 'ZB-001', name: 'Flightcase 19" 12HE', category: 'Zubehör', description: '19" Rack Flightcase 12HE, Front- und Rücktür, 4x 100mm Rollen, Birkenholz mit Alu-Kanten', brand: 'Thon', specs: { rack_units: '12 HE', depth: '600 mm', wheels: '4x 100mm' }, image: '' },
{ id: 16, code: 'ZB-002', name: 'XLR Kabel 20m', category: 'Zubehör', description: 'XLR3 male/female Mikrofonkabel, 20m, Neutrik Stecker, doppelt geschirmt', brand: 'Tasker', specs: { length: '20 m', connectors: 'XLR3 M/F', shielding: 'double' }, image: '' },
{ id: 17, code: 'ZB-003', name: 'DJ-Table Pro', category: 'Zubehör', description: 'Mobile DJ-Tisch mit Laptop-Ständer und integrierter LED-Beleuchtung, transportiert im Flightcase', brand: 'Stage Traps', specs: { width: '120 cm', height: '95 cm', features: 'LED, Laptop-Ständer' }, image: '' },
{ id: 18, code: 'LT-005', name: 'Sennheiser EW-DX 835', category: 'Tontechnik', description: 'Digitales Funkmikrofon-Set, Handheld + Empfänger, 100m Reichweite, 12 Std. Akkulaufzeit', brand: 'Sennheiser', specs: { type: 'Digital Wireless', range: '100 m', battery: '12 Std' }, image: '' }
];
const filteredEquipment = computed(() => {
let items = allEquipment;
if (activeCategory.value !== 'alle') items = items.filter(i => i.category === activeCategory.value);
if (searchQuery.value.trim()) { const q = searchQuery.value.toLowerCase(); items = items.filter(i => i.name.toLowerCase().includes(q) || i.code.toLowerCase().includes(q) || i.brand.toLowerCase().includes(q)); }
if (sortBy.value === 'name') items = [...items].sort((a,b) => a.name.localeCompare(b.name));
else if (sortBy.value === 'brand') items = [...items].sort((a,b) => a.brand.localeCompare(b.brand));
else if (sortBy.value === 'code') items = [...items].sort((a,b) => a.code.localeCompare(b.code));
return items;
});
onMounted(() => { setTimeout(() => { loading.value = false; }, 1000); });
function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
function navigateToDetail(item) { window.location.hash = '#/mietkatalog/' + item.id; window.scrollTo(0, 0); }
function retry() { loading.value = true; error.value = false; setTimeout(() => { loading.value = false; }, 800); }
return { loading, error, searchQuery, activeCategory, sortBy, categories, filteredEquipment, navigate, navigateToDetail, retry };
},
template: `
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div><h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Mietkatalog</h1><p style="color: var(--secondary)">Durchsuchen Sie unser Equipment-Sortiment und stellen Sie Ihre Mietanfrage zusammen.</p></div>
<button @click="navigate('#/warenkorb')" class="hms-btn hms-btn-secondary text-sm self-start sm:self-auto">🛒 Warenkorb ansehen</button>
</div>
<div class="hms-card p-4 mb-6">
<div class="flex flex-col lg:flex-row gap-4">
<div class="relative flex-1"><svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5" style="color: var(--secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg><input v-model="searchQuery" type="search" class="hms-input pl-10" placeholder="Gerät, Marke oder Artikelnummer suchen..." aria-label="Equipment suchen" /></div>
<select v-model="sortBy" class="hms-input lg:w-48" aria-label="Sortieren nach"><option value="name">Sortieren: Name (A-Z)</option><option value="brand">Sortieren: Marke (A-Z)</option><option value="code">Sortieren: Artikelnummer</option></select>
</div>
<div class="flex flex-wrap gap-2 mt-4" role="tablist" aria-label="Kategorie-Filter"><button v-for="cat in categories" :key="cat" @click="activeCategory = cat" :class="['hms-chip', activeCategory === cat ? 'active' : '']" role="tab" :aria-selected="activeCategory === cat">{{ cat === 'alle' ? 'Alle Kategorien' : cat }}</button></div>
</div>
<div v-if="!loading && !error" class="text-sm mb-4" style="color: var(--secondary)" aria-live="polite">{{ filteredEquipment.length }} {{ filteredEquipment.length === 1 ? 'Gerät' : 'Geräte' }} gefunden</div>
<loading-skeleton v-if="loading" :count="6" />
<error-state v-else-if="error" title="Katalog nicht erreichbar" message="Der Equipment-Katalog ist aktuell nicht verfügbar." @retry="retry" />
<empty-state v-else-if="filteredEquipment.length === 0" icon="🔍" title="Keine Geräte gefunden" message="Für Ihre Suchanfrage wurden keine Geräte gefunden." action-label="Filter zurücksetzen" @action="searchQuery=''; activeCategory='alle'" />
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"><equipment-card v-for="item in filteredEquipment" :key="item.id" :item="item" @click="navigateToDetail" @add-to-cart="$emit('add-to-cart', $event)" /></div>
</div>`
});
// ===== EQUIPMENT DETAIL PAGE =====
const EquipmentDetailPage = defineComponent({
name: 'EquipmentDetailPage',
props: { id: [String, Number], cart: Array },
emits: ['add-to-cart', 'navigate'],
setup(props, { emit }) {
const loading = ref(true); const error = ref(false); const quantity = ref(1);
const rentalStart = ref(''); const rentalEnd = ref(''); const item = ref(null);
const equipmentData = [
{ id: 1, code: 'LT-001', name: 'L-Acoustics K2', category: 'Tontechnik', description: 'Das L-Acoustics K2 ist das Flaggschiff der Line-Array-Serie und eignet sich für große Open-Air-Veranstaltungen und Hallenbeschallung.', brand: 'L-Acoustics', specs: { weight: '56 kg', power: '1440 W', freq_range: '35 Hz - 20 kHz', max_spl: '142 dB', coverage: '10°-110° variable' }, image: '', related: [2, 4, 3] },
{ id: 2, code: 'LT-002', name: 'd&b audiotechnik KSL8', category: 'Tontechnik', description: 'Bi-amplified Column Array Loudspeaker mit 80° horizontaler Abstrahlung.', brand: 'd&b audiotechnik', specs: { weight: '32 kg', power: '1200 W', freq_range: '55 Hz - 18 kHz', max_spl: '138 dB', coverage: '80° horizontal' }, image: '', related: [1, 4, 18] },
{ id: 3, code: 'LT-003', name: 'Shure SM58', category: 'Tontechnik', description: 'Das weltweit meistverkaufte dynamische Gesangsmikrofon.', brand: 'Shure', specs: { weight: '330 g', type: 'dynamisch', polar: 'Kardioid', freq_range: '50 Hz - 15 kHz', connector: 'XLR3' }, image: '', related: [4, 18, 1] },
{ id: 4, code: 'LT-004', name: 'Allen & Heath dLive S5000', category: 'Tontechnik', description: 'Professionelles digitales Mischpult mit 96 Input Fadern und 64 Mix Buses.', brand: 'Allen & Heath', specs: { channels: '96', buses: '64', fx: '16 Effekt-Engines', screens: '2x 15" Touch', i_o: '128x128 Dante' }, image: '', related: [1, 2, 3] },
{ id: 5, code: 'LL-001', name: 'Robe Pointe', category: 'Lichttechnik', description: '230W Moving Beam mit 5°-20° Zoom und 16-bit Dimming.', brand: 'Robe', specs: { power: '230 W', source: 'MSD 230', zoom: '5°-20°', dimming: '16-bit', pan: '540°', tilt: '270°' }, image: '', related: [6, 7, 8] },
{ id: 6, code: 'LL-002', name: 'Mac Aura PXL', category: 'Lichttechnik', description: 'RGBW Wash-Light mit 18 einzelnen pixel-mappable Zellen.', brand: 'Martin', specs: { power: '700 W', cells: '18', zoom: '4°-60°', control: 'DMX 512, Art-Net', color: 'RGBW' }, image: '', related: [5, 7, 8] },
{ id: 7, code: 'LL-003', name: 'Showtec Phantom 140', category: 'Lichttechnik', description: '140W LED Spot Moving Head mit 9 rotierenden und 9 festen Gobos.', brand: 'Showtec', specs: { power: '140 W', gobos: '9 rotierend + 9 fest', zoom: '12°-18°', color: '7+1 Farbrad', prism: '8-fach rotierend' }, image: '', related: [5, 6, 8] },
{ id: 8, code: 'LL-004', name: 'Astera Titan Tube', category: 'Lichttechnik', description: 'Pixel Tube 1m Länge, batteriebetrieben mit 20 Std. Laufzeit.', brand: 'Astera', specs: { length: '1015 mm', battery: '20 Std', cells: '16 Pixel', control: 'DMX, CRMX, App', color: 'RGBW' }, image: '', related: [5, 6, 7] },
{ id: 9, code: 'RG-001', name: 'Tomcat Truss 290', category: 'Rigging', description: 'Aluminium Traverse 4-Punkt, 290mm Profil, 2m Länge.', brand: 'Tomcat', specs: { profile: '290x290 mm', length: '2 m', load: '450 kg', material: 'Aluminium EN AW-6082' }, image: '', related: [10, 14, 15] },
{ id: 10, code: 'RG-002', name: 'Chain Motor 1T', category: 'Rigging', description: 'Bühnen-Motor 1000kg Tragkraft, D8+ Plus geprüft.', brand: 'Verlinde', specs: { capacity: '1000 kg', speed: '4-8 m/min', control: 'Direct Control', certification: 'D8+ Plus' }, image: '', related: [9, 14, 13] },
{ id: 11, code: 'VD-001', name: 'LED Video Wall P3.9', category: 'Video', description: 'Indoor LED Panel P3.9, 500x500mm, 3840Hz Refresh Rate.', brand: 'Absen', specs: { pixel_pitch: '3.9 mm', size: '500x500 mm', refresh: '3840 Hz', brightness: '1500 nits', ip: 'IP30 Indoor' }, image: '', related: [12, 5, 6] },
{ id: 12, code: 'VD-002', name: 'Blackmagic ATEM Mini Extreme', category: 'Video', description: '8-Input HDMI Switcher mit ISO Recording und Multiview.', brand: 'Blackmagic', specs: { inputs: '8x HDMI', outputs: '2x HDMI', recording: 'ISO Recording', streaming: 'Direct Streaming', audio: '2x 3.5mm' }, image: '', related: [11, 5, 7] },
{ id: 13, code: 'ST-001', name: 'WAGO 24V 40A Netzteilkasten', category: 'Strom', description: '24V DC 40A Netzteil in Flightcase mit 4x Schuko Ausgang.', brand: 'WAGO', specs: { output: '24V DC 40A', inputs: '4x Schuko', protection: 'IP54', cooling: 'Forced Air' }, image: '', related: [14, 10, 9] },
{ id: 14, code: 'ST-002', name: 'Duraplex 32A Stromverteiler', category: 'Strom', description: '32A 5-polig auf 3x 16A + 3x Schuko mit FI-Schutzschalter.', brand: 'Duraplex', specs: { input: '32A 5-pol CEE', outputs: '3x 16A + 3x Schuko', protection: 'FI/A', cable: 'H07BQ-F 5G6' }, image: '', related: [13, 10, 9] },
{ id: 15, code: 'ZB-001', name: 'Flightcase 19" 12HE', category: 'Zubehör', description: '19" Rack Flightcase 12HE mit Front- und Rücktür.', brand: 'Thon', specs: { rack_units: '12 HE', depth: '600 mm', wheels: '4x 100mm', material: 'Birkenholz + Alu-Kanten' }, image: '', related: [16, 4, 15] },
{ id: 16, code: 'ZB-002', name: 'XLR Kabel 20m', category: 'Zubehör', description: 'XLR3 male/female Mikrofonkabel, 20m, Neutrik Stecker.', brand: 'Tasker', specs: { length: '20 m', connectors: 'XLR3 M/F', shielding: 'double', cable: 'Tasker C118' }, image: '', related: [3, 18, 4] },
{ id: 17, code: 'ZB-003', name: 'DJ-Table Pro', category: 'Zubehör', description: 'Mobile DJ-Tisch mit Laptop-Ständer und LED-Beleuchtung.', brand: 'Stage Traps', specs: { width: '120 cm', height: '95 cm', features: 'LED, Laptop-Ständer', weight: '22 kg' }, image: '', related: [12, 5, 15] },
{ id: 18, code: 'LT-005', name: 'Sennheiser EW-DX 835', category: 'Tontechnik', description: 'Digitales Funkmikrofon-Set mit Handheld und Empfänger.', brand: 'Sennheiser', specs: { type: 'Digital Wireless', range: '100 m', battery: '12 Std', freq_range: '563-608 MHz', channels: 'gleichzeitig 95' }, image: '', related: [3, 4, 16] }
];
const relatedItems = computed(() => { if (!item.value || !item.value.related) return []; return item.value.related.map(id => equipmentData.find(e => e.id === id)).filter(Boolean); });
onMounted(() => { setTimeout(() => { const found = equipmentData.find(e => e.id === Number(props.id)); if (found) { item.value = found; } else { error.value = true; } loading.value = false; }, 800); });
function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
function addToCart() { emit('add-to-cart', { ...item.value, quantity: quantity.value, rentalStart: rentalStart.value, rentalEnd: rentalEnd.value }); navigate('#/warenkorb'); }
function addToCartSimple(e) { emit('add-to-cart', e); }
return { loading, error, item, quantity, rentalStart, rentalEnd, relatedItems, navigate, addToCart, addToCartSimple };
},
template: `
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<nav class="text-sm mb-6" aria-label="Breadcrumb"><a href="#/mietkatalog" @click.prevent="navigate('#/mietkatalog')" style="color: var(--secondary)">← Zurück zum Katalog</a></nav>
<div v-if="loading" class="grid lg:grid-cols-2 gap-8" aria-live="polite" aria-busy="true"><div class="hms-skeleton aspect-square rounded-lg"></div><div class="space-y-4"><div class="hms-skeleton h-8 w-3/4"></div><div class="hms-skeleton h-4 w-1/4"></div><div class="hms-skeleton h-24 w-full"></div><div class="hms-skeleton h-32 w-full"></div><div class="hms-skeleton h-12 w-full" style="border-radius:var(--radius-md)"></div></div></div>
<error-state v-else-if="error" title="Gerät nicht gefunden" message="Das angeforderte Gerät konnte nicht gefunden werden." @retry="navigate('#/mietkatalog')" />
<div v-else-if="item">
<div class="grid lg:grid-cols-2 gap-8 lg:gap-12">
<div class="aspect-square rounded-lg flex items-center justify-center relative overflow-hidden border" :style="{ background: 'var(--surface)', borderColor: 'var(--border)' }">
<img v-if="item.image" :src="item.image" :alt="item.name" class="absolute inset-0 w-full h-full object-cover" />
<div v-else class="text-center"><div class="text-8xl mb-2" style="color: var(--secondary)">📦</div><div class="text-sm" style="color: var(--secondary)">Kein Bild verfügbar</div></div>
<span class="hms-badge hms-badge-primary absolute top-4 left-4 text-sm">{{ item.category }}</span>
</div>
<div>
<div class="text-sm mb-1" style="color: var(--secondary)">Artikelnummer: {{ item.code }}</div>
<h1 class="text-2xl sm:text-3xl font-bold mb-2" style="color: var(--text)">{{ item.name }}</h1>
<div class="text-sm mb-4" style="color: var(--text-muted)">Marke: <span class="font-medium" style="color: var(--text)">{{ item.brand }}</span></div>
<p class="leading-relaxed mb-6" style="color: var(--text-muted)">{{ item.description }}</p>
<div class="hms-card p-4 mb-6"><h2 class="text-sm font-semibold mb-3" style="color: var(--text)">Technische Daten</h2><dl class="divide-y" :style="{ borderColor: 'var(--border)' }"><div v-for="(value, key) in item.specs" :key="key" class="flex justify-between py-2 text-sm" :style="{ borderColor: 'var(--border)' }"><dt class="capitalize" style="color: var(--secondary)">{{ key.replace(/_/g, ' ') }}</dt><dd class="font-medium text-right" style="color: var(--text)">{{ value }}</dd></div></dl></div>
<div class="hms-card p-6">
<h2 class="text-sm font-semibold mb-4" style="color: var(--text)">Mietanfrage</h2>
<div class="grid grid-cols-2 gap-3 mb-4"><div><label for="rental-start" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Mietbeginn</label><input id="rental-start" v-model="rentalStart" type="date" class="hms-input text-sm" /></div><div><label for="rental-end" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Mietende</label><input id="rental-end" v-model="rentalEnd" type="date" class="hms-input text-sm" /></div></div>
<div class="mb-4"><label for="quantity" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Anzahl</label><div class="flex items-center gap-3"><button @click="quantity = Math.max(1, quantity - 1)" class="hms-btn hms-btn-secondary px-3 py-2" aria-label="Anzahl verringern"></button><input id="quantity" v-model.number="quantity" type="number" min="1" class="hms-input text-center w-20" aria-label="Anzahl" /><button @click="quantity = quantity + 1" class="hms-btn hms-btn-secondary px-3 py-2" aria-label="Anzahl erhöhen">+</button></div></div>
<button @click="addToCart" class="hms-btn hms-btn-primary w-full py-3 text-base"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>Zur Mietanfrage hinzufügen</button>
<p class="text-xs mt-3 text-center" style="color: var(--secondary)">Preise auf Anfrage unverbindliche Mietanfrage</p>
</div>
</div>
</div>
<section v-if="relatedItems.length" class="mt-16" aria-labelledby="related-title"><h2 id="related-title" class="text-xl font-semibold mb-6" style="color: var(--text)">Ähnliche Geräte</h2><div class="grid grid-cols-1 sm:grid-cols-3 gap-6"><equipment-card v-for="ri in relatedItems" :key="ri.id" :item="ri" @click="navigate('#/mietkatalog/' + ri.id)" @add-to-cart="addToCartSimple" /></div></section>
</div>
</div>`
});
// ===== WARENKORB PAGE =====
const WarenkorbPage = defineComponent({
name: 'WarenkorbPage',
props: { cart: Array },
emits: ['remove-item', 'update-quantity', 'navigate', 'clear-cart'],
setup(props, { emit }) {
const submitting = ref(false); const submitted = ref(false);
const form = reactive({ name: '', email: '', phone: '', event_date: '', event_location: '', message: '' });
const errors = reactive({});
function removeItem(index) { emit('remove-item', index); }
function updateQty(index, delta) { emit('update-quantity', { index, delta }); }
function navigate(route) { window.location.hash = route; window.scrollTo(0, 0); }
function validate() {
const e = {};
if (!form.name.trim()) e.name = 'Name ist erforderlich';
if (!form.email.trim()) e.email = 'E-Mail ist erforderlich';
else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail';
if (!form.event_date) e.event_date = 'Veranstaltungsdatum erforderlich';
if (!form.event_location.trim()) e.event_location = 'Veranstaltungsort erforderlich';
Object.keys(errors).forEach(k => delete errors[k]); Object.assign(errors, e);
return Object.keys(e).length === 0;
}
function submitRequest() { if (!validate()) return; submitting.value = true; setTimeout(() => { submitting.value = false; submitted.value = true; emit('clear-cart'); }, 1800); }
function resetForm() { Object.assign(form, { name: '', email: '', phone: '', event_date: '', event_location: '', message: '' }); submitted.value = false; }
return { submitting, submitted, form, errors, removeItem, updateQty, navigate, validate, submitRequest, resetForm };
},
template: `
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Mietanfrage</h1>
<p class="mb-8" style="color: var(--secondary)">Überprüfen Sie Ihre Geräteauswahl und senden Sie die Anfrage ab.</p>
<div v-if="submitted" class="hms-card p-8 text-center" role="status">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-success-bg)', color: 'var(--color-success)' }"><svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg></div>
<h2 class="text-xl font-semibold mb-2" style="color: var(--text)">Mietanfrage übermittelt</h2>
<p class="mb-6" style="color: var(--secondary)">Vielen Dank! Wir melden uns innerhalb von 24 Stunden mit einem unverbindlichen Angebot.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center"><button @click="navigate('#/mietkatalog')" class="hms-btn hms-btn-secondary">Weiter stöbern</button><button @click="navigate('#/')" class="hms-btn hms-btn-primary">Zur Startseite</button></div>
</div>
<empty-state v-else-if="cart.length === 0" icon="🛒" title="Warenkorb ist leer" message="Fügen Sie Geräte aus dem Mietkatalog hinzu, um eine Mietanfrage zu stellen." action-label="Zum Mietkatalog" @action="navigate('#/mietkatalog')" />
<div v-else class="grid lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-4">
<div v-for="(item, index) in cart" :key="index" class="hms-card p-4 sm:p-6">
<div class="flex gap-4">
<div class="w-20 h-20 rounded-lg flex items-center justify-center flex-shrink-0 text-2xl" style="background: var(--surface); color: var(--secondary)">📦</div>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2"><div><h3 class="font-semibold text-sm sm:text-base" style="color: var(--text)">{{ item.name }}</h3><div class="text-xs mt-0.5" style="color: var(--secondary)">{{ item.code }} · {{ item.brand }}</div></div>
<button @click="removeItem(index)" class="p-1" style="color: var(--secondary)" :aria-label="item.name + ' entfernen'"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button>
</div>
<div class="flex flex-wrap items-center gap-3 mt-3"><div class="flex items-center gap-2"><button @click="updateQty(index, -1)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl verringern"></button><span class="w-8 text-center text-sm font-medium" style="color: var(--text)">{{ item.quantity }}</span><button @click="updateQty(index, 1)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl erhöhen">+</button></div><div v-if="item.rentalStart" class="text-xs" style="color: var(--secondary)">📅 {{ item.rentalStart }} <span v-if="item.rentalEnd">bis {{ item.rentalEnd }}</span></div></div>
</div>
</div>
</div>
<button @click="$emit('clear-cart')" class="text-sm hover:text-[var(--color-error)]" style="color: var(--secondary)">Warenkorb leeren</button>
</div>
<div>
<div class="hms-card p-6 sticky top-20">
<h2 class="text-lg font-semibold mb-4" style="color: var(--text)">Anfrage senden</h2>
<div class="text-sm mb-4 pb-4 border-b" :style="{ borderColor: 'var(--border)', color: 'var(--secondary)' }"><div class="flex justify-between mb-1"><span>Geräte gesamt:</span><span class="font-medium" style="color: var(--text)">{{ cart.reduce((s,i)=>s+i.quantity,0) }}</span></div><div class="flex justify-between"><span>Positionen:</span><span class="font-medium" style="color: var(--text)">{{ cart.length }}</span></div></div>
<form @submit.prevent="submitRequest" novalidate>
<div class="space-y-3">
<div><label for="req-name" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Name <span style="color: var(--color-error)">*</span></label><input id="req-name" v-model="form.name" type="text" :class="['hms-input text-sm', errors.name ? 'hms-input-error' : '']" placeholder="Ihr Name" /><p v-if="errors.name" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.name }}</p></div>
<div><label for="req-email" class="block text-xs font-medium mb-1" style="color: var(--secondary)">E-Mail <span style="color: var(--color-error)">*</span></label><input id="req-email" v-model="form.email" type="email" :class="['hms-input text-sm', errors.email ? 'hms-input-error' : '']" placeholder="ihre@email.de" /><p v-if="errors.email" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.email }}</p></div>
<div><label for="req-phone" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Telefon</label><input id="req-phone" v-model="form.phone" type="tel" class="hms-input text-sm" placeholder="+49 ..." /></div>
<div><label for="req-date" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsdatum <span style="color: var(--color-error)">*</span></label><input id="req-date" v-model="form.event_date" type="date" :class="['hms-input text-sm', errors.event_date ? 'hms-input-error' : '']" /><p v-if="errors.event_date" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_date }}</p></div>
<div><label for="req-location" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsort <span style="color: var(--color-error)">*</span></label><input id="req-location" v-model="form.event_location" type="text" :class="['hms-input text-sm', errors.event_location ? 'hms-input-error' : '']" placeholder="Ort / Location" /><p v-if="errors.event_location" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_location }}</p></div>
<div><label for="req-message" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Anmerkung</label><textarea id="req-message" v-model="form.message" rows="3" class="hms-input text-sm" placeholder="Zusätzliche Informationen..."></textarea></div>
<button type="submit" :disabled="submitting" class="hms-btn hms-btn-primary w-full py-3"><span v-if="submitting" class="hms-spinner" style="width:18px;height:18px;border-width:2px"></span><span v-else>Anfrage absenden</span></button>
<p class="text-xs text-center" style="color: var(--secondary)">Unverbindlich Angebot innerhalb 24 Std.</p>
</div>
</form>
</div>
</div>
</div>
</div>`
});
// ===== ADMIN PAGE =====
const AdminPage = defineComponent({
name: 'AdminPage',
setup() {
const username = ref(''); const password = ref(''); const error = ref(''); const loading = ref(false); const loggedIn = ref(false);
function login() { error.value = ''; if (!username.value || !password.value) { error.value = 'Bitte Benutzername und Passwort eingeben'; return; } loading.value = true; setTimeout(() => { loading.value = false; loggedIn.value = true; }, 1200); }
return { username, password, error, loading, loggedIn, login };
},
template: `
<div class="max-w-md mx-auto px-4 py-16"><div class="hms-card p-8"><div class="text-center mb-6"><hms-logo :size="48" /><h1 class="text-xl font-bold mt-4" style="color: var(--text)">Admin-Login</h1><p class="text-sm mt-1" style="color: var(--secondary)">Equipment-Sync Verwaltungsbereich</p></div>
<div v-if="loggedIn" class="text-center py-8" role="status"><div class="inline-flex items-center justify-center w-12 h-12 rounded-full mb-3" :style="{ background: 'var(--color-success-bg)', color: 'var(--color-success)' }"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg></div><p class="font-medium" style="color: var(--text)">Eingeloggt</p><p class="text-xs mt-1" style="color: var(--secondary)">(Prototyp keine echte Session)</p></div>
<form v-else @submit.prevent="login" novalidate><div class="space-y-4"><div><label for="admin-user" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Benutzername</label><input id="admin-user" v-model="username" type="text" class="hms-input" placeholder="admin" :aria-invalid="!!error" /></div><div><label for="admin-pass" class="block text-sm font-medium mb-1" style="color: var(--text-muted)">Passwort</label><input id="admin-pass" v-model="password" type="password" class="hms-input" placeholder="••••••••" :aria-invalid="!!error" /></div><p v-if="error" class="text-xs" style="color: var(--color-error)" role="alert">{{ error }}</p><button type="submit" :disabled="loading" class="hms-btn hms-btn-primary w-full py-3"><span v-if="loading" class="hms-spinner" style="width:18px;height:18px;border-width:2px"></span><span v-else>Einloggen</span></button></div></form></div></div>`
});
// ===== 404 =====
const NotFoundPage = defineComponent({
name: 'NotFoundPage',
template: `<div class="max-w-md mx-auto px-4 py-24 text-center"><div class="text-8xl font-bold mb-4" style="color: var(--secondary)">404</div><h1 class="text-2xl font-semibold mb-2" style="color: var(--text)">Seite nicht gefunden</h1><p class="mb-8" style="color: var(--secondary)">Die angeforderte Seite existiert nicht.</p><a href="#/" class="hms-btn hms-btn-primary">Zur Startseite</a></div>`
});
// ===== MAIN APP =====
const App = defineComponent({
name: 'App',
setup() {
const currentRoute = ref(window.location.hash || '#/');
const cart = reactive([]);
function addToCart(item) { const existing = cart.find(i => i.id === item.id); if (existing) { existing.quantity += item.quantity || 1; } else { cart.push({ ...item, quantity: item.quantity || 1 }); } }
function removeItem(index) { cart.splice(index, 1); }
function updateQuantity({ index, delta }) { const item = cart[index]; if (item) { item.quantity = Math.max(1, item.quantity + delta); } }
function clearCart() { cart.splice(0, cart.length); }
const routeParts = computed(() => { const hash = currentRoute.value.replace(/^#/, '') || '/'; return hash.split('/').filter(Boolean); });
const currentView = computed(() => { const parts = routeParts.value; if (parts.length === 0) return 'home'; if (parts[0] === 'referenzen') return 'referenzen'; if (parts[0] === 'kontakt') return 'kontakt'; if (parts[0] === 'mietkatalog') return parts.length > 1 ? 'equipment-detail' : 'mietkatalog'; if (parts[0] === 'warenkorb') return 'warenkorb'; if (parts[0] === 'admin') return 'admin'; return '404'; });
const detailId = computed(() => { const parts = routeParts.value; if (parts[0] === 'mietkatalog' && parts.length > 1) return parts[1]; return null; });
window.addEventListener('hashchange', () => { currentRoute.value = window.location.hash || '#/'; window.scrollTo(0, 0); });
return { currentRoute, currentView, detailId, cart, addToCart, removeItem, updateQuantity, clearCart };
},
template: `
<div class="min-h-screen flex flex-col">
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
<app-header />
<main id="main-content" class="flex-1" role="main">
<transition name="fade" mode="out-in">
<home-page v-if="currentView === 'home'" @navigate @add-to-cart="addToCart" key="home" />
<referenzen-page v-else-if="currentView === 'referenzen'" key="referenzen" />
<kontakt-page v-else-if="currentView === 'kontakt'" key="kontakt" />
<mietkatalog-page v-else-if="currentView === 'mietkatalog'" @navigate @add-to-cart="addToCart" key="mietkatalog" />
<equipment-detail-page v-else-if="currentView === 'equipment-detail'" :id="detailId" :cart="cart" @navigate @add-to-cart="addToCart" :key="'detail-' + detailId" />
<warenkorb-page v-else-if="currentView === 'warenkorb'" :cart="cart" @remove-item="removeItem" @update-quantity="updateQuantity" @navigate @clear-cart="clearCart" key="warenkorb" />
<admin-page v-else-if="currentView === 'admin'" key="admin" />
<not-found-page v-else key="404" />
</transition>
</main>
<app-footer @navigate />
</div>`
});
const app = createApp(App);
app.component('hms-logo', HmsLogo);
app.component('speaker-icon', SpeakerIcon);
app.component('app-header', AppHeader);
app.component('app-footer', AppFooter);
app.component('service-card', ServiceCard);
app.component('equipment-card', EquipmentCard);
app.component('loading-skeleton', LoadingSkeleton);
app.component('empty-state', EmptyState);
app.component('error-state', ErrorState);
app.component('home-page', HomePage);
app.component('referenzen-page', ReferenzenPage);
app.component('kontakt-page', KontaktPage);
app.component('mietkatalog-page', MietkatalogPage);
app.component('equipment-detail-page', EquipmentDetailPage);
app.component('warenkorb-page', WarenkorbPage);
app.component('admin-page', AdminPage);
app.component('not-found-page', NotFoundPage);
app.mount('#app');