Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@tanstack/react-query": "^5.64.2",
"canvas-confetti": "^1.9.3",
Expand Down
71 changes: 59 additions & 12 deletions src/components/ui/Toast/Toast.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,67 @@
}
}

.ToastViewport {
position: fixed;
left: 50%;
bottom: 7rem;
z-index: 9999;
width: fit-content;
display: flex;
flex-direction: column;
gap: 0.5rem;
transform: translateX(-50%);
width: calc(100% - 2.5rem);
}

.Toast {
width: 100%;
height: 3.25rem;
z-index: 2;
border-radius: 0.75rem;
background-color: white;
padding: 0.875rem 1.125rem;
&[data-swipe="cancel"] {
transform: translateX(0);
}
&[data-swipe="end"] {
transform: translateX(var(--radix-toast-swipe-end-x));
}
&[data-swipe="move"] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
&[data-state="open"] {
animation: slide-in-from-bottom-full 0.4s ease-out forwards;
}

&[data-state="closed"] {
animation: slide-out-to-bottom-full 0.4s ease-in forwards;
}
}

@keyframes slide-in-from-bottom-full {
from {
opacity: 0;
transform: translateY(300%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

transition:
opacity 0.3s,
transform 0.3s;
@keyframes slide-out-to-bottom-full {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(300%);
}
}

.show {
opacity: 1;
transform: translateY(0);
.Toaster {
border-radius: 0.75rem;
background-color: var(--color-white);
width: 100%;
padding: 0.875rem 1.125rem;

& > span {
line-height: 1.5rem;
}
}
20 changes: 0 additions & 20 deletions src/components/ui/Toast/Toast.stories.tsx

This file was deleted.

52 changes: 43 additions & 9 deletions src/components/ui/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
import { forwardRef } from "react";

import * as ToastPrimitives from "@radix-ui/react-toast";
import classNames from "classnames";

import Text from "@/components/ui/Text/Text";
import styles from "@/components/ui/Toast/Toast.module.scss";

export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
}
import { useToast } from "@/hooks/common/useToast";

const ToastProvider = ToastPrimitives.Provider;

const ToastViewport = forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={classNames(styles.ToastViewport, className)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;

const Toast = forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>
>(({ className, open, id, ...props }, ref) => {
const { removeToast } = useToast();

const Toast = forwardRef<HTMLDivElement, ToastProps>(({ text, className, ...props }, ref) => {
return (
<div ref={ref} className={classNames(styles.Toast, className)} {...props}>
<Text variant="bodyM">{text}</Text>
</div>
<ToastPrimitives.Root
ref={ref}
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
setTimeout(() => removeToast(id), 400); // 0.4초 뒤 제거
}
}}
className={classNames(styles.Toast, className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;

const ToastTitle = forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={className} {...props} />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;

export default Toast;
export { ToastProvider, ToastViewport, Toast, ToastTitle };
32 changes: 32 additions & 0 deletions src/components/ui/Toast/Toaster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Text from "@/components/ui/Text/Text";
import { Toast, ToastProvider, ToastViewport } from "@/components/ui/Toast/Toast";
import styles from "@/components/ui/Toast/Toast.module.scss";

import { useToast } from "@/hooks/common/useToast";

export function Toaster() {
const { toasts, removeToast } = useToast();

return (
<ToastProvider swipeDirection="right">
{toasts.map(({ id, duration, open }) => (
<Toast
key={id}
open={open}
duration={duration}
onOpenChange={(isOpen) => {
if (!isOpen) {
setTimeout(() => removeToast(id), 400);
}
}}
className={styles.Toaster}
>
<Text color="primary" variant="bodyM">
링크가 복사되었어요.
</Text>
</Toast>
))}
<ToastViewport />
</ToastProvider>
);
}
21 changes: 0 additions & 21 deletions src/hooks/common/useToast.ts

This file was deleted.

59 changes: 59 additions & 0 deletions src/hooks/common/useToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactNode } from "react";
import { createContext, useContext, useState } from "react";

type ToastType = {
id: string;
title: string;
open: boolean;
duration?: number;
};

type ToastContextType = {
toasts: ToastType[];
addToast: (title: string, duration?: number) => string;
removeToast: (id?: string) => void;
};

const ToastContext = createContext<ToastContextType | undefined>(undefined);

export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<ToastType[]>([]);

const addToast = (title: string, duration = 3000): string => {
const id = crypto.randomUUID();

const isToastOpen = toasts.some((toast) => toast.open);

if (isToastOpen) {
return "";
}

const newToast: ToastType = {
id,
title,
open: true,
duration,
};
setToasts((prev) => [...prev, newToast]);

return id;
};

const removeToast = (id?: string) => {
setToasts((prev) => prev.map((toast) => (toast.id === id ? { ...toast, open: false } : toast)));
};

return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
</ToastContext.Provider>
);
}

export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
}
10 changes: 8 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import AppRouter from "@/router/AppRouter";
import { AppBridgeProvider } from "@/components/provider/AppBridgeProvider/AppBridgeProvider";
import ReactQueryClientProvider from "@/components/provider/ReactQueryClientProvider";
import { UserAgentProvider } from "@/components/provider/UserAgentProvider";
import { Toaster } from "@/components/ui/Toast/Toaster";

import { ToastProvider } from "@/hooks/common/useToast";

import "@/styles/reset.scss";
import "@/styles/global.scss";
Expand All @@ -17,8 +20,11 @@ ReactDom.createRoot(document.getElementById("root")!).render(
<ReactQueryClientProvider>
<UserAgentProvider>
<AppBridgeProvider>
<AppRouter />
<ReactQueryDevtools initialIsOpen={false} />
<ToastProvider>
<AppRouter />
<ReactQueryDevtools initialIsOpen={false} />
<Toaster />
</ToastProvider>
</AppBridgeProvider>
</UserAgentProvider>
</ReactQueryClientProvider>
Expand Down
6 changes: 5 additions & 1 deletion src/pages/ReviewResultPage/ReviewResultPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Text from "@/components/ui/Text/Text";

import { useOverlay } from "@/hooks/common/useOverlay";
import { useRoute } from "@/hooks/common/useRoute";
import { useToast } from "@/hooks/common/useToast";

import styles from "@/pages/ReviewResultPage/ReviewResultPage.module.scss";

Expand All @@ -29,6 +30,8 @@ export default function ReviewResultPage() {

const { isOpen, handleClose, handleOpen } = useOverlay();

const { addToast } = useToast();

const { ocrText, hashTag, reviewStyle } = createReviewData;

const handleConfetti = () => {
Expand Down Expand Up @@ -86,11 +89,12 @@ export default function ReviewResultPage() {
size="sm"
onClick={() => {
send({ type: AppBridgeMessageType.COPY, payload: { review: generateReviewData } });

addToast("리뷰가 복사되었어요");
}}
/>
</div>
</div>

<div className={styles.Bottom}>
<Button text="다시생성" variant="secondary" onClick={handleRetryCreateReview} />
<Button text="홈으로 가기" onClick={handleOpen} />
Expand Down
Loading
Loading