feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m26s
Workflow / ⚫️ Test (push) Has been skipped

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:
Zaid Marzguioui
2026-03-31 16:35:46 +02:00
parent 16648c92eb
commit ebd0fd4638
176 changed files with 17133 additions and 981 deletions

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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);
}