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,77 +199,86 @@ 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.credentials.description', 'Share these credentials with your photobooth software.')} {t('photobooth.steps.activate.description', 'Schalte den Upload-Zugang fuer dieses Event frei.')}
</Text>
<Text fontSize="$xs" color={muted}>
{t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
</Text> </Text>
</YStack> </YStack>
<YStack alignItems="flex-end" space="$2"> <XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
<PillBadge tone={isActive ? 'success' : 'warning'}> <PillBadge tone={isActive ? 'success' : 'warning'}>
{isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')} {isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')}
</PillBadge> </PillBadge>
<XStack alignItems="center" space="$2">
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{isActive ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')} {t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
</Text> </Text>
<Switch </XStack>
size="$4" <XStack space="$2" marginTop="$2">
checked={isActive} <CTAButton
label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')}
onPress={() => (isActive ? handleDisable() : handleEnable())}
tone={isActive ? 'ghost' : 'primary'}
iconLeft={isActive ? <Power size={14} color={text} /> : <PlugZap size={14} color={surface} />}
disabled={updating} disabled={updating}
onCheckedChange={handleToggle} fullWidth={false}
aria-label={t('photobooth.actions.toggle', 'Toggle photobooth access')} />
> {isActive ? (
<Switch.Thumb /> <CTAButton
</Switch> label={t('photobooth.actions.rotate', 'Regenerate access')}
onPress={() => handleRotate()}
tone="ghost"
iconLeft={<RefreshCw size={14} color={text} />}
disabled={updating}
fullWidth={false}
/>
) : null}
</XStack> </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>
</MobileCard> </MobileCard>
<MobileCard space="$2"> <MobileCard space="$3">
<Text fontSize="$sm" fontWeight="700" color={text}> <YStack space="$1">
{t('photobooth.selector.title', 'Connection')} <Text fontSize="$sm" fontWeight="800" color={text}>
</Text> {t('photobooth.steps.download.title', '2. Uploader App herunterladen')}
<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>
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{t( {t(
'photobooth.uploaderDownload.description', 'photobooth.uploaderDownload.description',
'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.' 'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschuetzt bleiben und keine Dateien verloren gehen.'
)} )}
</Text> </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 <CTAButton
label={ label={
sendingEmail sendingEmail
@@ -290,58 +289,21 @@ export default function MobileEventPhotoboothPage() {
onPress={handleSendDownloadEmail} onPress={handleSendDownloadEmail}
iconLeft={<Mail size={14} color={text} />} iconLeft={<Mail size={14} color={text} />}
disabled={sendingEmail} disabled={sendingEmail}
fullWidth={false}
/> />
<CTAButton </XStack>
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>
<MobileCard space="$2"> <MobileCard space="$3">
<XStack alignItems="center" justifyContent="space-between"> <YStack space="$1">
<Text fontSize="$sm" fontWeight="700" color={text}> <Text fontSize="$sm" fontWeight="800" color={text}>
{t('photobooth.credentials.uploaderTitle', 'Uploader App (HTTP)')} {t('photobooth.steps.access.title', '3. Verbindungscode erstellen')}
</Text> </Text>
</XStack>
<YStack space="$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
label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')}
onPress={() => (isActive ? handleDisable() : handleEnable())}
tone={isActive ? 'ghost' : 'primary'}
iconLeft={isActive ? <Power size={14} color={text} /> : <PlugZap size={14} color={surface} />}
disabled={updating}
style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }}
/>
<YStack space="$2">
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{t('photobooth.connectCode.description', 'Create a 6-digit code for the uploader app.')} {t('photobooth.steps.access.description', 'Der Code verbindet die App sicher mit deinem Event.')}
</Text> </Text>
</YStack>
<XStack space="$2" marginTop="$2">
<CTAButton <CTAButton
label={ label={
connectLoading connectLoading
@@ -351,8 +313,20 @@ export default function MobileEventPhotoboothPage() {
onPress={handleGenerateConnectCode} onPress={handleGenerateConnectCode}
iconLeft={<PlugZap size={14} color={surface} />} iconLeft={<PlugZap size={14} color={surface} />}
disabled={!isActive || updating || connectLoading} disabled={!isActive || updating || connectLoading}
style={{ width: '100%', paddingHorizontal: 10, paddingVertical: 10 }} fullWidth={false}
/> />
<CTAButton
label={
showCredentials
? t('photobooth.credentials.hide', 'Hide credentials')
: t('photobooth.credentials.show', 'Show credentials')
}
tone="ghost"
onPress={() => setShowCredentials((current) => !current)}
fullWidth={false}
/>
</XStack>
<YStack space="$2" marginTop="$2">
{connectCode ? ( {connectCode ? (
<CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} /> <CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} />
) : null} ) : null}
@@ -363,16 +337,6 @@ export default function MobileEventPhotoboothPage() {
})} })}
</Text> </Text>
) : null} ) : null}
</YStack>
<CTAButton
label={
showCredentials
? t('photobooth.credentials.hide', 'Hide credentials')
: t('photobooth.credentials.show', 'Show credentials')
}
tone="ghost"
onPress={() => setShowCredentials((current) => !current)}
/>
{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} />