feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
This commit is contained in:
@@ -2,11 +2,19 @@
|
||||
|
||||
import { Render } from '@measured/puck';
|
||||
import { clubPuckConfig } from '../config/puck-config';
|
||||
import { SiteDataProvider, type SiteData } from '../context/site-data-context';
|
||||
|
||||
interface Props {
|
||||
data: Record<string, unknown>;
|
||||
siteData?: SiteData;
|
||||
}
|
||||
|
||||
export function SiteRenderer({ data }: Props) {
|
||||
return <Render config={clubPuckConfig} data={data as any} />;
|
||||
export function SiteRenderer({ data, siteData }: Props) {
|
||||
const defaultData: SiteData = { accountId: '', events: [], courses: [], posts: [] };
|
||||
|
||||
return (
|
||||
<SiteDataProvider data={siteData ?? defaultData}>
|
||||
<Render config={clubPuckConfig} data={data as any} />
|
||||
</SiteDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface SiteData {
|
||||
accountId: string;
|
||||
events: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
event_date: string;
|
||||
event_time?: string;
|
||||
location?: string;
|
||||
fee: number;
|
||||
status: string;
|
||||
}>;
|
||||
courses: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
fee: number;
|
||||
capacity?: number;
|
||||
enrolled_count: number;
|
||||
status?: string;
|
||||
}>;
|
||||
posts: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
cover_image?: string;
|
||||
published_at?: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const SiteDataContext = createContext<SiteData>({
|
||||
accountId: '',
|
||||
events: [],
|
||||
courses: [],
|
||||
posts: [],
|
||||
});
|
||||
|
||||
export function SiteDataProvider({
|
||||
data,
|
||||
children,
|
||||
}: {
|
||||
data: SiteData;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SiteDataContext.Provider value={data}>{children}</SiteDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSiteData() {
|
||||
return useContext(SiteDataContext);
|
||||
}
|
||||
Reference in New Issue
Block a user