feat: Implement initial website structure with core pages, layout, and reusable UI components, alongside ESLint configuration and SEO setup.
This commit is contained in:
97
components/layout/Footer.tsx
Normal file
97
components/layout/Footer.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import Link from "next/link";
|
||||
import { Mail, Phone, MapPin } from "lucide-react";
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ href: "/", label: "Startseite" },
|
||||
{ href: "/über-uns", label: "Über Uns" },
|
||||
{ href: "/leistungen", label: "Leistungen" },
|
||||
{ href: "/studio", label: "Studio" },
|
||||
{ href: "/aktuelles", label: "Aktuelles" },
|
||||
] as const;
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-primary text-secondary" role="contentinfo">
|
||||
<div
|
||||
className="mx-auto px-[var(--spacing-container-padding)] py-16"
|
||||
style={{ maxWidth: "var(--spacing-container)" }}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div>
|
||||
<p className="text-xl font-bold tracking-tight mb-4">SPORTBOX</p>
|
||||
<p className="text-sm text-secondary/70 leading-relaxed">
|
||||
Ihr Fitnessstudio in Reutte für individuelle Trainingspläne
|
||||
und persönliche Betreuung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase tracking-widest mb-4">
|
||||
Navigation
|
||||
</p>
|
||||
<nav aria-label="Footer-Navigation">
|
||||
<ul className="space-y-2">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-secondary/70 transition-colors hover:text-secondary"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase tracking-widest mb-4">
|
||||
Kontakt
|
||||
</p>
|
||||
<address className="not-italic space-y-3">
|
||||
<a
|
||||
href="mailto:kontakt@sportbox-reutte.at"
|
||||
className="flex items-center gap-2 text-sm text-secondary/70 transition-colors hover:text-secondary"
|
||||
>
|
||||
<Mail size={16} aria-hidden="true" />
|
||||
kontakt@sportbox-reutte.at
|
||||
</a>
|
||||
<a
|
||||
href="tel:+43123456789"
|
||||
className="flex items-center gap-2 text-sm text-secondary/70 transition-colors hover:text-secondary"
|
||||
>
|
||||
<Phone size={16} aria-hidden="true" />
|
||||
+43 123 456 789
|
||||
</a>
|
||||
<p className="flex items-center gap-2 text-sm text-secondary/70">
|
||||
<MapPin size={16} aria-hidden="true" />
|
||||
Reutte, Tirol, Österreich
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-secondary/20 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p className="text-xs text-secondary/50">
|
||||
© {new Date().getFullYear()} Sportbox Reutte. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link
|
||||
href="/impressum"
|
||||
className="text-xs text-secondary/50 transition-colors hover:text-secondary"
|
||||
>
|
||||
Impressum
|
||||
</Link>
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
className="text-xs text-secondary/50 transition-colors hover:text-secondary"
|
||||
>
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
97
components/layout/Header.tsx
Normal file
97
components/layout/Header.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/", label: "Startseite" },
|
||||
{ href: "/über-uns", label: "Über Uns" },
|
||||
{ href: "/leistungen", label: "Leistungen" },
|
||||
{ href: "/studio", label: "Studio" },
|
||||
{ href: "/aktuelles", label: "Aktuelles" },
|
||||
] as const;
|
||||
|
||||
export function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
|
||||
<div className="mx-auto flex items-center justify-between px-[var(--spacing-container-padding)] py-4" style={{ maxWidth: "var(--spacing-container)" }}>
|
||||
<Link href="/" className="text-xl font-bold tracking-tight" aria-label="Sportbox Reutte – Zur Startseite">
|
||||
SPORTBOX
|
||||
</Link>
|
||||
|
||||
<nav className="hidden lg:flex items-center gap-8" aria-label="Hauptnavigation">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`text-sm tracking-wide transition-colors hover:text-muted ${
|
||||
pathname === item.href ? "font-bold" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href="/leistungen#kontakt"
|
||||
className="bg-primary text-secondary px-5 py-2.5 text-sm font-medium transition-opacity hover:opacity-80"
|
||||
>
|
||||
Jetzt Termin buchen
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="lg:hidden p-2 -mr-2"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
aria-label={mobileMenuOpen ? "Menü schließen" : "Menü öffnen"}
|
||||
>
|
||||
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.nav
|
||||
id="mobile-menu"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="lg:hidden overflow-hidden border-t border-border bg-background"
|
||||
aria-label="Mobile Navigation"
|
||||
>
|
||||
<div className="flex flex-col px-[var(--spacing-container-padding)] py-4 gap-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={`py-3 text-base transition-colors hover:text-muted ${
|
||||
pathname === item.href ? "font-bold" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href="/leistungen#kontakt"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="mt-2 bg-primary text-secondary px-5 py-3 text-center text-sm font-medium transition-opacity hover:opacity-80"
|
||||
>
|
||||
Jetzt Termin buchen
|
||||
</Link>
|
||||
</div>
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
99
components/sections/FaqSection.tsx
Normal file
99
components/sections/FaqSection.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
question: "Brauche ich Vorkenntnisse, um bei Sportbox zu trainieren?",
|
||||
answer:
|
||||
"Nein, bei uns sind alle willkommen – vom absoluten Anfänger bis zum erfahrenen Sportler. Unsere Trainer erstellen individuelle Trainingspläne und begleiten Sie in Ihrem Tempo.",
|
||||
},
|
||||
{
|
||||
question: "Bieten Sie ein Probetraining an?",
|
||||
answer:
|
||||
"Ja, wir bieten ein unverbindliches Probetraining an. Kontaktieren Sie uns einfach über das Kontaktformular oder telefonisch, um einen Termin zu vereinbaren.",
|
||||
},
|
||||
{
|
||||
question: "Welche Öffnungszeiten haben Sie?",
|
||||
answer:
|
||||
"Wir haben großzügige Öffnungszeiten, damit Fitness in Ihren Alltag passt. Die aktuellen Öffnungszeiten erfahren Sie bei uns vor Ort oder telefonisch.",
|
||||
},
|
||||
{
|
||||
question: "Welche Kurse bieten Sie an?",
|
||||
answer:
|
||||
"Unser Kursangebot umfasst unter anderem Yoga, Pilates, HIIT, Functional Training und Krafttraining. Die Kurse sind für verschiedene Fitnesslevel geeignet. Schauen Sie sich unsere Leistungen an oder fragen Sie direkt bei uns an.",
|
||||
},
|
||||
{
|
||||
question: "Kann ich meinen Trainingsplan anpassen lassen?",
|
||||
answer:
|
||||
"Selbstverständlich. Wir passen Ihren Trainingsplan regelmäßig an Ihre Fortschritte und sich verändernde Ziele an. Sprechen Sie einfach Ihren Trainer an.",
|
||||
},
|
||||
{
|
||||
question: "Bieten Sie auch Online-Training an?",
|
||||
answer:
|
||||
"Wir arbeiten daran, unser Angebot um Online-Trainingsmöglichkeiten zu erweitern. Kontaktieren Sie uns für aktuelle Informationen.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function FaqSection() {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
|
||||
function toggle(index: number) {
|
||||
setOpenIndex(openIndex === index ? null : index);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-2">
|
||||
{FAQ_ITEMS.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-border bg-background"
|
||||
style={{ borderRadius: "var(--radius-md)" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggle(index)}
|
||||
aria-expanded={openIndex === index}
|
||||
className="w-full flex items-center justify-between px-6 py-4 text-left text-sm font-medium"
|
||||
>
|
||||
<span>{item.question}</span>
|
||||
<ChevronDown
|
||||
size={18}
|
||||
className={`shrink-0 ml-4 transition-transform duration-200 ${
|
||||
openIndex === index ? "rotate-180" : ""
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{openIndex === index && (
|
||||
<div className="px-6 pb-4">
|
||||
<p className="text-sm text-muted leading-relaxed">
|
||||
{item.answer}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* FAQ Schema Markup */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: FAQ_ITEMS.map((item) => ({
|
||||
"@type": "Question",
|
||||
name: item.question,
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: item.answer,
|
||||
},
|
||||
})),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
components/ui/Button.tsx
Normal file
45
components/ui/Button.tsx
Normal 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
components/ui/Card.tsx
Normal file
15
components/ui/Card.tsx
Normal 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
components/ui/ContactForm.tsx
Normal file
171
components/ui/ContactForm.tsx
Normal 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
components/ui/Container.tsx
Normal file
20
components/ui/Container.tsx
Normal 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
components/ui/FadeIn.tsx
Normal file
23
components/ui/FadeIn.tsx
Normal 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
components/ui/SectionHeading.tsx
Normal file
31
components/ui/SectionHeading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user