import { useEffect, useMemo, useState } from 'react'; /** * Live-Vorschau neben dem Formular. * * Die Vorschau ist absichtlich kontingentiert: Jede Generierung kostet einen * Aufruf der Scriptalizer-API (Tageslimit). Der Zähler ist daher Teil der * Bedienung und kein Beiwerk. * * Schriftstück und zugehöriger Umschlag gehören zusammen und werden gemeinsam * gezeigt (Schriftstück oben, Umschlag unten). Die Pfeile blättern je Empfänger. */ export type PreviewDoc = { art: 'brief' | 'umschlag'; svg: string }; // Vorschau-Optik „Kugelschreiber" – IDENTISCH zu Directus (neuauftrag): blaue // Tinte, etwas dicker (1.4) + dezenter Wobble (feTurbulence/Displacement) für die // typische Kuli-Linie. non-scaling-stroke bleibt (Glyphen skalieren per transform), // weißer Grund. Nur Anzeige/Download – die an die Produktion gehenden SVGs bleiben // schwarz. Inline-SVGs brauchen eine EINDEUTIGE Filter-ID pro Dokument. let inkSeq = 0; const inkify = (svg: string): string => { if (typeof svg !== 'string') return svg; const id = `skink${inkSeq++}`; return svg .replace(/stroke="#0{3,6}"/gi, 'stroke="#1a2a6c" stroke-opacity="0.95"') .replace(/stroke-width="0\.6"/gi, 'stroke-width="1.4"') .replace(/(]*>)/i, `$1`) .replace(/(]*fill="#FFFFFF"[^>]*\/>)/i, `$1`) .replace(/<\/svg>\s*$/i, ''); }; /** Ein Empfänger-Dokument: Schriftstück + optional zugehöriger Umschlag. */ type Gruppe = { brief?: PreviewDoc; umschlag?: PreviewDoc }; type Props = { titel: string; format: string; docs: PreviewDoc[]; gesamtDokumente: number; verbleibend: number; maximal: number; laedt: boolean; fehler: string | null; onGenerate: () => void; bereit: boolean; }; /** Flache [brief, umschlag, brief, …]-Liste zu Empfänger-Gruppen bündeln. */ function gruppiere(docs: PreviewDoc[]): Gruppe[] { const gruppen: Gruppe[] = []; for (const d of docs) { if (d.art === 'umschlag') { const letzte = gruppen[gruppen.length - 1]; if (letzte && letzte.brief && !letzte.umschlag) letzte.umschlag = d; else gruppen.push({ umschlag: d }); } else { gruppen.push({ brief: d }); } } return gruppen; } export default function PreviewPanel({ titel, format, docs, gesamtDokumente, verbleibend, maximal, laedt, fehler, onGenerate, bereit, }: Props) { const [index, setIndex] = useState(0); const [zoomSvg, setZoomSvg] = useState(null); // Die Zoom-Fläche wird auf den Inhaltsbereich begrenzt (unterhalb der // Preiszeile, rechts der Seitenleiste), statt das ganze Fenster zu belegen. const [zoomBox, setZoomBox] = useState({}); const gruppen = useMemo(() => gruppiere(docs), [docs]); function messeBox(): React.CSSProperties { if (typeof window === 'undefined' || window.innerWidth <= 900) return {}; const content = document.querySelector('.sk-content'); if (!content) return {}; const c = content.getBoundingClientRect(); const bar = document.querySelector('.sk-pricebar'); const top = Math.max(c.top, bar ? bar.getBoundingClientRect().bottom : c.top); return { top: `${top}px`, left: `${c.left}px`, width: `${c.width}px`, height: `${Math.max(0, c.bottom - top)}px`, right: 'auto', bottom: 'auto', }; } function openZoom(svg: string) { setZoomBox(messeBox()); setZoomSvg(svg); } /** Seitenverhältnis (Breite/Höhe) aus der viewBox – sperrt die Lightbox-Abmessungen. */ function seitenverhaeltnis(svg: string): string { const m = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/); return m ? `${m[1]} / ${m[2]}` : '210 / 297'; } /** Musterbild der großen Vorschau als PNG herunterladen (SVG → Canvas). */ function bildSpeichern() { if (!zoomSvg) return; const m = zoomSvg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/); const w = Math.round(m ? Number(m[1]) : 794); const h = Math.round(m ? Number(m[2]) : 1123); // Natürliche Größe (nicht 2×): non-scaling-stroke wirkt konstant in // Gerätepixeln – bei 2× würde die Linie relativ dünn. So sieht das PNG aus // wie die Großansicht. Gespeicherte Vorschau in Kuli-Optik (wie Anzeige); // die an die Produktion gehenden SVGs bleiben schwarz. const scale = 1; const inkSvg = inkify(zoomSvg); const url = URL.createObjectURL(new Blob([inkSvg], { type: 'image/svg+xml;charset=utf-8' })); const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = w * scale; canvas.height = h * scale; const ctx = canvas.getContext('2d'); if (ctx) { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob((png) => { if (png) { const a = document.createElement('a'); a.href = URL.createObjectURL(png); a.download = 'skrift-musterbild.png'; a.click(); URL.revokeObjectURL(a.href); } URL.revokeObjectURL(url); }, 'image/png'); } else { URL.revokeObjectURL(url); } }; img.onerror = () => URL.revokeObjectURL(url); img.src = url; } // Nach einer neuen Generierung wieder vorn beginnen. useEffect(() => { setIndex(0); }, [docs]); // Vergrößerte Ansicht: Escape schließt, Resize misst die Fläche neu. useEffect(() => { if (!zoomSvg) return; const zu = (e: KeyboardEvent) => e.key === 'Escape' && setZoomSvg(null); const neu = () => setZoomBox(messeBox()); window.addEventListener('keydown', zu); window.addEventListener('resize', neu); return () => { window.removeEventListener('keydown', zu); window.removeEventListener('resize', neu); }; }, [zoomSvg]); const aktuell = gruppen[index] ?? null; const anzahl = gruppen.length || gesamtDokumente; /** Ein einzelnes Dokument (Schriftstück oder Umschlag) mit Beschriftung. */ const DocBox = ({ label, svg }: { label: string; svg: string }) => (
openZoom(svg)} role="button" tabIndex={0} title="Zum Vergrößern klicken" onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') openZoom(svg); }} >
{label}
{/* Das SVG stammt aus unserer eigenen Generierung, nicht aus Nutzereingaben. */}
); return ( ); }