76 lines
3.3 KiB
JavaScript
76 lines
3.3 KiB
JavaScript
// ── 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,
|
||
},
|
||
};
|