Restructure photobooth page flow

This commit is contained in:
Codex Agent
2026-01-13 10:52:50 +01:00
parent 5f3d6af9f0
commit 249a5639a9
3 changed files with 144 additions and 154 deletions

View File

@@ -1208,6 +1208,19 @@
"uploader": { "uploader": {
"hint": "POST mit Mediendatei oder base64-Feld \"media\"; die App nutzt diese Zugangsdaten." "hint": "POST mit Mediendatei oder base64-Feld \"media\"; die App nutzt diese Zugangsdaten."
}, },
"steps": {
"activate": {
"title": "1. Photobooth aktivieren",
"description": "Schalte den Upload-Zugang fuer dieses Event frei."
},
"download": {
"title": "2. Uploader App herunterladen"
},
"access": {
"title": "3. Verbindungscode erstellen",
"description": "Der Code verbindet die App sicher mit deinem Event."
}
},
"uploaderDownload": { "uploaderDownload": {
"title": "Fotospiel Uploader App", "title": "Fotospiel Uploader App",
"description": "Die Fotospiel Uploader App wird benötigt, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.", "description": "Die Fotospiel Uploader App wird benötigt, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.",

View File

@@ -921,6 +921,19 @@
"uploader": { "uploader": {
"hint": "POST with media file or base64 \"media\" field; app uses these credentials." "hint": "POST with media file or base64 \"media\" field; app uses these credentials."
}, },
"steps": {
"activate": {
"title": "1. Activate photobooth",
"description": "Enable upload access for this event."
},
"download": {
"title": "2. Download uploader app"
},
"access": {
"title": "3. Generate connect code",
"description": "The code securely pairs the app with your event."
}
},
"uploaderDownload": { "uploaderDownload": {
"title": "Fotospiel Uploader App", "title": "Fotospiel Uploader App",
"description": "The Fotospiel Uploader App is required so uploads stay stable, credentials remain protected, and no files are lost.", "description": "The Fotospiel Uploader App is required so uploads stay stable, credentials remain protected, and no files are lost.",

View File

@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3, Mail } from 'lucide-react'; import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3, Mail, Download } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks'; import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text'; import { SizableText as Text } from '@tamagui/text';
import { Switch } from '@tamagui/switch';
import { Pressable } from '@tamagui/react-native-web-lite'; import { Pressable } from '@tamagui/react-native-web-lite';
import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
@@ -172,15 +171,6 @@ export default function MobileEventPhotoboothPage() {
const isActive = Boolean(status?.enabled); const isActive = Boolean(status?.enabled);
const title = t('photobooth.title', 'Photobooth'); const title = t('photobooth.title', 'Photobooth');
const handleToggle = (checked: boolean) => {
if (!slug || updating) return;
if (checked) {
void handleEnable();
} else {
void handleDisable();
}
};
return ( return (
<MobileShell <MobileShell
activeTab="home" activeTab="home"
@@ -209,161 +199,122 @@ export default function MobileEventPhotoboothPage() {
) : ( ) : (
<YStack space="$2"> <YStack space="$2">
<MobileCard space="$3"> <MobileCard space="$3">
<XStack justifyContent="space-between" alignItems="center" space="$3" flexWrap="wrap"> <YStack space="$1">
<YStack space="$1" flex={1} minWidth={0}> <Text fontSize="$sm" fontWeight="800" color={text}>
<Text fontSize="$md" fontWeight="800" color={text}> {t('photobooth.steps.activate.title', '1. Photobooth aktivieren')}
{t('photobooth.title', 'Photobooth')} </Text>
</Text> <Text fontSize="$xs" color={muted}>
<Text fontSize="$xs" color={muted}> {t('photobooth.steps.activate.description', 'Schalte den Upload-Zugang fuer dieses Event frei.')}
{t('photobooth.credentials.description', 'Share these credentials with your photobooth software.')} </Text>
</Text>
<Text fontSize="$xs" color={muted}>
{t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
</Text>
</YStack>
<YStack alignItems="flex-end" space="$2">
<PillBadge tone={isActive ? 'success' : 'warning'}>
{isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')}
</PillBadge>
<XStack alignItems="center" space="$2">
<Text fontSize="$xs" color={muted}>
{isActive ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')}
</Text>
<Switch
size="$4"
checked={isActive}
disabled={updating}
onCheckedChange={handleToggle}
aria-label={t('photobooth.actions.toggle', 'Toggle photobooth access')}
>
<Switch.Thumb />
</Switch>
</XStack>
</YStack>
</XStack>
<YStack space="$1" marginTop="$2">
<XStack justifyContent="space-between" alignItems="center">
<Text fontSize="$xs" color={muted}>
{t('photobooth.stats.lastUpload', 'Last upload')}
</Text>
<Text fontSize="$xs" fontWeight="700" color={text}>
{lastUploadAt ? formatEventDate(lastUploadAt, locale) : t('photobooth.status.never', 'Never')}
</Text>
</XStack>
<XStack justifyContent="space-between" alignItems="center">
<Text fontSize="$xs" color={muted}>
{t('photobooth.status.expires', 'Access expires')}
</Text>
<Text fontSize="$xs" fontWeight="700" color={text}>
{expiresAt ? formatEventDate(expiresAt, locale) : '—'}
</Text>
</XStack>
</YStack> </YStack>
</MobileCard> <XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
<PillBadge tone={isActive ? 'success' : 'warning'}>
<MobileCard space="$2"> {isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')}
<Text fontSize="$sm" fontWeight="700" color={text}> </PillBadge>
{t('photobooth.selector.title', 'Connection')} <Text fontSize="$xs" color={muted}>
</Text> {t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
<Text fontSize="$xs" color={muted}>
{t('photobooth.selector.description', 'Use the Fotospiel uploader app for HTTP uploads.')}
</Text>
</MobileCard>
<MobileCard space="$2">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('photobooth.uploaderDownload.title', 'Fotospiel Uploader App')}
</Text>
<Text fontSize="$xs" color={muted}>
{t(
'photobooth.uploaderDownload.description',
'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.'
)}
</Text>
<CTAButton
label={
sendingEmail
? t('common.processing', '...')
: t('photobooth.uploaderDownload.emailAction', 'Download-Links per E-Mail senden')
}
tone="ghost"
onPress={handleSendDownloadEmail}
iconLeft={<Mail size={14} color={text} />}
disabled={sendingEmail}
/>
<CTAButton
label={t('photobooth.uploaderDownload.actionWindows', 'Uploader herunterladen (Windows)')}
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-win-x64.exe', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
/>
<CTAButton
label={t('photobooth.uploaderDownload.actionMac', 'Uploader herunterladen (macOS)')}
tone="ghost"
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-macos-x64', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
/>
<CTAButton
label={t('photobooth.uploaderDownload.actionLinux', 'Uploader herunterladen (Linux)')}
tone="ghost"
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-linux-x64', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
/>
</MobileCard>
<MobileCard space="$2">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('photobooth.credentials.uploaderTitle', 'Uploader App (HTTP)')}
</Text> </Text>
</XStack> </XStack>
<YStack space="$2"> <XStack space="$2" marginTop="$2">
<CTAButton
label={updating ? t('common.processing', '...') : t('photobooth.actions.rotate', 'Regenerate access')}
onPress={() => handleRotate()}
iconLeft={<RefreshCw size={14} color={surface} />}
disabled={updating}
style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }}
/>
<CTAButton <CTAButton
label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')} label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')}
onPress={() => (isActive ? handleDisable() : handleEnable())} onPress={() => (isActive ? handleDisable() : handleEnable())}
tone={isActive ? 'ghost' : 'primary'} tone={isActive ? 'ghost' : 'primary'}
iconLeft={isActive ? <Power size={14} color={text} /> : <PlugZap size={14} color={surface} />} iconLeft={isActive ? <Power size={14} color={text} /> : <PlugZap size={14} color={surface} />}
disabled={updating} disabled={updating}
style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} fullWidth={false}
/> />
<YStack space="$2"> {isActive ? (
<Text fontSize="$xs" color={muted}>
{t('photobooth.connectCode.description', 'Create a 6-digit code for the uploader app.')}
</Text>
<CTAButton <CTAButton
label={ label={t('photobooth.actions.rotate', 'Regenerate access')}
connectLoading onPress={() => handleRotate()}
? t('common.processing', '...') tone="ghost"
: t('photobooth.connectCode.actions.generate', 'Generate connect code') iconLeft={<RefreshCw size={14} color={text} />}
} disabled={updating}
onPress={handleGenerateConnectCode} fullWidth={false}
iconLeft={<PlugZap size={14} color={surface} />}
disabled={!isActive || updating || connectLoading}
style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }}
/> />
{connectCode ? ( ) : null}
<CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} /> </XStack>
) : null} </MobileCard>
{connectExpiresAt ? (
<Text fontSize="$xs" color={muted}> <MobileCard space="$3">
{t('photobooth.connectCode.expires', 'Expires: {{date}}', { <YStack space="$1">
date: formatEventDateTime(connectExpiresAt, locale), <Text fontSize="$sm" fontWeight="800" color={text}>
})} {t('photobooth.steps.download.title', '2. Uploader App herunterladen')}
</Text> </Text>
) : null} <Text fontSize="$xs" color={muted}>
</YStack> {t(
'photobooth.uploaderDownload.description',
'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschuetzt bleiben und keine Dateien verloren gehen.'
)}
</Text>
</YStack>
<XStack space="$2" marginTop="$2" flexWrap="wrap">
<CTAButton
label={t('photobooth.uploaderDownload.actionWindows', 'Uploader herunterladen (Windows)')}
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-win-x64.exe', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
iconLeft={<Download size={14} color={surface} />}
fullWidth={false}
/>
<CTAButton
label={t('photobooth.uploaderDownload.actionMac', 'Uploader herunterladen (macOS)')}
tone="ghost"
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-macos-x64', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
fullWidth={false}
/>
<CTAButton
label={t('photobooth.uploaderDownload.actionLinux', 'Uploader herunterladen (Linux)')}
tone="ghost"
onPress={() => {
const url = new URL('/downloads/PhotoboothUploader-linux-x64', window.location.origin).toString();
window.open(url, '_blank', 'noopener,noreferrer');
}}
fullWidth={false}
/>
</XStack>
<XStack space="$2" marginTop="$2">
<CTAButton
label={
sendingEmail
? t('common.processing', '...')
: t('photobooth.uploaderDownload.emailAction', 'Download-Links per E-Mail senden')
}
tone="ghost"
onPress={handleSendDownloadEmail}
iconLeft={<Mail size={14} color={text} />}
disabled={sendingEmail}
fullWidth={false}
/>
</XStack>
</MobileCard>
<MobileCard space="$3">
<YStack space="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('photobooth.steps.access.title', '3. Verbindungscode erstellen')}
</Text>
<Text fontSize="$xs" color={muted}>
{t('photobooth.steps.access.description', 'Der Code verbindet die App sicher mit deinem Event.')}
</Text>
</YStack>
<XStack space="$2" marginTop="$2">
<CTAButton
label={
connectLoading
? t('common.processing', '...')
: t('photobooth.connectCode.actions.generate', 'Generate connect code')
}
onPress={handleGenerateConnectCode}
iconLeft={<PlugZap size={14} color={surface} />}
disabled={!isActive || updating || connectLoading}
fullWidth={false}
/>
<CTAButton <CTAButton
label={ label={
showCredentials showCredentials
@@ -372,7 +323,20 @@ export default function MobileEventPhotoboothPage() {
} }
tone="ghost" tone="ghost"
onPress={() => setShowCredentials((current) => !current)} onPress={() => setShowCredentials((current) => !current)}
fullWidth={false}
/> />
</XStack>
<YStack space="$2" marginTop="$2">
{connectCode ? (
<CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} />
) : null}
{connectExpiresAt ? (
<Text fontSize="$xs" color={muted}>
{t('photobooth.connectCode.expires', 'Expires: {{date}}', {
date: formatEventDateTime(connectExpiresAt, locale),
})}
</Text>
) : null}
{showCredentials ? ( {showCredentials ? (
<YStack space="$1"> <YStack space="$1">
<CredentialRow label={t('photobooth.credentials.postUrl', 'Upload URL')} value={uploadUrl ?? '—'} border={border} /> <CredentialRow label={t('photobooth.credentials.postUrl', 'Upload URL')} value={uploadUrl ?? '—'} border={border} />