Skip to content
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

feat: toast 컴포넌트를 구현합니다 #221

Merged
merged 8 commits into from
Aug 26, 2024
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
15 changes: 15 additions & 0 deletions app/api/memory/[memoryId]/reaction/eligibility/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';

import { fetchData } from '@/apis/fetch-data';

export async function GET(
request: NextRequest,
{ params }: { params: { memoryId: number } },
) {
const data = await fetchData<{ data: { eligibility: boolean } }>(
`/memory/${Number(params.memoryId)}/reaction/eligibility`,
'GET',
);

return NextResponse.json(data);
}
11 changes: 9 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import '../styles/global.css';

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { Metadata } from 'next';
import dynamic from 'next/dynamic';

import MetaTagImage from '@/public/images/meta-tag.png';
import { css } from '@/styled-system/css';
import { pretendard } from '@/styles/font';

import { PortalRoot } from './portal-root';
import ReactQueryProvider from './providers/ReactQueryProvider';

export const metadata: Metadata = {
Expand Down Expand Up @@ -35,6 +35,13 @@ const rootStyle = css({
overflow: 'scroll',
});

const DynamicPortalRoot = dynamic(
() => import('./portal-root').then(({ PortalRoot }) => PortalRoot),
{
ssr: false,
},
);

export default function RootLayout({
children,
}: Readonly<{
Expand All @@ -46,7 +53,7 @@ export default function RootLayout({
<ReactQueryProvider>
<ReactQueryDevtools initialIsOpen={true} />
<div className={containerStyle}>{children}</div>
<PortalRoot />
<DynamicPortalRoot />
</ReactQueryProvider>
</body>
</html>
Expand Down
7 changes: 5 additions & 2 deletions app/portal-root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Portal } from '@/components/atoms';
import { Portal, ToastDialog } from '@/components/atoms';
import { Dialog } from '@/components/molecules';
import { useDialog } from '@/hooks/use-dialog';

Expand All @@ -9,7 +9,10 @@ export const PortalRoot = () => {

return (
<Portal>
<Dialog {...dialogState} />
<>
<ToastDialog />
<Dialog {...dialogState} />
</>
</Portal>
);
};
1 change: 1 addition & 0 deletions components/atoms/dim/dim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Dim = {
* BottomSheet - dim: 500 / container: 550
* Modal - dim: 700 / container: 750
* Dialog - dim: 800 / container: 850
* Toast - container: 900
*/
export const Dim = ({ onClick, zIndex = 800 }: Dim) => {
return (
Expand Down
1 change: 1 addition & 0 deletions components/atoms/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './star-icon-fill';
export * from './success-check-icon';
export * from './swim-icon';
export * from './swimmer-icon';
export * from './toast';
export * from './triangle-arrow-icon';
export * from './triangle-arrow-icon-reverse';
export * from './types';
27 changes: 27 additions & 0 deletions components/atoms/icons/toast/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const Error = (props?: React.SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_18199_7240)">
<circle cx="12" cy="12" r="8" fill="white" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.1001 12.0001C2.1001 6.53248 6.53247 2.1001 12.0001 2.1001C17.4677 2.1001 21.9001 6.53248 21.9001 12.0001C21.9001 17.4677 17.4677 21.9001 12.0001 21.9001C6.53247 21.9001 2.1001 17.4677 2.1001 12.0001ZM13.0001 16.2501C13.0001 16.8024 12.5524 17.2501 12.0001 17.2501C11.4478 17.2501 11.0001 16.8024 11.0001 16.2501C11.0001 15.6978 11.4478 15.2501 12.0001 15.2501C12.5524 15.2501 13.0001 15.6978 13.0001 16.2501ZM11.1001 6.75012V13.7501H12.9001V6.75012H11.1001Z"
fill="#FF4242"
/>
</g>
<defs>
<clipPath id="clip0_18199_7240">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
};
3 changes: 3 additions & 0 deletions components/atoms/icons/toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './error';
export * from './success';
export * from './warning';
27 changes: 27 additions & 0 deletions components/atoms/icons/toast/success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const Success = (props?: React.SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_18199_7226)">
<circle cx="12" cy="12" r="8" fill="white" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.1459 9.62682L10.6766 16.2922L6.85426 12.3541L8.14591 11.1004L10.6766 13.7078L15.8543 8.37317L17.1459 9.62682ZM2.1001 12C2.1001 6.53236 6.53248 2.09998 12.0001 2.09998C17.4677 2.09998 21.9001 6.53236 21.9001 12C21.9001 17.4676 17.4677 21.9 12.0001 21.9C6.53248 21.9 2.1001 17.4676 2.1001 12Z"
fill="#00BF40"
/>
</g>
<defs>
<clipPath id="clip0_18199_7226">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
};
19 changes: 19 additions & 0 deletions components/atoms/icons/toast/warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const Warning = (props?: React.SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.1001 12.0001C2.1001 6.53248 6.53247 2.1001 12.0001 2.1001C17.4677 2.1001 21.9001 6.53248 21.9001 12.0001C21.9001 17.4677 17.4677 21.9001 12.0001 21.9001C6.53247 21.9001 2.1001 17.4677 2.1001 12.0001ZM13.0001 16.2501C13.0001 16.8024 12.5524 17.2501 12.0001 17.2501C11.4478 17.2501 11.0001 16.8024 11.0001 16.2501C11.0001 15.6978 11.4478 15.2501 12.0001 15.2501C12.5524 15.2501 13.0001 15.6978 13.0001 16.2501ZM11.1001 6.75012V13.7501H12.9001V6.75012H11.1001Z"
fill="#FFCF58"
/>
</svg>
);
};
1 change: 1 addition & 0 deletions components/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './icons';
export * from './image';
export * from './loading';
export * from './portal';
export * from './toast';
export * from './waves';
1 change: 1 addition & 0 deletions components/atoms/toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './toast';
88 changes: 88 additions & 0 deletions components/atoms/toast/toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client';

import { useAtomValue } from 'jotai';
import { PropsWithChildren, ReactNode } from 'react';

import { toastAtom } from '@/store';
import { css } from '@/styled-system/css';
import { flex } from '@/styled-system/patterns';

import { Error, Success, Warning } from '../icons';

export type ToastType = 'success' | 'error' | 'warning';
export type ToastProps = {
content: ReactNode;
type: ToastType;
};

export const ToastDialog = () => {
const toastState = useAtomValue(toastAtom);
const isOpen = toastState.size > 0;

return (
<dialog
Copy link
Member

@wokbjso wokbjso Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dialog 태그의 속성으로 인해 스크롤이 존재하는 페이지에서 toast가 띄워질 시,
사라질 때 스크롤이 페이지 바닥으로 이동하는 현상이 존재하는 것 같아요!

dialog 태그 스타일을 수정, 혹은 스크롤을 현재 위치로 고정해주는 로직이 추가되어야 자연스러운 동작이 될 것 같습니다~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 맞아요 방금 그 부분 수정했습니다!
layout shifting이 발생하여 dialog component의 스타일 속성을 변경했습니다.

const containerStyle = css({
  position: 'fixed',
  backgroundColor: 'transparent',
  zIndex: 900,
});

className={containerStyle}
open={isOpen}
ref={(el) => {
if (el && !el.open) el.show();
}}
>
<div className={toastWrapperStyle}>
{Array.from(toastState.entries()).map(([id, { content, type }]) => (
<Toast type={type} key={id}>
{content}
</Toast>
))}
</div>
</dialog>
);
};

const Toast = ({
type,
children,
}: PropsWithChildren<{
type: ToastType;
}>) => {
return (
<div className={toastStyle}>
{
{
success: <Success />,
error: <Error />,
warning: <Warning />,
}[type]
}
{children}
</div>
);
};

const containerStyle = css({
position: 'fixed',
backgroundColor: 'transparent',
zIndex: 900,
});

const toastWrapperStyle = flex({
direction: 'column',
gap: '4px',
position: 'fixed',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
marginBottom: 'calc(35px + env(safe-area-inset-bottom))',
});

const toastStyle = flex({
textWrap: 'nowrap',
alignItems: 'center',
gap: '10px',
textStyle: 'body2.normal',
fontWeight: 'medium',
color: 'white',
p: '12px 16px',
backgroundColor: 'fill.highlight',
rounded: '16px',
animation: 'fadeUp 0.3s',
});
1 change: 1 addition & 0 deletions features/record-detail/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './use-cheer';
export * from './use-cheer-eligibility';
export * from './use-cheer-list';
export * from './use-cheer-preview-list';
export * from './use-cheer-remove';
27 changes: 27 additions & 0 deletions features/record-detail/apis/use-cheer-eligibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useQuery } from '@tanstack/react-query';

const fetchCheerEligibility = async (memoryId: number) => {
const res = await fetch(`/api/memory/${memoryId}/reaction/eligibility`, {
headers: {
'Content-Type': 'application/json',
},
});

return res.json();
};

export const useCheerEligibility = (memoryId: number, isMyMemory?: boolean) => {
const query = useQuery<{ data: { isRegistrable: boolean } }>({
queryKey: ['useCheerEligibility', memoryId],
queryFn: () => fetchCheerEligibility(memoryId),
retry: 3,
enabled: !!memoryId && !isMyMemory,
});

return {
...query,
data: query.data?.data,
};
};
20 changes: 17 additions & 3 deletions features/record-detail/sections/detail-cheer-fab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,42 @@

import { useState } from 'react';

import { useBottomSheet } from '@/hooks';
import { useBottomSheet, useToast } from '@/hooks';
import { css } from '@/styled-system/css';

import { useCheer, useCheerPreviewList } from '../apis';
import { useCheer, useCheerEligibility, useCheerPreviewList } from '../apis';
import { CheerBottomSheet, CheerProgress } from '../components';
import { initialCheerList } from '../data';
import { DetailCheerItemSelected, RecordDetailType } from '../types';

export const DetailCheerFabSection = ({ data }: { data: RecordDetailType }) => {
const { mutate: mutateCheer } = useCheer();
const { refetch: refetchCheer } = useCheerPreviewList(data.id);
const { data: eligibilityData } = useCheerEligibility(
data.id,
data.isMyMemory,
);

const [cheerList, setCheerList] = useState(initialCheerList);
const [selectedCheerItem, setSelectedCheerItem] =
useState<DetailCheerItemSelected>();

const { toast } = useToast();
const {
isOpen: isOpenBottomSheet,
open: openBottomSheet,
close: closeBottomSheet,
} = useBottomSheet();

const handleClickFab = () => {
if (!eligibilityData?.isRegistrable) {
toast('하나의 기록에 3번까지 응원을 보낼 수 있어요', { type: 'warning' });
return;
}

openBottomSheet();
};

const handleClickCheerItem = (index: number) => {
setCheerList((prev) =>
prev.map((item, idx) =>
Expand Down Expand Up @@ -70,7 +84,7 @@ export const DetailCheerFabSection = ({ data }: { data: RecordDetailType }) => {
<>
{/* NOTE: 응원 FAB Button */}
{!isMyMemory && (
<button className={cheerButtonWrapperStyle} onClick={openBottomSheet}>
<button className={cheerButtonWrapperStyle} onClick={handleClickFab}>
{data.member?.name}님에게 응원 보내기 👏
</button>
)}
Expand Down
1 change: 1 addition & 0 deletions hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './use-drag-scroll';
export * from './use-intersection-observer';
export * from './use-modal';
export * from './use-prevent-body-scroll';
export * from './use-toast';
Loading