feat: Implement initial website structure with core pages, layout, and reusable UI components, alongside ESLint configuration and SEO setup.

This commit is contained in:
1elle1
2026-01-30 14:45:52 +01:00
parent f0e917ef5d
commit f1cb4ef2cc
20 changed files with 6800 additions and 19 deletions
+45
View File
@@ -0,0 +1,45 @@
import Link from "next/link";
interface ButtonProps {
children: React.ReactNode;
href?: string;
variant?: "primary" | "secondary" | "ghost";
className?: string;
type?: "button" | "submit" | "reset";
onClick?: () => void;
}
export function Button({
children,
href,
variant = "primary",
className = "",
type = "button",
onClick,
}: ButtonProps) {
const base =
"inline-flex items-center justify-center px-6 py-3 text-sm font-medium transition-all duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
const variants = {
primary: "bg-primary text-secondary hover:opacity-80",
secondary:
"border border-primary text-primary hover:bg-primary hover:text-secondary",
ghost: "text-primary underline-offset-4 hover:underline",
};
const classes = `${base} ${variants[variant]} ${className}`;
if (href) {
return (
<Link href={href} className={classes}>
{children}
</Link>
);
}
return (
<button type={type} className={classes} onClick={onClick}>
{children}
</button>
);
}
+15
View File
@@ -0,0 +1,15 @@
interface CardProps {
children: React.ReactNode;
className?: string;
}
export function Card({ children, className = "" }: CardProps) {
return (
<div
className={`border border-border p-6 md:p-8 transition-shadow duration-200 hover:shadow-lg ${className}`}
style={{ borderRadius: "var(--radius-md)" }}
>
{children}
</div>
);
}
+171
View File
@@ -0,0 +1,171 @@
"use client";
import { useState } from "react";
import { Button } from "./Button";
interface FormState {
name: string;
email: string;
phone: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
message?: string;
}
export function ContactForm() {
const [form, setForm] = useState<FormState>({
name: "",
email: "",
phone: "",
message: "",
});
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
function validate(): FormErrors {
const newErrors: FormErrors = {};
if (!form.name.trim()) newErrors.name = "Bitte geben Sie Ihren Namen ein.";
if (!form.email.trim()) {
newErrors.email = "Bitte geben Sie Ihre E-Mail-Adresse ein.";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
newErrors.email = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
}
if (!form.message.trim())
newErrors.message = "Bitte geben Sie eine Nachricht ein.";
return newErrors;
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const newErrors = validate();
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
setSubmitted(true);
}
}
function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
if (errors[name as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
}
if (submitted) {
return (
<div
className="border border-success p-8 text-center"
style={{ borderRadius: "var(--radius-md)" }}
role="status"
aria-live="polite"
>
<p className="text-lg font-bold mb-2">Vielen Dank für Ihre Nachricht!</p>
<p className="text-muted">
Wir werden uns so schnell wie möglich bei Ihnen melden.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} noValidate className="space-y-6">
<div>
<label htmlFor="contact-name" className="block text-sm font-medium mb-1.5">
Name <span aria-hidden="true">*</span>
</label>
<input
id="contact-name"
name="name"
type="text"
autoComplete="name"
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? "name-error" : undefined}
value={form.name}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.name && (
<p id="name-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.name}
</p>
)}
</div>
<div>
<label htmlFor="contact-email" className="block text-sm font-medium mb-1.5">
E-Mail <span aria-hidden="true">*</span>
</label>
<input
id="contact-email"
name="email"
type="email"
autoComplete="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
value={form.email}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.email && (
<p id="email-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="contact-phone" className="block text-sm font-medium mb-1.5">
Telefon
</label>
<input
id="contact-phone"
name="phone"
type="tel"
autoComplete="tel"
value={form.phone}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
</div>
<div>
<label htmlFor="contact-message" className="block text-sm font-medium mb-1.5">
Nachricht <span aria-hidden="true">*</span>
</label>
<textarea
id="contact-message"
name="message"
rows={5}
aria-required="true"
aria-invalid={!!errors.message}
aria-describedby={errors.message ? "message-error" : undefined}
value={form.message}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none resize-y"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.message && (
<p id="message-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.message}
</p>
)}
</div>
<Button type="submit" variant="primary">
Nachricht senden
</Button>
</form>
);
}
+20
View File
@@ -0,0 +1,20 @@
interface ContainerProps {
children: React.ReactNode;
className?: string;
as?: "div" | "section" | "article";
}
export function Container({
children,
className = "",
as: Component = "div",
}: ContainerProps) {
return (
<Component
className={`mx-auto px-[var(--spacing-container-padding)] ${className}`}
style={{ maxWidth: "var(--spacing-container)" }}
>
{children}
</Component>
);
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { motion } from "framer-motion";
interface FadeInProps {
children: React.ReactNode;
className?: string;
delay?: number;
}
export function FadeIn({ children, className = "", delay = 0 }: FadeInProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, delay, ease: "easeOut" }}
className={className}
>
{children}
</motion.div>
);
}
+31
View File
@@ -0,0 +1,31 @@
interface SectionHeadingProps {
title: string;
subtitle?: string;
align?: "left" | "center";
}
export function SectionHeading({
title,
subtitle,
align = "center",
}: SectionHeadingProps) {
return (
<div className={`mb-12 ${align === "center" ? "text-center" : "text-left"}`}>
<h2
className="text-3xl md:text-4xl font-bold tracking-tight"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
{title}
</h2>
{subtitle && (
<p className="mt-4 text-muted max-w-2xl mx-auto" style={{ fontSize: "var(--text-lg)" }}>
{subtitle}
</p>
)}
</div>
);
}