125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import React from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { createInviteLink, getEvent, getEventStats, toggleEvent } from '../api';
|
|
import { isAuthError } from '../auth/tokens';
|
|
|
|
export default function EventDetailPage() {
|
|
const [sp] = useSearchParams();
|
|
const id = Number(sp.get('id'));
|
|
const nav = useNavigate();
|
|
const [ev, setEv] = React.useState<any | null>(null);
|
|
const [stats, setStats] = React.useState<{ total: number; featured: number; likes: number } | null>(null);
|
|
const [invite, setInvite] = React.useState<string | null>(null);
|
|
|
|
const load = React.useCallback(async () => {
|
|
try {
|
|
const event = await getEvent(id);
|
|
setEv(event);
|
|
setStats(await getEventStats(id));
|
|
} catch (error) {
|
|
if (!isAuthError(error)) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
}, [id]);
|
|
|
|
React.useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
async function onToggle() {
|
|
try {
|
|
const isActive = await toggleEvent(id);
|
|
setEv((previous: any) => ({ ...(previous || {}), is_active: isActive }));
|
|
} catch (error) {
|
|
if (!isAuthError(error)) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function onInvite() {
|
|
try {
|
|
const link = await createInviteLink(id);
|
|
setInvite(link);
|
|
try {
|
|
await navigator.clipboard.writeText(link);
|
|
} catch {
|
|
// clipboard may be unavailable
|
|
}
|
|
} catch (error) {
|
|
if (!isAuthError(error)) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ev) {
|
|
return <div className="p-4">Lade ...</div>;
|
|
}
|
|
|
|
const joinLink = `${window.location.origin}/e/${ev.slug}`;
|
|
const qrUrl = `/admin/qr?data=${encodeURIComponent(joinLink)}`;
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl space-y-4 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-lg font-semibold">Event: {renderName(ev.name)}</h1>
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" onClick={onToggle}>
|
|
{ev.is_active ? 'Deaktivieren' : 'Aktivieren'}
|
|
</Button>
|
|
<Button variant="secondary" onClick={() => nav(`/admin/events/photos?id=${id}`)}>
|
|
Fotos moderieren
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="rounded border p-3 text-sm">
|
|
<div>Slug: {ev.slug}</div>
|
|
<div>Datum: {ev.date ?? '-'}</div>
|
|
<div>Status: {ev.is_active ? 'Aktiv' : 'Inaktiv'}</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 text-center text-sm sm:grid-cols-3">
|
|
<StatCard label="Fotos" value={stats?.total ?? 0} />
|
|
<StatCard label="Featured" value={stats?.featured ?? 0} />
|
|
<StatCard label="Likes gesamt" value={stats?.likes ?? 0} />
|
|
</div>
|
|
<div className="rounded border p-3 text-sm">
|
|
<div className="mb-2 font-medium">Join-Link</div>
|
|
<div className="mb-2 flex items-center gap-2">
|
|
<input className="w-full rounded border p-2 text-sm" value={joinLink} readOnly />
|
|
<Button variant="secondary" onClick={() => navigator.clipboard.writeText(joinLink)}>
|
|
Kopieren
|
|
</Button>
|
|
</div>
|
|
<div className="mb-2 font-medium">QR</div>
|
|
<img src={qrUrl} alt="QR" width={200} height={200} className="rounded border" />
|
|
<div className="mt-3">
|
|
<Button variant="secondary" onClick={onInvite}>
|
|
Einladungslink erzeugen
|
|
</Button>
|
|
{invite && (
|
|
<div className="mt-2 text-xs text-muted-foreground">Erzeugt und kopiert: {invite}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatCard({ label, value }: { label: string; value: number }) {
|
|
return (
|
|
<div className="rounded border p-3">
|
|
<div className="text-2xl font-semibold">{value}</div>
|
|
<div>{label}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderName(name: any): string {
|
|
if (typeof name === 'string') return name;
|
|
if (name && (name.de || name.en)) return name.de || name.en;
|
|
return JSON.stringify(name);
|
|
}
|