feat: initial Funktechnik Schubert website

This commit is contained in:
Schubert Ferenc 2026-07-03 19:14:34 +02:00
commit 9a648e2ec8
35 changed files with 7521 additions and 0 deletions

25
app/api/contact/route.ts Normal file
View file

@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
function text(value: FormDataEntryValue | null) {
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request) {
const form = await request.formData();
const name = text(form.get("name"));
const email = text(form.get("email"));
const message = text(form.get("message"));
if (!name || !email.includes("@") || message.length < 10 || form.get("privacy") !== "on") {
return NextResponse.json({ message: "Bitte füllen Sie alle Pflichtfelder aus." }, { status: 400 });
}
console.info("contact_request.received", {
messageLength: message.length,
hasEmail: true,
});
return NextResponse.json({
message: "Ihre Nachricht wurde erfasst. Sie erhalten eine Rückmeldung.",
});
}

34
app/api/repair/route.ts Normal file
View file

@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
function required(value: FormDataEntryValue | null) {
return typeof value === "string" && value.trim().length > 0;
}
export async function POST(request: Request) {
const form = await request.formData();
const name = form.get("name");
const email = form.get("email");
const manufacturer = form.get("manufacturer");
const model = form.get("model");
const description = form.get("description");
const privacy = form.get("privacy");
if (!required(name) || !required(email) || !required(manufacturer) || !required(model) || !required(description) || privacy !== "on") {
return NextResponse.json({ message: "Bitte füllen Sie alle Pflichtfelder aus." }, { status: 400 });
}
const intakeUrl = process.env.OLYMPUS_INTAKE_API_URL;
const hasIntegrationToken = Boolean(process.env.OLYMPUS_INTAKE_API_TOKEN);
console.info("repair_intake.received", {
hasIntakeIntegration: Boolean(intakeUrl && hasIntegrationToken),
manufacturer: String(manufacturer),
model: String(model),
hasPhone: required(form.get("phone")),
hasSerialNumber: required(form.get("serialNumber")),
});
return NextResponse.json({
message: "Ihre Reparaturanfrage wurde erfasst. Sie erhalten nach Prüfung eine Rückmeldung.",
});
}

26
app/datenschutz/page.tsx Normal file
View file

@ -0,0 +1,26 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Datenschutz", "Datenschutzhinweise von Funktechnik Schubert.", "/datenschutz");
export default function DatenschutzPage() {
return (
<>
<PageHero eyebrow="Rechtliches" title="Datenschutz">Informationen zur Verarbeitung personenbezogener Daten.</PageHero>
<section className="section">
<div className="container legal">
<p className="notice">TODO: Rechtliche Angaben ergänzen</p>
<h2>Verantwortlicher</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<h2>Kontakt- und Reparaturanfragen</h2>
<p>Die Formulare erfassen Angaben, die zur Bearbeitung der jeweiligen Anfrage erforderlich sind. Eine produktive Datenschutzerklärung muss vor Veröffentlichung rechtlich geprüft und ergänzt werden.</p>
<h2>Hosting und Server-Logs</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<h2>Betroffenenrechte</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,35 @@
import type { Metadata } from "next";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Funkgeräte-Service", "Werkstattservice für CB-Funk, Amateurfunk und Funkgeräte-Abgleich.", "/funkgeraete-service");
export default function RadioServicePage() {
return (
<>
<PageHero eyebrow="Funkgeräte-Service" title="Prüfung, Diagnose und Abgleich">
Funkgeräte zeigen Fehler oft erst im Zusammenspiel von Empfang, Sendeteil, Versorgung, Antennenanpassung und Bedienung. Der Service betrachtet diese Zusammenhänge strukturiert.
</PageHero>
<section className="section">
<div className="container split">
<div>
<h2>Typische Fehlerbilder</h2>
<ul className="list">
<li>Kein oder schwacher Empfang</li>
<li>Keine, leise oder verzerrte Modulation</li>
<li>Frequenzversatz, PLL-Rasten oder instabile Kanäle</li>
<li>Aussetzer durch Stromversorgung, Buchsen oder Bedienelemente</li>
<li>Unklare Vorarbeiten oder verbastelte Geräte</li>
</ul>
</div>
<div className="panel">
<h2>Serviceablauf</h2>
<p className="lead">Eine gute Reparatur beginnt mit vollständigen Angaben zu Gerät, Fehlerbild und Vorgeschichte. Danach folgt eine technische Einschätzung und die weitere Abstimmung.</p>
<Link className="button" href="/reparatur">Reparatur anfragen</Link>
</div>
</div>
</section>
</>
);
}

404
app/globals.css Normal file
View file

@ -0,0 +1,404 @@
:root {
--bg: #f6f8fb;
--surface: #ffffff;
--ink: #101827;
--muted: #5b677a;
--line: #d9e1ec;
--navy: #0b1728;
--navy-2: #13243a;
--cyan: #16b6d9;
--orange: #f28c28;
--radius: 8px;
--shadow: 0 20px 50px rgba(10, 23, 40, 0.12);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Arial, Helvetica, sans-serif;
letter-spacing: 0;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
select,
textarea {
font: inherit;
}
.container {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(11, 23, 40, 0.96);
color: #fff;
backdrop-filter: blur(14px);
}
.header-inner {
display: flex;
min-height: 72px;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 12px;
font-weight: 800;
}
.brand-mark {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border: 1px solid rgba(22, 182, 217, 0.45);
border-radius: 8px;
background: linear-gradient(135deg, rgba(22, 182, 217, 0.22), rgba(242, 140, 40, 0.18));
}
.brand-mark::before {
width: 16px;
height: 16px;
border: 3px solid var(--cyan);
border-left-color: transparent;
border-radius: 999px;
content: "";
}
.nav {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.nav a {
border-radius: 8px;
padding: 9px 11px;
color: rgba(255, 255, 255, 0.82);
font-size: 14px;
}
.nav a:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.hero {
position: relative;
overflow: hidden;
min-height: calc(100vh - 72px);
display: grid;
align-items: center;
color: #fff;
background:
linear-gradient(90deg, rgba(8, 18, 32, 0.94), rgba(8, 18, 32, 0.72), rgba(8, 18, 32, 0.58)),
url("/workbench-signal.svg"),
linear-gradient(135deg, #0b1728, #17324e);
background-position: center;
background-size: cover;
}
.hero-content {
max-width: 760px;
padding: 76px 0 118px;
}
.eyebrow {
color: var(--cyan);
font-size: 13px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 18px;
font-size: clamp(44px, 7vw, 76px);
line-height: 0.96;
}
h2 {
margin-bottom: 14px;
color: var(--ink);
font-size: clamp(30px, 4vw, 44px);
line-height: 1.05;
}
h3 {
margin-bottom: 8px;
font-size: 20px;
}
.lead {
max-width: 720px;
color: var(--muted);
font-size: 19px;
line-height: 1.65;
}
.hero .lead {
color: rgba(255, 255, 255, 0.82);
font-size: 21px;
}
.hero-actions,
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 28px;
}
.button {
display: inline-flex;
min-height: 44px;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 18px;
background: var(--cyan);
color: #06111d;
font-weight: 800;
}
.button.secondary {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.button.light {
border-color: var(--line);
background: #fff;
color: var(--ink);
}
.section {
padding: 82px 0;
}
.section.dark {
background: var(--navy);
color: #fff;
}
.section.dark h2,
.section.dark h3 {
color: #fff;
}
.section.dark .lead,
.section.dark p {
color: rgba(255, 255, 255, 0.72);
}
.section-head {
max-width: 760px;
margin-bottom: 34px;
}
.grid {
display: grid;
gap: 18px;
}
.grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
padding: 24px;
box-shadow: 0 8px 26px rgba(12, 28, 48, 0.05);
}
.dark .card {
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
}
.card p,
.list li {
color: var(--muted);
line-height: 1.6;
}
.dark .card p,
.dark .list li {
color: rgba(255, 255, 255, 0.72);
}
.list {
display: grid;
gap: 10px;
padding-left: 19px;
}
.split {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 34px;
align-items: start;
}
.panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: #fff;
padding: 28px;
box-shadow: var(--shadow);
}
.form {
display: grid;
gap: 16px;
}
.field {
display: grid;
gap: 7px;
}
.field label {
color: var(--ink);
font-size: 14px;
font-weight: 800;
}
.field input,
.field select,
.field textarea {
width: 100%;
border: 1px solid #cfd8e5;
border-radius: 8px;
background: #fff;
padding: 11px 12px;
color: var(--ink);
}
.field textarea {
min-height: 130px;
resize: vertical;
}
.checkbox {
display: flex;
align-items: flex-start;
gap: 10px;
color: var(--muted);
font-size: 14px;
line-height: 1.45;
}
.error {
color: #b42318;
font-size: 13px;
}
.success {
border: 1px solid #a6d8bd;
border-radius: 8px;
background: #edf9f1;
padding: 12px;
color: #15562d;
}
.notice {
border-left: 4px solid var(--orange);
background: #fff8ef;
padding: 14px 16px;
color: #5a3713;
}
.footer {
background: #08111f;
color: rgba(255, 255, 255, 0.76);
padding: 44px 0;
}
.footer-grid {
display: grid;
grid-template-columns: 1.3fr 1fr 1fr;
gap: 28px;
}
.footer a {
color: #fff;
}
.legal {
max-width: 860px;
}
.legal h2 {
margin-top: 28px;
font-size: 26px;
}
@media (max-width: 860px) {
.header-inner,
.nav {
justify-content: flex-start;
}
.header-inner {
flex-direction: column;
align-items: flex-start;
padding: 14px 0;
}
.hero {
min-height: auto;
}
.hero-content {
padding: 64px 0 92px;
}
.grid.three,
.grid.two,
.split,
.footer-grid {
grid-template-columns: 1fr;
}
}

24
app/impressum/page.tsx Normal file
View file

@ -0,0 +1,24 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Impressum", "Impressum von Funktechnik Schubert.", "/impressum");
export default function ImpressumPage() {
return (
<>
<PageHero eyebrow="Rechtliches" title="Impressum">Rechtliche Angaben für die öffentliche Firmenwebsite.</PageHero>
<section className="section">
<div className="container legal">
<p className="notice">TODO: Rechtliche Angaben ergänzen</p>
<h2>Anbieterkennzeichnung</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<h2>Kontakt</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<h2>Umsatzsteuer / Registerangaben</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
</div>
</section>
</>
);
}

27
app/kontakt/page.tsx Normal file
View file

@ -0,0 +1,27 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import ContactForm from "@/components/ContactForm";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Kontakt", "Kontakt zu Funktechnik Schubert aufnehmen.", "/kontakt");
export default function ContactPage() {
return (
<>
<PageHero eyebrow="Kontakt" title="Kontakt aufnehmen">
Beschreiben Sie kurz Ihr Anliegen. Für Reparaturen nutzen Sie idealerweise die strukturierte Reparaturannahme.
</PageHero>
<section className="section">
<div className="container split">
<div>
<h2>Direkt und technisch</h2>
<p className="lead">Je genauer Gerät, Anliegen und gewünschte Unterstützung beschrieben werden, desto gezielter kann die Rückmeldung erfolgen.</p>
</div>
<div className="panel">
<ContactForm />
</div>
</div>
</section>
</>
);
}

22
app/layout.tsx Normal file
View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import "./globals.css";
import { createMetadata } from "./seo";
export const metadata: Metadata = createMetadata(
"Funktechnik Schubert",
"Service, Reparatur und technische Unterstützung für Funkgeräte und Kommunikationstechnik.",
);
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="de">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}

33
app/leistungen/page.tsx Normal file
View file

@ -0,0 +1,33 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import ServiceCard from "@/components/ServiceCard";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Leistungen", "Funkgeräte-Service, Diagnose, Abgleich und Dokumentation für Kommunikationstechnik.", "/leistungen");
const services = [
["Funkgeräte-Service", "Prüfung, Wartung und technische Beurteilung von Funkgeräten mit Fokus auf nachvollziehbare Fehlerbilder."],
["CB-Funk", "Service für klassische und moderne CB-Funkgeräte, inklusive Empfangs-, Sende- und Modulationsproblemen."],
["Amateurfunk", "Unterstützung bei ausgewählter Amateurfunktechnik, Abgleichfragen und gerätespezifischen Fehlern."],
["Betriebsfunk", "Technische Einschätzung und Prüfung von betriebsnaher Kommunikationstechnik im Rahmen der Werkstattmöglichkeiten."],
["Fehlersuche", "Strukturierte Diagnose bei Aussetzern, Störungen, instabiler Versorgung oder auffälligem Verhalten."],
["Modulationsprobleme", "Analyse von leiser, verzerrter oder fehlender Modulation sowie Prüfung relevanter Baugruppen."],
["PLL-/Frequenzprobleme", "Prüfung von Frequenzstabilität, Rastung, Synthesizer- und Referenzproblemen."],
["Abgleich und Prüfung", "Sorgfältiger Abgleich nach technischen Unterlagen und dokumentierte Funktionsprüfung."],
["Dokumentation", "Einbindung von Serviceunterlagen, Schaltplänen und Reparaturhinweisen in den Prüfprozess."],
] as const;
export default function ServicesPage() {
return (
<>
<PageHero eyebrow="Leistungen" title="Service für Funkgeräte und Kommunikationstechnik">
Technische Unterstützung für Geräte, bei denen Diagnose, Erfahrung und saubere Dokumentation zählen.
</PageHero>
<section className="section">
<div className="container grid three">
{services.map(([title, text]) => <ServiceCard key={title} title={title}>{text}</ServiceCard>)}
</div>
</section>
</>
);
}

57
app/page.tsx Normal file
View file

@ -0,0 +1,57 @@
import Link from "next/link";
import ServiceCard from "@/components/ServiceCard";
export default function HomePage() {
return (
<>
<section className="hero">
<div className="container hero-content">
<p className="eyebrow">Funktechnik, Elektronikservice, Werkstatt</p>
<h1>Funktechnik Schubert</h1>
<p className="lead">Service, Reparatur und technische Unterstützung für Funkgeräte und Kommunikationstechnik.</p>
<div className="hero-actions">
<Link className="button" href="/reparatur">Reparatur anfragen</Link>
<Link className="button secondary" href="/leistungen">Leistungen ansehen</Link>
</div>
</div>
</section>
<section className="section">
<div className="container">
<div className="section-head">
<p className="eyebrow">Leistungsbereiche</p>
<h2>Technischer Service mit sauberer Diagnose</h2>
<p className="lead">Von der ersten Fehlerbeschreibung bis zur dokumentierten Prüfung: Funktechnik Schubert verbindet Werkstatterfahrung mit strukturierter Kommunikation.</p>
</div>
<div className="grid three">
<ServiceCard title="Funkgeräte-Service">Prüfung, Abgleich und Service für CB-Funk, Amateurfunk und ausgewählte Kommunikationstechnik.</ServiceCard>
<ServiceCard title="Reparatur & Diagnose">Systematische Fehlersuche bei Empfang, Modulation, PLL, Frequenzstabilität und Stromversorgung.</ServiceCard>
<ServiceCard title="Serviceunterlagen">Sorgfältiger Umgang mit Schaltplänen, Service Manuals und gerätespezifischen Dokumentationen.</ServiceCard>
</div>
</div>
</section>
<section className="section dark">
<div className="container split">
<div>
<p className="eyebrow">Persönlicher Support</p>
<h2>Direkte technische Einschätzung statt anonymer Abwicklung</h2>
<p className="lead">Viele Fehlerbilder lassen sich bereits mit einer klaren Beschreibung eingrenzen. Die Reparaturannahme fragt deshalb die wichtigsten technischen Details strukturiert ab.</p>
<div className="actions">
<Link className="button" href="/reparatur">Reparaturformular öffnen</Link>
</div>
</div>
<div className="card">
<h3>Warum Funktechnik Schubert?</h3>
<ul className="list">
<li>Technischer Fokus auf Funkgeräte und Kommunikationstechnik</li>
<li>Nachvollziehbare Diagnose statt pauschaler Austauschlogik</li>
<li>Klare Rückmeldung zu Aufwand, Zustand und nächsten Schritten</li>
<li>Vorbereitung für strukturierte Service- und Dokumentationsprozesse</li>
</ul>
</div>
</div>
</section>
</>
);
}

33
app/reparatur/page.tsx Normal file
View file

@ -0,0 +1,33 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import RepairForm from "@/components/RepairForm";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Reparaturannahme", "Reparaturanfrage für Funkgeräte strukturiert vorbereiten.", "/reparatur");
export default function RepairPage() {
return (
<>
<PageHero eyebrow="Reparaturannahme" title="Reparatur anfragen">
Beschreiben Sie Gerät, Fehlerbild und bisherige Vorarbeiten. Die Anfrage wird strukturiert erfasst und für eine spätere technische Übergabe vorbereitet.
</PageHero>
<section className="section">
<div className="container split">
<div>
<h2>Vor der Einsendung</h2>
<p className="lead">Bitte senden Sie Geräte erst nach Rückmeldung ein. Eine klare Fehlerbeschreibung hilft, den Aufwand besser einzuschätzen.</p>
<ul className="list">
<li>Hersteller, Modell und Seriennummer bereithalten</li>
<li>Fehler möglichst reproduzierbar beschreiben</li>
<li>Vorarbeiten und geöffnete Geräte ehrlich angeben</li>
<li>Zubehör nur nach Absprache mitsenden</li>
</ul>
</div>
<div className="panel">
<RepairForm />
</div>
</div>
</section>
</>
);
}

12
app/robots.ts Normal file
View file

@ -0,0 +1,12 @@
import type { MetadataRoute } from "next";
import { siteUrl } from "./seo";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: new URL("/sitemap.xml", siteUrl).toString(),
};
}

37
app/seo.ts Normal file
View file

@ -0,0 +1,37 @@
import type { Metadata } from "next";
const siteName = "Funktechnik Schubert";
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3010";
const keywords = [
"Funktechnik Schubert",
"Funkgeräte Reparatur",
"CB Funk Service",
"Amateurfunk Reparatur",
"Funkgeräte Abgleich",
"Funktechnik Service",
"Kommunikationstechnik",
];
export function createMetadata(title: string, description: string, path = "/"): Metadata {
const url = new URL(path, siteUrl).toString();
const fullTitle = title === siteName ? title : `${title} | ${siteName}`;
return {
title: fullTitle,
description,
keywords,
alternates: {
canonical: url,
},
openGraph: {
type: "website",
locale: "de_DE",
url,
siteName,
title: fullTitle,
description,
},
};
}
export { siteName, siteUrl };

22
app/sitemap.ts Normal file
View file

@ -0,0 +1,22 @@
import type { MetadataRoute } from "next";
import { siteUrl } from "./seo";
const routes = [
"",
"/leistungen",
"/funkgeraete-service",
"/reparatur",
"/ueber-uns",
"/kontakt",
"/impressum",
"/datenschutz",
];
export default function sitemap(): MetadataRoute.Sitemap {
return routes.map((route) => ({
url: new URL(route, siteUrl).toString(),
lastModified: new Date(),
changeFrequency: route === "" ? "weekly" : "monthly",
priority: route === "" ? 1 : 0.7,
}));
}

27
app/ueber-uns/page.tsx Normal file
View file

@ -0,0 +1,27 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import { createMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Über uns", "Funktechnik Schubert steht für technischen Service und klare Kommunikation.", "/ueber-uns");
export default function AboutPage() {
return (
<>
<PageHero eyebrow="Über uns" title="Technik verstehen, Fehler nachvollziehbar lösen">
Funktechnik Schubert richtet sich an Kunden, die bei Funkgeräten und Kommunikationstechnik eine persönliche, technische Einschätzung suchen.
</PageHero>
<section className="section">
<div className="container grid two">
<article className="card">
<h2>Arbeitsweise</h2>
<p>Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und der respektvolle Umgang mit bestehenden Geräten und Serviceunterlagen.</p>
</article>
<article className="card">
<h2>Fokus</h2>
<p>Der Schwerpunkt liegt auf Funktechnik, Elektronikservice, Abgleichfragen und technischer Unterstützung rund um Kommunikationstechnik.</p>
</article>
</div>
</section>
</>
);
}