diff --git a/index.html b/index.html index 7e0a32b..37d2b72 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@
+
diff --git a/src/App.tsx b/src/App.tsx index 9eb1038..3913385 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,14 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./Router"; +import ToastContainer from "@/components/Toast/ToastContainer"; function App() { - return ; + return ( + <> + + + + ); } export default App; diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx new file mode 100644 index 0000000..edace5e --- /dev/null +++ b/src/components/Toast/Toast.tsx @@ -0,0 +1,19 @@ +import { cn } from "@/utils/cn"; + +interface ToastProps { + label: string; + className?: string; +} + +export default function Toast({ label, className }: ToastProps) { + return ( +
+ {label} +
+ ); +} diff --git a/src/components/Toast/ToastContainer.tsx b/src/components/Toast/ToastContainer.tsx new file mode 100644 index 0000000..225168d --- /dev/null +++ b/src/components/Toast/ToastContainer.tsx @@ -0,0 +1,23 @@ +import { useToast } from "@/hooks/useToast"; +import ToastPortal from "@/components/Toast/ToastPortal"; +import Toast from "@/components/Toast/Toast"; + +export default function ToastContainer() { + const { toasts } = useToast(); + + return ( + +
+ {toasts.map((toast) => ( + + ))} +
+
+ ); +} diff --git a/src/components/Toast/ToastPortal.tsx b/src/components/Toast/ToastPortal.tsx new file mode 100644 index 0000000..dbdeeff --- /dev/null +++ b/src/components/Toast/ToastPortal.tsx @@ -0,0 +1,21 @@ +import { createPortal } from "react-dom"; +import { ReactNode, useEffect, useState } from "react"; + +interface ToastPortalProps { + children: ReactNode; +} + +export default function ToastPortal({ children }: ToastPortalProps) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (typeof window === "undefined") return null; + + const el = document.getElementById("toast-root"); + if (!el || !mounted) return null; + + return createPortal(children, el); +} diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx new file mode 100644 index 0000000..073ef62 --- /dev/null +++ b/src/hooks/useToast.tsx @@ -0,0 +1,41 @@ +import { create } from "zustand"; + +interface ToastItem { + id: number; + label: string; + isVisible: boolean; +} + +interface ToastState { + toasts: ToastItem[]; + showToast: (label: string) => Promise; + removeToast: (id: number) => void; +} + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const useToast = create((set) => ({ + toasts: [], + showToast: async (label) => { + const id = Date.now(); + set((state) => ({ + toasts: [...state.toasts, { id, label, isVisible: true }], + })); + + await delay(1000); + set((state) => ({ + toasts: state.toasts.map((toast) => + toast.id === id ? { ...toast, isVisible: false } : toast, + ), + })); + + await delay(500); + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })); + }, + removeToast: (id) => + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })), +}));