feat: implement tenant OAuth flow and guest achievements
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { getEvent, getEventStats, toggleEvent, createInviteLink } from '../api';
|
||||
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();
|
||||
@@ -11,35 +12,67 @@ export default function EventDetailPage() {
|
||||
const [stats, setStats] = React.useState<{ total: number; featured: number; likes: number } | null>(null);
|
||||
const [invite, setInvite] = React.useState<string | null>(null);
|
||||
|
||||
async function load() {
|
||||
const e = await getEvent(id);
|
||||
setEv(e);
|
||||
setStats(await getEventStats(id));
|
||||
}
|
||||
React.useEffect(() => { load(); }, [id]);
|
||||
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() {
|
||||
const isActive = await toggleEvent(id);
|
||||
setEv((o: any) => ({ ...(o || {}), is_active: isActive }));
|
||||
try {
|
||||
const isActive = await toggleEvent(id);
|
||||
setEv((previous: any) => ({ ...(previous || {}), is_active: isActive }));
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onInvite() {
|
||||
const link = await createInviteLink(id);
|
||||
setInvite(link);
|
||||
try { await navigator.clipboard.writeText(link); } catch {}
|
||||
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 = `${location.origin}/e/${ev.slug}`;
|
||||
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 p-4 space-y-4">
|
||||
<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>
|
||||
<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">
|
||||
@@ -47,31 +80,45 @@ export default function EventDetailPage() {
|
||||
<div>Datum: {ev.date ?? '-'}</div>
|
||||
<div>Status: {ev.is_active ? 'Aktiv' : 'Inaktiv'}</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-center text-sm">
|
||||
<div className="rounded border p-3"><div className="text-2xl font-semibold">{stats?.total ?? 0}</div><div>Fotos</div></div>
|
||||
<div className="rounded border p-3"><div className="text-2xl font-semibold">{stats?.featured ?? 0}</div><div>Featured</div></div>
|
||||
<div className="rounded border p-3"><div className="text-2xl font-semibold">{stats?.likes ?? 0}</div><div>Likes gesamt</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">
|
||||
<div className="mb-2 text-sm font-medium">Join-Link</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>
|
||||
<Button variant="secondary" onClick={() => navigator.clipboard.writeText(joinLink)}>
|
||||
Kopieren
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-2 text-sm font-medium">QR</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>}
|
||||
<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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user