Skip to content

Commit

Permalink
Merge pull request #130 from LellowMellow/feature/231122-add-toast-co…
Browse files Browse the repository at this point in the history
…mponent

Feature(#133): Toast 공용 컴포넌트 구현
  • Loading branch information
LellowMellow authored Nov 23, 2023
2 parents 13324d9 + e5bd81d commit 00457bd
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 794 deletions.
793 changes: 0 additions & 793 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import MicTest from "./pages/MicTest/MicTest";
import { RecoilRoot } from "recoil";
import Example from "./pages/Example/Example";

import ToastContainer from "./components/Toast/ToastContainer";

const App = () => {
return (
<RecoilRoot>
<ToastContainer />
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/assets/svgs/toast/alert.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/svgs/toast/default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/svgs/toast/success.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/components/Header/components/HeaderLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const HeaderLogo = ({ type }: LogoButtonProps) => {
return (
<>
<button type="button" className="flex flex-row items-center gap-4" onClick={moveToMainPage}>
<img src={logoSmall} />
<img src={logoSmall} alt="로고" />
{type === "normal" && <h1>Boarlog</h1>}
</button>
{type === "lecture" && <h1>lecture name</h1>}
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/components/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import { toastListState } from "./toastAtom";
import { useRecoilValue } from "recoil";
import usePortal from "./usePortal";
import ReactDOM from "react-dom";
import SuccessIcon from "@/assets/svgs/toast/success.svg?react";
import AlertIcon from "@/assets/svgs/toast/alert.svg?react";
import DefaultIcon from "@/assets/svgs/toast/default.svg?react";
import { TOAST_AVAILABLE_TIME, TOAST_ANIMATION_TIME } from "./constants";

interface ToastProps {
toastKey: number;
message: string;
type: "alert" | "success" | "default";
}

const Toast = ({ toastKey, message, type }: ToastProps) => {
const [animation, setAnimation] = useState(true);
const [toastList, setToastList] = useRecoilState(toastListState);

const handleClickToast = (id: number) => {
setToastList(() => toastList.filter((toast) => toast.id !== id));
};

useEffect(() => {
const timer = setTimeout(() => {
setAnimation(false);
}, TOAST_AVAILABLE_TIME - TOAST_ANIMATION_TIME);

return () => clearTimeout(timer);
}, []);

return (
<div
key={toastKey}
className={`rounded-xl medium-16 w-full text-grayscale-black px-4 py-4 flex flex-row items-center justify-start gap-3 cursor-pointer shadow-ml ${
animation ? "toast-fade-in" : "toast-fade-out"
} ${type === "alert" && "bg-alert-10"} ${type === "success" && "bg-boarlog-10"} ${
type === "default" && "bg-grayscale-lightgray"
}`}
style={{ transform: `translateY(-${top}px)`, transition: "transform 0.5s ease" }}
onClick={() => handleClickToast(toastKey)}
>
{type === "alert" && <AlertIcon className="fill-alert-100 w-5 h-5" />}
{type === "success" && <SuccessIcon className="fill-boarlog-100 w-5 h-5" />}
{type === "default" && <DefaultIcon className="fill-grayscale-black w-5 h-5" />}

<p className="w-full break-all">{message}</p>
</div>
);
};

const ToastContainer = () => {
const toastList = useRecoilValue(toastListState);
const portalRoot = usePortal("toast-portal");

return portalRoot
? ReactDOM.createPortal(
<div className="fixed w-11/12 max-w-xs bottom-3 left-1/2 -translate-x-1/2 space-y-3">
{toastList.map((toast) => (
<Toast key={toast.id} toastKey={toast.id} message={toast.message} type={toast.type} />
))}
</div>,
portalRoot
)
: null;
};

export default ToastContainer;
2 changes: 2 additions & 0 deletions frontend/src/components/Toast/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TOAST_AVAILABLE_TIME = 3000;
export const TOAST_ANIMATION_TIME = 500;
7 changes: 7 additions & 0 deletions frontend/src/components/Toast/toastAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { atom } from "recoil";
import { ToastMessage } from "./toastType";

export const toastListState = atom<ToastMessage[]>({
key: "toastListState",
default: []
});
5 changes: 5 additions & 0 deletions frontend/src/components/Toast/toastType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ToastMessage {
id: number;
message: string;
type: "alert" | "success" | "default";
}
28 changes: 28 additions & 0 deletions frontend/src/components/Toast/usePortal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useRef } from "react";

const usePortal = (id: string) => {
const rootElementRef = useRef<HTMLElement | null>(null);

useEffect(() => {
let parentElement = document.querySelector(`#${id}`) as HTMLElement | null;

// id가 있는 element가 없으면 새로 생성
if (!parentElement) {
parentElement = document.createElement("div");
parentElement.setAttribute("id", id);
document.body.appendChild(parentElement);
}

rootElementRef.current = parentElement;

return () => {
if (parentElement && !parentElement.childElementCount) {
parentElement.remove();
}
};
}, [id]);

return rootElementRef.current;
};

export default usePortal;
24 changes: 24 additions & 0 deletions frontend/src/components/Toast/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useRecoilState } from "recoil";
import { toastListState } from "./toastAtom";
import { ToastMessage } from "./toastType";
import { TOAST_AVAILABLE_TIME } from "./constants";

interface UseToastProps {
message: string;
type: "alert" | "success" | "default";
}

export const useToast = () => {
const [toastList, setToastList] = useRecoilState(toastListState);

const showToast = ({ message, type }: UseToastProps) => {
const newToast: ToastMessage = { id: Date.now(), message, type };
setToastList([...toastList, newToast]);

setTimeout(() => {
setToastList((currentList) => currentList.filter((toast) => toast.id !== newToast.id));
}, TOAST_AVAILABLE_TIME);
};

return showToast;
};
30 changes: 30 additions & 0 deletions frontend/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,33 @@
.display-center {
@apply fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}

@keyframes fadeIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

@keyframes fadeOut {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-20%);
opacity: 0;
}
}

.toast-fade-in {
animation: fadeIn 0.5s ease-in-out;
}

.toast-fade-out {
animation: fadeOut 0.5s ease-in-out;
}
22 changes: 22 additions & 0 deletions frontend/src/pages/Example/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { useState } from "react";
import Header from "@/components/Header/Header";
import Button from "@/components/Button/Button";
import Modal from "@/components/Modal/Modal";
import { useToast } from "@/components/Toast/useToast";

const Example = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const showToast = useToast();

return (
<>
Expand Down Expand Up @@ -85,6 +87,26 @@ const Example = () => {
/>

<hr className="mt-3" />

<h2 className="semibold-32 ml-3 mt-3">Toast Component</h2>
<p className="semibold-16 ml-3 mt-2 mb-3 text-grayscale-darkgray">
Toast 컴포넌트입니다. 경우에 따라 Alert, Success, Default로 나누어 사용할 수 있습니다.
</p>

<h3 className="semibold-20 ml-3 mt-3 mb-3">Alert Toast</h3>
<Button type="fit" buttonStyle="red" onClick={() => showToast({ message: "경고 메세지", type: "alert" })}>
Alert Toast
</Button>

<h3 className="semibold-20 ml-3 mt-3 mb-3">Success Toast</h3>
<Button type="fit" buttonStyle="blue" onClick={() => showToast({ message: "성공 메세지", type: "success" })}>
Success Toast
</Button>

<h3 className="semibold-20 ml-3 mt-3 mb-3">Black Toast</h3>
<Button type="fit" buttonStyle="black" onClick={() => showToast({ message: "안내 메세지", type: "default" })}>
Black Toast
</Button>
</>
);
};
Expand Down
3 changes: 3 additions & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
"boarlog-30": "#cbcbfe",
"boarlog-10": "#eeeeff",
"alert-100": "#fb4f4f",
"alert-80": "#dc807a",
"alert-30": "#efcfcd",
"alert-10": "#faefee",
"grayscale-black": "#000000",
"grayscale-darkgray": "#595959",
"grayscale-gray": "#b3b3b3",
Expand Down
7 changes: 7 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build]
ignore = "git diff --quiet HEAD^ HEAD frontend/"

[[redirects]]
from = "/*"
to = "/index.html"
status = 200

0 comments on commit 00457bd

Please sign in to comment.