import React from 'react'; import { useTranslation } from 'react-i18next'; import { Download, Loader2, Plus, Printer, RotateCcw, Save, UploadCloud } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import type { EventQrInvite, EventQrInviteLayout } from '../../api'; import { authorizedFetch } from '../../auth/tokens'; export type QrLayoutCustomization = { layout_id?: string; headline?: string; subtitle?: string; description?: string; badge_label?: string; instructions_heading?: string; instructions?: string[]; link_heading?: string; link_label?: string; cta_label?: string; accent_color?: string; text_color?: string; background_color?: string; secondary_color?: string; badge_color?: string; background_gradient?: { angle?: number; stops?: string[] } | null; logo_data_url?: string | null; logo_url?: string | null; }; type InviteLayoutCustomizerPanelProps = { invite: EventQrInvite | null; eventName: string; saving: boolean; resetting: boolean; onSave: (customization: QrLayoutCustomization) => Promise; onReset: () => Promise; initialCustomization: QrLayoutCustomization | null; }; const MAX_INSTRUCTIONS = 5; export function InviteLayoutCustomizerPanel({ invite, eventName, saving, resetting, onSave, onReset, initialCustomization, }: InviteLayoutCustomizerPanelProps): JSX.Element { const { t } = useTranslation('management'); const inviteUrl = invite?.url ?? ''; const defaultInstructions = React.useMemo(() => { const value = t('tasks.customizer.defaults.instructions', { returnObjects: true }) as unknown; return Array.isArray(value) ? (value as string[]) : ['QR-Code scannen', 'Profil anlegen', 'Fotos teilen']; }, [t]); const [availableLayouts, setAvailableLayouts] = React.useState(invite?.layouts ?? []); const [layoutsLoading, setLayoutsLoading] = React.useState(false); const [layoutsError, setLayoutsError] = React.useState(null); const [selectedLayoutId, setSelectedLayoutId] = React.useState(initialCustomization?.layout_id ?? invite?.layouts?.[0]?.id); const [form, setForm] = React.useState({}); const [instructions, setInstructions] = React.useState([]); const [error, setError] = React.useState(null); const formRef = React.useRef(null); React.useEffect(() => { if (!invite) { setAvailableLayouts([]); setSelectedLayoutId(undefined); return; } const layouts = invite.layouts ?? []; setAvailableLayouts(layouts); setLayoutsError(null); setSelectedLayoutId((current) => { if (current && layouts.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && layouts.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return layouts[0]?.id; }); }, [invite?.id, initialCustomization?.layout_id]); React.useEffect(() => { let cancelled = false; async function loadLayouts(url: string) { try { setLayoutsLoading(true); setLayoutsError(null); const target = (() => { try { if (url.startsWith('http://') || url.startsWith('https://')) { const parsed = new URL(url); return parsed.pathname + parsed.search; } } catch (parseError) { console.warn('[Invites] Failed to parse layout URL', parseError); } return url; })(); console.debug('[Invites] Fetching layouts', target); const response = await authorizedFetch(target, { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { console.error('[Invites] Layout request failed', response.status, response.statusText); throw new Error(`Failed with status ${response.status}`); } const json = await response.json(); const items = Array.isArray(json?.data) ? (json.data as EventQrInviteLayout[]) : []; console.debug('[Invites] Layout response items', items); if (!cancelled) { setAvailableLayouts(items); setSelectedLayoutId((current) => { if (current && items.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && items.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return items[0]?.id; }); } } catch (err) { if (!cancelled) { console.error('[Invites] Failed to load layouts', err); setLayoutsError(t('invites.customizer.loadingError', 'Layouts konnten nicht geladen werden.')); } } finally { if (!cancelled) { setLayoutsLoading(false); } } } if (!invite || availableLayouts.length > 0 || !invite.layouts_url) { return () => { cancelled = true; }; } void loadLayouts(invite.layouts_url); return () => { cancelled = true; }; }, [invite, availableLayouts.length, initialCustomization?.layout_id, t]); React.useEffect(() => { if (!availableLayouts.length) { return; } setSelectedLayoutId((current) => { if (current && availableLayouts.some((layout) => layout.id === current)) { return current; } if (initialCustomization?.layout_id && availableLayouts.some((layout) => layout.id === initialCustomization.layout_id)) { return initialCustomization.layout_id; } return availableLayouts[0].id; }); }, [availableLayouts, initialCustomization?.layout_id]); const activeLayout = React.useMemo(() => { if (!availableLayouts.length) { return null; } if (selectedLayoutId) { const match = availableLayouts.find((layout) => layout.id === selectedLayoutId); if (match) { return match; } } return availableLayouts[0]; }, [availableLayouts, selectedLayoutId]); React.useEffect(() => { if (!invite || !activeLayout) { setForm({}); setInstructions([]); return; } const baseInstructions = Array.isArray(initialCustomization?.instructions) && initialCustomization.instructions?.length ? [...(initialCustomization.instructions as string[])] : [...defaultInstructions]; setInstructions(baseInstructions); setForm({ layout_id: activeLayout.id, headline: initialCustomization?.headline ?? eventName, subtitle: initialCustomization?.subtitle ?? activeLayout.subtitle ?? '', description: initialCustomization?.description ?? activeLayout.description ?? '', badge_label: initialCustomization?.badge_label ?? t('tasks.customizer.defaults.badgeLabel'), instructions_heading: initialCustomization?.instructions_heading ?? t('tasks.customizer.defaults.instructionsHeading'), link_heading: initialCustomization?.link_heading ?? t('tasks.customizer.defaults.linkHeading'), link_label: initialCustomization?.link_label ?? inviteUrl, cta_label: initialCustomization?.cta_label ?? t('tasks.customizer.defaults.ctaLabel'), accent_color: initialCustomization?.accent_color ?? activeLayout.preview?.accent ?? '#6366F1', text_color: initialCustomization?.text_color ?? activeLayout.preview?.text ?? '#111827', background_color: initialCustomization?.background_color ?? activeLayout.preview?.background ?? '#FFFFFF', secondary_color: initialCustomization?.secondary_color ?? 'rgba(15,23,42,0.08)', badge_color: initialCustomization?.badge_color ?? activeLayout.preview?.accent ?? '#2563EB', background_gradient: initialCustomization?.background_gradient ?? activeLayout.preview?.background_gradient ?? null, logo_data_url: initialCustomization?.logo_data_url ?? initialCustomization?.logo_url ?? null, }); setError(null); }, [invite?.id, activeLayout?.id, defaultInstructions, initialCustomization, eventName, inviteUrl, t]); const effectiveInstructions = instructions.filter((entry) => entry.trim().length > 0); function updateForm(key: T, value: QrLayoutCustomization[T]) { setForm((prev) => ({ ...prev, [key]: value })); } function handleLayoutSelect(layout: EventQrInviteLayout) { setSelectedLayoutId(layout.id); updateForm('layout_id', layout.id); setForm((prev) => ({ ...prev, accent_color: prev.accent_color ?? layout.preview?.accent ?? '#6366F1', text_color: prev.text_color ?? layout.preview?.text ?? '#111827', background_color: prev.background_color ?? layout.preview?.background ?? '#FFFFFF', background_gradient: prev.background_gradient ?? layout.preview?.background_gradient ?? null, })); } function handleInstructionChange(index: number, value: string) { setInstructions((prev) => { const next = [...prev]; next[index] = value; return next; }); } function handleAddInstruction() { setInstructions((prev) => (prev.length < MAX_INSTRUCTIONS ? [...prev, ''] : prev)); } function handleRemoveInstruction(index: number) { setInstructions((prev) => prev.filter((_, idx) => idx !== index)); } function handleLogoUpload(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) { return; } if (file.size > 1024 * 1024) { setError(t('tasks.customizer.errors.logoTooLarge', 'Das Logo darf maximal 1 MB groß sein.')); return; } const reader = new FileReader(); reader.onload = () => { updateForm('logo_data_url', typeof reader.result === 'string' ? reader.result : null); setError(null); }; reader.readAsDataURL(file); } function handleLogoRemove() { updateForm('logo_data_url', null); } async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!invite || !activeLayout) { return; } const payload: QrLayoutCustomization = { ...form, layout_id: activeLayout.id, instructions: effectiveInstructions, }; await onSave(payload); } async function handleResetClick() { await onReset(); } function handleDownload(format: string, url: string) { const link = document.createElement('a'); link.href = url; link.download = `${invite?.token ?? 'invite'}-${format.toLowerCase()}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function handlePrint(preferredUrl?: string | null) { const url = preferredUrl ?? activeLayout?.download_urls?.pdf ?? activeLayout?.download_urls?.a4 ?? null; if (!url) { setError(t('invites.labels.noPrintSource', 'Keine druckbare Version verfügbar.')); return; } const printWindow = window.open(url, '_blank', 'noopener,noreferrer'); printWindow?.focus(); } const previewStyles = React.useMemo(() => { const gradient = form.background_gradient ?? activeLayout?.preview?.background_gradient ?? null; if (gradient?.stops && gradient.stops.length > 0) { const angle = gradient.angle ?? 180; const stops = gradient.stops.join(', '); return { backgroundImage: `linear-gradient(${angle}deg, ${stops})` }; } return { backgroundColor: form.background_color ?? activeLayout?.preview?.background ?? '#F8FAFC' }; }, [form.background_color, form.background_gradient, activeLayout]); if (!invite) { return ( ); } if (!availableLayouts.length) { if (layoutsLoading) { return ( ); } return ( ); } if (!activeLayout) { return ( ); } return (

{t('invites.customizer.heading', 'Layout anpassen')}

{t('invites.customizer.copy', 'Verleihe der Einladung euren Ton: Texte, Farben und Logo lassen sich live bearbeiten.')}

{error ? (
{error}
) : null}

{t('invites.customizer.sections.layouts', 'Layouts')}

{t('invites.customizer.sections.layoutsHint', 'Wähle eine Vorlage als Basis aus. Du kannst jederzeit wechseln.')}

{availableLayouts.map((layout) => ( ))}

{t('invites.customizer.sections.text', 'Texte')}

updateForm('headline', event.target.value)} />
updateForm('subtitle', event.target.value)} />