Add a contact page with a form and essential contact details
Create a new contact page with a form that submits to an API endpoint. Register the route for this page and add a link to the footer. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ff98bd6b-82f0-4f2d-a2ce-62874cb0f0af Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/koutl3W Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
a1dd9a1414
commit
72e4df3fe1
@ -16,6 +16,7 @@ import EmpfangPage from "@/pages/empfang";
|
||||
import AboutPage from "@/pages/about";
|
||||
import ImpressumPage from "@/pages/impressum";
|
||||
import DatenschutzPage from "@/pages/datenschutz";
|
||||
import KontaktPage from "@/pages/kontakt";
|
||||
import AdminGalleryPage from "@/pages/admin-gallery";
|
||||
import CookieConsent from "@/components/cookie-consent";
|
||||
|
||||
@ -35,6 +36,7 @@ function Router() {
|
||||
<Route path="/ueber-uns" component={AboutPage} />
|
||||
<Route path="/impressum" component={ImpressumPage} />
|
||||
<Route path="/datenschutz" component={DatenschutzPage} />
|
||||
<Route path="/kontakt" component={KontaktPage} />
|
||||
<Route path="/admin/gallery" component={AdminGalleryPage} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
||||
@ -93,6 +93,9 @@ export default function Footer() {
|
||||
<Link href="/ueber-uns">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-ueber-uns">Über uns</span>
|
||||
</Link>
|
||||
<Link href="/kontakt">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-kontakt">Kontakt</span>
|
||||
</Link>
|
||||
<Link href="/impressum">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-impressum">Impressum</span>
|
||||
</Link>
|
||||
|
||||
241
client/src/pages/kontakt.tsx
Normal file
241
client/src/pages/kontakt.tsx
Normal file
@ -0,0 +1,241 @@
|
||||
import { useState } from "react";
|
||||
import { Mail, Phone, MapPin, Send, CheckCircle, AlertCircle } from "lucide-react";
|
||||
import { SiFacebook, SiInstagram, SiYoutube, SiTiktok } from "react-icons/si";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { PageSideAds } from "@/components/adsense";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export default function KontaktPage() {
|
||||
usePageMeta("Kontakt - FOLX TV", "Kontaktieren Sie FOLX TV – Ihr Fernsehsender für Volksmusik und Schlager.");
|
||||
|
||||
const [formData, setFormData] = useState({ name: "", email: "", subject: "", message: "" });
|
||||
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name || !formData.email || !formData.message) {
|
||||
setErrorMsg("Bitte füllen Sie alle Pflichtfelder aus.");
|
||||
setStatus("error");
|
||||
return;
|
||||
}
|
||||
setStatus("sending");
|
||||
setErrorMsg("");
|
||||
try {
|
||||
await apiRequest("POST", "/api/contact", formData);
|
||||
setStatus("success");
|
||||
setFormData({ name: "", email: "", subject: "", message: "" });
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.");
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<PageSideAds contentHalfWidth={384} />
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<Mail className="w-7 h-7 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground" data-testid="text-kontakt-title">
|
||||
Kontakt
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
|
||||
<div className="space-y-6">
|
||||
<p className="text-muted-foreground text-[15px] leading-relaxed">
|
||||
Haben Sie Fragen, Anregungen oder möchten Sie mit uns in Kontakt treten? Wir freuen uns auf Ihre Nachricht!
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<a
|
||||
href="tel:+491725675800"
|
||||
className="flex items-start gap-3 group"
|
||||
data-testid="link-kontakt-phone"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors">
|
||||
<Phone className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Telefon</p>
|
||||
<p className="text-foreground font-medium group-hover:text-primary transition-colors" data-testid="text-kontakt-phone">+49 172 567 58 00</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="mailto:office@folx.tv"
|
||||
className="flex items-start gap-3 group"
|
||||
data-testid="link-kontakt-email"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors">
|
||||
<Mail className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">E-Mail</p>
|
||||
<p className="text-foreground font-medium group-hover:text-primary transition-colors" data-testid="text-kontakt-email">office@folx.tv</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<MapPin className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Adresse</p>
|
||||
<p className="text-foreground font-medium" data-testid="text-kontakt-address">BoldFrame Productions d.o.o.</p>
|
||||
<p className="text-muted-foreground text-sm">Sokolska ulica 46</p>
|
||||
<p className="text-muted-foreground text-sm">2000 Maribor, Slowenien</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<p className="text-xs text-muted-foreground mb-3">Folgen Sie uns</p>
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
{ href: "https://www.facebook.com/folxtv/", icon: SiFacebook, label: "Facebook" },
|
||||
{ href: "https://www.instagram.com/folxtv/", icon: SiInstagram, label: "Instagram" },
|
||||
{ href: "https://www.youtube.com/@folxmtv", icon: SiYoutube, label: "YouTube" },
|
||||
{ href: "https://www.tiktok.com/@folxtv", icon: SiTiktok, label: "TikTok" },
|
||||
].map((s) => (
|
||||
<a
|
||||
key={s.label}
|
||||
href={s.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-lg bg-card border border-card-border flex items-center justify-center text-muted-foreground hover:text-primary hover:border-primary/30 transition-colors"
|
||||
aria-label={s.label}
|
||||
data-testid={`link-kontakt-${s.label.toLowerCase()}`}
|
||||
>
|
||||
<s.icon className="w-4 h-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-card-border rounded-xl p-6" data-testid="section-kontakt-form">
|
||||
<h2 className="text-lg font-semibold text-card-foreground mb-4">Nachricht senden</h2>
|
||||
|
||||
{status === "success" ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center" data-testid="kontakt-success">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mb-3" />
|
||||
<h3 className="text-foreground font-semibold text-lg mb-1">Vielen Dank!</h3>
|
||||
<p className="text-muted-foreground text-sm">Ihre Nachricht wurde erfolgreich gesendet. Wir melden uns so schnell wie möglich bei Ihnen.</p>
|
||||
<button
|
||||
onClick={() => setStatus("idle")}
|
||||
className="mt-4 text-primary text-sm hover:underline"
|
||||
data-testid="button-kontakt-another"
|
||||
>
|
||||
Weitere Nachricht senden
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-card-foreground mb-1">
|
||||
Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50"
|
||||
placeholder="Ihr Name"
|
||||
data-testid="input-kontakt-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-card-foreground mb-1">
|
||||
E-Mail <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50"
|
||||
placeholder="ihre@email.com"
|
||||
data-testid="input-kontakt-email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-card-foreground mb-1">
|
||||
Betreff
|
||||
</label>
|
||||
<select
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50"
|
||||
data-testid="select-kontakt-subject"
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
<option value="Allgemeine Anfrage">Allgemeine Anfrage</option>
|
||||
<option value="Programmvorschlag">Programmvorschlag</option>
|
||||
<option value="Technischer Support">Technischer Support</option>
|
||||
<option value="Werbung & Kooperationen">Werbung & Kooperationen</option>
|
||||
<option value="Presse">Presse</option>
|
||||
<option value="Sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-card-foreground mb-1">
|
||||
Nachricht <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
rows={5}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 resize-none"
|
||||
placeholder="Ihre Nachricht..."
|
||||
data-testid="input-kontakt-message"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === "error" && (
|
||||
<div className="flex items-center gap-2 text-red-500 text-sm" data-testid="kontakt-error">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "sending"}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary/90 disabled:bg-primary/50 text-primary-foreground font-medium text-sm rounded-lg transition-colors"
|
||||
data-testid="button-kontakt-submit"
|
||||
>
|
||||
{status === "sending" ? (
|
||||
<div className="w-4 h-4 border-2 border-primary-foreground/50 border-t-primary-foreground rounded-full animate-spin" />
|
||||
) : (
|
||||
<Send className="w-4 h-4" />
|
||||
)}
|
||||
{status === "sending" ? "Wird gesendet..." : "Nachricht senden"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
server/contact-messages.json
Normal file
1
server/contact-messages.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
@ -462,6 +462,34 @@ export async function registerRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/contact", async (req, res) => {
|
||||
try {
|
||||
const { name, email, subject, message } = req.body;
|
||||
if (!name || !email || !message) {
|
||||
return res.status(400).json({ message: "Name, E-Mail und Nachricht sind erforderlich." });
|
||||
}
|
||||
const contactPath = path.join(process.cwd(), "server/contact-messages.json");
|
||||
let messages: any[] = [];
|
||||
if (fs.existsSync(contactPath)) {
|
||||
messages = JSON.parse(fs.readFileSync(contactPath, "utf-8"));
|
||||
}
|
||||
messages.push({
|
||||
id: Date.now(),
|
||||
name,
|
||||
email,
|
||||
subject: subject || "Keine Angabe",
|
||||
message,
|
||||
createdAt: new Date().toISOString(),
|
||||
read: false,
|
||||
});
|
||||
fs.writeFileSync(contactPath, JSON.stringify(messages, null, 2));
|
||||
console.log(`[contact] New message from ${name} <${email}>: ${subject || "No subject"}`);
|
||||
res.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/gallery/artists", (_req, res) => {
|
||||
try {
|
||||
const aPath = path.join(process.cwd(), "server/gallery-artist-overrides.json");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user