qr code layouts im mobile admin perfektioniert.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', 'QR‑Code')}
|
||||
</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', 'QR‑Code')}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user