Skip to content

Commit

Permalink
Merge pull request #230 from Neogasogaeseo/feat/#220
Browse files Browse the repository at this point in the history
  • Loading branch information
SeojinSeojin authored Mar 13, 2022
2 parents 1cac972 + b708c41 commit 74384ba
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 25 deletions.
11 changes: 7 additions & 4 deletions src/application/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useRecoilState } from 'recoil';

import { Toast, toastState } from '@stores/toast';
import { getRandomID } from '@utils/etc';
import { useRecoilState } from 'recoil';

export function useToast() {
const [toasts, setToasts] = useRecoilState(toastState);

const removeToast = (toastID: Toast['id']) =>
const removeToast = (toastID: Toast['id']) => {
setToasts((prev) => prev.filter((toast) => toast.id !== toastID));
};

const fireToast = (toast: Toast) => {
setToasts((prev) => [...prev, { ...toast, id: getRandomID() }]);
setTimeout(() => removeToast(toast.id), 600 + (toast.duration ?? 1000));
const toastID = getRandomID();
setToasts((prev) => [...prev, { ...toast, id: toastID, duration: toast.duration ?? 1000 }]);
setTimeout(() => removeToast(toastID), 600 + (toast.duration ?? 1000));
};

return { toasts, fireToast };
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/ic_meatball.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 src/assets/icons/ic_pick.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/ic_trash.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 src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ export { default as icNewTag } from './ic_new_tag.svg';
export { ReactComponent as IcEmptyFeedback } from './ic_empty_feedback.svg';
export { ReactComponent as IcEmptyKeyword } from './ic_empty_keyword.svg';
export { ReactComponent as IcLinkCoral } from './ic_link_coral.svg';
export { default as icPick } from './ic_pick.svg';
export { default as icTrash } from './ic_trash.svg';
export { ReactComponent as IcMeatball } from './ic_meatball.svg';
1 change: 1 addition & 0 deletions src/infrastructure/api/neoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface NeogaService {
getMainResultCard(): Promise<NeogaResultCardItem>;
getFormResultCard(): Promise<NeogaResultCardItem>;
postAnswerBookmark(answerID: number): Promise<{ isSuccess: boolean }>;
deleteAnswer(answerID: number): Promise<{ isSuccess: boolean }>;
postCreateForm(formID: number): Promise<{ isCreated: boolean; q: string }>;
getCreateFormInfo(formID: number): Promise<CreateFormInfo>;
getNeososeoInfo(formID: number): Promise<ResultDetail>;
Expand Down
6 changes: 6 additions & 0 deletions src/infrastructure/mock/neoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export function neogaDataMock(): NeogaService {
return { isSuccess: true };
};

const deleteAnswer = async () => {
await wait(1000);
return { isSuccess: true };
};

const postCreateForm = async () => {
await wait(2000);
return { isCreated: true, q: '큐' };
Expand Down Expand Up @@ -66,6 +71,7 @@ export function neogaDataMock(): NeogaService {
getMainResultCard,
getFormResultCard,
postAnswerBookmark,
deleteAnswer,
postCreateForm,
getCreateFormInfo,
getNeososeoInfo,
Expand Down
5 changes: 5 additions & 0 deletions src/infrastructure/remote/neoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export function NeogaDataRemote(): NeogaService {
};
};

const deleteAnswer = async () => {
return { isSuccess: true };
};

return {
getBannerTemplate,
getMainTemplate,
Expand All @@ -213,5 +217,6 @@ export function NeogaDataRemote(): NeogaService {
getCreateFormInfo,
getNeososeoInfo,
getNeososeoFeedback,
deleteAnswer,
};
}
23 changes: 7 additions & 16 deletions src/presentation/components/NeogaDetailFormCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { api } from '@api/index';
import { FeedAnswer } from '@api/types/neoga';
import { icBookmarkSelected, icBookmarkUnselected } from '@assets/icons';
import { IcMeatball } from '@assets/icons';
import ImmutableKeywordList from '@components/common/Keyword/ImmutableList';
import { useState } from 'react';
import {
StFeedContent,
StFeedDate,
Expand All @@ -11,16 +9,13 @@ import {
StNeogaDetailFormCard,
} from './style';

type NeogaDetailFormCardProps = FeedAnswer;
type NeogaDetailFormCardProps = FeedAnswer & {
openBottomSheet: (isPinned: boolean, id: number) => void;
};

function NeogaDetailFormCard(props: NeogaDetailFormCardProps) {
const { id, name, relationship, content, createdAt, keywordList } = props;
const [isBookmarked, setIsBookmarked] = useState(props.isPinned);

const bookmarkAnswer = async () => {
const response = await api.neogaService.postAnswerBookmark(id);
if (response.isSuccess) setIsBookmarked((prev) => !prev);
};
const { id, name, relationship, content, createdAt, keywordList, openBottomSheet, isPinned } =
props;

return (
<StNeogaDetailFormCard>
Expand All @@ -33,11 +28,7 @@ function NeogaDetailFormCard(props: NeogaDetailFormCardProps) {
<div>
<StFeedDate>
<div>{createdAt}</div>
<img
src={isBookmarked ? icBookmarkSelected : icBookmarkUnselected}
alt="bookmark"
onClick={bookmarkAnswer}
/>
<IcMeatball onClick={() => openBottomSheet(isPinned, id)} />
</StFeedDate>
</div>
</StFeedHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { api } from '@api/index';
import { icPick, icTrash } from '@assets/icons';
import { useToast } from '@hooks/useToast';
import BottomSheet from '..';

type NeososeoPickerBottomSheetProps = {
opened: boolean;
close: () => void;
isPinned: boolean;
id: number;
};

function NeososeoPickerBottomSheet(props: NeososeoPickerBottomSheetProps) {
const { opened, close, isPinned, id } = props;
const { fireToast } = useToast();

const bookmarkAnswer = async () => {
const response = await api.neogaService.postAnswerBookmark(id);
if (response.isSuccess) {
if (isPinned) fireToast({ content: '픽한 답변 삭제 완료' });
else fireToast({ content: 'MY에서 픽한 피드백을 확인할 수 있어요' });
close();
}
};

const removeAnswer = async () => {
const response = await api.neogaService.deleteAnswer(id);
if (response.isSuccess) {
fireToast({ content: '삭제 완료' });
close();
}
};

return (
<BottomSheet
buttonList={[
{ icon: icPick, label: isPinned ? '픽 취소하기' : '픽 하기', onClick: bookmarkAnswer },
{ icon: icTrash, label: '삭제하기', onClick: removeAnswer },
]}
closeBottomSheet={close}
isOpened={opened}
/>
);
}

export default NeososeoPickerBottomSheet;
54 changes: 54 additions & 0 deletions src/presentation/components/common/BottomSheet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useState } from 'react';
import {
StAbsoluteWrapper,
StBlackBlur,
StWrapper,
StButton,
StCancelButton,
StButtonWrapper,
} from './style';

type BottomSheetButton = {
icon: string;
label: string;
onClick: () => void;
};

type BottomSheetProps = {
isOpened: boolean;
buttonList: BottomSheetButton[];
closeBottomSheet: () => void;
};

function BottomSheet(props: BottomSheetProps) {
const { buttonList: buttons, isOpened: opened, closeBottomSheet: close } = props;
const [isClosing, setIsClosing] = useState(false);
const closeModal = () => {
setIsClosing(true);
setTimeout(() => {
close();
setIsClosing(false);
}, 1000);
};

return opened ? (
<StAbsoluteWrapper>
<StBlackBlur onClick={closeModal} isClosing={isClosing} />
<StWrapper isClosing={isClosing}>
<StButtonWrapper>
{buttons.map((button) => (
<StButton onClick={button.onClick} key={button.label}>
<img src={button.icon} alt={button.label} />
<div>{button.label}</div>
</StButton>
))}
</StButtonWrapper>
<StCancelButton onClick={closeModal}>취소</StCancelButton>
</StWrapper>
</StAbsoluteWrapper>
) : (
<></>
);
}

export default BottomSheet;
62 changes: 62 additions & 0 deletions src/presentation/components/common/BottomSheet/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ANIMATION } from '@styles/common/animation';
import { COLOR } from '@styles/common/color';
import { FONT_STYLES } from '@styles/common/font-style';
import styled from 'styled-components';

export const StAbsoluteWrapper = styled.div`
position: fixed;
width: 100%;
height: 100vh;
top: 0;
`;

export const StBlackBlur = styled.div<{ isClosing: boolean }>`
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgb(0, 0, 0, 0.44);
z-index: 50;
animation: ${({ isClosing }) => (isClosing ? ANIMATION.FADE_OUT : ANIMATION.FADE_IN)} 1s;
`;

export const StWrapper = styled.div<{ isClosing: boolean }>`
position: fixed;
width: min(100vw, 400px);
border-top-left-radius: 32px;
border-top-right-radius: 32px;
background-color: ${COLOR.WHITE};
bottom: 0;
z-index: 60;
animation: ${({ isClosing }) =>
isClosing ? ANIMATION.SWIPE_DOWN : ANIMATION.SWIPE_UP({ from: 0 })}
1s;
`;

export const StButton = styled.div`
display: flex;
align-items: center;
gap: 8px;
color: ${COLOR.GRAY_7};
${FONT_STYLES.R_16_BODY}
`;

export const StButtonWrapper = styled.div`
padding-top: 30px;
padding-bottom: 35px;
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 25px;
`;

export const StCancelButton = styled.div`
text-align: center;
border-top: 1px solid ${COLOR.GRAY_3};
padding-top: 16px;
padding-bottom: 52px;
color: ${COLOR.GRAY_6};
${FONT_STYLES.M_16_TITLE}
`;
4 changes: 3 additions & 1 deletion src/presentation/components/common/Toast/Item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ function ToastItem(props: Toast) {
setIsClosing(true);
clearTimeout(setExistTimeout);
}, duration ?? 1000);
});

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

return (
<StToastItem bottom={bottom} isClosing={isClosing}>
Expand Down
33 changes: 29 additions & 4 deletions src/presentation/pages/Neoga/FormDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery } from 'react-query';

import { useToast } from '@hooks/useToast';
import { copyClipboard } from '@utils/copyClipboard';
import { DOMAIN } from '@utils/constant';
import NeososeoFormHeader from '@components/common/NeososeoFormHeader';
import ImmutableKeywordList from '@components/common/Keyword/ImmutableList';
import NeogaDetailFormEmptyView from '@components/common/Empty/NeogaDetailForm';
import NeogaDetailFormCard from '@components/NeogaDetailFormCard';
import {
StNeogaDetailForm,
StDate,
Expand All @@ -20,12 +20,18 @@ import {
StDivisionLine,
} from './style';
import { icLink, IcArrowDown, IcArrowUp } from '@assets/icons/index';
import { useQuery } from 'react-query';
import { api } from '@api/index';
import NeogaDetailFormCard from '@components/NeogaDetailFormCard';
import NeososeoPickerBottomSheet from '@components/common/BottomSheet/NeososeoPicker';

function NeogaDetailForm() {
const { formID } = useParams();
const [lookMoreButton, setLookMoreButton] = useState(false);
const [bottomSheetOpened, setBottomSheetOpened] = useState(false);
const [bottomSheetState, setBottomSheetState] = useState<{ id: number; isPinned: boolean }>({
id: 0,
isPinned: false,
});

const { fireToast } = useToast();
const navigate = useNavigate();
Expand All @@ -39,12 +45,22 @@ function NeogaDetailForm() {
);
const link = `${DOMAIN}/neososeoform/${resultDetail?.q ?? ''}`;

const { data: resultFeedback } = useQuery(
const { data: resultFeedback, refetch: refetchFeedbacks } = useQuery(
['nssFeedbacksDetail', formID],
() => api.neogaService.getNeososeoFeedback(+(formID ?? 0)),
{ useErrorBoundary: true, retry: 1 },
);

const openBottomSheet = (isPinned: boolean, id: number) => {
setBottomSheetState({ id, isPinned });
setBottomSheetOpened(true);
};

const closeBottomSheet = () => {
setBottomSheetOpened(false);
refetchFeedbacks();
};

if (!resultDetail) return <>로딩중</>;
return (
<StNeogaDetailForm>
Expand Down Expand Up @@ -116,12 +132,21 @@ function NeogaDetailForm() {
의 답변을 받았어요
</StFeedTitle>
{resultFeedback.answerList.map((feedback) => (
<NeogaDetailFormCard key={feedback.id} {...feedback} />
<NeogaDetailFormCard
key={feedback.id}
{...feedback}
openBottomSheet={openBottomSheet}
/>
))}
</>
) : (
<NeogaDetailFormEmptyView link={link} />
)}
<NeososeoPickerBottomSheet
opened={bottomSheetOpened}
close={closeBottomSheet}
{...bottomSheetState}
/>
</StNeogaDetailForm>
);
}
Expand Down
Loading

0 comments on commit 74384ba

Please sign in to comment.