-
Notifications
You must be signed in to change notification settings - Fork 4
[feat] Toast 컴포넌트 구현 #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
91914cb
504aea1
2f68450
f141276
e48acab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,14 @@ | ||
| import { RouterProvider } from "react-router-dom"; | ||
|
|
||
| import { router } from "./Router"; | ||
| import ToastContainer from "@/components/Toast/ToastContainer"; | ||
|
|
||
| function App() { | ||
| return <RouterProvider router={router} />; | ||
| return ( | ||
| <> | ||
| <RouterProvider router={router} /> | ||
| <ToastContainer /> | ||
| </> | ||
| ); | ||
| } | ||
| export default App; |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { cn } from "@/utils/cn"; | ||
|
|
||
| interface ToastProps { | ||
| label: string; | ||
| className?: string; | ||
| } | ||
|
|
||
| export default function Toast({ label, className }: ToastProps) { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| "inline-block rounded-md bg-red-30 px-4 py-[10px] text-white body1-regular", | ||
| className, | ||
| )} | ||
| > | ||
| {label} | ||
| </div> | ||
| ); | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉 넵 수정하겠습니다! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| interface ToastProps { | ||
| label: string; | ||
| onClose?: () => void; | ||
| } | ||
|
|
||
| export default function Toast({ label, onClose }: ToastProps) { | ||
| return ( | ||
| <div className="relative inline-block rounded-md bg-red-30 px-4 py-[10px] text-white body1-regular"> | ||
| {label} | ||
| {onClose && ( | ||
|
||
| <button | ||
| onClick={onClose} | ||
| className="absolute top-1 right-2 text-white text-xs" | ||
| > | ||
| ✕ | ||
| </button> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 ( | ||||||||||||||||||||||||||||||||||
| <ToastPortal> | ||||||||||||||||||||||||||||||||||
| <div className="fixed inset-0 z-50 flex flex-col items-center justify-center pointer-events-none"> | ||||||||||||||||||||||||||||||||||
| {toasts.map((toast) => ( | ||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||
| key={toast.id} | ||||||||||||||||||||||||||||||||||
| className={`transition-opacity duration-500 ${ | ||||||||||||||||||||||||||||||||||
| toast.isVisible ? "opacity-100" : "opacity-0" | ||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| <Toast label={toast.label} /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| <div | |
| key={toast.id} | |
| className={`transition-opacity duration-500 ${ | |
| toast.isVisible ? "opacity-100" : "opacity-0" | |
| }`} | |
| > | |
| <Toast label={toast.label} /> | |
| </div> | |
| <Toast | |
| key={toast.id} | |
| label={toast.label} | |
| className={cn( | |
| "transition-opacity duration-500", | |
| toast.isVisible ? "opacity-100" : "opacity-0", | |
| )} | |
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵! 말씀해주신 부분들 반영하여 코드 수정해보겠습니다!!😊
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<ToastState>((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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | |
| }, | |
| showToast: async (label) => { | |
| const id = Date.now(); | |
| set((state) => ({ | |
| toasts: [...state.toasts, { id, label, isVisible: true }], | |
| })); | |
| await new Promise((r) => setTimeout(r, 1000)); | |
| set((state) => ({ | |
| toasts: state.toasts.map((t) => | |
| t.id === id ? { ...t, isVisible: false } : t | |
| ), | |
| })); | |
| await new Promise((r) => setTimeout(r, 500)); | |
| set((state) => ({ | |
| toasts: state.toasts.filter((t) => t.id !== id), | |
| })); | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
말씀해주신 부분들 반영하여 코드 수정하도록 하겠습니다!!😊

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Toast라고 하는 UI가 할 수 있어야 할 동작들이 있을 것 같아요! 🤔사용자가 어떤 행동(ex: 버튼 클릭 등)을 했을 때,
해당 행동이 정상적으로 잘 이뤄졌는지 피드백을 하기 위해서
브라우저 어딘가에 작은 창으로 메시지를 안내하는 것처럼요!
그래서 UI뿐만 아니라 전역적으로 해당 메시지를 바로 나타나게 할 수 있는 훅 등도 같이 구현되면 좋을 것 같아요 😅
UI는 다르지만, 참고할 만한 링크도 같이 첨부해봅니다.
참고1
참고2
혹시 어려우신 부분이 있으시다면, 알려주시면 같이 구현해봐도 좋을 것 같습니다! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 넵 링크 참고해서 코드 다시 수정해보도록 하겠습니다!👍👍