qr code layouts im mobile admin perfektioniert.

This commit is contained in:
Codex Agent
2025-12-14 22:14:30 +01:00
parent c8b149d887
commit a8b6e5d9c4
7 changed files with 369 additions and 230 deletions

View File

@@ -1847,6 +1847,7 @@
"title": "QR-Code & Druck-Layouts",
"heroTitle": "Einlass-QR-Code",
"description": "Scannen, um zur Gäste-App zu gelangen.",
"bottomNote": "Unterer Hinweistext",
"missing": "Kein QR-Link vorhanden",
"download": "Download",
"downloadStarted": "Download gestartet",
@@ -1859,6 +1860,13 @@
"createLink": "Neuen QR-Link erstellen",
"created": "Neuer QR-Link erstellt",
"createFailed": "Link konnte nicht erstellt werden.",
"headline": "Headline",
"subtitle": "Untertitel",
"align": "Ausrichtung",
"lineHeight": "Zeilenhöhe",
"fontFamily": "Schriftfamilie",
"exportPdf": "PDF exportieren",
"exportPng": "PNG exportieren",
"format": {
"poster": "A4 Poster",
"posterSubtitle": "Hochformat für Aushänge",

View File

@@ -1868,6 +1868,7 @@
"title": "QR Code & Print Layouts",
"heroTitle": "Entrance QR Code",
"description": "Scan to access the event guest app.",
"bottomNote": "Bottom note text",
"missing": "No QR link available",
"download": "Download",
"downloadStarted": "Download started",
@@ -1880,6 +1881,13 @@
"createLink": "Create new QR link",
"created": "New QR link created",
"createFailed": "Could not create link.",
"headline": "Headline",
"subtitle": "Subtitle",
"align": "Align",
"lineHeight": "Line height",
"fontFamily": "Font family",
"exportPdf": "Export PDF",
"exportPng": "Export PNG",
"format": {
"poster": "A4 Poster",
"posterSubtitle": "Portrait for posters",

View File

@@ -1,11 +1,14 @@
import React from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowLeft, RefreshCcw, Plus } from 'lucide-react';
import { ArrowLeft, RefreshCcw, Plus, ChevronDown } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Input, TextArea } from 'tamagui';
import { Accordion } from '@tamagui/accordion';
import { HexColorPicker } from 'react-colorful';
import { Portal } from '@tamagui/portal';
import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
import {
@@ -394,11 +397,11 @@ function renderEventName(name: TenantEvent['name'] | null | undefined): string |
function getDefaultSlots(): Record<string, SlotDefinition> {
return {
headline: { x: 0.08, y: 0.15, w: 0.84, fontSize: 50, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
headline: { x: 0.08, y: 0.12, w: 0.84, fontSize: 90, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: 'Montserrat', align: 'center' },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 13, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
instructions: { x: 0.1, y: 0.34, w: 0.8, fontSize: 14, fontFamily: 'Lora', lineHeight: 1.35 },
qr: { x: 0.3, y: 0.3, w: 0.28 },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 26, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
instructions: { x: 0.1, y: 0.7, w: 0.8, fontSize: 24, fontFamily: 'Lora', lineHeight: 1.5 },
qr: { x: 0.39, y: 0.37, w: 0.27 },
};
}
@@ -521,26 +524,28 @@ function buildFabricOptions({
const widthPx = localWidth * scaleX;
const heightPx = localHeight * scaleY;
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localWidth / 2;
const baseCenterY = slot.y * panelHeight + localHeight / 2;
let targetCenterX = baseCenterX;
let targetCenterY = baseCenterY;
// Default: top-left positioning for non-rotated panels
let targetX = opts.panelOffset + slot.x * panelWidth;
let targetY = slot.y * panelHeight;
let rotation = 0;
// Foldable layouts rotate each panel around its own center; Fabric expects center-based coords when rotated.
if (isFoldable) {
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localWidth / 2;
const baseCenterY = slot.y * panelHeight + localHeight / 2;
const panelCenterX = opts.panelOffset + panelWidth / 2;
const panelCenterY = panelHeight / 2;
rotation = opts.panelOffset === 0 ? 90 : -90;
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
targetCenterX = rotated.x;
targetCenterY = rotated.y;
targetX = rotated.x;
targetY = rotated.y;
}
elements.push({
id: `${type}-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
type,
x: targetCenterX * scaleX,
y: targetCenterY * scaleY,
x: targetX * scaleX,
y: targetY * scaleY,
width: widthPx,
height: heightPx,
fontSize: slot.fontSize ?? (type === 'headline' ? 30 : type === 'subtitle' ? 18 : 16),
@@ -557,26 +562,26 @@ function buildFabricOptions({
if (!qrUrl) return;
const localSize = slot.w * panelWidth;
const sizePx = localSize * scaleX;
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localSize / 2;
const baseCenterY = slot.y * panelHeight + localSize / 2;
let targetCenterX = baseCenterX;
let targetCenterY = baseCenterY;
let targetX = opts.panelOffset + slot.x * panelWidth;
let targetY = slot.y * panelHeight;
let rotation = 0;
if (isFoldable) {
const baseCenterX = opts.panelOffset + slot.x * panelWidth + localSize / 2;
const baseCenterY = slot.y * panelHeight + localSize / 2;
const panelCenterX = opts.panelOffset + panelWidth / 2;
const panelCenterY = panelHeight / 2;
rotation = opts.panelOffset === 0 ? 90 : -90;
const rotated = rotatePoint(panelCenterX, panelCenterY, baseCenterX, baseCenterY, rotation);
targetCenterX = rotated.x;
targetCenterY = rotated.y;
targetX = rotated.x;
targetY = rotated.y;
}
elements.push({
id: `qr-${opts.panelOffset}-${opts.mirrored ? 'm' : 'n'}`,
type: 'qr',
x: targetCenterX * scaleX,
y: targetCenterY * scaleY,
x: targetX * scaleX,
y: targetY * scaleY,
width: sizePx,
height: sizePx,
rotation,
@@ -858,7 +863,7 @@ function TextStep({
size="$4"
/>
<TextArea
placeholder={t('events.qr.description', 'Beschreibung')}
placeholder={t('events.qr.bottomNote', 'Unterer Hinweistext')}
value={textFields.description}
onChangeText={(val) => updateField('description', val)}
size="$4"
@@ -1082,7 +1087,7 @@ function PreviewStep({
<LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />
<XStack space="$2">
<XStack space="$2" width="100%" flexWrap="wrap">
<CTAButton
label={t('events.qr.exportPdf', 'Export PDF')}
onPress={async () => {
@@ -1096,6 +1101,7 @@ function PreviewStep({
console.error(err);
}
}}
style={{ flex: 1, minWidth: 0 }}
/>
<CTAButton
label={t('events.qr.exportPng', 'Export PNG')}
@@ -1109,6 +1115,7 @@ function PreviewStep({
console.error(err);
}
}}
style={{ flex: 1, minWidth: 0 }}
/>
</XStack>
</YStack>
@@ -1129,6 +1136,7 @@ function LayoutControls({
qrUrl: string | null;
}) {
const { t } = useTranslation('management');
const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null);
const fontOptions = React.useMemo(() => {
const preset = ['Playfair Display', 'Lora', 'Montserrat', 'Inter', 'Roboto'];
const tenant = tenantFonts.map((font) => font.family);
@@ -1169,6 +1177,8 @@ function LayoutControls({
}) => {
const dec = () => onChange(clampValue(value - step, min, max));
const inc = () => onChange(clampValue(value + step, min, max));
const decimals = step % 1 === 0 ? 0 : Math.min(4, `${step}`.split('.')[1]?.length ?? 1);
const formatValue = (val: number) => val.toFixed(decimals);
return (
<XStack space="$1" alignItems="center">
<Pressable onPress={dec}>
@@ -1183,8 +1193,13 @@ function LayoutControls({
inputMode="decimal"
min={min}
max={max}
value={value.toFixed(step < 1 ? 1 : 0)}
onChange={(event) => onChange(Number(event.target.value))}
value={formatValue(value)}
onChange={(event) => {
const next = Number(event.target.value);
if (Number.isFinite(next)) {
onChange(next);
}
}}
style={numberInputStyle}
/>
<Pressable onPress={inc}>
@@ -1222,140 +1237,221 @@ function LayoutControls({
onUpdateSlot(slotKey, { fontFamily: value || undefined });
};
return (
<YStack key={slotKey} space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
{label}
</Text>
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
</Text>
<XStack space="$1" alignItems="center">
<Pressable onPress={() => onPercentChange('x')(((override.x ?? slot.x) * 100) - 0.5)}>
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
<Text fontSize="$md" fontWeight="800" color="#111827">
</Text>
</XStack>
</Pressable>
<input
type="text"
inputMode="decimal"
step="0.5"
min="0"
max="100"
value={((override.x ?? slot.x) * 100).toFixed(1)}
onChange={(event) => onPercentChange('x')(Number(event.target.value))}
style={numberInputStyle}
/>
<Pressable onPress={() => onPercentChange('x')(((override.x ?? slot.x) * 100) + 0.5)}>
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
<Text fontSize="$md" fontWeight="800" color="#111827">
+
</Text>
</XStack>
</Pressable>
</XStack>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
</Text>
<StepperInput
value={(override.y ?? slot.y) * 100}
min={0}
max={100}
step={0.5}
onChange={(val) => onPercentChange('y')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Breite (%)
</Text>
<StepperInput
value={(override.w ?? slot.w) * 100}
min={10}
max={100}
step={0.5}
onChange={(val) => onPercentChange('w')(val)}
/>
</YStack>
</XStack>
const currentX = override.x ?? slot.x;
const currentY = override.y ?? slot.y;
const currentW = override.w ?? slot.w;
const centerX = () => {
const centered = clampValue((1 - currentW) / 2, 0, 1);
onUpdateSlot(slotKey, { x: centered });
};
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Font Size (px)
return (
<Accordion.Item value={slotKey} key={slotKey}>
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
<XStack justifyContent="space-between" alignItems="center" flex={1}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
{label}
</Text>
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Font Family
</Text>
<select
value={override.fontFamily ?? slot.fontFamily ?? ''}
onChange={(event) => onFontFamilyChange(event.target.value)}
style={selectStyle}
>
<option value="">{t('events.qr.defaultFont', 'Standard')}</option>
{fontOptions.map((family) => (
<option key={family} value={family}>
{family}
</option>
))}
</select>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<XStack space="$2" alignItems="center">
<input
type="color"
value={override.color ?? slot.color ?? '#0f172a'}
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
style={{ width: 48, height: 36, border: '1px solid #e5e7eb', borderRadius: 8, padding: 0, background: '#fff' }}
/>
<input
type="text"
value={override.color ?? slot.color ?? ''}
placeholder="#0f172a"
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
style={numberInputStyle}
/>
<ChevronDown size={16} color="#6b7280" />
</XStack>
</Accordion.Trigger>
<Accordion.Content paddingTop="$2">
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
</Text>
<XStack space="$2" alignItems="center">
<StepperInput
value={currentX * 100}
min={0}
max={100}
step={0.5}
onChange={(val) => onPercentChange('x')(val)}
/>
<Pressable onPress={centerX}>
<XStack
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
>
<Text fontSize="$xs" color="#111827">
{t('common.center', 'Zentrieren')}
</Text>
</XStack>
</Pressable>
</XStack>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
</Text>
<StepperInput
value={currentY * 100}
min={0}
max={100}
step={0.5}
onChange={(val) => onPercentChange('y')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Breite (%)
</Text>
<StepperInput
value={currentW * 100}
min={10}
max={100}
step={0.5}
onChange={(val) => onPercentChange('w')(val)}
/>
</YStack>
</XStack>
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Font Size (px)
</Text>
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Font Family
</Text>
<select
value={override.fontFamily ?? slot.fontFamily ?? ''}
onChange={(event) => onFontFamilyChange(event.target.value)}
style={selectStyle}
>
<option value="">{t('events.qr.defaultFont', 'Standard')}</option>
{fontOptions.map((family) => (
<option key={family} value={family}>
{family}
</option>
))}
</select>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<YStack space="$2">
<XStack space="$2" alignItems="center">
<Pressable onPress={() => setOpenColorSlot(openColorSlot === slotKey ? null : slotKey)}>
<XStack
width={48}
height={36}
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor={override.color ?? slot.color ?? '#0f172a'}
/>
</Pressable>
<input
type="text"
value={override.color ?? slot.color ?? ''}
placeholder="#0f172a"
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
style={{ ...numberInputStyle, width: 110 }}
/>
</XStack>
{openColorSlot === slotKey ? (
<Portal>
<YStack
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
alignItems="center"
justifyContent="center"
backgroundColor="rgba(0,0,0,0.35)"
zIndex={9999}
onPress={() => setOpenColorSlot(null)}
>
<YStack
padding="$3"
borderRadius={16}
backgroundColor="#fff"
borderWidth={1}
borderColor="#e5e7eb"
elevation="$4"
shadowColor="rgba(0,0,0,0.08)"
shadowOffset={{ width: 0, height: 4 }}
shadowOpacity={0.2}
shadowRadius={12}
gap="$2"
>
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<HexColorPicker
color={override.color ?? slot.color ?? '#0f172a'}
onChange={(val) => onUpdateSlot(slotKey, { color: val })}
style={{ width: 240, height: 200 }}
/>
<XStack space="$2" justifyContent="flex-end">
<Pressable onPress={() => setOpenColorSlot(null)}>
<XStack
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
>
<Text fontSize="$xs" color="#111827">
{t('common.close', 'Schließen')}
</Text>
</XStack>
</Pressable>
</XStack>
</YStack>
</YStack>
</Portal>
) : null}
</YStack>
</YStack>
</XStack>
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Align
</Text>
<select
value={override.align ?? slot.align ?? 'left'}
onChange={(event) => onUpdateSlot(slotKey, { align: event.target.value as SlotDefinition['align'] })}
style={selectStyle}
>
<option value="left">{t('common.left', 'Links')}</option>
<option value="center">{t('common.center', 'Zentriert')}</option>
<option value="right">{t('common.right', 'Rechts')}</option>
</select>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Line Height
</Text>
<StepperInput
value={override.lineHeight ?? slot.lineHeight ?? 1.35}
min={0.8}
max={3}
step={0.05}
onChange={(val) => onLineHeightChange(val)}
/>
</YStack>
<YStack flex={1} />
<YStack flex={1} />
</XStack>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Line Height
</Text>
<StepperInput value={override.lineHeight ?? slot.lineHeight ?? 1.35} min={0.8} max={3} step={0.05} onChange={(val) => onLineHeightChange(val)} />
</YStack>
</XStack>
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Align
</Text>
<select
value={override.align ?? slot.align ?? 'left'}
onChange={(event) => onUpdateSlot(slotKey, { align: event.target.value as SlotDefinition['align'] })}
style={selectStyle}
>
<option value="left">{t('common.left', 'Links')}</option>
<option value="center">{t('common.center', 'Zentriert')}</option>
<option value="right">{t('common.right', 'Rechts')}</option>
</select>
</YStack>
<YStack flex={1} />
<YStack flex={1} />
<YStack flex={1} />
</XStack>
</YStack>
</Accordion.Content>
</Accordion.Item>
);
};
@@ -1367,74 +1463,79 @@ function LayoutControls({
onUpdateSlot('qr', { [field]: pct / 100 });
};
const accordionDefaults = ['headline'];
return (
<YStack space="$3" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.layoutControls', 'Layout & Schrift')}
</Text>
<YStack space="$2">
<Accordion type="multiple" defaultValue={accordionDefaults}>
{renderTextSlot('headline', t('events.qr.headline', 'Headline'))}
{renderTextSlot('subtitle', t('events.qr.subtitle', 'Subtitle'))}
{renderTextSlot('description', t('events.qr.description', 'Beschreibung'))}
{renderTextSlot('description', t('events.qr.bottomNote', 'Unterer Hinweistext'))}
{renderTextSlot('instructions', t('events.qr.instructions', 'Anleitung'))}
</YStack>
{qrSlot ? (
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.qr_code_label', 'QRCode')}
</Text>
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
</Text>
<input
type="number"
step="0.5"
min="0"
max="100"
value={((qrOverride.x ?? qrSlot.x) * 100).toFixed(1)}
onChange={(event) => onQrPercentChange('x')(Number(event.target.value))}
style={numberInputStyle}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
</Text>
<input
type="number"
step="0.5"
min="0"
max="100"
value={((qrOverride.y ?? qrSlot.y) * 100).toFixed(1)}
onChange={(event) => onQrPercentChange('y')(Number(event.target.value))}
style={numberInputStyle}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Größe (%)
</Text>
<input
type="number"
step="0.5"
min="10"
max="100"
value={((qrOverride.w ?? qrSlot.w) * 100).toFixed(1)}
onChange={(event) => onQrPercentChange('w')(Number(event.target.value))}
style={numberInputStyle}
/>
</YStack>
</XStack>
{!qrUrl ? (
<Text fontSize="$xs" color="#b91c1c">
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
) : null}
</YStack>
) : null}
{qrSlot ? (
<Accordion.Item value="qr">
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
<XStack justifyContent="space-between" alignItems="center" flex={1}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.qr_code_label', 'QRCode')}
</Text>
<ChevronDown size={16} color="#6b7280" />
</XStack>
</Accordion.Trigger>
<Accordion.Content paddingTop="$2">
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
</Text>
<StepperInput
value={(qrOverride.x ?? qrSlot.x) * 100}
min={0}
max={100}
step={0.5}
onChange={(val) => onQrPercentChange('x')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
</Text>
<StepperInput
value={(qrOverride.y ?? qrSlot.y) * 100}
min={0}
max={100}
step={0.5}
onChange={(val) => onQrPercentChange('y')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Größe (%)
</Text>
<StepperInput
value={(qrOverride.w ?? qrSlot.w) * 100}
min={10}
max={100}
step={0.5}
onChange={(val) => onQrPercentChange('w')(val)}
/>
</YStack>
</XStack>
{!qrUrl ? (
<Text fontSize="$xs" color="#b91c1c">
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
) : null}
</YStack>
</Accordion.Content>
</Accordion.Item>
) : null}
</Accordion>
</YStack>
);
}

View File

@@ -103,18 +103,23 @@ export default function MobileQrPrintPage() {
justifyContent="center"
overflow="hidden"
>
{qrUrl ? (
{qrImage ? (
<img
src={qrImage || `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrUrl)}`}
src={qrImage}
alt="QR"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
) : (
<Text color="#9ca3af" fontSize="$sm">
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
)}
</YStack>
{qrUrl ? (
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
{qrUrl}
</Text>
) : null}
<Text fontSize="$xs" color="#6b7280">
{t('events.qr.description', 'Scan to access the event guest app.')}
</Text>
@@ -123,12 +128,12 @@ export default function MobileQrPrintPage() {
label={t('events.qr.download', 'Download')}
fullWidth={false}
onPress={() => {
if (!qrUrl) {
if (!qrImage) {
toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden'));
return;
}
const link = document.createElement('a');
link.href = qrImage || `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrUrl)}`;
link.href = qrImage;
link.download = 'event-qr.png';
document.body.appendChild(link);
link.click();
@@ -577,12 +582,17 @@ function PreviewStep({
))}
</YStack>
<YStack alignItems="center" justifyContent="center">
{qrUrl ? (
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(qrUrl)}`}
alt="QR"
style={{ width: 140, height: 140, objectFit: 'contain' }}
/>
{qrImage ? (
<>
<img
src={qrImage}
alt="QR"
style={{ width: 140, height: 140, objectFit: 'contain' }}
/>
<Text fontSize="$xs" color="#334155" textAlign="center" marginTop="$2" style={{ wordBreak: 'break-word' }}>
{qrUrl}
</Text>
</>
) : (
<Text fontSize="$xs" color="#6b7280">
{t('events.qr.missing', 'Kein QR-Link vorhanden')}

View File

@@ -705,7 +705,7 @@ export async function createFabricObject({
width: element.width,
height: element.height,
fontSize: element.fontSize ?? 36,
fill: textColor,
fill: element.fill ?? textColor,
fontFamily: element.fontFamily ?? 'Lora',
textAlign: mapTextAlign(element.align),
lineHeight: element.lineHeight ?? 1.5,
@@ -718,7 +718,7 @@ export async function createFabricObject({
width: element.width,
height: element.height,
fontSize: element.fontSize ?? 24,
fill: accentColor,
fill: element.fill ?? accentColor,
fontFamily: element.fontFamily ?? 'Montserrat',
underline: true,
textAlign: mapTextAlign(element.align),
@@ -732,7 +732,7 @@ export async function createFabricObject({
text: element.content ?? '',
width: element.width,
height: element.height,
backgroundColor: badgeColor,
backgroundColor: element.fill ?? badgeColor,
textColor: '#ffffff',
fontSize: element.fontSize ?? 22,
lineHeight: element.lineHeight ?? 1.5,
@@ -744,7 +744,7 @@ export async function createFabricObject({
text: element.content ?? '',
width: element.width,
height: element.height,
backgroundColor: accentColor,
backgroundColor: element.fill ?? accentColor,
textColor: '#ffffff',
fontSize: element.fontSize ?? 24,
cornerRadius: 18,