|
|
|
|
@@ -1,5 +1,6 @@
|
|
|
|
|
import type { Config } from '@measured/puck';
|
|
|
|
|
import React from 'react';
|
|
|
|
|
import { useSiteData } from '../context/site-data-context';
|
|
|
|
|
|
|
|
|
|
// Block components inline for simplicity
|
|
|
|
|
|
|
|
|
|
@@ -165,80 +166,385 @@ const MemberLoginBlock = ({ title, description }: { title: string; description:
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const NewsFeedBlock = ({ count, showImage }: { count: number; showImage: boolean }) => (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Neuigkeiten</h2>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{Array.from({ length: count || 3 }, (_, i) => (
|
|
|
|
|
<div key={i} className="rounded-lg border p-4 hover:bg-muted/30 transition-colors">
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
{showImage && <div className="h-20 w-20 shrink-0 rounded bg-muted" />}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-semibold">Beitragstitel {i + 1}</h3>
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">Kurzbeschreibung des Beitrags...</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">01.01.2026</p>
|
|
|
|
|
const NewsFeedBlock = ({ count, showImage }: { count: number; showImage: boolean }) => {
|
|
|
|
|
const { posts } = useSiteData();
|
|
|
|
|
const items = posts.slice(0, count || 5);
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Neuigkeiten</h2>
|
|
|
|
|
<p className="text-muted-foreground">Noch keine Beiträge vorhanden.</p>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Neuigkeiten</h2>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{items.map((post) => (
|
|
|
|
|
<div key={post.id} className="rounded-lg border p-4 hover:bg-muted/30 transition-colors">
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
{showImage && post.cover_image && <img src={post.cover_image} alt="" className="h-20 w-20 shrink-0 rounded object-cover" />}
|
|
|
|
|
{showImage && !post.cover_image && <div className="h-20 w-20 shrink-0 rounded bg-muted" />}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-semibold">{post.title}</h3>
|
|
|
|
|
{post.excerpt && <p className="text-sm text-muted-foreground mt-1">{post.excerpt}</p>}
|
|
|
|
|
{post.published_at && <p className="text-xs text-muted-foreground mt-2">{new Date(post.published_at).toLocaleDateString('de-DE')}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const EventListBlock = ({ count, showPastEvents }: { count: number; showPastEvents: boolean }) => (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Veranstaltungen</h2>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{Array.from({ length: count || 3 }, (_, i) => (
|
|
|
|
|
<div key={i} className="flex items-center gap-4 rounded-lg border p-4">
|
|
|
|
|
<div className="flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
|
|
|
<span className="text-lg font-bold">{15 + i}</span>
|
|
|
|
|
<span className="text-xs">Apr</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-semibold">Veranstaltung {i + 1}</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground">10:00 — Vereinsheim</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
const EventListBlock = ({ count, showPastEvents }: { count: number; showPastEvents: boolean }) => {
|
|
|
|
|
const { events } = useSiteData();
|
|
|
|
|
const now = new Date().toISOString().slice(0, 10);
|
|
|
|
|
const filtered = showPastEvents ? events : events.filter(e => e.event_date >= now);
|
|
|
|
|
const items = filtered.slice(0, count || 5);
|
|
|
|
|
|
|
|
|
|
const CourseCatalogBlock = ({ count, showPrice }: { count: number; showPrice: boolean }) => (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Kursangebot</h2>
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
|
|
{Array.from({ length: count || 4 }, (_, i) => (
|
|
|
|
|
<div key={i} className="rounded-lg border p-4">
|
|
|
|
|
<h3 className="font-semibold">Kurs {i + 1}</h3>
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">Mo, 18:00 — 20:00</p>
|
|
|
|
|
<div className="mt-3 flex items-center justify-between">
|
|
|
|
|
{showPrice && <span className="text-sm font-semibold text-primary">49,00 €</span>}
|
|
|
|
|
<span className="text-xs text-muted-foreground">5/15 Plätze</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
|
|
|
|
const [formData, setFormData] = React.useState({ firstName: '', lastName: '', email: '', phone: '', dateOfBirth: '' });
|
|
|
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
|
|
|
const [successId, setSuccessId] = React.useState<string | null>(null);
|
|
|
|
|
const [errorMsg, setErrorMsg] = React.useState('');
|
|
|
|
|
|
|
|
|
|
const CardShopBlock = ({ title, description }: { title: string; description: string }) => (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-2">{title || 'Mitgliedschaft'}</h2>
|
|
|
|
|
{description && <p className="text-muted-foreground mb-6">{description}</p>}
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
|
|
|
{['Basis', 'Standard', 'Familie'].map((name, i) => (
|
|
|
|
|
<div key={name} className="rounded-lg border p-6 text-center hover:border-primary transition-colors">
|
|
|
|
|
<h3 className="text-lg font-bold">{name}</h3>
|
|
|
|
|
<p className="text-3xl font-bold text-primary mt-2">{[5, 10, 18][i]} €</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">pro Monat</p>
|
|
|
|
|
<button className="mt-4 w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground">Auswählen</button>
|
|
|
|
|
const handleSubmit = async (eventId: string) => {
|
|
|
|
|
if (!formData.firstName || !formData.lastName || !formData.email) {
|
|
|
|
|
setErrorMsg('Bitte füllen Sie alle Pflichtfelder aus.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
|
|
|
setErrorMsg('Ungültige E-Mail-Adresse.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
setErrorMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/club/event-register', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
eventId,
|
|
|
|
|
firstName: formData.firstName,
|
|
|
|
|
lastName: formData.lastName,
|
|
|
|
|
email: formData.email,
|
|
|
|
|
phone: formData.phone || undefined,
|
|
|
|
|
dateOfBirth: formData.dateOfBirth || undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const result = await res.json();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setSuccessId(eventId);
|
|
|
|
|
setExpandedId(null);
|
|
|
|
|
setFormData({ firstName: '', lastName: '', email: '', phone: '', dateOfBirth: '' });
|
|
|
|
|
} else {
|
|
|
|
|
setErrorMsg(result.error || 'Anmeldung fehlgeschlagen.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setErrorMsg('Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Veranstaltungen</h2>
|
|
|
|
|
<p className="text-muted-foreground">Keine anstehenden Veranstaltungen.</p>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Veranstaltungen</h2>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{items.map((event) => {
|
|
|
|
|
const d = new Date(event.event_date);
|
|
|
|
|
const isExpanded = expandedId === event.id;
|
|
|
|
|
const isSuccess = successId === event.id;
|
|
|
|
|
return (
|
|
|
|
|
<div key={event.id} className="rounded-lg border overflow-hidden">
|
|
|
|
|
<div className="flex items-center gap-4 p-4">
|
|
|
|
|
<div className="flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
|
|
|
<span className="text-lg font-bold">{d.getDate()}</span>
|
|
|
|
|
<span className="text-xs">{d.toLocaleDateString('de-DE', { month: 'short' })}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h3 className="font-semibold">{event.name}</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{event.event_time ? event.event_time.slice(0, 5) : ''}{event.location ? ` — ${event.location}` : ''}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{event.fee > 0 && <span className="text-sm font-semibold text-primary shrink-0">{event.fee.toFixed(2)} €</span>}
|
|
|
|
|
{isSuccess ? (
|
|
|
|
|
<span className="shrink-0 flex items-center gap-1 text-sm font-semibold text-green-600">
|
|
|
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
|
|
|
|
Angemeldet
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setExpandedId(isExpanded ? null : event.id); setErrorMsg(''); setSuccessId(null); }}
|
|
|
|
|
className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground hover:bg-primary/90"
|
|
|
|
|
>
|
|
|
|
|
Anmelden
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="border-t bg-muted/30 p-4">
|
|
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
|
|
|
<input placeholder="Vorname *" value={formData.firstName} onChange={e => setFormData(p => ({ ...p, firstName: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Nachname *" value={formData.lastName} onChange={e => setFormData(p => ({ ...p, lastName: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="E-Mail *" type="email" value={formData.email} onChange={e => setFormData(p => ({ ...p, email: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Telefon" type="tel" value={formData.phone} onChange={e => setFormData(p => ({ ...p, phone: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
<input placeholder="Geburtsdatum" type="date" value={formData.dateOfBirth} onChange={e => setFormData(p => ({ ...p, dateOfBirth: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleSubmit(event.id)}
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
className="rounded-md bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50 flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{submitting && <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" /><path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="4" strokeLinecap="round" className="opacity-75" /></svg>}
|
|
|
|
|
Anmeldung absenden
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setExpandedId(null)} className="rounded-md border px-4 py-2 text-sm hover:bg-muted">Abbrechen</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CourseCatalogBlock = ({ count, showPrice }: { count: number; showPrice: boolean }) => {
|
|
|
|
|
const { courses } = useSiteData();
|
|
|
|
|
const items = courses.slice(0, count || 4);
|
|
|
|
|
|
|
|
|
|
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
|
|
|
|
const [formData, setFormData] = React.useState({ firstName: '', lastName: '', email: '', phone: '' });
|
|
|
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
|
|
|
const [successId, setSuccessId] = React.useState<string | null>(null);
|
|
|
|
|
const [errorMsg, setErrorMsg] = React.useState('');
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (courseId: string) => {
|
|
|
|
|
if (!formData.firstName || !formData.lastName || !formData.email) {
|
|
|
|
|
setErrorMsg('Bitte füllen Sie alle Pflichtfelder aus.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
|
|
|
setErrorMsg('Ungültige E-Mail-Adresse.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
setErrorMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/club/course-register', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
courseId,
|
|
|
|
|
firstName: formData.firstName,
|
|
|
|
|
lastName: formData.lastName,
|
|
|
|
|
email: formData.email,
|
|
|
|
|
phone: formData.phone || undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const result = await res.json();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setSuccessId(courseId);
|
|
|
|
|
setExpandedId(null);
|
|
|
|
|
setFormData({ firstName: '', lastName: '', email: '', phone: '' });
|
|
|
|
|
} else {
|
|
|
|
|
setErrorMsg(result.error || 'Anmeldung fehlgeschlagen.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setErrorMsg('Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Kursangebot</h2>
|
|
|
|
|
<p className="text-muted-foreground">Aktuell keine Kurse verfügbar.</p>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-4xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-6">Kursangebot</h2>
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
|
|
{items.map((course) => {
|
|
|
|
|
const isExpanded = expandedId === course.id;
|
|
|
|
|
const isSuccess = successId === course.id;
|
|
|
|
|
return (
|
|
|
|
|
<div key={course.id} className="rounded-lg border overflow-hidden">
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<h3 className="font-semibold">{course.name}</h3>
|
|
|
|
|
{course.start_date && (
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">Ab {new Date(course.start_date).toLocaleDateString('de-DE')}</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="mt-3 flex items-center justify-between">
|
|
|
|
|
{showPrice && <span className="text-sm font-semibold text-primary">{course.fee.toFixed(2)} €</span>}
|
|
|
|
|
{course.capacity && <span className="text-xs text-muted-foreground">{course.enrolled_count}/{course.capacity} Plätze</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
{isSuccess ? (
|
|
|
|
|
<span className="flex items-center gap-1 text-sm font-semibold text-green-600">
|
|
|
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
|
|
|
|
Anmeldung erfolgreich!
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setExpandedId(isExpanded ? null : course.id); setErrorMsg(''); setSuccessId(null); }}
|
|
|
|
|
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground hover:bg-primary/90"
|
|
|
|
|
>
|
|
|
|
|
Anmelden
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="border-t bg-muted/30 p-4">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<input placeholder="Vorname *" value={formData.firstName} onChange={e => setFormData(p => ({ ...p, firstName: e.target.value }))} className="w-full rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Nachname *" value={formData.lastName} onChange={e => setFormData(p => ({ ...p, lastName: e.target.value }))} className="w-full rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="E-Mail *" type="email" value={formData.email} onChange={e => setFormData(p => ({ ...p, email: e.target.value }))} className="w-full rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Telefon" type="tel" value={formData.phone} onChange={e => setFormData(p => ({ ...p, phone: e.target.value }))} className="w-full rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleSubmit(course.id)}
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{submitting && <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" /><path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="4" strokeLinecap="round" className="opacity-75" /></svg>}
|
|
|
|
|
Absenden
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setExpandedId(null)} className="rounded-md border px-4 py-2 text-sm hover:bg-muted">Abbrechen</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CardShopBlock = ({ title, description }: { title: string; description: string }) => {
|
|
|
|
|
const { accountId } = useSiteData();
|
|
|
|
|
const [formData, setFormData] = React.useState({
|
|
|
|
|
firstName: '', lastName: '', email: '', phone: '',
|
|
|
|
|
street: '', postalCode: '', city: '', dateOfBirth: '', message: '',
|
|
|
|
|
});
|
|
|
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
|
|
|
const [success, setSuccess] = React.useState(false);
|
|
|
|
|
const [errorMsg, setErrorMsg] = React.useState('');
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!formData.firstName || !formData.lastName || !formData.email) {
|
|
|
|
|
setErrorMsg('Bitte füllen Sie alle Pflichtfelder aus.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
|
|
|
setErrorMsg('Ungültige E-Mail-Adresse.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
setErrorMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/club/membership-apply', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
accountId,
|
|
|
|
|
firstName: formData.firstName,
|
|
|
|
|
lastName: formData.lastName,
|
|
|
|
|
email: formData.email,
|
|
|
|
|
phone: formData.phone || undefined,
|
|
|
|
|
street: formData.street || undefined,
|
|
|
|
|
postalCode: formData.postalCode || undefined,
|
|
|
|
|
city: formData.city || undefined,
|
|
|
|
|
dateOfBirth: formData.dateOfBirth || undefined,
|
|
|
|
|
message: formData.message || undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const result = await res.json();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setSuccess(true);
|
|
|
|
|
setFormData({ firstName: '', lastName: '', email: '', phone: '', street: '', postalCode: '', city: '', dateOfBirth: '', message: '' });
|
|
|
|
|
} else {
|
|
|
|
|
setErrorMsg(result.error || 'Bewerbung fehlgeschlagen.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setErrorMsg('Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-2xl mx-auto text-center">
|
|
|
|
|
<div className="rounded-lg border border-green-200 bg-green-50 p-8">
|
|
|
|
|
<svg className="mx-auto h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
|
|
|
|
<h2 className="mt-4 text-2xl font-bold text-green-800">Bewerbung eingereicht!</h2>
|
|
|
|
|
<p className="mt-2 text-green-700">Ihre Bewerbung wurde eingereicht! Wir melden uns bei Ihnen.</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="py-12 px-6 max-w-2xl mx-auto">
|
|
|
|
|
<h2 className="text-2xl font-bold mb-2">{title || 'Mitglied werden'}</h2>
|
|
|
|
|
{description && <p className="text-muted-foreground mb-6">{description}</p>}
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
|
|
<input placeholder="Vorname *" value={formData.firstName} onChange={e => setFormData(p => ({ ...p, firstName: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Nachname *" value={formData.lastName} onChange={e => setFormData(p => ({ ...p, lastName: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="E-Mail *" type="email" value={formData.email} onChange={e => setFormData(p => ({ ...p, email: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" required />
|
|
|
|
|
<input placeholder="Telefon" type="tel" value={formData.phone} onChange={e => setFormData(p => ({ ...p, phone: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
<input placeholder="Straße" value={formData.street} onChange={e => setFormData(p => ({ ...p, street: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<input placeholder="PLZ" value={formData.postalCode} onChange={e => setFormData(p => ({ ...p, postalCode: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
<input placeholder="Ort" value={formData.city} onChange={e => setFormData(p => ({ ...p, city: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
<input placeholder="Geburtsdatum" type="date" value={formData.dateOfBirth} onChange={e => setFormData(p => ({ ...p, dateOfBirth: e.target.value }))} className="rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
<textarea placeholder="Nachricht (optional)" rows={3} value={formData.message} onChange={e => setFormData(p => ({ ...p, message: e.target.value }))} className="w-full rounded-md border px-3 py-2 text-sm" />
|
|
|
|
|
{errorMsg && <p className="text-sm text-red-600">{errorMsg}</p>}
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
className="w-full rounded-md bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
{submitting && <svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" /><path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="4" strokeLinecap="round" className="opacity-75" /></svg>}
|
|
|
|
|
Mitgliedschaft beantragen
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ColumnsBlock = ({ columns }: { columns: number }) => (
|
|
|
|
|
<section className="py-8 px-6">
|
|
|
|
|
|