feat: add rate limiting for CoinGecko API and global error handling
This commit is contained in:
220
js/components/error-boundary.js
Normal file
220
js/components/error-boundary.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { t } from '../i18n.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') || 'Etwas ist schiefgelaufen'}</h3>
|
||||
<p class="error-message">${this.escapeHtml(errorMessage)}</p>
|
||||
<button class="btn btn-primary error-retry" type="button">
|
||||
${t('error.retry') || 'Erneut versuchen'}
|
||||
</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
|
||||
}
|
||||
|
||||
escapeHtml(str) {
|
||||
if (!str) return ''
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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="Schließen">×</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'))
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return ''
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
export { ErrorBoundary }
|
||||
Reference in New Issue
Block a user