tududi/frontend/components/Shared/ToastContext.tsx
Chris 10d96397c3
Sorting fixes (#174)
* Update version

* Order Inbox items by creation timestamp, desc

* Fix input keyboard tab and enter handling

* Fix lint issues

* Move in progress items to the top of today list

* Make tags and projects clickable

* fixup! Make tags and projects clickable

* fixup! fixup! Make tags and projects clickable
2025-07-17 17:43:56 +03:00

106 lines
3.4 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback } from 'react';
import {
CheckCircleIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
interface Toast {
id: number;
message: string | React.ReactNode;
type: 'success' | 'error';
}
interface ToastContextProps {
showSuccessToast: (message: string | React.ReactNode) => void;
showErrorToast: (message: string | React.ReactNode) => void;
}
const ToastContext = createContext<ToastContextProps | undefined>(undefined);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const removeToast = useCallback((id: number) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const showSuccessToast = useCallback(
(message: string | React.ReactNode) => {
const id = Date.now() + Math.random();
const newToast: Toast = { id, message, type: 'success' };
setToasts((prev) => [...prev, newToast]);
setTimeout(() => removeToast(id), 4000);
},
[removeToast]
);
const showErrorToast = useCallback(
(message: string | React.ReactNode) => {
const id = Date.now() + Math.random();
const newToast: Toast = { id, message, type: 'error' };
setToasts((prev) => [...prev, newToast]);
setTimeout(() => removeToast(id), 4000);
},
[removeToast]
);
return (
<ToastContext.Provider value={{ showSuccessToast, showErrorToast }}>
{children}
<div className="fixed top-20 right-4 z-50 space-y-2">
{toasts.map((toast, index) => (
<ToastComponent
key={toast.id}
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
style={{ transform: `translateY(${index * 4}px)` }}
/>
))}
</div>
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
const ToastComponent: React.FC<{
message: string | React.ReactNode;
type: 'success' | 'error';
onClose: () => void;
style?: React.CSSProperties;
}> = ({ message, type, onClose, style }) => {
return (
<div
className={`px-4 py-3 rounded-lg shadow-md text-white transition-all duration-300 ${
type === 'success' ? 'bg-green-600' : 'bg-red-500'
}`}
style={style}
>
<div className="flex items-center">
<div className="flex-shrink-0 mr-3">
{type === 'success' ? (
<CheckCircleIcon className="h-5 w-5" />
) : (
<ExclamationTriangleIcon className="h-5 w-5" />
)}
</div>
<div className="flex-1">{message}</div>
<button
onClick={onClose}
className="ml-4 text-xl leading-none hover:opacity-75 flex-shrink-0"
>
&times;
</button>
</div>
</div>
);
};