From 91914cbb285734467802c6670b921beef30a7f7e Mon Sep 17 00:00:00 2001 From: ghdtnals Date: Mon, 28 Apr 2025 17:52:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20Toast=20=EC=BB=B4=ED=8F=B0=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/components/Toast.tsx diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..2d3706f --- /dev/null +++ b/src/components/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} +
+ ); +} From 504aea1ea71968671d7aeb52ecff1c2bf9629b1b Mon Sep 17 00:00:00 2001 From: ghdtnals Date: Tue, 29 Apr 2025 11:33:33 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20Toast=20=ED=9B=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1 + src/App.tsx | 8 ++++- src/components/Toast/Toast.tsx | 20 ++++++++++++ src/components/Toast/ToastContainer.tsx | 24 +++++++++++++++ src/components/Toast/ToastPortal.tsx | 21 +++++++++++++ src/hooks/useToast.tsx | 41 +++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/components/Toast/Toast.tsx create mode 100644 src/components/Toast/ToastContainer.tsx create mode 100644 src/components/Toast/ToastPortal.tsx create mode 100644 src/hooks/useToast.tsx 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..2875733 --- /dev/null +++ b/src/components/Toast/Toast.tsx @@ -0,0 +1,20 @@ +interface ToastProps { + label: string; + onClose?: () => void; +} + +export default function Toast({ label, onClose }: ToastProps) { + return ( +
+ {label} + {onClose && ( + + )} +
+ ); +} diff --git a/src/components/Toast/ToastContainer.tsx b/src/components/Toast/ToastContainer.tsx new file mode 100644 index 0000000..8900095 --- /dev/null +++ b/src/components/Toast/ToastContainer.tsx @@ -0,0 +1,24 @@ +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..cd9455a --- /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) => void; + removeToast: (id: number) => void; +} + +export const useToast = create((set) => ({ + toasts: [], + showToast: (label) => { + const id = Date.now(); + set((state) => ({ + toasts: [...state.toasts, { id, label, isVisible: true }], + })); + + setTimeout(() => { + set((state) => ({ + toasts: state.toasts.map((toast) => + toast.id === id ? { ...toast, isVisible: false } : toast, + ), + })); + + setTimeout(() => { + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })); + }, 500); + }, 1000); + }, + removeToast: (id) => + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })), +})); From 2f684502587e06fb0a4d7fe2be09a67d9db575e4 Mon Sep 17 00:00:00 2001 From: ghdtnals Date: Tue, 29 Apr 2025 16:45:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/components/Toast.tsx diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx deleted file mode 100644 index 2d3706f..0000000 --- a/src/components/Toast.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { cn } from "@/utils/cn"; - -interface ToastProps { - label: string; - className?: string; -} - -export default function Toast({ label, className }: ToastProps) { - return ( -
- {label} -
- ); -} From f14127693b67b593faf3906fc3937d2a8e6a9eb9 Mon Sep 17 00:00:00 2001 From: ghdtnals Date: Tue, 29 Apr 2025 17:10:16 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20Toast=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Toast/Toast.tsx | 21 +++++++++---------- src/components/Toast/ToastContainer.tsx | 9 ++++---- src/hooks/useToast.tsx | 28 ++++++++++++------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 2875733..edace5e 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,20 +1,19 @@ +import { cn } from "@/utils/cn"; + interface ToastProps { label: string; - onClose?: () => void; + className?: string; } -export default function Toast({ label, onClose }: ToastProps) { +export default function Toast({ label, className }: ToastProps) { return ( -
- {label} - {onClose && ( - +
+ {label}
); } diff --git a/src/components/Toast/ToastContainer.tsx b/src/components/Toast/ToastContainer.tsx index 8900095..225168d 100644 --- a/src/components/Toast/ToastContainer.tsx +++ b/src/components/Toast/ToastContainer.tsx @@ -7,16 +7,15 @@ export default function ToastContainer() { return ( -
+
{toasts.map((toast) => ( -
- -
+ /> ))}
diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx index cd9455a..073ef62 100644 --- a/src/hooks/useToast.tsx +++ b/src/hooks/useToast.tsx @@ -8,31 +8,31 @@ interface ToastItem { interface ToastState { toasts: ToastItem[]; - showToast: (label: string) => void; + 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: (label) => { + showToast: async (label) => { const id = Date.now(); set((state) => ({ toasts: [...state.toasts, { id, label, isVisible: true }], })); - setTimeout(() => { - set((state) => ({ - toasts: state.toasts.map((toast) => - toast.id === id ? { ...toast, isVisible: false } : toast, - ), - })); + await delay(1000); + set((state) => ({ + toasts: state.toasts.map((toast) => + toast.id === id ? { ...toast, isVisible: false } : toast, + ), + })); - setTimeout(() => { - set((state) => ({ - toasts: state.toasts.filter((toast) => toast.id !== id), - })); - }, 500); - }, 1000); + await delay(500); + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })); }, removeToast: (id) => set((state) => ({