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
- 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>
91 lines
2.4 KiB
JavaScript
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
|
|
}
|