// ── API Client ──────────────────────────────────────────────────────────────── // Einheitlicher Fetch-Wrapper für alle Tools und Core-Komponenten export const api = async (path, { body, method, isFile } = {}) => { const token = localStorage.getItem('sk_token'); const headers = token ? { Authorization: `Bearer ${token}` } : {}; if (!isFile) headers['Content-Type'] = 'application/json'; const res = await fetch(`/api${path}`, { method: method || (body ? 'POST' : 'GET'), headers, body: isFile ? body : body ? JSON.stringify(body) : undefined, }); if (path.includes('/admin/backup') && res.ok) return res.blob(); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Fehler'); return data; }; // ── Upload mit echtem Byte-Fortschritt ────────────────────────────────────── // fetch() liefert keinerlei Upload-Progress-Events, daher hier bewusst auf // XMLHttpRequest aufgebaut. Nur für Datei-Uploads gedacht (FormData-Body). // onProgress(percent: 0-100) wird während des Uploads laufend aufgerufen. export const apiUpload = (path, formData, { onProgress } = {}) => { const token = localStorage.getItem('sk_token'); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', `/api${path}`); if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.upload.onprogress = e => { if (e.lengthComputable && onProgress) { onProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => { let data = {}; try { data = JSON.parse(xhr.responseText); } catch { /* leere/ungültige Antwort */ } if (xhr.status >= 200 && xhr.status < 300) resolve(data); else reject(new Error(data.error || 'Fehler')); }; xhr.onerror = () => reject(new Error('Verbindung fehlgeschlagen')); xhr.send(formData); }); }; // ── Design Tokens ───────────────────────────────────────────────────────────── // Einheitliche Styles für alle Tools – in jedem Tool importierbar export const S = { // Eingabefeld inp: { width: '100%', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 6, padding: '8px 10px', color: '#fff', fontSize: 13, fontFamily: "'Space Mono',monospace", outline: 'none', boxSizing: 'border-box', }, // Button-Factory: btn('#4ecdc4') oder btn('#ff6b9d', true) für kleine Variante btn: (c = '#4ecdc4', sm = false) => ({ background: `${c}18`, border: `1px solid ${c}44`, borderRadius: 6, padding: sm ? '5px 10px' : '8px 14px', color: c, cursor: 'pointer', fontFamily: 'monospace', fontSize: sm ? 11 : 12, whiteSpace: 'nowrap', transition: 'all 0.15s', }), // Karten-Container card: { background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 12, padding: 20, }, // Abschnitts-Überschrift head: { color: 'rgba(255,255,255,0.6)', fontSize: 10, fontFamily: 'monospace', letterSpacing: 2, marginBottom: 14, }, };