fix: 1:1 prototype port – exact hms-* CSS, real logo, all components from app.js
This commit is contained in:
+48
-271
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user