diff --git a/frontend/src/library/toast/Toast.tsx b/frontend/src/library/toast/Toast.tsx new file mode 100644 index 0000000..90e69cc --- /dev/null +++ b/frontend/src/library/toast/Toast.tsx @@ -0,0 +1,138 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ToastContext } from './toast-context'; +import './toast.css'; + +function useTimeout(callback: () => void, duration: number) { + const savedCallback = useRef(callback); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + const functionId = setTimeout(() => savedCallback.current(), duration); + + return () => { + clearTimeout(functionId); + }; + }, [duration]); +} +type toastProperties = { + message: string; + close: () => void; + duration: number; + position: string; + color: string; +}; + +export function Toast({ + message, + close, + duration, + position, + color, +}: toastProperties) { + useTimeout(() => { + close(); + }, duration); + return ( +
+

{message}

+ +
+ ); +} + +type ToastProviderProperties = { + children: React.ReactElement; +}; +type ToastType = { + message: string; + id: number; + duration: number; + position: string; + color: string; +}; + +export function ToastProvider({ children }: ToastProviderProperties) { + const [toasts, setToasts] = useState([]); + const [position, setPosition] = useState('top-left'); + type Options = { + message?: string; + duration?: number; + position?: string; + color?: 'info' | 'warning' | 'error' | 'success'; + }; + const openToast = useCallback( + ({ + message = '', + duration = 5000, + position = 'top-center', + color = 'info', + }: Options = {}) => { + const newToast = { + message: message, + id: Date.now(), + duration: duration, + position: position, + color: color, + }; + setToasts((prevToast) => [...prevToast, newToast]); + setPosition(position); + }, + [] + ); + + const closeToast = useCallback((id: number) => { + setTimeout(() => { + setToasts((prevToasts) => + prevToasts.filter((toast) => toast.id !== id) + ); + }, 300); + + setToasts((toasts) => { + return toasts.map((toast) => { + if (toast.id === id) { + if (toast.position == 'top-left') + toast.position = 'fade-out-left'; + else if (toast.position == 'top-right') + toast.position = 'fade-out-right'; + else if (toast.position == 'top-center') + toast.position = 'fade-out-center'; + } + return toast; + }); + }); + }, []); + const contextValue = useMemo( + () => ({ + open: openToast, + close: closeToast, + }), + [openToast, closeToast] + ); + return ( + + {children} +
+ {toasts && + toasts.map((toast) => { + return ( + { + closeToast(toast.id); + }} + duration={toast.duration} + position={toast.position} + color={toast.color} + /> + ); + })} +
+
+ ); +} diff --git a/frontend/src/library/toast/toast-context.ts b/frontend/src/library/toast/toast-context.ts new file mode 100644 index 0000000..625bc97 --- /dev/null +++ b/frontend/src/library/toast/toast-context.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react'; + +type Options = { + message?: string; + duration?: number; + position?: string; + color?: 'info' | 'warning' | 'error' | 'success'; +}; + +type ToastContextValue = { + open: (options?: Options) => void; + close: (id: number) => void; +}; + +export const ToastContext = createContext({ + open: () => {}, + close: () => {}, +}); + +export const useToast = () => useContext(ToastContext); diff --git a/frontend/src/library/toast/toast.css b/frontend/src/library/toast/toast.css new file mode 100644 index 0000000..42cf727 --- /dev/null +++ b/frontend/src/library/toast/toast.css @@ -0,0 +1,181 @@ +.toasts { + display: flex; + flex-direction: column; + gap: 10px; +} + +.toast { + color: black; + border-radius: 5px; + padding: 10px 10px; + width: 300px; + position: relative; + display: flex; +} + +.top-right { + position: fixed; + top: 10px; + right: 10px; +} + +.top-left { + position: fixed; + top: 10px; + left: 10px; +} +.top-center { + position: fixed; + top: 10px; + left: 38%; +} + +.top-center-animation { + animation-name: slideinCenter; + animation-duration: 0.35s; +} + +.top-right-animation { + animation-name: slideinRight; + animation-duration: 0.35s; +} + +.top-left-animation { + animation-name: slideinLeft; + animation-duration: 0.35s; +} + +@keyframes slideinRight { + 0% { + transform: translateX(100%); + } + 60% { + transform: translateX(-15%); + } + 80% { + transform: translateX(5%); + } + 80% { + transform: translateX(0); + } +} +@keyframes slideinCenter { + 0% { + transform: translateY(-100%); + } + 60% { + transform: translateY(15%); + } + 80% { + transform: translateY(-5%); + } + 80% { + transform: translateY(0); + } +} + +@keyframes slideinLeft { + 0% { + transform: translateX(-100%); + } + 60% { + transform: translateX(15%); + } + 80% { + transform: translateX(-5%); + } + 80% { + transform: translateX(0); + } +} + +.fade-out-left-animation { + animation-name: fade-out-left; + animation-duration: 0.35s; +} + +.fade-out-right-animation { + animation-name: fade-out-right; + animation-duration: 0.35s; +} + +.fade-out-center-animation { + animation-name: fade-out-center; + animation-duration: 0.35s; +} + +@keyframes fade-out-left { + 0% { + transform: translateX(0%); + } + + 60% { + transform: translateX(-100%); + } + + 80% { + transform: translateX(-195%); + } + + 100% { + transform: translateX(-200%); + } +} +@keyframes fade-out-right { + 0% { + transform: translateX(0%); + } + + 60% { + transform: translateX(100%); + } + + 80% { + transform: translateX(195%); + } + + 100% { + transform: translateX(200%); + } +} + +@keyframes fade-out-center { + 0% { + transform: translateY(-100%); + } + 30% { + transform: translateY(-300%); + } + 80% { + transform: translateY(-700%); + } + 100% { + transform: translateY(-1000%); + } +} + +.info { + background-color: cyan; +} + +.success { + background-color: #5cb85c; +} + +.error { + background-color: #d9534f; +} +.warning { + background-color: #f0ad4e; +} + +.close-button { + position: absolute; + right: 0px; + top: 0px; + padding: 0px 5px; + background: none; + cursor: pointer; + border: transparent; + color: black; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 12afe2b..a3323d8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,9 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; +import { ToastProvider } from './library/toast/Toast.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( - + + + );