feat: initial Funktechnik Schubert website
This commit is contained in:
commit
9a648e2ec8
35 changed files with 7521 additions and 0 deletions
61
components/ContactForm.tsx
Normal file
61
components/ContactForm.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
export default function ContactForm() {
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError("");
|
||||
setMessage("");
|
||||
const form = new FormData(event.currentTarget);
|
||||
const name = String(form.get("name") ?? "").trim();
|
||||
const email = String(form.get("email") ?? "").trim();
|
||||
const text = String(form.get("message") ?? "").trim();
|
||||
|
||||
if (!name || !email.includes("@") || text.length < 10 || form.get("privacy") !== "on") {
|
||||
setError("Bitte füllen Sie alle Pflichtfelder aus und bestätigen Sie die Datenschutz-Hinweise.");
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch("/api/contact", { method: "POST", body: form });
|
||||
const result = await response.json() as { message?: string };
|
||||
if (!response.ok) throw new Error(result.message ?? "Nachricht konnte nicht gesendet werden.");
|
||||
event.currentTarget.reset();
|
||||
setMessage(result.message ?? "Ihre Nachricht wurde vorbereitet.");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Nachricht konnte nicht gesendet werden.");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="form" onSubmit={submit} noValidate>
|
||||
<div className="field">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input id="name" name="name" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="message">Nachricht</label>
|
||||
<textarea id="message" name="message" />
|
||||
</div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" name="privacy" />
|
||||
<span>Ich stimme zu, dass meine Angaben zur Bearbeitung der Kontaktanfrage verarbeitet werden.</span>
|
||||
</label>
|
||||
{error && <p className="error">{error}</p>}
|
||||
{message && <p className="success">{message}</p>}
|
||||
<button className="button" type="submit" disabled={pending}>{pending ? "Wird gesendet..." : "Nachricht senden"}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
26
components/Footer.tsx
Normal file
26
components/Footer.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container footer-grid">
|
||||
<div>
|
||||
<h3>Funktechnik Schubert</h3>
|
||||
<p>Service, Reparatur und technische Unterstützung für Funkgeräte und Kommunikationstechnik.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Service</h3>
|
||||
<p><Link href="/leistungen">Leistungen</Link></p>
|
||||
<p><Link href="/funkgeraete-service">Funkgeräte-Service</Link></p>
|
||||
<p><Link href="/reparatur">Reparaturannahme</Link></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Rechtliches</h3>
|
||||
<p><Link href="/impressum">Impressum</Link></p>
|
||||
<p><Link href="/datenschutz">Datenschutz</Link></p>
|
||||
<p><Link href="/kontakt">Kontakt</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
27
components/Header.tsx
Normal file
27
components/Header.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Link from "next/link";
|
||||
|
||||
const navigation = [
|
||||
["Leistungen", "/leistungen"],
|
||||
["Funkgeräte-Service", "/funkgeraete-service"],
|
||||
["Reparatur", "/reparatur"],
|
||||
["Über uns", "/ueber-uns"],
|
||||
["Kontakt", "/kontakt"],
|
||||
] as const;
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="container header-inner">
|
||||
<Link href="/" className="brand" aria-label="Funktechnik Schubert Startseite">
|
||||
<span className="brand-mark" aria-hidden="true" />
|
||||
<span>Funktechnik Schubert</span>
|
||||
</Link>
|
||||
<nav className="nav" aria-label="Hauptnavigation">
|
||||
{navigation.map(([label, href]) => (
|
||||
<Link key={href} href={href}>{label}</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
19
components/PageHero.tsx
Normal file
19
components/PageHero.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function PageHero({ eyebrow, title, children }: Props) {
|
||||
return (
|
||||
<section className="section dark">
|
||||
<div className="container">
|
||||
{eyebrow && <p className="eyebrow">{eyebrow}</p>}
|
||||
<h1>{title}</h1>
|
||||
<p className="lead">{children}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
92
components/RepairForm.tsx
Normal file
92
components/RepairForm.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
type Errors = Partial<Record<"name" | "email" | "manufacturer" | "model" | "description" | "privacy", string>>;
|
||||
|
||||
export default function RepairForm() {
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [status, setStatus] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setStatus("");
|
||||
const form = new FormData(event.currentTarget);
|
||||
const nextErrors: Errors = {};
|
||||
|
||||
if (!String(form.get("name") ?? "").trim()) nextErrors.name = "Bitte geben Sie Ihren Namen ein.";
|
||||
if (!String(form.get("email") ?? "").includes("@")) nextErrors.email = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
|
||||
if (!String(form.get("manufacturer") ?? "").trim()) nextErrors.manufacturer = "Bitte nennen Sie den Hersteller.";
|
||||
if (!String(form.get("model") ?? "").trim()) nextErrors.model = "Bitte nennen Sie das Modell.";
|
||||
if (String(form.get("description") ?? "").trim().length < 20) nextErrors.description = "Bitte beschreiben Sie den Fehler mit mindestens 20 Zeichen.";
|
||||
if (form.get("privacy") !== "on") nextErrors.privacy = "Bitte stimmen Sie der Verarbeitung Ihrer Angaben zu.";
|
||||
|
||||
setErrors(nextErrors);
|
||||
if (Object.keys(nextErrors).length > 0) return;
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch("/api/repair", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
const result = await response.json() as { message?: string };
|
||||
if (!response.ok) throw new Error(result.message ?? "Anfrage konnte nicht gesendet werden.");
|
||||
event.currentTarget.reset();
|
||||
setStatus(result.message ?? "Ihre Reparaturanfrage wurde vorbereitet.");
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "Anfrage konnte nicht gesendet werden.");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="form" onSubmit={submit} noValidate>
|
||||
<div className="grid two">
|
||||
<Field label="Name" name="name" error={errors.name} />
|
||||
<Field label="E-Mail" name="email" type="email" error={errors.email} />
|
||||
<Field label="Telefon" name="phone" />
|
||||
<Field label="Hersteller" name="manufacturer" error={errors.manufacturer} />
|
||||
<Field label="Modell" name="model" error={errors.model} />
|
||||
<Field label="Seriennummer optional" name="serialNumber" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="opened">Wurde das Gerät bereits geöffnet?</label>
|
||||
<select id="opened" name="opened" defaultValue="unknown">
|
||||
<option value="unknown">Bitte auswählen</option>
|
||||
<option value="no">Nein</option>
|
||||
<option value="yes">Ja</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="previousWork">Gibt es Vorarbeiten?</label>
|
||||
<textarea id="previousWork" name="previousWork" placeholder="z. B. Bauteile getauscht, Abgleich versucht, Fremdwerkstatt" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="description">Fehlerbeschreibung</label>
|
||||
<textarea id="description" name="description" placeholder="Bitte Gerät, Fehlerbild und bisherige Beobachtungen beschreiben." />
|
||||
{errors.description && <span className="error">{errors.description}</span>}
|
||||
</div>
|
||||
<p className="notice">Datei- und Bild-Upload ist für eine spätere Ausbaustufe vorbereitet. Bitte senden Sie Bilder aktuell erst nach Rückmeldung.</p>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" name="privacy" />
|
||||
<span>Ich stimme zu, dass meine Angaben zur Bearbeitung der Reparaturanfrage verarbeitet werden.</span>
|
||||
</label>
|
||||
{errors.privacy && <span className="error">{errors.privacy}</span>}
|
||||
{status && <p className="success">{status}</p>}
|
||||
<button className="button" type="submit" disabled={pending}>{pending ? "Wird gesendet..." : "Reparatur anfragen"}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, name, type = "text", error }: { label: string; name: string; type?: string; error?: string }) {
|
||||
return (
|
||||
<div className="field">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
<input id={name} name={name} type={type} />
|
||||
{error && <span className="error">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
components/ServiceCard.tsx
Normal file
15
components/ServiceCard.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function ServiceCard({ title, children }: Props) {
|
||||
return (
|
||||
<article className="card">
|
||||
<h3>{title}</h3>
|
||||
<p>{children}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue