neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user