Files
hms-licht-ton/docs/prototype-app.js
T

762 lines
73 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================
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');