2026-07-09 01:17:35 +02:00
|
|
|
|
<template>
|
|
|
|
|
|
<div>
|
2026-07-10 01:02:27 +02:00
|
|
|
|
<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>
|
2026-07-09 01:17:35 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-07-10 01:02:27 +02:00
|
|
|
|
|
2026-07-09 01:17:35 +02:00
|
|
|
|
<script setup lang="ts">
|
2026-07-10 01:02:27 +02:00
|
|
|
|
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>
|