ü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:
Codex Agent
2025-11-02 21:52:38 +01:00
parent 792b5dfe8b
commit 073b51e2d5
33 changed files with 3013 additions and 961 deletions

View File

@@ -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>