Initial commit: Family Planner application
Complete family planning application with: - React frontend with TypeScript - Node.js/Express backend with TypeScript - Python ingestion service for document processing - Planning ingestion service with LLM integration - Shared UI components and type definitions - OAuth integration for calendar synchronization - Comprehensive documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
69
frontend/src/components/ToastProvider.js
Normal file
69
frontend/src/components/ToastProvider.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
const ToastContext = createContext(undefined);
|
||||
const slideIn = keyframes `
|
||||
from { transform: translateY(10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
`;
|
||||
const ToastContainer = styled.div `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
`;
|
||||
const ToastItem = styled.div `
|
||||
min-width: 260px;
|
||||
max-width: 420px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: ${({ $level }) => $level === "success"
|
||||
? "rgba(76, 217, 100, 0.15)"
|
||||
: $level === "error"
|
||||
? "rgba(255, 59, 48, 0.18)"
|
||||
: "rgba(85, 98, 255, 0.15)"};
|
||||
border: 1px solid
|
||||
${({ $level }) => $level === "success"
|
||||
? "rgba(76, 217, 100, 0.35)"
|
||||
: $level === "error"
|
||||
? "rgba(255, 59, 48, 0.35)"
|
||||
: "rgba(85, 98, 255, 0.35)"};
|
||||
color: #e9ebff;
|
||||
animation: ${slideIn} 160ms ease-out;
|
||||
`;
|
||||
export const ToastProvider = ({ children }) => {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
const addToast = useCallback((message, opts) => {
|
||||
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const t = { id, message, level: opts?.level ?? "info", timeoutMs: opts?.timeoutMs ?? 3500 };
|
||||
setToasts((prev) => [t, ...prev].slice(0, 5));
|
||||
window.setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((x) => x.id !== id));
|
||||
}, t.timeoutMs);
|
||||
}, []);
|
||||
const value = useMemo(() => ({ addToast }), [addToast]);
|
||||
return (_jsxs(ToastContext.Provider, { value: value, children: [children, _jsx(Bridge, { addToast: addToast }), _jsx(ToastContainer, { children: toasts.map((t) => (_jsx(ToastItem, { "$level": t.level ?? "info", children: t.message }, t.id))) })] }));
|
||||
};
|
||||
export const useToasts = () => {
|
||||
const ctx = useContext(ToastContext);
|
||||
if (!ctx)
|
||||
throw new Error("useToasts must be used within ToastProvider");
|
||||
return ctx;
|
||||
};
|
||||
const Bridge = ({ addToast }) => {
|
||||
useEffect(() => {
|
||||
window.__fp_toast = (success, message) => addToast(message, { level: success ? "success" : "error" });
|
||||
return () => {
|
||||
try {
|
||||
delete window.__fp_toast;
|
||||
}
|
||||
catch {
|
||||
// Ignore errors when cleaning up
|
||||
}
|
||||
};
|
||||
}, [addToast]);
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user