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

This commit is contained in:
Implementation Engineer
2026-07-10 22:28:26 +02:00
parent 786cb0c040
commit 90a7d7f2e0
38 changed files with 2733 additions and 3250 deletions
+48 -271
View File
@@ -1,285 +1,62 @@
<template>
<div>
<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="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">
<!-- 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 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>
<!-- 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>
<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 <NuxtLink to="/datenschutz" style="color: var(--color-accent)">Datenschutzerklärung</NuxtLink> 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>
<!-- 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">
import { reactive, ref } from "vue";
import { reactive, ref } from 'vue'
useHead({
title: "Kontakt HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
const form = reactive({ name: '', email: '', phone: '', subject: '', message: '', privacy: false })
const errors = reactive<Record<string, string>>({})
const submitted = ref(false)
const submitting = ref(false)
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;
function validate(): boolean {
const e: Record<string, string> = {}
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
}
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>
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 }
</script>