geschenkgutscheine implementiert ("Paket verschenken"). Neuer Upload-Provider: Sparkbooth.
This commit is contained in:
@@ -40,6 +40,8 @@ export default function EventPhotoboothPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(['management', 'common']);
|
||||
|
||||
const [mode, setMode] = React.useState<'ftp' | 'sparkbooth'>('ftp');
|
||||
|
||||
const [state, setState] = React.useState<State>({
|
||||
event: null,
|
||||
status: null,
|
||||
@@ -96,17 +98,27 @@ export default function EventPhotoboothPage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function handleEnable(): Promise<void> {
|
||||
React.useEffect(() => {
|
||||
if (state.status?.mode) {
|
||||
setMode(state.status.mode);
|
||||
}
|
||||
}, [state.status?.mode]);
|
||||
|
||||
async function handleEnable(targetMode?: 'ftp' | 'sparkbooth'): Promise<void> {
|
||||
if (!slug) return;
|
||||
setState((prev) => ({ ...prev, updating: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await enableEventPhotobooth(slug);
|
||||
const selectedMode = targetMode ?? mode;
|
||||
const result = await enableEventPhotobooth(slug, { mode: selectedMode });
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
status: result,
|
||||
updating: false,
|
||||
}));
|
||||
if (result.mode) {
|
||||
setMode(result.mode);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({
|
||||
@@ -125,7 +137,7 @@ export default function EventPhotoboothPage() {
|
||||
setState((prev) => ({ ...prev, updating: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await rotateEventPhotobooth(slug);
|
||||
const result = await rotateEventPhotobooth(slug, { mode });
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
status: result,
|
||||
@@ -153,7 +165,7 @@ export default function EventPhotoboothPage() {
|
||||
setState((prev) => ({ ...prev, updating: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await disableEventPhotobooth(slug);
|
||||
const result = await disableEventPhotobooth(slug, { mode });
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
status: result,
|
||||
@@ -178,7 +190,7 @@ export default function EventPhotoboothPage() {
|
||||
: t('management.photobooth.title', 'Fotobox-Uploads');
|
||||
const subtitle = t(
|
||||
'management.photobooth.subtitle',
|
||||
'Erstelle einen einfachen FTP-Link für Photobooth-Software. Rate-Limit: 20 Fotos/Minute.'
|
||||
'Erstelle einen einfachen Photobooth-Link per FTP oder Sparkbooth-Upload. Rate-Limit: 20 Fotos/Minute.'
|
||||
);
|
||||
const eventTabs = React.useMemo(() => {
|
||||
if (!event || !slug) {
|
||||
@@ -192,7 +204,10 @@ export default function EventPhotoboothPage() {
|
||||
}, [event, slug, t]);
|
||||
|
||||
const recentPhotos = React.useMemo(() => toolkit?.photos?.recent ?? [], [toolkit?.photos?.recent]);
|
||||
const photoboothRecent = React.useMemo(() => recentPhotos.filter((photo) => photo.ingest_source === 'photobooth'), [recentPhotos]);
|
||||
const photoboothRecent = React.useMemo(
|
||||
() => recentPhotos.filter((photo) => photo.ingest_source === 'photobooth' || photo.ingest_source === 'sparkbooth'),
|
||||
[recentPhotos]
|
||||
);
|
||||
const effectiveRecentPhotos = React.useMemo(
|
||||
() => (photoboothRecent.length > 0 ? photoboothRecent : recentPhotos),
|
||||
[photoboothRecent, recentPhotos],
|
||||
@@ -260,6 +275,42 @@ export default function EventPhotoboothPage() {
|
||||
<PhotoboothSkeleton />
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-3xl border border-slate-200/80 bg-white/70 p-5 shadow-sm">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
{t('management.photobooth.mode.title', 'Photobooth-Typ auswählen')}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{t(
|
||||
'management.photobooth.mode.description',
|
||||
'Wähle zwischen klassischem FTP und Sparkbooth HTTP-Upload. Umschalten generiert neue Zugangsdaten.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={mode === 'ftp' ? 'default' : 'outline'}
|
||||
onClick={() => handleEnable('ftp')}
|
||||
disabled={updating || mode === 'ftp'}
|
||||
>
|
||||
FTP (Classic)
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode === 'sparkbooth' ? 'default' : 'outline'}
|
||||
onClick={() => handleEnable('sparkbooth')}
|
||||
disabled={updating || mode === 'sparkbooth'}
|
||||
>
|
||||
Sparkbooth (HTTP)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
{t('management.photobooth.mode.active', 'Aktuell: {{mode}}', {
|
||||
mode: mode === 'sparkbooth' ? 'Sparkbooth / HTTP' : 'FTP',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
|
||||
<StatusCard status={status} />
|
||||
<SetupChecklistCard status={status} />
|
||||
@@ -338,7 +389,7 @@ function ModePresetsCard({ status, updating, onEnable, onDisable, onRotate }: Mo
|
||||
{
|
||||
key: 'live' as const,
|
||||
title: t('photobooth.presets.liveTitle', 'Live-Modus'),
|
||||
description: t('photobooth.presets.liveDescription', 'FTP ist aktiv und Uploads werden direkt entgegen genommen.'),
|
||||
description: t('photobooth.presets.liveDescription', 'Uploads sind aktiv (FTP oder Sparkbooth) und werden direkt verarbeitet.'),
|
||||
badge: t('photobooth.presets.badgeLive', 'Live'),
|
||||
icon: <PlugZap className="h-5 w-5 text-emerald-500" />,
|
||||
},
|
||||
@@ -524,6 +575,7 @@ function StatusCard({ status }: { status: PhotoboothStatus | null }) {
|
||||
const isActive = Boolean(status?.enabled);
|
||||
const badgeColor = isActive ? 'bg-emerald-600 text-white' : 'bg-slate-300 text-slate-800';
|
||||
const icon = isActive ? <PlugZap className="h-5 w-5 text-emerald-500" /> : <Power className="h-5 w-5 text-slate-400" />;
|
||||
const modeLabel = status?.mode === 'sparkbooth' ? 'Sparkbooth / HTTP' : 'FTP';
|
||||
|
||||
return (
|
||||
<Card className="rounded-3xl border border-slate-200/80 shadow-sm">
|
||||
@@ -543,13 +595,18 @@ function StatusCard({ status }: { status: PhotoboothStatus | null }) {
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{status?.expires_at ? (
|
||||
<CardContent className="text-sm text-slate-600">
|
||||
{t('photobooth.status.expiresAt', 'Automatisches Abschalten am {{date}}', {
|
||||
date: new Date(status.expires_at).toLocaleString(),
|
||||
})}
|
||||
</CardContent>
|
||||
) : null}
|
||||
<CardContent className="space-y-1 text-sm text-slate-600">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||
{t('photobooth.status.mode', 'Modus')}: {modeLabel}
|
||||
</p>
|
||||
{status?.expires_at ? (
|
||||
<p>
|
||||
{t('photobooth.status.expiresAt', 'Automatisches Abschalten am {{date}}', {
|
||||
date: new Date(status.expires_at).toLocaleString(),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -565,27 +622,48 @@ type CredentialCardProps = {
|
||||
function CredentialsCard({ status, updating, onEnable, onRotate, onDisable }: CredentialCardProps) {
|
||||
const { t } = useTranslation('management');
|
||||
const isActive = Boolean(status?.enabled);
|
||||
const isSparkbooth = status?.mode === 'sparkbooth';
|
||||
|
||||
return (
|
||||
<Card className="rounded-3xl border border-rose-100/80 shadow-lg shadow-rose-100/40">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('photobooth.credentials.heading', 'FTP-Zugangsdaten')}</CardTitle>
|
||||
<CardTitle>
|
||||
{isSparkbooth ? t('photobooth.credentials.sparkboothTitle', 'Sparkbooth-Upload (HTTP)') : t('photobooth.credentials.heading', 'FTP-Zugangsdaten')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
'photobooth.credentials.description',
|
||||
'Teile die Zugangsdaten mit der Photobooth-Software. Passwörter werden max. 8 Zeichen lang generiert.'
|
||||
)}
|
||||
{isSparkbooth
|
||||
? t(
|
||||
'photobooth.credentials.sparkboothDescription',
|
||||
'Trage URL, Benutzername und Passwort in Sparkbooth ein. Antworten erfolgen als JSON (optional XML).'
|
||||
)
|
||||
: t(
|
||||
'photobooth.credentials.description',
|
||||
'Teile die Zugangsdaten mit der Photobooth-Software. Passwörter werden max. 8 Zeichen lang generiert.'
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label={t('photobooth.credentials.host', 'Host')} value={status?.ftp.host ?? '—'} />
|
||||
<Field label={t('photobooth.credentials.port', 'Port')} value={String(status?.ftp.port ?? 2121)} />
|
||||
<Field label={t('photobooth.credentials.username', 'Benutzername')} value={status?.username ?? '—'} copyable />
|
||||
<Field label={t('photobooth.credentials.password', 'Passwort')} value={status?.password ?? '—'} copyable sensitive />
|
||||
<Field label={t('photobooth.credentials.path', 'Upload-Pfad')} value={status?.path ?? '/photobooth/...'} copyable />
|
||||
<Field label="FTP-Link" value={status?.ftp_url ?? '—'} copyable className="md:col-span-2" />
|
||||
</div>
|
||||
{isSparkbooth ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label={t('photobooth.credentials.username', 'Benutzername')} value={status?.username ?? '—'} copyable />
|
||||
<Field label={t('photobooth.credentials.password', 'Passwort')} value={status?.password ?? '—'} copyable sensitive />
|
||||
<Field label={t('photobooth.credentials.postUrl', 'Upload-URL')} value={status?.upload_url ?? '—'} copyable className="md:col-span-2" />
|
||||
<Field
|
||||
label={t('photobooth.credentials.responseFormat', 'Antwort-Format')}
|
||||
value={status?.sparkbooth?.response_format === 'xml' ? 'XML' : 'JSON'}
|
||||
className="md:col-span-2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label={t('photobooth.credentials.host', 'Host')} value={status?.ftp.host ?? '—'} />
|
||||
<Field label={t('photobooth.credentials.port', 'Port')} value={String(status?.ftp.port ?? 2121)} />
|
||||
<Field label={t('photobooth.credentials.username', 'Benutzername')} value={status?.username ?? '—'} copyable />
|
||||
<Field label={t('photobooth.credentials.password', 'Passwort')} value={status?.password ?? '—'} copyable sensitive />
|
||||
<Field label={t('photobooth.credentials.path', 'Upload-Pfad')} value={status?.path ?? '/photobooth/...'} copyable />
|
||||
<Field label="FTP-Link" value={status?.ftp_url ?? '—'} copyable className="md:col-span-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{isActive ? (
|
||||
|
||||
Reference in New Issue
Block a user