neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.

This commit is contained in:
Codex Agent
2025-12-30 10:24:06 +01:00
parent 902e78cae9
commit efe2f25b3e
85 changed files with 95235 additions and 19197 deletions

View File

@@ -21,12 +21,14 @@ import toast from 'react-hot-toast';
import { ADMIN_BASE_PATH, adminPath } from '../constants';
import { resolveLayoutForFormat } from './qr/utils';
import { useBackNavigation } from './hooks/useBackNavigation';
import { useAdminTheme } from './theme';
export default function MobileQrPrintPage() {
const { slug: slugParam } = useParams<{ slug?: string }>();
const slug = slugParam ?? null;
const navigate = useNavigate();
const { t } = useTranslation('management');
const { textStrong, text, muted, subtle, border, surfaceMuted, primary, danger, accentSoft } = useAdminTheme();
const [event, setEvent] = React.useState<TenantEvent | null>(null);
const [selectedInvite, setSelectedInvite] = React.useState<EventQrInvite | null>(null);
@@ -80,13 +82,13 @@ export default function MobileQrPrintPage() {
onBack={back}
headerActions={
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color="#0f172a" />
<RefreshCcw size={18} color={textStrong} />
</HeaderActionButton>
}
>
{error ? (
<MobileCard>
<Text fontWeight="700" color="#b91c1c">
<Text fontWeight="700" color={danger}>
{error}
</Text>
<CTAButton
@@ -99,7 +101,7 @@ export default function MobileQrPrintPage() {
) : null}
<MobileCard space="$3" alignItems="center">
<Text fontSize="$md" fontWeight="800" color="#111827">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.qr.heroTitle', 'Entrance QR Code')}
</Text>
<YStack
@@ -107,8 +109,8 @@ export default function MobileQrPrintPage() {
height={180}
borderRadius={16}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#f8fafc"
borderColor={border}
backgroundColor={surfaceMuted}
alignItems="center"
justifyContent="center"
overflow="hidden"
@@ -116,21 +118,21 @@ export default function MobileQrPrintPage() {
{qrImage ? (
<img
src={qrImage}
alt="QR"
alt={t('events.qr.qrAlt', 'QR code')}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
) : (
<Text color="#9ca3af" fontSize="$sm">
<Text color={subtle} fontSize="$sm">
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
)}
</YStack>
{qrUrl ? (
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
<Text fontSize="$xs" color={text} textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
{qrUrl}
</Text>
) : null}
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.description', 'Scan to access the event guest app.')}
</Text>
<XStack space="$2" width="100%" marginTop="$2">
@@ -177,10 +179,10 @@ export default function MobileQrPrintPage() {
</MobileCard>
<MobileCard space="$2">
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.step1', 'Schritt 1: Format wählen')}
</Text>
<Text fontSize="$md" fontWeight="800" color="#111827">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.qr.layouts', 'Print Layouts')}
</Text>
<FormatSelection
@@ -212,7 +214,7 @@ export default function MobileQrPrintPage() {
</MobileCard>
<MobileCard space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.createLink', 'Neuen QR-Link erstellen')}
</Text>
<CTAButton
@@ -220,7 +222,7 @@ export default function MobileQrPrintPage() {
onPress={async () => {
if (!slug) return;
try {
const invite = await createQrInvite(slug, { label: 'Mobile Link' });
const invite = await createQrInvite(slug, { label: t('events.qr.mobileLinkLabel', 'Mobile link') });
setQrUrl(invite.url);
setQrImage(invite.qr_code_data_url ?? '');
setSelectedInvite(invite);
@@ -246,6 +248,7 @@ function FormatSelection({
onSelect: (format: 'a4-poster' | 'a5-foldable', layoutId: string | null) => void;
}) {
const { t } = useTranslation('management');
const { border, primary, accentSoft, surface, textStrong, muted, subtle } = useAdminTheme();
const selectLayoutId = (format: 'a4-poster' | 'a5-foldable'): string | null => {
return resolveLayoutForFormat(format, layouts);
@@ -278,16 +281,16 @@ function FormatSelection({
>
<MobileCard
padding="$3"
borderColor={isSelected ? '#2563EB' : '#e5e7eb'}
borderColor={isSelected ? primary : border}
borderWidth={isSelected ? 2 : 1}
backgroundColor={isSelected ? '#eff6ff' : '#fff'}
backgroundColor={isSelected ? accentSoft : surface}
>
<XStack alignItems="center" justifyContent="space-between" space="$3">
<YStack space="$1" flex={1}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{card.title}
</Text>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{card.subtitle}
</Text>
<XStack space="$2" alignItems="center" flexWrap="wrap">
@@ -298,7 +301,7 @@ function FormatSelection({
))}
</XStack>
</YStack>
<ChevronRight size={16} color="#9ca3af" />
<ChevronRight size={16} color={subtle} />
</XStack>
</MobileCard>
</Pressable>
@@ -328,6 +331,7 @@ function BackgroundStep({
saving: boolean;
}) {
const { t } = useTranslation('management');
const { textStrong, muted, border, primary, surfaceMuted } = useAdminTheme();
const resolvedLayout =
selectedLayout ??
layouts.find((layout) => {
@@ -348,8 +352,8 @@ function BackgroundStep({
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
@@ -360,7 +364,7 @@ function BackgroundStep({
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t(
'events.qr.backgroundPicker',
disablePresets ? 'Hintergrund für A5 (Gradient/Farbe)' : `Hintergrund auswählen (${formatLabel})`
@@ -379,8 +383,8 @@ function BackgroundStep({
borderRadius={14}
overflow="hidden"
borderWidth={isSelected ? 2 : 1}
borderColor={isSelected ? '#2563EB' : '#e5e7eb'}
backgroundColor="#f8fafc"
borderColor={isSelected ? primary : border}
backgroundColor={surfaceMuted}
>
<YStack
flex={1}
@@ -389,7 +393,7 @@ function BackgroundStep({
backgroundPosition="center"
/>
<XStack padding="$2" justifyContent="space-between" alignItems="center" backgroundColor="rgba(255,255,255,0.92)">
<Text fontSize="$xs" color="#111827">
<Text fontSize="$xs" color={textStrong}>
{preset.label}
</Text>
{isSelected ? <PillBadge tone="muted">{t('common.selected', 'Ausgewählt')}</PillBadge> : null}
@@ -399,12 +403,12 @@ function BackgroundStep({
);
})}
</XStack>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.backgroundNote', 'Diese Presets sind für A4 Hochformat. Spiegelung erfolgt automatisch bei Tischkarten.')}
</Text>
</>
) : (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.foldableBackgroundNote', 'Für A5 Tischkarten bitte Gradient oder Vollfarbe wählen.')}
</Text>
)}
@@ -433,6 +437,7 @@ function TextStep({
saving: boolean;
}) {
const { t } = useTranslation('management');
const { textStrong } = useAdminTheme();
const updateField = (key: 'headline' | 'subtitle' | 'description', value: string) => {
onChange({ ...textFields, [key]: value });
@@ -458,8 +463,8 @@ function TextStep({
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
@@ -468,7 +473,7 @@ function TextStep({
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.textFields', 'Texte')}
</Text>
<StyledInput
@@ -489,7 +494,7 @@ function TextStep({
</YStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.instructions', 'Anleitung')}
</Text>
{textFields.instructions.map((item, idx) => (
@@ -537,16 +542,19 @@ function PreviewStep({
onExport: (format: 'pdf' | 'png') => void;
}) {
const { t } = useTranslation('management');
const { textStrong, text, muted, border, surfaceMuted } = useAdminTheme();
const presetSrc = backgroundPreset ? presets.find((p) => p.id === backgroundPreset)?.src ?? null : null;
const resolvedBg = presetSrc ?? layout?.preview?.background ?? '#f8fafc';
const resolvedBg = presetSrc ?? layout?.preview?.background ?? surfaceMuted;
const previewText = layout?.preview?.text ?? textStrong;
const previewBody = layout?.preview?.text ?? text;
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
@@ -559,15 +567,15 @@ function PreviewStep({
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.preview', 'Vorschau')}
</Text>
<YStack
borderRadius={16}
borderWidth={1}
borderColor="#e5e7eb"
borderColor={border}
overflow="hidden"
backgroundColor={presetSrc ? 'transparent' : '#f8fafc'}
backgroundColor={presetSrc ? 'transparent' : surfaceMuted}
style={
presetSrc
? {
@@ -575,27 +583,27 @@ function PreviewStep({
backgroundSize: 'cover',
backgroundPosition: 'center',
}
: { background: resolvedBg ?? '#f8fafc' }
: { background: resolvedBg ?? surfaceMuted }
}
padding="$3"
gap="$3"
>
<Text fontSize="$lg" fontWeight="800" color={layout?.preview?.text ?? '#0f172a'}>
<Text fontSize="$lg" fontWeight="800" color={previewText}>
{textFields.headline || layout?.name || t('events.qr.previewHeadline', 'Event QR')}
</Text>
{textFields.subtitle ? (
<Text fontSize="$sm" color={layout?.preview?.text ?? '#1f2937'}>
<Text fontSize="$sm" color={previewBody}>
{textFields.subtitle}
</Text>
) : null}
{textFields.description ? (
<Text fontSize="$sm" color={layout?.preview?.text ?? '#1f2937'}>
<Text fontSize="$sm" color={previewBody}>
{textFields.description}
</Text>
) : null}
<YStack space="$1">
{textFields.instructions.filter((i) => i.trim().length > 0).map((item, idx) => (
<Text key={idx} fontSize="$xs" color={layout?.preview?.text ?? '#1f2937'}>
<Text key={idx} fontSize="$xs" color={previewBody}>
{item}
</Text>
))}
@@ -605,15 +613,15 @@ function PreviewStep({
<>
<img
src={qrImage}
alt="QR"
alt={t('events.qr.qrAlt', 'QR code')}
style={{ width: 140, height: 140, objectFit: 'contain' }}
/>
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
<Text fontSize="$xs" color={text} textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
{qrUrl}
</Text>
</>
) : (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
)}
@@ -639,13 +647,14 @@ function StyledInput({
placeholder?: string;
flex?: number;
}) {
const { border } = useAdminTheme();
return (
<input
style={{
width: '100%',
padding: '10px 12px',
borderRadius: 12,
border: '1px solid #e5e7eb',
border: `1px solid ${border}`,
fontSize: 14,
outline: 'none',
flex: flex ?? undefined,
@@ -666,13 +675,14 @@ function StyledTextarea({
onChangeText: (value: string) => void;
placeholder?: string;
}) {
const { border } = useAdminTheme();
return (
<textarea
style={{
width: '100%',
padding: '10px 12px',
borderRadius: 12,
border: '1px solid #e5e7eb',
border: `1px solid ${border}`,
fontSize: 14,
outline: 'none',
minHeight: 96,