/** * BreakPilot Studio - API Helpers Module * * Gemeinsame Funktionen für API-Aufrufe und Status-Verwaltung: * - fetchJSON: Wrapper für fetch mit Error-Handling * - postJSON: POST-Requests mit JSON-Body * - setStatus: Status-Leiste aktualisieren * - showNotification: Toast-Benachrichtigungen * * Refactored: 2026-01-19 */ import { t } from './i18n.js'; // Status-Bar Element-Referenzen (werden bei init gesetzt) let statusBar = null; let statusDot = null; let statusMain = null; let statusSub = null; /** * Initialisiert die Status-Bar Referenzen * Sollte beim DOMContentLoaded aufgerufen werden */ export function initStatusBar() { statusBar = document.getElementById('status-bar'); statusDot = document.getElementById('status-dot'); statusMain = document.getElementById('status-main'); statusSub = document.getElementById('status-sub'); } /** * Setzt den Status in der Status-Leiste * @param {string} type - 'ready'|'working'|'success'|'error' * @param {string} main - Haupttext * @param {string} [sub] - Optionaler Untertext */ export function setStatus(type, main, sub = '') { if (!statusBar || !statusDot || !statusMain) { console.log(`[Status ${type}]: ${main}`, sub); return; } // Alle Status-Klassen entfernen statusBar.classList.remove('status-ready', 'status-working', 'status-success', 'status-error'); statusDot.classList.remove('dot-ready', 'dot-working', 'dot-success', 'dot-error'); // Neue Status-Klasse setzen statusBar.classList.add(`status-${type}`); statusDot.classList.add(`dot-${type}`); // Texte setzen statusMain.textContent = main; if (statusSub) { statusSub.textContent = sub; } } /** * Setzt den Status auf "Bereit" */ export function setStatusReady() { setStatus('ready', t('status_ready') || 'Bereit', ''); } /** * Setzt den Status auf "Arbeitet..." * @param {string} message - Was gerade gemacht wird */ export function setStatusWorking(message) { setStatus('working', message, ''); } /** * Setzt den Status auf "Erfolg" * @param {string} message - Erfolgsmeldung * @param {string} [details] - Optionale Details */ export function setStatusSuccess(message, details = '') { setStatus('success', message, details); } /** * Setzt den Status auf "Fehler" * @param {string} message - Fehlermeldung * @param {string} [details] - Optionale Details */ export function setStatusError(message, details = '') { setStatus('error', message, details); } /** * Führt einen GET-Request aus und parst JSON * @param {string} url - Die URL * @param {Object} [options] - Zusätzliche fetch-Optionen * @returns {Promise} - Das geparste JSON * @throws {Error} - Bei Netzwerk- oder Parse-Fehlern */ export async function fetchJSON(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'Accept': 'application/json', ...options.headers, }, }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`HTTP ${response.status}: ${errorText}`); } return response.json(); } /** * Führt einen POST-Request mit JSON-Body aus * @param {string} url - Die URL * @param {Object} data - Die zu sendenden Daten * @param {Object} [options] - Zusätzliche fetch-Optionen * @returns {Promise} - Das geparste JSON * @throws {Error} - Bei Netzwerk- oder Parse-Fehlern */ export async function postJSON(url, data = {}, options = {}) { const response = await fetch(url, { method: 'POST', ...options, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers, }, body: JSON.stringify(data), }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`HTTP ${response.status}: ${errorText}`); } return response.json(); } /** * Führt einen POST-Request ohne Body aus (für Trigger-Endpoints) * @param {string} url - Die URL * @param {Object} [options] - Zusätzliche fetch-Optionen * @returns {Promise} - Das geparste JSON */ export async function postTrigger(url, options = {}) { const response = await fetch(url, { method: 'POST', ...options, headers: { 'Accept': 'application/json', ...options.headers, }, }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`HTTP ${response.status}: ${errorText}`); } return response.json(); } /** * Führt einen DELETE-Request aus * @param {string} url - Die URL * @param {Object} [options] - Zusätzliche fetch-Optionen * @returns {Promise} - Das geparste JSON */ export async function deleteRequest(url, options = {}) { const response = await fetch(url, { method: 'DELETE', ...options, headers: { 'Accept': 'application/json', ...options.headers, }, }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`HTTP ${response.status}: ${errorText}`); } return response.json(); } /** * Lädt eine Datei hoch * @param {string} url - Die Upload-URL * @param {File|FormData} file - Die Datei oder FormData * @param {function} [onProgress] - Progress-Callback (0-100) * @returns {Promise} - Das geparste JSON */ export async function uploadFile(url, file, onProgress = null) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); if (onProgress && xhr.upload) { xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); onProgress(percent); } }); } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch (e) { resolve({ status: 'OK', message: xhr.responseText }); } } else { reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`)); } }; xhr.onerror = () => reject(new Error('Network error during upload')); // FormData erstellen falls nötig let formData; if (file instanceof FormData) { formData = file; } else { formData = new FormData(); formData.append('file', file); } xhr.send(formData); }); } /** * Zeigt eine kurze Benachrichtigung (Toast) * @param {string} message - Die Nachricht * @param {string} [type='info'] - 'info'|'success'|'error'|'warning' * @param {number} [duration=3000] - Anzeigedauer in ms */ export function showNotification(message, type = 'info', duration = 3000) { // Prüfe ob Toast-Container existiert, sonst erstellen let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;'; document.body.appendChild(container); } // Toast erstellen const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.style.cssText = ` padding: 12px 16px; border-radius: 8px; background: var(--bp-card-bg, #1e293b); color: var(--bp-text, #e2e8f0); box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-size: 13px; animation: slideIn 0.3s ease; border-left: 4px solid ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#3b82f6'}; `; toast.textContent = message; container.appendChild(toast); // Nach duration entfernen setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); } /** * Wrapper für API-Aufrufe mit Status-Anzeige und Error-Handling * @param {function} apiCall - Die async API-Funktion * @param {Object} options - Optionen * @param {string} options.workingMessage - Nachricht während des Ladens * @param {string} options.successMessage - Nachricht bei Erfolg * @param {string} options.errorMessage - Nachricht bei Fehler * @returns {Promise} - Das Ergebnis oder null bei Fehler */ export async function withStatus(apiCall, options = {}) { const { workingMessage = 'Wird geladen...', successMessage = 'Erfolgreich', errorMessage = 'Fehler', } = options; setStatusWorking(workingMessage); try { const result = await apiCall(); setStatusSuccess(successMessage); return result; } catch (error) { console.error(errorMessage, error); setStatusError(errorMessage, String(error.message || error)); return null; } } /** * Debounce-Funktion für häufige Events * @param {function} func - Die zu debouncende Funktion * @param {number} wait - Wartezeit in ms * @returns {function} - Die gedebouncte Funktion */ export function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Throttle-Funktion für Rate-Limiting * @param {function} func - Die zu throttlende Funktion * @param {number} limit - Minimaler Abstand in ms * @returns {function} - Die gethrottlete Funktion */ export function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // CSS für Toast-Animationen (einmal injizieren) if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) { const style = document.createElement('style'); style.id = 'toast-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); }