Files
kashilo/js/components/error-boundary.js

275 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { t } from '../i18n.js'
import { escapeHTML } from '../utils/helpers.js'
/**
* Error Boundary Component
* Catches errors in child components and displays fallback UI
*/
class ErrorBoundary extends HTMLElement {
constructor() {
super()
this.hasError = false
this.error = null
}
connectedCallback() {
this.setupErrorHandling()
}
setupErrorHandling() {
// Catch errors from child components
this.addEventListener('error', (e) => {
e.stopPropagation()
this.handleError(e.error || e.message || 'Unknown error')
})
}
handleError(error) {
console.error('ErrorBoundary caught:', error)
this.hasError = true
this.error = error
this.renderError()
}
renderError() {
const errorMessage = this.error?.message || String(this.error) || t('common.error')
this.innerHTML = /* html */`
<div class="error-boundary">
<div class="error-icon">⚠️</div>
<h3 class="error-title">${t('error.title')}</h3>
<p class="error-message">${escapeHTML(errorMessage)}</p>
<button class="btn btn-primary error-retry" type="button">
${t('error.retry')}
</button>
</div>
`
this.querySelector('.error-retry')?.addEventListener('click', () => this.retry())
}
retry() {
this.hasError = false
this.error = null
this.innerHTML = ''
this.dispatchEvent(new CustomEvent('retry', { bubbles: true }))
}
reset() {
this.hasError = false
this.error = null
}
}
customElements.define('error-boundary', ErrorBoundary)
/**
* Global error handler for uncaught errors
*/
export function setupGlobalErrorHandler() {
window.addEventListener('error', (event) => {
console.error('Global error:', event.error)
// Don't show UI for script loading errors
if (event.filename && event.filename.includes('.js')) {
showErrorToast(event.message || 'Ein Fehler ist aufgetreten')
}
})
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason)
showErrorToast(event.reason?.message || 'Ein Fehler ist aufgetreten')
})
// Offline/Online detection
setupOfflineIndicator()
}
/**
* Shows/hides offline indicator based on network status
*/
function setupOfflineIndicator() {
const updateStatus = () => {
let indicator = document.querySelector('.offline-indicator')
if (!navigator.onLine) {
if (!indicator) {
indicator = document.createElement('div')
indicator.className = 'offline-indicator'
indicator.innerHTML = `
<span class="offline-icon">📡</span>
<span class="offline-text">Offline</span>
`
document.body.appendChild(indicator)
requestAnimationFrame(() => indicator.classList.add('visible'))
}
} else {
if (indicator) {
indicator.classList.remove('visible')
setTimeout(() => indicator.remove(), 300)
}
}
}
window.addEventListener('online', updateStatus)
window.addEventListener('offline', updateStatus)
// Check on init
updateStatus()
}
/**
* Shows a temporary error toast notification
*/
function showErrorToast(message) {
// Remove existing toast
document.querySelector('.error-toast')?.remove()
const toast = document.createElement('div')
toast.className = 'error-toast'
toast.innerHTML = /* html */`
<span class="error-toast-icon">⚠️</span>
<span class="error-toast-message">${escapeHTML(message)}</span>
<button class="error-toast-close" aria-label="${t('common.close')}">×</button>
`
document.body.appendChild(toast)
// Auto-dismiss after 5 seconds
const timeout = setTimeout(() => toast.remove(), 5000)
toast.querySelector('.error-toast-close')?.addEventListener('click', () => {
clearTimeout(timeout)
toast.remove()
})
// Trigger animation
requestAnimationFrame(() => toast.classList.add('visible'))
}
// Styles
const style = document.createElement('style')
style.textContent = /* css */`
error-boundary {
display: block;
}
error-boundary .error-boundary {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-2xl);
text-align: center;
min-height: 200px;
}
error-boundary .error-icon {
font-size: 3rem;
margin-bottom: var(--space-md);
filter: grayscale(1);
}
error-boundary .error-title {
font-size: var(--font-size-lg);
margin: 0 0 var(--space-sm);
color: var(--color-text);
}
error-boundary .error-message {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0 0 var(--space-lg);
max-width: 400px;
}
/* Error Toast */
.error-toast {
position: fixed;
bottom: var(--space-lg);
left: 50%;
transform: translateX(-50%) translateY(100px);
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 10000;
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.error-toast.visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.error-toast-icon {
filter: grayscale(1);
}
.error-toast-message {
font-size: var(--font-size-sm);
color: var(--color-text);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.error-toast-close {
background: none;
border: none;
font-size: var(--font-size-lg);
color: var(--color-text-muted);
cursor: pointer;
padding: 0;
line-height: 1;
}
.error-toast-close:hover {
color: var(--color-text);
}
/* Offline Indicator */
.offline-indicator {
position: fixed;
top: var(--space-md);
left: 50%;
transform: translateX(-50%) translateY(-100px);
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-md);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
box-shadow: var(--shadow-md);
z-index: 10001;
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.offline-indicator.visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.offline-indicator .offline-icon {
filter: grayscale(1);
}
.offline-indicator .offline-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
`
document.head.appendChild(style)
export { ErrorBoundary }