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>
70 lines
2.5 KiB
JavaScript
70 lines
2.5 KiB
JavaScript
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;
|
|
};
|