From 8f6a682cea64ced5c94a17b5e0e9a6f847855f19 Mon Sep 17 00:00:00 2001 From: jjunyjjuny Date: Fri, 3 Dec 2021 13:44:21 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20feed=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Feed/index.tsx | 175 ------ src/pages/feeds/FeedErrandDetail/index.tsx | 560 ------------------ .../FeedRequestForm/ImageAppender/index.tsx | 89 --- .../feeds/FeedRequestForm/ImageBox/index.tsx | 49 -- src/pages/feeds/FeedRequestForm/index.tsx | 385 ------------ 5 files changed, 1258 deletions(-) delete mode 100644 src/pages/Feed/index.tsx delete mode 100644 src/pages/feeds/FeedErrandDetail/index.tsx delete mode 100644 src/pages/feeds/FeedRequestForm/ImageAppender/index.tsx delete mode 100644 src/pages/feeds/FeedRequestForm/ImageBox/index.tsx delete mode 100644 src/pages/feeds/FeedRequestForm/index.tsx diff --git a/src/pages/Feed/index.tsx b/src/pages/Feed/index.tsx deleted file mode 100644 index f3b4014..0000000 --- a/src/pages/Feed/index.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useCallback, useState } from "react"; -import styled from "@emotion/styled"; -import { SectionWrapper, StickyFooter, StickyPageWrpper } from "@styles/shared"; -import CustomScreenHelmet from "@components/CustomScreenHelmet"; -import { Title } from "@pages/Home"; -import Button from "@components/Button"; -import NodataImage from "@assets/images/no-data.png"; -import { CATEGORYS } from "@constant/category"; -import { Category } from "@type/response"; -import { Check } from "@assets/icon"; -import { useNavigator } from "@karrotframe/navigator"; - -export default function Feed() { - const { push } = useNavigator(); - - const [selectedCategoryId, setSelectedCategoryId] = useState( - null - ); - - const handleClickCategory = useCallback( - (id: number) => { - setSelectedCategoryId(id); - }, - [setSelectedCategoryId] - ); - - const handleClickButton = () => { - push(`/errand-request?categoryId=${selectedCategoryId}`); - }; - - return ( - - -

당근심부름

- Beta - - } - /> - - -
-

당근 심부름을 이용해 보세요.

-
-
- -

- 당근심부름은 동네 이웃에게 필요한 심부름을 부탁하는 서비스에요. -

-
- nodata -
-
-
-
- -
-

심부름 카테고리를 선택해주세요.

-
-
- {CATEGORYS.map((category) => ( - - ))} -
-
-
- - - -
- ); -} - -const FeedWrapper = styled.div` - padding-top: 2rem; - ${({ theme }) => theme.container} - height: 100%; -`; - -const Banner = styled.div` - background: #fff0e8; - border: 0.1rem solid #f1d6c7; - border-radius: 0.8rem; - - display: flex; - height: 11.8rem; - - & > p { - padding: 2rem 1.8rem; - } - - & > div { - display: flex; - align-items: flex-end; - } -`; - -const CategoryItem = ({ - id, - name, - subscribe, - imageUrl, - isSelected, - handleClickCategory, -}: Category & { - isSelected: boolean; - handleClickCategory: (id: number) => void; -}) => { - return ( - { - handleClickCategory(id); - }} - > -
- -
-
-

{name}

- {subscribe &&

{subscribe}

} -
-
- {name} -
-
- ); -}; - -const CategoryItemWrapper = styled.div<{ isSelected: boolean }>` - width: 100%; - display: flex; - align-items: center; - - .category-item__check { - display: flex; - justify-content: center; - align-items: center; - - margin: 2.3rem 1.7rem; - & > svg { - width: 3rem; - height: 3rem; - fill: ${({ theme, isSelected }) => - isSelected ? theme.color.primary : theme.color.grey8}; - stroke: ${({ theme, isSelected }) => - isSelected ? "white" : theme.color.grey6}; - stroke-width: 0.05rem; - } - } - - .category-item__text { - flex: 1; - } - - &:not(:first-child) { - border-top: 0.1rem solid ${({ theme }) => theme.color.grey7}; - } -`; diff --git a/src/pages/feeds/FeedErrandDetail/index.tsx b/src/pages/feeds/FeedErrandDetail/index.tsx deleted file mode 100644 index 9f0660a..0000000 --- a/src/pages/feeds/FeedErrandDetail/index.tsx +++ /dev/null @@ -1,560 +0,0 @@ -import styled from "@emotion/styled"; -import usePush from "@hooks/usePush"; -import { StickyFooter, StickyPageWrpper } from "@styles/shared"; -import { - confirmIsAppliable, - useCompleteErrand, - useDeleteErrand, - useErrandDetail, -} from "@api/errands"; -import CustomScreenHelmet from "@components/CustomScreenHelmet"; -import { Copy, Meatballs } from "@assets/icon"; -import { convertToKRW } from "@utils/convert"; -import Modal, { ModalInfoType } from "@components/Modal"; -import useModal from "@hooks/useModal"; -import Button from "@components/Button"; -import { getComparedTime } from "@utils/utils"; -import { - getRefinedFromData, - modalInfoFlagType, - specifyStatus, -} from "@utils/getRefinedFromData"; -import { useNavigator } from "@karrotframe/navigator"; -import { WithParamsProps } from "@hoc/withParams"; -import { ErrandDetailResponseBody } from "@type/response"; -import { useCallback } from "react"; -import { useCancelAPply } from "@api/help"; -import Slider from "react-slick"; -import CustomMixPanel from "@utils/mixpanel"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { toast } from "@components/Toast/Index"; - -export default function FeedErrandDetail({ errandId }: WithParamsProps) { - const { isOpen, openModal, closeModal, innerMode } = useModal(); - const { status, data } = useErrandDetail(errandId); - const { - color, - statusText, - buttonText, - buttonDisabled, - modalInfoFlag = "noModal", - buttonCallback, - } = getRefinedFromData(data); - const { push, replace, pop } = useNavigator(); - - const mutationDeleteErrand = useDeleteErrand({ - onSuccess: () => { - closeModal(); - if (localStorage.getItem("depth") === "0") { - replace("/"); - } else { - pop(); - } - toast("심부름이 삭제되었어요"); - }, - onError: () => { - closeModal(); - }, - }); - const mutationCancelApply = useCancelAPply({ - onSuccess: () => { - closeModal(); - toast("지원이 취소되었어요"); - }, - onError: () => { - closeModal(); - }, - }); - const mutationCompleteErrand = useCompleteErrand({ - onSuccess: () => { - closeModal(); - toast("🎉 수고했어요 🎉"); - }, - onError: () => { - closeModal(); - }, - }); - - const moveToApplyForm = usePush(`/apply-form?errandId=${errandId}`); - const moveToResume = useCallback(() => { - push(`/helps/${data?.helpId}`); - }, [data, push]); - const moveToAppliers = () => { - push(`/errands/${errandId}/appliers`); - }; - - const requestDeleteMyErrand = () => { - if (!errandId) return; - mutationDeleteErrand.mutate(errandId); - }; - const requestCancelApply = () => { - if (!data) return; - mutationCancelApply.mutate(String(data?.helpId)); - }; - const requestCompleteErrand = () => { - if (!errandId) return; - mutationCompleteErrand.mutate(errandId); - }; - - const applyToErrand = async () => { - const res = await confirmIsAppliable(errandId); - if (res.canApply) { - moveToApplyForm(); - } else { - toast("지원이 불가능해요"); - } - }; - - const handleClickButton = () => { - if (buttonDisabled) { - return; - } - switch (buttonCallback) { - case "moveToAppliers": - CustomMixPanel.track(CustomMixPanel.eventName.clickCTA, { - page: "심부름 상세", - clickTarget: "지원자 선택하기", - }); - moveToAppliers(); - break; - case "moveToApplyForm": - CustomMixPanel.track(CustomMixPanel.eventName.clickCTA, { - page: "심부름 상세", - clickTarget: "지원하기", - }); - applyToErrand(); - break; - case "moveToResume": - CustomMixPanel.track(CustomMixPanel.eventName.clickCTA, { - page: "심부름 상세", - clickTarget: "지원자 정보 보기", - }); - moveToResume(); - break; - case "openConfirmModal": - CustomMixPanel.track(CustomMixPanel.eventName.clickCTA, { - page: "심부름 상세", - clickTarget: "심부름 완료", - }); - openModal("confirm"); - break; - default: - break; - } - }; - - const getModalInfo = (flag: modalInfoFlagType) => { - switch (flag) { - case "isMyErrand": - return modalInfoOfIsMyErrand; - case "isApplier": - return modalInfoOfIsApplier; - case "resume": - return modalInfoOfResume; - case "isHelper": - return modalInfoOfHelper; - default: - return null; - } - }; - - const modalInfoOfIsMyErrand: ModalInfoType = { - list: [ - { - text: "삭제", - confirm: { - text: "삭제하시겠습니까?", - no: ( - - ), - yes: , - }, - }, - ], - }; - - // 내가 지원한 글, 모집중일 때 - const modalInfoOfIsApplier: ModalInfoType = { - list: [ - { - text: "지원취소", - confirm: { - text: "지원을 취소하시겠습니까?", - no: ( - - ), - yes: , - }, - }, - { - text: ( - - ), - }, - ], - }; - - // 헬퍼 / 모집 끝난 지원자 - const modalInfoOfResume: ModalInfoType = { - list: [ - { - text: ( - - ), - }, - ], - }; - - // 헬퍼 상태에서 완료 버튼 모달 정보 - const modalInfoOfHelper: ModalInfoType = { - list: [ - { - text: ( - - ), - }, - ], - confirm: { - text: "심부름을 완료했나요?", - no: ( - - ), - yes: , - }, - }; - - const modalInfo = getModalInfo(modalInfoFlag); - - return ( - - { - openModal("list"); - }} - /> - ) : ( - "" - ) - } - /> - - {status !== "loading" && data ? ( - <> -
- - {data?.errand.images?.map((image) => ( -
- -
- ))} -
-
-
-
-
- {data?.errand.category.name} - {data?.errand.region.name} - - {getComparedTime( - new Date(), - new Date(data?.errand.createdAt) - )} - -
- {renderStatus(color, statusText)} -
-
-
-
심부름 금액
-
{convertToKRW(data?.errand.reward ?? 0)}
-
-
-
심부름 장소
- {renderPrivateData(data, "detailAddress")} -
-
-
전화번호
- {renderPrivateData(data, "customerPhoneNumber")} -
-
-

{data?.errand.detail}

-
- - ) : ( -
- )} -
- {isOpen && modalInfo && innerMode && ( - - )} - - - -
- ); -} - -const ErrandDetailWrapper = styled.div` - .errand-detail { - &__dots { - position: absolute; - bottom: 2.4rem; - display: block; - width: 100%; - padding: 0; - margin: 0; - list-style: none; - text-align: center; - z-index: 99; - - & > li { - position: relative; - display: inline-block; - width: 10px; - height: 2rem; - margin: 0 5px; - padding: 0; - cursor: pointer; - & > button { - font-size: 0; - line-height: 0; - display: block; - width: 2rem; - height: 2rem; - padding: 5px; - cursor: pointer; - color: transparent; - border: 0; - outline: none; - background: transparent; - - &:hover, - &:focus { - outline: none; - } - &:hover:before, - &:focus:before { - opacity: 1; - } - &:before { - font-family: "slick"; - font-size: 0.6rem; - line-height: 2rem; - - position: absolute; - top: 0; - left: 0; - width: 2rem; - height: 2rem; - - content: "•"; - text-align: center; - - opacity: 0.5; - color: white; - - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - } - &.slick-active button:before { - opacity: 1; - color: white; - } - } - } - - &__image { - width: 100%; - height: 0; - padding-bottom: 90%; - /* height: 30rem; */ - overflow: hidden; - - & > img { - width: 100%; - } - } - &__status { - &.PRIMARY { - color: ${({ theme }) => theme.color.primary}; - } - &.GREY { - color: ${({ theme }) => theme.color.grey4}; - } - } - &__contents { - background: white; - padding: 2.2rem 0; - ${({ theme }) => theme.container} - transform: translateY(-2rem); - z-index: 10; - h2 { - ${({ theme }) => theme.font("large", "bold")} - } - - &__title { - ${({ theme }) => theme.font("xsmall", "regular")} - color: ${({ theme }) => theme.color.grey4}; - margin-top: 0.7rem; - - display: flex; - justify-content: space-between; - - & > div > span + span::before { - content: " • "; - margin: 0 0.5rem; - } - } - - &__info { - ${({ theme }) => theme.font("large", "regular")} - margin-top: 3rem; - - & > div { - display: flex; - justify-content: space-between; - - & > div:nth-of-type(1) { - color: ${({ theme }) => theme.color.grey4}; - } - - & > div:nth-of-type(2) { - max-width: 23.3rem; - text-align: right; - } - } - - & > div + div { - margin-top: 2.4rem; - } - } - - & > p { - border-top: 0.1rem solid ${({ theme }) => theme.color.grey7}; - padding-top: 2rem; - ${({ theme }) => theme.font("large", "regular")}; - margin-top: 2.3rem; - margin-bottom: 3.8rem; - } - } - } -`; - -type privateDataType = "detailAddress" | "customerPhoneNumber"; - -const renderPrivateData = ( - data: ErrandDetailResponseBody, - target: privateDataType -) => { - if (data.errand.detailAddress && target === "detailAddress") { - return
{data.errand.detailAddress}
; - } - if (data.errand.customerPhoneNumber && target === "customerPhoneNumber") { - return ( -
-
{data.errand.customerPhoneNumber}
- { - toast("전화번호가 복사되었어요."); - }} - > - - -
- ); - } - - if ( - specifyStatus(data) !== "isMyErrand" && - data.errand.status === "COMPLETE" - ) { - return
심부름이 완료되었어요
; - } - - return
매칭 시 공개돼요
; -}; - -const renderStatus = (color: string, detailStatus: string) => { - return
{detailStatus}
; -}; diff --git a/src/pages/feeds/FeedRequestForm/ImageAppender/index.tsx b/src/pages/feeds/FeedRequestForm/ImageAppender/index.tsx deleted file mode 100644 index dfbf31e..0000000 --- a/src/pages/feeds/FeedRequestForm/ImageAppender/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled from "@emotion/styled"; -import { BoxWrapper } from "../ImageBox"; -import { Loader, Plus } from "@assets/icon"; -import CustomMixPanel from "@utils/mixpanel"; - -interface ImageAppenderProps { - len: number; - children: React.ReactNode; - watchImages: File[] | undefined; -} - -export default function ImageAppender({ - children, - len, - watchImages, -}: ImageAppenderProps) { - const [isUploading, setIsUploading] = useState(false); - - useEffect(() => { - setIsUploading(false); - }, [watchImages]); - - return ( - { - setIsUploading(true); - }} - > - - {children} - - ); -} - -const ImageAppenderWrapper = styled(BoxWrapper)` - border: 0.1rem solid ${({ theme }) => theme.color.grey6}; - width: 7rem; - height: 7rem; - & > label { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - & > svg { - stroke: ${({ theme }) => theme.color.grey6}; - } - & > .appender__loader { - width: 100%; - height: 100%; - } - & > .appender__plus { - margin-bottom: 0.5rem; - } - & > span { - ${({ theme }) => theme.font("xsmall")}; - color: ${({ theme }) => theme.color.grey6}; - } - } -`; diff --git a/src/pages/feeds/FeedRequestForm/ImageBox/index.tsx b/src/pages/feeds/FeedRequestForm/ImageBox/index.tsx deleted file mode 100644 index 93cdeac..0000000 --- a/src/pages/feeds/FeedRequestForm/ImageBox/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Close } from "@assets/icon"; -import styled from "@emotion/styled"; -import { memo } from "react"; - -type ImageBoxProps = { - file: File; - removeImage: (id: number) => void; -}; -export default memo(function ImageBox({ file, removeImage }: ImageBoxProps) { - return ( - -
- removeImage(file.lastModified)} /> -
-
- ); -}); - -export const BoxWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - border-radius: 1rem; - min-width: 7rem; - min-height: 7rem; - width: 7rem; - height: 7rem; -`; - -const ImageBoxWrapper = styled(BoxWrapper)<{ imgURL: string }>` - background: no-repeat ${({ imgURL }) => `url(${imgURL})`}; - background-size: cover; - position: relative; - - & > .image__remover { - position: absolute; - top: 0; - right: 0; - width: 2.2rem; - height: 2.2rem; - background: rgba(51, 51, 51, 0.7); - border-radius: 3rem; - display: flex; - justify-content: center; - align-items: center; - - transform: translate(50%, -50%); - } -`; diff --git a/src/pages/feeds/FeedRequestForm/index.tsx b/src/pages/feeds/FeedRequestForm/index.tsx deleted file mode 100644 index 0583548..0000000 --- a/src/pages/feeds/FeedRequestForm/index.tsx +++ /dev/null @@ -1,385 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import styled from "@emotion/styled"; -import { useNavigator } from "@karrotframe/navigator"; -import { useForm, SubmitHandler } from "react-hook-form"; -import { - ErrorText, - PriceInput, - SectionWrapper, - StickyFooter, - StickyPageWrpper, - TextAreaWrapper, -} from "@styles/shared"; -import CustomScreenHelmet from "@components/CustomScreenHelmet"; -import useModal from "@hooks/useModal"; -import Modal, { ModalInfoType } from "@components/Modal"; - -import Button from "@components/Button"; -import ImageBox from "./ImageBox"; -import ImageAppender from "./ImageAppender"; -import { getRegion, getValueFromSearch } from "@utils/utils"; -import { useRegisterErrand } from "@api/errands"; -import { PHONE_NUMBER_REGEX } from "@constant/validation"; -import { Dropdown } from "@assets/icon"; -import CustomMixPanel from "@utils/mixpanel"; -import { toast } from "@components/Toast/Index"; - -type Inputs = { - categoryId: number; - images?: File[]; - detail: string; - reward: number; - detailAddress: string; - phoneNumber: string; -}; - -export default function FeedRequestForm({ categoryId }: { categoryId?: string }) { - const { - register, - handleSubmit, - watch, - formState: { errors, isValid }, - } = useForm({ - mode: "onChange", - defaultValues: { categoryId: Number(categoryId) }, - }); - const { isOpen, openModal, closeModal, innerMode } = useModal(); - const watchCategory = watch("categoryId"); - const watchTextArea = watch("detail"); - const watchImages = watch("images"); - const [imageList, setImageList] = useState([]); - const { replace } = useNavigator(); - const region = getRegion(); - const mutationRegisterErrand = useRegisterErrand({ - onSuccess: (id: string) => { - closeModal(); - replace(`feed/errands/${id}`); - toast("요청이 완료되었어요"); - }, - }); - const modalInfo: ModalInfoType = { - confirm: { - text: "작성 완료 후 수정할 수 없어요.\n완료 전 꼼꼼하게 확인해 주세요.", - no: ( - - ), - yes: , - }, - }; - - const onSubmit: SubmitHandler = async (result) => { - const { categoryId, detail, reward, detailAddress, phoneNumber } = result; - const regionId = getValueFromSearch("region_id") ?? ""; - const formData = new FormData(); - imageList.forEach((file) => { - formData.append("images", file); - }); - formData.append("categoryId", String(categoryId)); - formData.append("detail", detail); - formData.append("reward", String(reward)); - formData.append("detailAddress", detailAddress); - formData.append("phoneNumber", phoneNumber); - formData.append("regionId", regionId); - - mutationRegisterErrand.mutate(formData); - }; - - const removeImage = useCallback((targetLastModified: number) => { - setImageList((images) => { - return images.filter( - (image) => image.lastModified !== targetLastModified - ); - }); - }, []); - - useEffect(() => { - if (watchImages) { - setImageList(Array.from(watchImages)); - } - }, [watchImages]); - - return ( - - - - -
- - {errors.categoryId && ( - 카테고리를 선택해 주세요. - )} -
-
-
- - -
-
-
- -
- - (선택) -
- - - - - {imageList && - imageList.map((file) => ( - - ))} - -
- -
- - {errors.detail && ( - 세부사항을 10자 이상 입력해 주세요. - )} -
-
- -