Skip to content

Commit eccdd2e

Browse files
authored
Merge pull request #44 from CodeitPart3/COMPONENT-42-HONG
[feat] Toast 컴포넌트 구현
2 parents 580259a + e48acab commit eccdd2e

File tree

6 files changed

+112
-1
lines changed

6 files changed

+112
-1
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</head>
1313
<body>
1414
<div id="root"></div>
15+
<div id="toast-root"></div>
1516
<script type="module" src="/src/main.tsx"></script>
1617
</body>
1718
</html>

src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { RouterProvider } from "react-router-dom";
22

33
import { router } from "./Router";
4+
import ToastContainer from "@/components/Toast/ToastContainer";
45

56
function App() {
6-
return <RouterProvider router={router} />;
7+
return (
8+
<>
9+
<RouterProvider router={router} />
10+
<ToastContainer />
11+
</>
12+
);
713
}
814
export default App;

src/components/Toast/Toast.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cn } from "@/utils/cn";
2+
3+
interface ToastProps {
4+
label: string;
5+
className?: string;
6+
}
7+
8+
export default function Toast({ label, className }: ToastProps) {
9+
return (
10+
<div
11+
className={cn(
12+
"relative inline-block rounded-md bg-red-30 px-4 py-[10px] text-white body1-regular",
13+
className,
14+
)}
15+
>
16+
{label}
17+
</div>
18+
);
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useToast } from "@/hooks/useToast";
2+
import ToastPortal from "@/components/Toast/ToastPortal";
3+
import Toast from "@/components/Toast/Toast";
4+
5+
export default function ToastContainer() {
6+
const { toasts } = useToast();
7+
8+
return (
9+
<ToastPortal>
10+
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center gap-y-2 pointer-events-none">
11+
{toasts.map((toast) => (
12+
<Toast
13+
key={toast.id}
14+
label={toast.label}
15+
className={`transition-opacity duration-500 ${
16+
toast.isVisible ? "opacity-100" : "opacity-0"
17+
}`}
18+
/>
19+
))}
20+
</div>
21+
</ToastPortal>
22+
);
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createPortal } from "react-dom";
2+
import { ReactNode, useEffect, useState } from "react";
3+
4+
interface ToastPortalProps {
5+
children: ReactNode;
6+
}
7+
8+
export default function ToastPortal({ children }: ToastPortalProps) {
9+
const [mounted, setMounted] = useState(false);
10+
11+
useEffect(() => {
12+
setMounted(true);
13+
}, []);
14+
15+
if (typeof window === "undefined") return null;
16+
17+
const el = document.getElementById("toast-root");
18+
if (!el || !mounted) return null;
19+
20+
return createPortal(children, el);
21+
}

src/hooks/useToast.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { create } from "zustand";
2+
3+
interface ToastItem {
4+
id: number;
5+
label: string;
6+
isVisible: boolean;
7+
}
8+
9+
interface ToastState {
10+
toasts: ToastItem[];
11+
showToast: (label: string) => Promise<void>;
12+
removeToast: (id: number) => void;
13+
}
14+
15+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
16+
17+
export const useToast = create<ToastState>((set) => ({
18+
toasts: [],
19+
showToast: async (label) => {
20+
const id = Date.now();
21+
set((state) => ({
22+
toasts: [...state.toasts, { id, label, isVisible: true }],
23+
}));
24+
25+
await delay(1000);
26+
set((state) => ({
27+
toasts: state.toasts.map((toast) =>
28+
toast.id === id ? { ...toast, isVisible: false } : toast,
29+
),
30+
}));
31+
32+
await delay(500);
33+
set((state) => ({
34+
toasts: state.toasts.filter((toast) => toast.id !== id),
35+
}));
36+
},
37+
removeToast: (id) =>
38+
set((state) => ({
39+
toasts: state.toasts.filter((toast) => toast.id !== id),
40+
})),
41+
}));

0 commit comments

Comments
 (0)