import { useEffect, useMemo, useState } from 'react'; import { fetchCheckFormats, fetchSettings, generatePreview } from '../lib/api'; import type { Format, PublicSettings, PreviewDoc, PreviewOverflow } from '../lib/api'; /** Schriftarten wie im Konfigurator/manuellen Druck. */ const SCHRIFTEN = [ { key: 'tilda', name: 'Tilda' }, { key: 'alva', name: 'Alva' }, { key: 'ellie', name: 'Ellie' }, ]; /** Kugelschreiber-Optik (identisch zum PreviewPanel): blaue Tinte, dezenter Wobble. */ 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, ''); }; /** Musterbild als PNG herunterladen (SVG → Canvas), wie im PreviewPanel. */ function bildSpeichern(svg: string) { const m = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/); const w = Math.round(m ? Number(m[1]) : 794); const h = Math.round(m ? Number(m[2]) : 1123); const inkSvg = inkify(svg); 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; canvas.height = h; 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-textmenge.png'; a.click(); URL.revokeObjectURL(a.href); } URL.revokeObjectURL(url); }, 'image/png'); } else { URL.revokeObjectURL(url); } }; img.onerror = () => URL.revokeObjectURL(url); img.src = url; } /** Stabile anonyme Kennung fürs Vorschau-Kontingent (wie im Konfigurator). */ function clientId(): string { const KEY = 'skrift.client'; let id = localStorage.getItem(KEY); if (!id) { id = `c-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`; localStorage.setItem(KEY, id); } return id; } /** Fehlermeldung bei Zeilen-Überlauf (wie im Konfigurator). */ function overflowMeldung(overflow?: PreviewOverflow[]) { const schlimmste = (overflow ?? []).reduce<{ lineCount: number; lineLimit: number } | null>( (m, o) => (o.lineCount > (m?.lineCount ?? 0) ? o : m), null, ); const detail = schlimmste ? ` (${schlimmste.lineCount} von ${schlimmste.lineLimit} Zeilen)` : ''; return `Der Text ist zu lang für das gewählte Format${detail}. Bitte kürze den Text.`; } /** * Textmengen-Tool (/textmenge): Format- und Schriftwahl, Texteingabe (Tabulator, * gesperrte Zeichen) und „Vorschau generieren" → rendert EIN Schriftstück * (Kugelschreiber-Optik). Passt der Text nicht auf eine Seite, kommt die Fehlermeldung. * Bewusst einseitig: kein Seitenumbruch. */ export default function Textmenge() { const [formate, setFormate] = useState([]); const [settings, setSettings] = useState(null); const [format, setFormat] = useState('a4'); const [font, setFont] = useState('tilda'); const [realistic, setRealistic] = useState(true); const [text, setText] = useState(''); const [hinweis, setHinweis] = useState(''); const [doc, setDoc] = useState(null); const [laedt, setLaedt] = useState(false); const [fehler, setFehler] = useState(null); const [gross, setGross] = useState(false); useEffect(() => { fetchCheckFormats() .then((f) => { setFormate(f); setFormat(f.find((x) => x.key === 'a4')?.key ?? f[0]?.key ?? 'a4'); }) .catch(() => {}); fetchSettings().then(setSettings).catch(() => {}); }, []); // Escape schließt die Großansicht. useEffect(() => { if (!gross) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setGross(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [gross]); // Gesperrte Zeichen wie im Konfigurator (Zeilenumbruch/Tab bleiben erlaubt). const verbotenRe = useMemo(() => { const wl = settings?.char_whitelist; if (!wl) return null; try { return new RegExp(`[^${wl}\\n\\r\\t]`, 'g'); } catch { return null; } }, [settings?.char_whitelist]); const saeubere = (v: string) => { if (!verbotenRe) return v; const treffer = v.match(verbotenRe); if (!treffer?.length) return v; const einmalig = [...new Set(treffer)].map((c) => (c === ' ' ? '␣' : c)).join(' '); setHinweis(`Zeichen ${einmalig} ist nicht erlaubt und wurde entfernt.`); window.setTimeout(() => setHinweis(''), 4000); return v.replace(verbotenRe, ''); }; // Änderung an Eingaben → alte Vorschau verwerfen. useEffect(() => { setDoc(null); setFehler(null); setGross(false); }, [text, font, format, realistic]); async function generieren() { if (!text.trim() || laedt) return; setLaedt(true); setFehler(null); try { const ergebnis = await generatePreview({ client_id: clientId(), text, font, format, realistic, entries: [{}], }); if (ergebnis.has_overflow) { setDoc(null); setFehler(overflowMeldung(ergebnis.overflow)); } else { setDoc((ergebnis.docs ?? []).find((d) => d.art === 'brief') ?? ergebnis.docs?.[0] ?? null); } } catch (e) { setDoc(null); setFehler((e as Error)?.message || 'Vorschau fehlgeschlagen.'); } finally { setLaedt(false); } } return (

Textmenge prüfen

Text eingeben, Format und Schrift wählen, dann eine Vorschau erzeugen. So siehst du, ob der Text auf ein Schriftstück passt.