/* ============================================================ BAN YARO — UI Helpers Alle UI-Interaktionen an einem Ort. Toast, Modal, Loading, Confirm — einmal gebaut, überall nutzbar. ============================================================ */ const UI = (() => { // ---------------------------------------------------------- // PHOSPHOR ICON HELPER — erzeugt SVG-String für Templates // ---------------------------------------------------------- function _svgIcon(name, extraClass = '') { return ``; } // ---------------------------------------------------------- // TOAST // ---------------------------------------------------------- const toast = (() => { const container = () => document.getElementById('toast-container'); function show(message, type = 'default', duration = 3500) { const el = document.createElement('div'); el.className = `toast${type !== 'default' ? ` toast-${type}` : ''}`; const iconName = { success: 'check', danger: 'x', warning: 'warning', info: 'info' }[type]; el.innerHTML = iconName ? `${_svgIcon(iconName)}${message}` : `${message}`; container().appendChild(el); const timer = setTimeout(() => remove(el), duration); el.addEventListener('click', () => { clearTimeout(timer); remove(el); }); } function remove(el) { el.classList.add('removing'); const fallback = setTimeout(() => el.remove(), 300); el.addEventListener('animationend', () => { clearTimeout(fallback); el.remove(); }, { once: true }); } return { show, success: (msg, dur) => show(msg, 'success', dur), error: (msg, dur) => show(msg, 'danger', dur || 5000), warning: (msg, dur) => show(msg, 'warning', dur), info: (msg, dur) => show(msg, 'info', dur), }; })(); // ---------------------------------------------------------- // MODAL // ---------------------------------------------------------- const modal = (() => { let _current = null; function open({ title, body, footer, onClose } = {}) { close(); // vorheriges schließen const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; overlay.querySelector('.modal-close-btn')?.addEventListener('click', close); document.getElementById('modal-container').appendChild(overlay); document.body.style.overflow = 'hidden'; _current = { overlay, onClose }; return overlay.querySelector('.modal'); } function close() { if (!_current) return; _current.onClose?.(); _current.overlay.remove(); document.body.style.overflow = ''; _current = null; } // Bestätigungsdialog function confirm({ title, message, confirmText = 'OK', cancelText = 'Abbrechen', danger = false } = {}) { return new Promise(resolve => { const m = open({ title, body: `

${message}

`, footer: ` `, onClose: () => resolve(false), }); m.parentElement.querySelector('#modal-cancel').addEventListener('click', () => { resolve(false); close(); }); m.parentElement.querySelector('#modal-confirm').addEventListener('click', () => { resolve(true); close(); }); }); } return { open, close, confirm }; })(); // ---------------------------------------------------------- // LOADING STATE für Buttons // ---------------------------------------------------------- function setLoading(btn, loading) { if (loading) { btn._originalContent = btn.innerHTML; btn.innerHTML = ''; btn.disabled = true; } else { btn.innerHTML = btn._originalContent || btn.innerHTML; btn.disabled = false; } } // ---------------------------------------------------------- // ASYNC BUTTON: Button-Click → Loader → Ergebnis → Toast // Verwendung: UI.asyncButton(btn, async () => { await API.something() }) // ---------------------------------------------------------- async function asyncButton(btn, fn, { successMsg, errorMsg } = {}) { setLoading(btn, true); try { const result = await fn(); if (successMsg) toast.success(successMsg); return result; } catch (err) { const msg = errorMsg || err.message || 'Ein Fehler ist aufgetreten.'; toast.error(msg); throw err; } finally { setLoading(btn, false); } } // ---------------------------------------------------------- // FORMULAR-HELPER // ---------------------------------------------------------- function formData(form) { const data = {}; new FormData(form).forEach((v, k) => { data[k] = v; }); return data; } function setFormError(form, fieldName, message) { const field = form.querySelector(`[name="${fieldName}"]`); if (!field) return; field.classList.add('is-invalid'); let hint = field.parentElement.querySelector('.form-error'); if (!hint) { hint = document.createElement('span'); hint.className = 'form-error'; field.parentElement.appendChild(hint); } hint.textContent = message; } function clearFormErrors(form) { form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); form.querySelectorAll('.form-error').forEach(el => el.remove()); } // ---------------------------------------------------------- // LEERER ZUSTAND (Empty State) // ---------------------------------------------------------- function emptyState({ icon, title, text, action } = {}) { return `
${icon ? `
${icon}
` : ''} ${title ? `
${title}
` : ''} ${text ? `
${text}
` : ''} ${action ? `
${action}
` : ''}
`; } // ---------------------------------------------------------- // DATUM-FORMATIERUNG (Deutsch, relativ) // ---------------------------------------------------------- const time = (() => { const fmt = new Intl.RelativeTimeFormat('de', { numeric: 'auto' }); const fmtDate = new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }); const fmtDateShort = new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'short' }); function relative(dateStr) { const diff = (new Date(dateStr) - Date.now()) / 1000; const abs = Math.abs(diff); if (abs < 60) return fmt.format(Math.round(diff), 'second'); if (abs < 3600) return fmt.format(Math.round(diff / 60), 'minute'); if (abs < 86400)return fmt.format(Math.round(diff / 3600), 'hour'); if (abs < 604800) return fmt.format(Math.round(diff / 86400), 'day'); return fmtDate.format(new Date(dateStr)); } return { relative, format: d => fmtDate.format(new Date(d)), formatShort: d => fmtDateShort.format(new Date(d)), }; })(); // ---------------------------------------------------------- // FOTO-VORSCHAU (Input[type=file] → img) // ---------------------------------------------------------- function setupPhotoPreview(input, imgEl) { input.addEventListener('change', () => { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { imgEl.src = e.target.result; }; reader.readAsDataURL(file); }); } // ---------------------------------------------------------- // SCROLL TO TOP der Seite // ---------------------------------------------------------- function scrollTop() { document.getElementById('page-content')?.scrollTo({ top: 0, behavior: 'smooth' }); } // ---------------------------------------------------------- // SKELETON LOADER (Platzhalter während Laden) // ---------------------------------------------------------- function skeleton(lines = 3) { return Array.from({ length: lines }, (_, i) => `
`).join('') + ` `; } function escape(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // Öffentliche API return { toast, modal, setLoading, asyncButton, formData, setFormError, clearFormErrors, emptyState, time, setupPhotoPreview, scrollTop, skeleton, icon: _svgIcon, escape, }; })();