überarbeitet: 300 neue tasks von gemini erzeugen lassen. dazu event types "Konfirmation" und "Schulabschluss" ergänzt. alles in Kollektionen gepackt und die seeder angepasst.
Des weiteren: neue Blogartikel und howto-Artikel von ChatGPT. Das QR-Code-Canvas funktioniert nun noch besser. die Layouts sehen besser aus. Der PaketSeeder enthält nun die Paddle Sandbox ProductIDs
This commit is contained in:
@@ -169,6 +169,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
const [copiedInviteId, setCopiedInviteId] = React.useState<number | null>(null);
|
||||
const [customizerSaving, setCustomizerSaving] = React.useState(false);
|
||||
const [customizerResetting, setCustomizerResetting] = React.useState(false);
|
||||
const [customizerDraft, setCustomizerDraft] = React.useState<QrLayoutCustomization | null>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tabParam = searchParams.get('tab');
|
||||
const initialTab = tabParam === 'export' || tabParam === 'links' ? (tabParam as TabKey) : 'layout';
|
||||
@@ -277,6 +278,10 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
});
|
||||
}, [state.invites]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCustomizerDraft(null);
|
||||
}, [selectedInviteId]);
|
||||
|
||||
const currentCustomization = React.useMemo(() => {
|
||||
if (!selectedInvite) {
|
||||
return null;
|
||||
@@ -286,12 +291,14 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
return raw && typeof raw === 'object' ? (raw as QrLayoutCustomization) : null;
|
||||
}, [selectedInvite]);
|
||||
|
||||
const effectiveCustomization = customizerDraft ?? currentCustomization;
|
||||
|
||||
const exportLayout = React.useMemo(() => {
|
||||
if (!selectedInvite || selectedInvite.layouts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetId = currentCustomization?.layout_id;
|
||||
const targetId = effectiveCustomization?.layout_id;
|
||||
if (targetId) {
|
||||
const match = selectedInvite.layouts.find((layout) => layout.id === targetId);
|
||||
if (match) {
|
||||
@@ -300,14 +307,14 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
}
|
||||
|
||||
return selectedInvite.layouts[0];
|
||||
}, [selectedInvite, currentCustomization?.layout_id]);
|
||||
}, [selectedInvite, effectiveCustomization?.layout_id]);
|
||||
|
||||
const exportPreview = React.useMemo(() => {
|
||||
if (!exportLayout || !selectedInvite) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const customization = currentCustomization ?? null;
|
||||
const customization = effectiveCustomization ?? null;
|
||||
const layoutPreview = exportLayout.preview ?? {};
|
||||
|
||||
const backgroundColor = normalizeHexColor(customization?.background_color ?? (layoutPreview.background as string | undefined)) ?? '#F8FAFC';
|
||||
@@ -370,26 +377,26 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
t('invites.export.tips.default3', 'Fotografiere den gedruckten QR-Code testweise, um die Lesbarkeit zu prüfen.'),
|
||||
],
|
||||
};
|
||||
}, [exportLayout, currentCustomization, selectedInvite, eventName, t]);
|
||||
}, [exportLayout, effectiveCustomization, selectedInvite, eventName, t]);
|
||||
|
||||
const exportElements = React.useMemo<LayoutElement[]>(() => {
|
||||
if (!exportLayout) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (currentCustomization?.mode === 'advanced' && Array.isArray(currentCustomization.elements) && currentCustomization.elements.length) {
|
||||
return normalizeElements(payloadToElements(currentCustomization.elements));
|
||||
if (effectiveCustomization?.mode === 'advanced' && Array.isArray(effectiveCustomization.elements) && effectiveCustomization.elements.length) {
|
||||
return normalizeElements(payloadToElements(effectiveCustomization.elements));
|
||||
}
|
||||
|
||||
const baseForm: QrLayoutCustomization = {
|
||||
...currentCustomization,
|
||||
...effectiveCustomization,
|
||||
layout_id: exportLayout.id,
|
||||
link_label: currentCustomization?.link_label ?? selectedInvite?.url ?? '',
|
||||
badge_label: currentCustomization?.badge_label ?? exportLayout.badge_label ?? undefined,
|
||||
instructions: ensureInstructionList(currentCustomization?.instructions, exportLayout.instructions ?? []),
|
||||
instructions_heading: currentCustomization?.instructions_heading ?? exportLayout.instructions_heading ?? undefined,
|
||||
logo_data_url: currentCustomization?.logo_data_url ?? undefined,
|
||||
logo_url: currentCustomization?.logo_url ?? undefined,
|
||||
link_label: effectiveCustomization?.link_label ?? selectedInvite?.url ?? '',
|
||||
badge_label: effectiveCustomization?.badge_label ?? exportLayout.badge_label ?? undefined,
|
||||
instructions: ensureInstructionList(effectiveCustomization?.instructions, exportLayout.instructions ?? []),
|
||||
instructions_heading: effectiveCustomization?.instructions_heading ?? exportLayout.instructions_heading ?? undefined,
|
||||
logo_data_url: effectiveCustomization?.logo_data_url ?? undefined,
|
||||
logo_url: effectiveCustomization?.logo_url ?? undefined,
|
||||
};
|
||||
|
||||
return buildDefaultElements(
|
||||
@@ -398,7 +405,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
eventName,
|
||||
exportLayout.preview?.qr_size_px ?? 480
|
||||
);
|
||||
}, [exportLayout, currentCustomization, selectedInvite?.url, eventName]);
|
||||
}, [exportLayout, effectiveCustomization, selectedInvite?.url, eventName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeTab !== 'export') {
|
||||
@@ -427,12 +434,23 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
[selectedInvite?.id, exportLayout?.id, exportPreview?.mode]
|
||||
);
|
||||
|
||||
const exportLogo = currentCustomization?.logo_data_url ?? currentCustomization?.logo_url ?? null;
|
||||
const exportLogo = effectiveCustomization?.logo_data_url ?? effectiveCustomization?.logo_url ?? null;
|
||||
const exportQr = selectedInvite?.qr_code_data_url ?? null;
|
||||
|
||||
const handlePreviewSelect = React.useCallback((_id: string | null) => undefined, []);
|
||||
const handlePreviewChange = React.useCallback((_id: string, _patch: Partial<LayoutElement>) => undefined, []);
|
||||
|
||||
const handleCustomizerDraftChange = React.useCallback((draft: QrLayoutCustomization | null) => {
|
||||
setCustomizerDraft((previous) => {
|
||||
const prevSignature = previous ? JSON.stringify(previous) : null;
|
||||
const nextSignature = draft ? JSON.stringify(draft) : null;
|
||||
if (prevSignature === nextSignature) {
|
||||
return previous;
|
||||
}
|
||||
return draft;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const inviteCountSummary = React.useMemo(() => {
|
||||
const active = state.invites.filter((invite) => invite.is_active && !invite.revoked_at).length;
|
||||
const total = state.invites.length;
|
||||
@@ -523,6 +541,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
...prev,
|
||||
invites: prev.invites.map((invite) => (invite.id === updated.id ? updated : invite)),
|
||||
}));
|
||||
setCustomizerDraft(null);
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({ ...prev, error: 'Anpassung konnte nicht gespeichert werden.' }));
|
||||
@@ -547,6 +566,7 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
...prev,
|
||||
invites: prev.invites.map((invite) => (invite.id === updated.id ? updated : invite)),
|
||||
}));
|
||||
setCustomizerDraft(null);
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({ ...prev, error: 'Anpassungen konnten nicht zurückgesetzt werden.' }));
|
||||
@@ -794,6 +814,8 @@ export default function EventInvitesPage(): React.ReactElement {
|
||||
onSave={handleSaveCustomization}
|
||||
onReset={handleResetCustomization}
|
||||
initialCustomization={currentCustomization}
|
||||
draftCustomization={customizerDraft}
|
||||
onDraftChange={handleCustomizerDraftChange}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user