shinkan-jinkendo/frontend/src/context/ToastContext.jsx
Lars 49adb395dd
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 1m15s
feat(version): bump to 0.8.110 and update project specifications
- Updated app version to 0.8.110 and database schema version to 20260512057, reflecting recent enhancements.
- Revised project status documentation to include new versioning and next steps for development.
- Enhanced the functional specification for training modules and combination exercises, detailing upcoming features and improvements.
- Improved technical specifications to align with the latest code changes, ensuring consistency across documentation.
- Introduced new UI elements for toast notifications and unsaved changes prompts to enhance user experience.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:34:38 +02:00

91 lines
2.4 KiB
JavaScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
/** Kurze Meldungen ohne OK-Dialog; echte Bestätigungen erfolgen gesondert (modale Wahlmöglichkeiten). */
const ToastContext = createContext(null)
function nextId() {
return `toast-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
}
export function ToastProvider({ children }) {
const [items, setItems] = useState([])
const timers = useRef(new Map())
const removeToast = useCallback((id) => {
const t = timers.current.get(id)
if (t) {
window.clearTimeout(t)
timers.current.delete(id)
}
setItems((prev) => prev.filter((x) => x.id !== id))
}, [])
const pushToast = useCallback(
(message, { variant = 'info', duration = 3200, id: forcedId } = {}) => {
const id = forcedId || nextId()
setItems((prev) => [...prev, { id, message: String(message || '').trim(), variant }])
const ms =
variant === 'error' ? Math.max(duration, 5200) : Math.max(duration, 2400)
const handle = window.setTimeout(() => removeToast(id), ms)
timers.current.set(id, handle)
return id
},
[removeToast],
)
useEffect(
() => () => {
timers.current.forEach((h) => window.clearTimeout(h))
timers.current.clear()
},
[],
)
const api = useMemo(
() => ({
/** Erfolgs- und Hinweis-Meldungen (auto-dismiss). */
show: pushToast,
success: (msg, opts) => pushToast(msg, { ...opts, variant: 'success' }),
info: (msg, opts) => pushToast(msg, { ...opts, variant: 'info' }),
error: (msg, opts) => pushToast(msg, { ...opts, variant: 'error' }),
dismiss: removeToast,
}),
[pushToast, removeToast],
)
return (
<ToastContext.Provider value={api}>
{children}
<div className="toast-stack" aria-live="polite" aria-relevant="additions removals">
{items.map((t) => (
<button
key={t.id}
type="button"
className={`toast toast--${t.variant}`}
onClick={() => removeToast(t.id)}
title="Schließen"
>
<span className="toast__text">{t.message}</span>
</button>
))}
</div>
</ToastContext.Provider>
)
}
export function useToast() {
const ctx = useContext(ToastContext)
if (!ctx) {
throw new Error('useToast muss innerhalb von ToastProvider verwendet werden.')
}
return ctx
}