Files
certain/web/src/toast.jsx
T
GuidoandClaude Opus 4.8 2c795a57c8 feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)
Grundgerüst für Certain Finance:
- Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode),
  argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin,
  Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer.
- Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus,
  inkl. SPA-Deeplink-Fallback.
- Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN,
  Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter,
  Benutzer- und Einstellungsseiten.
- Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 18:27:42 +02:00

70 lines
2.2 KiB
React
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 { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import Icon from "./components/Icon";
/**
* Kurze Rückmeldungen als Overlay am unteren Rand.
*
* Vorher standen sie als Hinweisleiste mitten in der Seite – dadurch sprang
* beim Speichern das ganze Layout ein Stück nach unten. Ein Overlay liegt über
* dem Inhalt und verschiebt nichts.
*
* Bewusst nur für *Erfolgsmeldungen*: Fehler bleiben als Leiste an ihrer Stelle
* stehen. Eine Fehlermeldung, die nach fünf Sekunden verschwindet, ist genau
* dann weg, wenn man sie noch braucht.
*
* const toast = useToast();
* toast("Gespeichert.");
*/
const ToastContext = createContext(() => {});
const DAUER = 5000;
export function ToastProvider({ children }) {
const [meldungen, setMeldungen] = useState([]);
const uhren = useRef(new Map());
const entfernen = useCallback((id) => {
setMeldungen((alt) => alt.filter((m) => m.id !== id));
const uhr = uhren.current.get(id);
if (uhr) {
clearTimeout(uhr);
uhren.current.delete(id);
}
}, []);
const zeigen = useCallback((text, art = "ok") => {
if (!text) return;
const id = `${Date.now()}-${Math.random()}`;
setMeldungen((alt) => [...alt, { id, text, art }]);
uhren.current.set(id, setTimeout(() => entfernen(id), DAUER));
}, [entfernen]);
// Beim Verlassen der Seite laufende Uhren abräumen.
useEffect(() => {
const laufend = uhren.current;
return () => laufend.forEach((uhr) => clearTimeout(uhr));
}, []);
return (
<ToastContext.Provider value={zeigen}>
{children}
{/* aria-live: Bildschirmleser sagen die Meldung an, ohne den Fokus zu stehlen. */}
<div className="toasts" role="status" aria-live="polite">
{meldungen.map((m) => (
<div key={m.id} className={`toast ${m.art}`}>
<Icon name={m.art === "ok" ? "check" : "alert"} size={16} />
<span className="toast-text">{m.text}</span>
<button className="btn-icon" onClick={() => entfernen(m.id)} title="Ausblenden">
<Icon name="close" size={15} />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
return useContext(ToastContext);
}