feat(T02): Static pages – home, referenzen, kontakt, legal (impressum/datenschutz/agb)

This commit is contained in:
Implementation Engineer
2026-07-10 01:02:27 +02:00
parent e9da21b57e
commit db5080df48
15 changed files with 1618 additions and 109 deletions
+104 -3
View File
@@ -1,7 +1,108 @@
<template>
<LegalContentPage title="AGB Vermietung" :content="content" />
</template>
<script setup lang="ts">
useHead({ title: "AGB Vermietung HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
const content = `<p>Allgemeine Geschäftsbedingungen für die Vermietung.</p>`;
</script>
useHead({
title: "AGB Vermietung HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
const content = `
<h2>§ 1 Geltungsbereich</h2>
<p>
Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für alle Mietverträge über
Veranstaltungstechnik zwischen der Hammerschmidt u. Mössle GbR (nachfolgend „HMS
Licht & Ton“ oder „Vermieter“) und dem Mieter. Sie gelten ausschließlich;
entgegenstehende oder von unseren AGB abweichende Bedingungen des Mieters
erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer
Geltung zugestimmt.
</p>
<h2>§ 2 Mietgegenstand</h2>
<p>
Gegenstand des Mietvertrages ist die Überlassung von Veranstaltungstechnik
(Lautsprecher, Mischpulte, Lichtanlagen, Rigging-Material, Traversen,
Zubehör etc.) gemäß der im Mietvertrag bzw. in der Auftragsbestätigung
angegebenen Spezifikation. Der Vermieter schuldet nicht die Durchführung der
Veranstaltung, sondern nur die Überlassung der gemieteten Technik in
vertragsgemäßem Zustand.
</p>
<h2>§ 3 Preise</h2>
<p>
Es gelten die zum Zeitpunkt der Auftragsbestätigung im Mietkatalog bzw. im
Angebot angegebenen Preise. Alle Preise verstehen sich zzgl. der gesetzlich
geltenden Mehrwertsteuer. Miettage berechnen sich ab Abholung bzw. Anlieferung
bis zur Rückgabe bzw. Abholung durch den Vermieter. Ein Miettag entspricht
24 Stunden. Bei Überschreitung der vereinbarten Mietzeit werden zusätzliche
Miettage berechnet.
</p>
<h2>§ 4 Zahlungsbedingungen</h2>
<p>
Die Zahlung ist innerhalb von 14 Tagen nach Rechnungsstellung ohne Abzug
fällig, sofern nichts anderes vereinbart wurde. Bei Neukunden kann eine
Anzahlung in Höhe von 50 % des Gesamtmietpreises vor Übergabe der Technik
verlangt werden. Die Aufrechnung mit Gegenansprüchen ist nur zulässig,
soweit diese unbestritten oder rechtskräftig festgestellt sind.
</p>
<h2>§ 5 Haftung des Mieters</h2>
<p>
Der Mieter hat das Mietmaterial pfleglich zu behandeln und vor Beschädigung,
Diebstahl und unsachgemäßer Nutzung zu schützen. Der Mieter haftet für alle
Schäden am Mietmaterial, die während der Mietzeit entstehen, soweit sie nicht
auf normaler Abnutzung beruhen. Bei Verlust oder Diebstahl trägt der Mieter
den Wiederbeschaffungswert. Bei Beschädigung trägt der Mieter die
Reparaturkosten oder den Zeitwert bei Totalschaden.
</p>
<p>
Eine Haftung des Vermieters für indirekte Schäden, insbesondere für
Gewinnverluste, Ausfallkosten oder Folgeschäden der Veranstaltung, ist
ausgeschlossen, soweit diese nicht auf Vorsatz oder grober Fahrlässigkeit
beruhen.
</p>
<h2>§ 6 Übergabe und Rückgabe</h2>
<p>
Die Übergabe des Mietmaterials erfolgt am Lager des Vermieters oder an dem
vereinbarten Veranstaltungsort. Der Mieter hat das Mietmaterial bei
Übergabe auf Vollständigkeit und Schäden zu prüfen. Mängel sind sofort
schriftlich zu melden. Bei der Rückgabe wird das Mietmaterial erneut
geprüft. Fehlbestände oder Schäden, die erst bei der Rückgabe festgestellt
werden, gelten als während der Mietzeit entstanden, sofern der Mieter nicht
den Beweis erbringt, dass der Schaden bereits bei Übergabe vorlag.
</p>
<p>
Das Mietmaterial ist nach Ende der Mietzeit in gereinigtem und
sortiertem Zustand zurückzugeben. Verpackungsmaterial ist vollständig
zurückzugeben. Nicht rechtzeitig zurückgegebenes Material wird bis zur
Rückgabe weiterberechnet.
</p>
<h2>§ 7 Kündigung und Rücktritt</h2>
<p>
Der Mieter kann den Mietvertrag jederzeit vor Übergabe des Mietmaterials
schriftlich kündigen. Bei Kündigung bis 14 Tage vor Mietbeginn werden 25 %,
bis 7 Tage 50 %, bis 3 Tage 75 % und ab 3 Tagen 100 % des Mietpreises als
Stornokosten berechnet.
</p>
<p>
Der Vermieter behält sich vor, vom Vertrag zurückzutreten, wenn die
Verfügung über das Mietmaterial aus Gründen, die der Vermieter nicht zu
vertreten hat, unmöglich wird (z. B. höhere Gewalt, höhere Gewalt,
unvorhersehbare Ereignisse). In diesem Fall werden bereits gezahlte
Mietkosten erstattet.
</p>
<h2>§ 8 Schlussbestimmungen</h2>
<p>
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist
Günzburg. Sollten einzelne Bestimmungen dieser AGB unwirksam sein, so
bleibt die Wirksamkeit der übrigen Bestimmungen unberührt. An die Stelle
der unwirksamen Bestimmung tritt eine Regelung, die dem wirtschaftlichen
Zweck der unwirksamen Bestimmung am nächsten kommt.
</p>
`;
</script>
+104 -3
View File
@@ -1,7 +1,108 @@
<template>
<LegalContentPage title="Datenschutz (DSGVO)" :content="content" />
</template>
<script setup lang="ts">
useHead({ title: "Datenschutz HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
const content = `<p>Datenschutzerklärung gemäß DSGVO.</p>`;
</script>
useHead({
title: "Datenschutz HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
const content = `
<h2>1. Datenschutz auf einen Blick</h2>
<p>
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren
personenbezogenen Daten passiert, wenn Sie diese Website besuchen oder unser
Kontaktformular nutzen. Personenbezogene Daten sind alle Daten, mit denen Sie
persönlich identifiziert werden können.
</p>
<h3>Datenerhebung</h3>
<p>
Die Datenerhebung auf dieser Website erfolgt technisch notwendig zur Bereitstellung
der Website und zur Gewährleistung der Funktionstüchtigkeit und Sicherheit der
Systeme. Darüber hinaus werden Daten nur erhoben, wenn Sie uns diese im Rahmen
des Kontaktformulars freiwillig zur Verfügung stellen.
</p>
<h3>Cookies</h3>
<p>
Diese Website verwendet keine Tracking-Cookies. Technisch notwendige Cookies,
die für den Betrieb der Seite erforderlich sind, werden nur verwendet, soweit
dies für die Bereitstellung der Funktionalität der Website zwingend notwendig
ist. Sie können die Speicherung solcher Cookies durch entsprechende Einstellungen
in Ihrem Browser verhindern.
</p>
<h2>2. Verantwortliche Stelle</h2>
<p>
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br/>
Hammerschmidt u. Mössle GbR<br/>
Grockelhofen 10<br/>
89340 Leipheim<br/>
Telefon: +49 (0) 8221 / 204433<br/>
E-Mail: info@hms-licht-ton.de
</p>
<h2>3. Erhebung von Daten beim Kontaktformular</h2>
<p>
Wenn Sie uns über das Kontaktformular Anfragen zukommen lassen, werden Ihre
Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen
Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen
bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
</p>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b
DSGVO, sofern Ihre Anfrage mit der Erfüllung eines Vertrags zusammenhängt oder
zur Durchführung vorvertraglicher Maßnahmen erforderlich ist. In allen
übrigen Fällen ist die Verarbeitung auf Grundlage Ihres berechtigten
Interesses (Art. 6 Abs. 1 lit. f DSGVO) oder auf Grundlage Ihrer Einwilligung
(Art. 6 Abs. 1 lit. a DSGVO) erfolgt.
</p>
<p>
Die Daten werden gelöscht, sobald sie für die Erreichung des Zweckes ihrer
Erhebung nicht mehr erforderlich sind. Für die personenbezogenen Daten aus
dem Eingabeformular der Kontaktanfrage ist dies der Fall, wenn die jeweilige
Konversation mit Ihnen beendet ist. Der Gesprächsbeginn endet, wenn aus den
Umständen zu entnehmen ist, dass der betroffene Sachverhalt abschließend
geklärt ist.
</p>
<h2>4. Server-Log-Dateien</h2>
<p>
Der Provider der Seiten erhebt und speichert automatisch Informationen in
sogenannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt.
Diese sind:
</p>
<ul>
<li>Browsertyp und Browserversion</li>
<li>verwendetes Betriebssystem</li>
<li>Referrer URL</li>
<li>Hostname des zugreifenden Rechners</li>
<li>Uhrzeit der Serveranfrage</li>
<li>IP-Adresse (anonymisiert)</li>
</ul>
<p>
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht
vorgenommen. Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f
DSGVO zum Zweck der Gewährleistung eines reibungslosen Betriebs der Website.
</p>
<h2>5. Weitergabe an Drittanbieter</h2>
<p>
Eine Übermittlung Ihrer persönlichen Daten an Dritte zu anderen als den im
Folgenden aufgeführten Zwecken findet nicht statt. Eine Weitergabe erfolgt nur,
wenn Sie eine ausdrückliche Einwilligung dazu erteilt haben oder die
Weitergabe gesetzlich zulässig ist.
</p>
<h2>6. Ihre Rechte</h2>
<p>
Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der
Verarbeitung, Widerspruch gegen die Verarbeitung und das Recht auf
Datenübertragbarkeit (Art. 1520 DSGVO). Zudem haben Sie das Recht, sich bei
einer Datenschutz-Aufsichtsbehörde über die Verarbeitung Ihrer
personenbezogenen Daten zu beschweren.
</p>
`;
</script>
+75 -3
View File
@@ -1,7 +1,79 @@
<template>
<LegalContentPage title="Impressum" :content="impressumContent" />
</template>
<script setup lang="ts">
useHead({ title: "Impressum HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
const impressumContent = `<p>Hammerschmidt u. Mössle GbR</p><p>Grockelhofen 10, 89340 Leipheim</p><p>Telefon: +49 (0) 8221 / 204433</p><p>E-Mail: info@hms-licht-ton.de</p>`;
</script>
useHead({
title: "Impressum HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
const impressumContent = `
<h2>Angaben gemäß § 5 TMG</h2>
<p>
Hammerschmidt u. Mössle GbR<br/>
Grockelhofen 10<br/>
89340 Leipheim
</p>
<h2>Kontakt</h2>
<p>
Telefon: +49 (0) 8221 / 204433<br/>
E-Mail: <a href="mailto:info@hms-licht-ton.de">info@hms-licht-ton.de</a>
</p>
<h2>Vertretungsberechtigte</h2>
<p>
Markus Hammerschmidt<br/>
Thomas Mössle
</p>
<h2>Registereintrag</h2>
<p>
Eintragung im Registergericht: Amtsgericht Günzburg<br/>
Registernummer: HRA 12345 (Beispiel wird nachgeliefert)
</p>
<h2>Umsatzsteuer-ID</h2>
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:<br/>
DE 123456789 (Beispiel wird nachgeliefert)
</p>
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Markus Hammerschmidt<br/>
Grockelhofen 10<br/>
89340 Leipheim
</p>
<h2>Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht
verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach
Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen
Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt
der Kenntnis einer konkreten Rechtsverletzung möglich.
</p>
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält ggf. Links zu externen Websites Dritter, auf deren Inhalte wir keinen
Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.
Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der
Seiten verantwortlich.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des
jeweiligen Autors bzw. Erstellers.
</p>
`;
</script>
+153 -12
View File
@@ -1,19 +1,83 @@
<template>
<div>
<section class="mb-12">
<p class="text-sm text-accent font-semibold mb-2">Seit 2003 &middot; Über 20 Jahre Veranstaltungstechnik</p>
<h1 class="text-4xl lg:text-5xl font-extrabold mb-4">Professionelle Technik für Ihre Veranstaltung</h1>
<p class="text-text-muted text-lg mb-6 max-w-2xl">
Von der Firmenevent-Beschallung bis zur Open-Air-Großproduktion: HMS Licht &amp; Ton liefert
Tontechnik, Lichttechnik, Rigging und Komplettlösungen aus Leipheim und Ellzee.
</p>
<div class="flex flex-wrap gap-4">
<NuxtLink to="/mietkatalog" class="btn-primary">Mietkatalog öffnen</NuxtLink>
<NuxtLink to="/kontakt" class="btn-secondary">Beratung anfragen</NuxtLink>
<!-- Hero Section -->
<section class="relative mb-16 overflow-hidden rounded-lg">
<div class="absolute inset-0 bg-bg">
<img
src="https://images.unsplash.com/photo-1505236858219-8359eb222e89?w=1600&q=80"
alt="Veranstaltungstechnik"
class="w-full h-full object-cover opacity-30"
loading="eager"
/>
</div>
<div class="absolute inset-0 bg-gradient-to-t from-bg via-bg/80 to-bg/50" />
<div class="relative px-6 py-20 lg:py-32 max-w-4xl">
<p class="text-sm text-accent font-semibold mb-2">Seit 2003 &middot; Über 20 Jahre Veranstaltungstechnik</p>
<h1 class="text-4xl lg:text-6xl font-extrabold mb-4" data-testid="hero-headline">
Veranstaltungstechnik
</h1>
<p class="text-text-muted text-lg lg:text-xl mb-8 max-w-2xl">
Professionelle Tontechnik, Lichttechnik, Rigging und Komplettlösungen
für Ihre Veranstaltung. Von der Firmenevent-Beschallung bis zur
Open-Air-Großproduktion HMS Licht &amp; Ton liefert aus Leipheim und Ellzee.
</p>
<div class="flex flex-wrap gap-4">
<NuxtLink to="/mietkatalog" class="btn-primary" data-testid="hero-cta-mietkatalog">
Mietkatalog öffnen
</NuxtLink>
<NuxtLink to="/kontakt" class="btn-secondary" data-testid="hero-cta-kontakt">
Beratung anfragen
</NuxtLink>
</div>
</div>
</section>
<section class="mb-12">
<!-- Speaker Grid (Equipment Types) -->
<section class="mb-16">
<h2 class="text-2xl font-bold mb-6">Unser Equipment</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4" data-testid="speaker-grid">
<div
v-for="eq in equipmentTypes"
:key="eq.label"
class="flex flex-col items-center gap-3 p-5 rounded-lg border border-border-default bg-panel hover:border-accent-border transition-colors duration-fast"
>
<div class="w-12 h-12 flex items-center justify-center text-secondary">
<component :is="eq.icon" />
</div>
<span class="text-xs text-text-muted text-center">{{ eq.label }}</span>
</div>
</div>
</section>
<!-- Über uns Section -->
<section class="mb-16 bg-panel rounded-lg p-8" data-testid="about-section">
<h2 class="text-2xl font-bold mb-6">Über uns</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-6">
<div class="text-center">
<p class="text-4xl font-extrabold text-accent mb-1" data-testid="stat-years">20+</p>
<p class="text-sm text-text-muted">Jahre Erfahrung</p>
</div>
<div class="text-center">
<p class="text-4xl font-extrabold text-accent mb-1" data-testid="stat-devices">1000+</p>
<p class="text-sm text-text-muted">Geräte im Mietpark</p>
</div>
<div class="text-center">
<p class="text-4xl font-extrabold text-accent mb-1" data-testid="stat-events">500+</p>
<p class="text-sm text-text-muted">Events betreut</p>
</div>
</div>
<p class="text-text-muted text-sm max-w-3xl">
HMS Licht &amp; Ton ist Ihr verlässlicher Partner für Veranstaltungstechnik in
Bayerisch-Schwaben und darüber hinaus. Seit über 20 Jahren planen, liefern und
betreiben wir Technik für Konzerte, Firmenevents, Festspiele und private
Feiern. Unser Team aus erfahrenen Tontechnikern, Lichttechnikern und Rigging-Spezialisten
sorgt dafür, dass Ihre Veranstaltung perfekt läuft vom ersten Beratungsgespräch
bis zum letzten Kabel am Abbau.
</p>
</section>
<!-- 8 Service Cards -->
<section class="mb-16" data-testid="services-section">
<h2 class="text-2xl font-bold mb-6">Leistungen</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div
@@ -26,10 +90,24 @@
</div>
</div>
</section>
<!-- Mietkatalog CTA -->
<section class="mb-12 text-center bg-panel rounded-lg p-10" data-testid="mietkatalog-cta">
<h2 class="text-2xl font-bold mb-4">Bereit für Ihr nächstes Event?</h2>
<p class="text-text-muted mb-6 max-w-2xl mx-auto">
Durchstöbern Sie unseren Mietkatalog mit über 1.000 Artikeln von Lautsprechern
über Lichtanlagen bis zu Rigging-Material.
</p>
<NuxtLink to="/mietkatalog" class="btn-primary" data-testid="cta-mietkatalog-link">
Zum Mietkatalog
</NuxtLink>
</section>
</div>
</template>
<script setup lang="ts">
import { h } from "vue";
useHead({
title: "HMS Licht & Ton Veranstaltungstechnik Leipheim/Ellzee",
meta: [
@@ -48,5 +126,68 @@ const services = [
{ title: "Verkauf", description: "Neu- und Gebrauchtgeräte führender Hersteller mit Beratung, Inbetriebnahme und Service." },
{ title: "Personal", description: "Qualifizierte Tontechniker, Lichttechniker und Rigger für Aufbau, Betrieb und Abbau." },
{ title: "Transport", description: "Eigene Transportfahrzeuge für sichere An- und Ablieferung inklusive Verladekonzept." },
{ title: "Lagerung", description: "Klimatisierte und gesicherte Lagerung für empfindliche Technik zwischen den Einsätzen." },
{ title: "Werkstatt", description: "Reparatur und Wartung eigener sowie fremder Geräte durch unsere zertifizierten Techniker." },
{ title: "Installation", description: "Festinstallationen in Veranstaltungsräumen, Kirchen, Schulen und Firmengebäuden." },
{ title: "Booking", description: "Vermittlung von Künstlern, DJs und Technikern für Ihre Veranstaltung aus unserem Partnernetzwerk." },
];
</script>
type EquipmentIcon = () => ReturnType<typeof h>;
const equipmentTypes: { label: string; icon: EquipmentIcon }[] = [
{
label: "Lautsprecher",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("rect", { x: 8, y: 8, width: 32, height: 32, rx: 2, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("circle", { cx: 24, cy: 24, r: 8, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("circle", { cx: 24, cy: 24, r: 4, fill: "none", stroke: "currentColor", "stroke-width": 1.5 }),
]),
},
{
label: "Mischpulte",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("rect", { x: 6, y: 12, width: 36, height: 24, rx: 2, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("line", { x1: 14, y1: 18, x2: 14, y2: 30, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 22, y1: 18, x2: 22, y2: 30, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 30, y1: 18, x2: 30, y2: 30, stroke: "currentColor", "stroke-width": 1.5 }),
h("circle", { cx: 14, cy: 30, r: 3, fill: "none", stroke: "currentColor", "stroke-width": 1.5 }),
]),
},
{
label: "Lichtanlagen",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("rect", { x: 18, y: 6, width: 12, height: 10, rx: 1, fill: "currentColor" }),
h("path", { d: "M24 16 L24 24", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round" }),
h("circle", { cx: 24, cy: 30, r: 6, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("path", { d: "M18 30 L14 30 M30 30 L34 30", stroke: "currentColor", "stroke-width": 1.5, "stroke-linecap": "round", opacity: 0.5 }),
]),
},
{
label: "Rigging",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("path", { d: "M24 6 L24 18", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round" }),
h("rect", { x: 16, y: 18, width: 16, height: 20, rx: 2, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("path", { d: "M20 18 L16 10 M28 18 L32 10", stroke: "currentColor", "stroke-width": 1.5, "stroke-linecap": "round" }),
]),
},
{
label: "Traversen",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("rect", { x: 6, y: 20, width: 36, height: 8, rx: 1, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("line", { x1: 12, y1: 20, x2: 12, y2: 28, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 18, y1: 20, x2: 18, y2: 28, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 24, y1: 20, x2: 24, y2: 28, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 30, y1: 20, x2: 30, y2: 28, stroke: "currentColor", "stroke-width": 1.5 }),
h("line", { x1: 36, y1: 20, x2: 36, y2: 28, stroke: "currentColor", "stroke-width": 1.5 }),
]),
},
{
label: "Komplett-Setups",
icon: () => h("svg", { width: 48, height: 48, viewBox: "0 0 48 48", fill: "none" }, [
h("circle", { cx: 24, cy: 24, r: 18, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
h("path", { d: "M24 6 L24 18 M6 24 L18 24 M24 30 L24 42 M30 24 L42 24", stroke: "currentColor", "stroke-width": 1.5, "stroke-linecap": "round" }),
h("circle", { cx: 24, cy: 24, r: 6, fill: "none", stroke: "currentColor", "stroke-width": 2 }),
]),
},
];
</script>
+280 -4
View File
@@ -1,9 +1,285 @@
<template>
<div>
<h1 class="text-3xl font-bold text-text mb-4">Kontakt</h1>
<p class="text-text-muted">Kontaktformular wird hier angezeigt.</p>
<h1 class="text-3xl font-bold text-text mb-2" data-testid="kontakt-headline">Kontakt</h1>
<p class="text-text-muted mb-8">Planen Sie mit uns Ihre nächste Veranstaltung.</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<!-- Left Column: Addresses + Persons -->
<div class="space-y-6">
<!-- Office Address -->
<div class="bg-panel border border-border-default rounded-lg p-6" data-testid="office-card">
<h2 class="text-accent text-sm font-bold mb-3">Büro</h2>
<p class="text-text mb-1">Hammerschmidt u. Mössle GbR</p>
<p class="text-text-muted text-sm mb-1">Grockelhofen 10</p>
<p class="text-text-muted text-sm mb-3">89340 Leipheim</p>
<p class="text-text-muted text-sm mb-1">Tel: <a href="tel:+498221204433" class="text-accent hover:underline">+49 8221 204433</a></p>
<p class="text-text-muted text-sm">E-Mail: <a href="mailto:info@hms-licht-ton.de" class="text-accent hover:underline">info@hms-licht-ton.de</a></p>
</div>
<!-- Warehouse Address -->
<div class="bg-panel border border-border-default rounded-lg p-6" data-testid="warehouse-card">
<h2 class="text-accent text-sm font-bold mb-3">Lager / Werkstatt</h2>
<p class="text-text-muted text-sm mb-1">Ellzee</p>
<p class="text-text-muted text-sm mb-3">Bayern, Deutschland</p>
<p class="text-text-muted text-sm">Besuche nach Terminvereinbarung.</p>
</div>
<!-- Opening Hours -->
<div class="bg-panel border border-border-default rounded-lg p-6" data-testid="hours-card">
<h2 class="text-accent text-sm font-bold mb-3">Öffnungszeiten</h2>
<div class="space-y-1 text-text-muted text-sm">
<p>Mo Do: 08:00 17:00</p>
<p>Fr: 08:00 14:00</p>
<p>Sa So: Geschlossen</p>
<p class="text-accent text-xs mt-2">Termine außerhalb der Öffnungszeiten nach Vereinbarung.</p>
</div>
</div>
<!-- Contact Persons -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div v-for="person in contactPersons" :key="person.name" class="bg-panel border border-border-default rounded-lg p-5" :data-testid="'contact-person-' + person.name.split(' ')[0].toLowerCase()">
<div class="w-16 h-16 rounded-full bg-card border border-border-default mb-3 flex items-center justify-center text-secondary">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<circle cx="16" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2" />
<path d="M6 28 Q16 20 26 28" fill="none" stroke="currentColor" stroke-width="2" />
</svg>
</div>
<p class="text-text text-sm font-bold">{{ person.name }}</p>
<p class="text-accent text-xs mb-1">{{ person.role }}</p>
<a :href="'mailto:' + person.email" class="text-text-muted text-xs hover:text-accent transition-colors duration-fast">{{ person.email }}</a>
</div>
</div>
</div>
<!-- Right Column: Contact Form -->
<div class="bg-panel border border-border-default rounded-lg p-6" data-testid="contact-form-card">
<h2 class="text-2xl font-bold mb-6">Anfrage senden</h2>
<!-- Success State -->
<div v-if="submitSuccess" class="bg-success-bg border border-success/30 rounded-lg p-6 text-center" data-testid="success-message">
<p class="text-success font-bold text-lg mb-2">Vielen Dank!</p>
<p class="text-text-muted text-sm">Ihre Nachricht wurde erfolgreich gesendet. Wir melden uns in Kürze bei Ihnen.</p>
<button class="btn-secondary mt-4" @click="resetForm" data-testid="reset-form">Neue Nachricht</button>
</div>
<!-- Error State -->
<div v-else-if="submitError" class="bg-error-bg border border-error/30 rounded-lg p-6 text-center" data-testid="error-message">
<p class="text-error font-bold text-lg mb-2">Fehler</p>
<p class="text-text-muted text-sm mb-4">Beim Senden ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.</p>
<button class="btn-primary" @click="submitError = false" data-testid="retry-submit">Erneut versuchen</button>
</div>
<!-- Form -->
<form v-else @submit.prevent="handleSubmit" novalidate data-testid="contact-form">
<!-- Name -->
<div class="mb-4">
<label for="name" class="block text-text-muted text-sm mb-1">Name *</label>
<input
id="name"
v-model="form.name"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2 text-text focus:border-accent focus:outline-none"
:class="{ 'border-error': errors.name }"
data-testid="form-name"
@blur="validateField('name')"
/>
<p v-if="errors.name" class="text-error text-xs mt-1" data-testid="error-name">{{ errors.name }}</p>
</div>
<!-- Email -->
<div class="mb-4">
<label for="email" class="block text-text-muted text-sm mb-1">E-Mail *</label>
<input
id="email"
v-model="form.email"
type="email"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2 text-text focus:border-accent focus:outline-none"
:class="{ 'border-error': errors.email }"
data-testid="form-email"
@blur="validateField('email')"
/>
<p v-if="errors.email" class="text-error text-xs mt-1" data-testid="error-email">{{ errors.email }}</p>
</div>
<!-- Phone -->
<div class="mb-4">
<label for="phone" class="block text-text-muted text-sm mb-1">Telefon</label>
<input
id="phone"
v-model="form.phone"
type="tel"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2 text-text focus:border-accent focus:outline-none"
data-testid="form-phone"
/>
</div>
<!-- Message -->
<div class="mb-4">
<label for="message" class="block text-text-muted text-sm mb-1">Nachricht *</label>
<textarea
id="message"
v-model="form.message"
rows="5"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2 text-text focus:border-accent focus:outline-none"
:class="{ 'border-error': errors.message }"
data-testid="form-message"
@blur="validateField('message')"
></textarea>
<p v-if="errors.message" class="text-error text-xs mt-1" data-testid="error-message-field">{{ errors.message }}</p>
</div>
<!-- Privacy Consent -->
<div class="mb-6">
<label class="flex items-start gap-3 cursor-pointer">
<input
v-model="form.privacy"
type="checkbox"
class="mt-1 w-4 h-4 rounded border-border-default bg-surface accent-accent"
:class="{ 'border-error': errors.privacy }"
data-testid="form-privacy"
/>
<span class="text-text-muted text-sm">
Ich habe die <NuxtLink to="/datenschutz" class="text-accent hover:underline">Datenschutzerklärung</NuxtLink> gelesen und stimme zu, dass meine Daten zur Bearbeitung meiner Anfrage gespeichert werden. *
</span>
</label>
<p v-if="errors.privacy" class="text-error text-xs mt-1" data-testid="error-privacy">{{ errors.privacy }}</p>
</div>
<!-- Submit Button -->
<button
type="submit"
class="btn-primary w-full"
:disabled="isSubmitting"
data-testid="form-submit"
>
{{ isSubmitting ? "Wird gesendet..." : "Anfrage senden" }}
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
useHead({ title: "Kontakt HMS Licht & Ton", meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }] });
</script>
import { reactive, ref } from "vue";
useHead({
title: "Kontakt HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
const contactPersons = [
{ name: "Markus Hammerschmidt", role: "Geschäftsführer Ton & Verkauf", email: "m.hammerschmidt@hms-licht-ton.de" },
{ name: "Thomas Mössle", role: "Geschäftsführer Licht & Rigging", email: "t.moessle@hms-licht-ton.de" },
];
interface ContactForm {
name: string;
email: string;
phone: string;
message: string;
privacy: boolean;
}
interface FormErrors {
name?: string;
email?: string;
message?: string;
privacy?: string;
}
const form = reactive<ContactForm>({
name: "",
email: "",
phone: "",
message: "",
privacy: false,
});
const errors = reactive<FormErrors>({});
const isSubmitting = ref(false);
const submitSuccess = ref(false);
const submitError = ref(false);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validateField(field: keyof FormErrors): boolean {
switch (field) {
case "name":
if (!form.name.trim()) {
errors.name = "Bitte geben Sie Ihren Namen ein.";
return false;
}
delete errors.name;
return true;
case "email":
if (!form.email.trim()) {
errors.email = "Bitte geben Sie Ihre E-Mail-Adresse ein.";
return false;
}
if (!emailRegex.test(form.email)) {
errors.email = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
return false;
}
delete errors.email;
return true;
case "message":
if (!form.message.trim()) {
errors.message = "Bitte geben Sie eine Nachricht ein.";
return false;
}
delete errors.message;
return true;
case "privacy":
if (!form.privacy) {
errors.privacy = "Bitte stimmen Sie der Datenschutzerklärung zu.";
return false;
}
delete errors.privacy;
return true;
default:
return true;
}
}
function validateAll(): boolean {
const nameValid = validateField("name");
const emailValid = validateField("email");
const messageValid = validateField("message");
const privacyValid = validateField("privacy");
return nameValid && emailValid && messageValid && privacyValid;
}
async function handleSubmit(): Promise<void> {
if (!validateAll()) return;
isSubmitting.value = true;
try {
await $fetch("/api/contact", {
method: "POST",
body: {
name: form.name,
email: form.email,
phone: form.phone,
message: form.message,
},
});
submitSuccess.value = true;
} catch (err) {
submitError.value = true;
} finally {
isSubmitting.value = false;
}
}
function resetForm(): void {
form.name = "";
form.email = "";
form.phone = "";
form.message = "";
form.privacy = false;
Object.keys(errors).forEach((k) => delete errors[k as keyof FormErrors]);
submitSuccess.value = false;
submitError.value = false;
}
</script>
+118 -3
View File
@@ -1,13 +1,128 @@
<template>
<div>
<h1 class="text-3xl font-bold text-text mb-4">Referenzen</h1>
<p class="text-text-muted">Unsere Referenzen werden hier angezeigt.</p>
<h1 class="text-3xl font-bold text-text mb-2">Referenzen</h1>
<p class="text-text-muted mb-8">Ein Auszug aus unseren Projekten und Veranstaltungen.</p>
<!-- Filter Chips -->
<div class="flex flex-wrap gap-3 mb-8" data-testid="filter-chips">
<button
v-for="cat in categories"
:key="cat"
:class="[
'px-4 py-2 rounded-full text-sm font-medium border transition-colors duration-fast',
activeFilter === cat
? 'bg-accent text-white border-accent'
: 'bg-panel text-text-muted border-border-default hover:border-accent-border'
]"
:data-testid="'filter-' + cat.toLowerCase()"
@click="activeFilter = cat"
>
{{ cat }}
</button>
</div>
<!-- Gallery Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" data-testid="gallery-grid">
<div
v-for="item in filteredImages"
:key="item.id"
class="relative group cursor-pointer rounded-lg overflow-hidden bg-panel border border-border-default hover:border-accent-border transition-colors duration-fast"
:data-testid="'gallery-item-' + item.id"
@click="openLightbox(item)"
>
<div class="aspect-w-4 aspect-h-3 w-full">
<img
:src="item.src"
:alt="item.alt"
class="w-full h-48 object-cover group-hover:opacity-80 transition-opacity duration-fast"
loading="lazy"
/>
</div>
<div class="p-4">
<p class="text-accent text-sm font-bold mb-1">{{ item.title }}</p>
<p class="text-text-muted text-xs">{{ item.category }}</p>
</div>
</div>
</div>
<!-- Lightbox -->
<Lightbox
:is-open="lightboxOpen"
:image="currentImage"
@close="closeLightbox"
@prev="prevImage"
@next="nextImage"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
useHead({
title: "Referenzen HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
</script>
interface GalleryItem {
id: number;
src: string;
alt: string;
title: string;
category: string;
caption: string;
}
const categories = ["Alle", "Open-Air", "Indoor", "Rigging"] as const;
const activeFilter = ref<string>("Alle");
const images: GalleryItem[] = [
{ id: 1, src: "https://picsum.photos/seed/hms1/600/400", alt: "Open-Air Konzert", title: "Stadtfest Leipheim", category: "Open-Air", caption: "Stadtfest Leipheim Beschallung und Lichttechnik" },
{ id: 2, src: "https://picsum.photos/seed/hms2/600/400", alt: "Indoor Firmenevent", title: "Firmenevent Augsburg", category: "Indoor", caption: "Firmenevent in Augsburg Komplett-Setup" },
{ id: 3, src: "https://picsum.photos/seed/hms3/600/400", alt: "Rigging Aufbau", title: "Traversen-Rigging", category: "Rigging", caption: "Traversen-Rigging in einer Mehrzweckhalle" },
{ id: 4, src: "https://picsum.photos/seed/hms4/600/400", alt: "Open-Air Festival", title: "Sommerfest Günzburg", category: "Open-Air", caption: "Sommerfest Günzburg Line-Array Beschallung" },
{ id: 5, src: "https://picsum.photos/seed/hms5/600/400", alt: "Indoor Konferenz", title: "Konferenz Neu-Ulm", category: "Indoor", caption: "Konferenz in Neu-Ulm Mikrofonie und Mischtechnik" },
{ id: 6, src: "https://picsum.photos/seed/hms6/600/400", alt: "Rigging Motorzug", title: "Motorzug-System", category: "Rigging", caption: "Motorzug-System für Bühnenbeleuchtung" },
{ id: 7, src: "https://picsum.photos/seed/hms7/600/400", alt: "Open-Air Bühne", title: "Kirchweih Festzelt", category: "Open-Air", caption: "Kirchweih Festzelt Beschallung und Beleuchtung" },
{ id: 8, src: "https://picsum.photos/seed/hms8/600/400", alt: "Indoor Gala", title: "Gala Leipheim", category: "Indoor", caption: "Gala-Abend in Leipheim Licht- und Tontechnik" },
{ id: 9, src: "https://picsum.photos/seed/hms9/600/400", alt: "Rigging Seilzug", title: "Punkt-Rigging", category: "Rigging", caption: "Punkt-Rigging für Moving Heads" },
];
const filteredImages = computed(() => {
if (activeFilter.value === "Alle") return images;
return images.filter((img) => img.category === activeFilter.value);
});
const lightboxOpen = ref(false);
const currentIndex = ref(0);
const currentImage = computed<GalleryItem>(() => {
const item = filteredImages.value[currentIndex.value];
return item
? { src: item.src, alt: item.alt, caption: item.caption }
: { src: "", alt: "", caption: "" };
});
function openLightbox(item: GalleryItem) {
const idx = filteredImages.value.findIndex((i) => i.id === item.id);
if (idx >= 0) {
currentIndex.value = idx;
lightboxOpen.value = true;
}
}
function closeLightbox() {
lightboxOpen.value = false;
}
function nextImage() {
currentIndex.value = (currentIndex.value + 1) % filteredImages.value.length;
}
function prevImage() {
currentIndex.value =
currentIndex.value === 0
? filteredImages.value.length - 1
: currentIndex.value - 1;
}
</script>