From 136efebdd7c90a7f1ac09efd374006da3587dee2 Mon Sep 17 00:00:00 2001 From: yunseok Date: Fri, 9 Aug 2024 23:15:00 +0900 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 34 +++++++++++++++++++++------------- src/pages/ChallengeDetail.tsx | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 src/pages/ChallengeDetail.tsx diff --git a/src/App.tsx b/src/App.tsx index f51d48c..8fb63cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,21 +7,29 @@ import MyPage from "./pages/MyPage"; import SponsorRegister from "./pages/SponsorRegister"; import Challenge from "./pages/Challenge"; import SponsorDetail from "./pages/SponsorDetail"; +import ChallengeDetail from "./pages/ChallengeDetail"; function App() { - return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ); + return ( + + + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + + + ); } export default App; diff --git a/src/pages/ChallengeDetail.tsx b/src/pages/ChallengeDetail.tsx new file mode 100644 index 0000000..16b329c --- /dev/null +++ b/src/pages/ChallengeDetail.tsx @@ -0,0 +1,18 @@ +import Button from "../components/Common/Button"; +import DefaultLayout from "../components/Common/DefaultLayout"; + +const ChallengeDetail = () => { + return ( + +
+
자기계발
+

1주 1권 독서하기

+

+ 한 달 동안 매주 한 권씩 독서 후 독후감을 작성합니다. +

+
+
+ ); +}; + +export default ChallengeDetail; From c044323018d946af6acba94f79e5a2eb1cfe7384 Mon Sep 17 00:00:00 2001 From: ej070961 Date: Sat, 10 Aug 2024 02:06:49 +0900 Subject: [PATCH 02/25] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/index.ts | 32 +++--- src/apis/user.ts | 133 +++++++++++++++------- src/components/Common/NavBar/User.tsx | 57 +++++----- src/components/Sponsor/AllPostSection.tsx | 11 +- src/pages/Main.tsx | 113 +++++++++--------- 5 files changed, 205 insertions(+), 141 deletions(-) diff --git a/src/apis/index.ts b/src/apis/index.ts index 560b170..0afedab 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,6 +1,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import useAuthStore from "../storage/useAuthStore"; import { CommonResponse } from "../@types/api"; +import { getTokenRefresh } from "./user"; export const axiosInstance = axios.create({ headers: { @@ -9,20 +10,19 @@ export const axiosInstance = axios.create({ withCredentials: true, }); -// axiosInstance.interceptors.response.use( -// (response) => response, -// async (error) => { -// const originalRequest = error.config; -// if (error.response.status === 401 && !originalRequest._retry) { -// originalRequest._retry = true; //재시도 플래그 설정 -// // 로그아웃 함수 호출 -// useAuthStore.getState().logout(); +axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; //재시도 플래그 설정 -// // 메인 페이지로 리다이렉트 -// window.location.href = "/"; - -// return Promise.reject(error); -// } -// return Promise.reject(error); -// } -// ); + const accessToken = useAuthStore((state) => state.accessToken); + const refreshToken = useAuthStore((state) => state.refreshToken); + return getTokenRefresh(accessToken!, refreshToken!).then(() => { + return axiosInstance(originalRequest); + }); + } + return Promise.reject(error); + } +); diff --git a/src/apis/user.ts b/src/apis/user.ts index 9220be7..f244659 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,54 +1,107 @@ import axios from "axios"; import { axiosInstance } from "."; import { CommonError } from "../@types/api"; +import useAuthStore from "../storage/useAuthStore"; export const getUserInfo = async (accessToken: string) => { - try { - const res = await axiosInstance.get( - "https://fledge.site/api/v1/members/me", - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); + try { + const res = await axiosInstance.get( + "https://fledge.site/api/v1/members/me", + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); - return res.data; - } catch (error) { - console.log(error); - if (axios.isAxiosError(error) && error.response) { - const errorCode = error.response.data.errorCode; - const message = error.response.data.message; - console.log(`${errorCode}: ${message}`); - alert(message); - } + return res.data; + } catch (error) { + console.log(error); + if (axios.isAxiosError(error) && error.response) { + const errorCode = error.response.data.errorCode; + const message = error.response.data.message; + console.log(`${errorCode}: ${message}`); + alert(message); } + } }; export const putUserNickname = async ( - accessToken: string, - id: number, - name: string + accessToken: string, + id: number, + name: string ) => { - try { - const res = await axiosInstance.put( - `https://fledge.site/api/v1/members/${id}/nickname`, - { nickname: name }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); + try { + const res = await axiosInstance.put( + `https://fledge.site/api/v1/members/${id}/nickname`, + { nickname: name }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); - return res.data; - } catch (error) { - console.log(error); - if (axios.isAxiosError(error) && error.response) { - const errorCode = error.response.data.errorCode; - const message = error.response.data.message; - console.log(`${errorCode}: ${message}`); - alert(message); - } + return res.data; + } catch (error) { + console.log(error); + if (axios.isAxiosError(error) && error.response) { + const errorCode = error.response.data.errorCode; + const message = error.response.data.message; + console.log(`${errorCode}: ${message}`); + alert(message); } + } +}; + +export const getTokenRefresh = async ( + accessToken: string, + refreshToken: string +) => { + try { + const res = await axiosInstance.get( + `https://fledge.site/api/v1/auth/tokenRefresh`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "X-Refresh-Token": refreshToken, + }, + } + ); + if (res.status === 200) { + useAuthStore.setState({ + isLoggedIn: true, + accessToken: res.data.accessToken, + refreshToken: res.data.refreshToken, + }); + } + return res.data; + } catch (error) { + console.log(error); + useAuthStore.getState().logout(); + window.location.href = "/"; + } +}; +export const postLogout = async (accessToken: string) => { + try { + const res = await axiosInstance.post( + "https://fledge.site/api/v1/auth/logout", + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + return res.data; + } catch (error) { + console.log(error); + if (axios.isAxiosError(error) && error.response) { + const errorCode = error.response.data.errorCode; + const message = error.response.data.message; + console.log(`${errorCode}: ${message}`); + alert(message); + } + } }; diff --git a/src/components/Common/NavBar/User.tsx b/src/components/Common/NavBar/User.tsx index 5d1e486..fa938f1 100644 --- a/src/components/Common/NavBar/User.tsx +++ b/src/components/Common/NavBar/User.tsx @@ -2,42 +2,43 @@ import styled from "styled-components"; import tw from "twin.macro"; import useAuthStore from "../../../storage/useAuthStore"; import { useNavigate } from "react-router-dom"; +import { postLogout } from "../../../apis/user"; interface UserContainerProps { - nickname: string; - profile?: string; + nickname: string; + profile?: string; } const User = ({ nickname, profile }: UserContainerProps) => { - const navigate = useNavigate(); - const onLogout = () => { - useAuthStore.getState().logout(); - navigate("/"); - }; - const onClickProfile = () => { - navigate("/mypage"); - }; + const navigate = useNavigate(); + const accessToken = useAuthStore((state) => state.accessToken); + const onLogout = async () => { + const res = await postLogout(accessToken!); + if (res.success) { + useAuthStore.getState().logout(); + navigate("/"); + } + }; + const onClickProfile = () => { + navigate("/mypage"); + }; - return ( - -
- {nickname} 님, - 환영합니다! - onLogout()}>로그아웃 -
- onClickProfile()} - /> -
- ); + return ( + +
+ {nickname} 님, + 환영합니다! + onLogout()}>로그아웃 +
+ onClickProfile()} /> +
+ ); }; export default User; const Container = styled.div` - ${tw` + ${tw` text-medium-20 font-medium text-fontColor1 @@ -48,13 +49,13 @@ const Container = styled.div` `; const Profile = styled.img` - ${tw` + ${tw` ml-[17px] w-[44px] h-[44px] rounded-full object-cover cursor-pointer `} `; const Nickname = styled.span` - ${tw` + ${tw` text-bold-20 font-bold text-fontColor1 @@ -62,7 +63,7 @@ const Nickname = styled.span` `; const Logout = styled.button` - ${tw` + ${tw` ml-[18px] text-medium-20 font-medium text-fontColor2 `} `; diff --git a/src/components/Sponsor/AllPostSection.tsx b/src/components/Sponsor/AllPostSection.tsx index 42bdb0d..7142a01 100644 --- a/src/components/Sponsor/AllPostSection.tsx +++ b/src/components/Sponsor/AllPostSection.tsx @@ -37,10 +37,15 @@ function AllPostSection() { }); const handleUserPermission = () => { - if (userData.role === "USER") { - alert("자립준비청년만 이용 가능한 기능입니다."); + if (Object.keys(userData).length !== 0) { + if (userData.role === "USER") { + alert("자립준비청년만 이용 가능한 기능입니다."); + } else { + navigate("/sponsor-register"); + window.scrollTo(0, 0); + } } else { - navigate("/sponsor-register"); + alert("로그인 후 이용 가능한 기능입니다."); } }; console.log(PostData); diff --git a/src/pages/Main.tsx b/src/pages/Main.tsx index eac8ea6..b6c4c9f 100644 --- a/src/pages/Main.tsx +++ b/src/pages/Main.tsx @@ -15,66 +15,71 @@ import useAuthStore from "../storage/useAuthStore"; import { getUserInfo } from "../apis/user"; function Main() { - // redirection 주소로 부터 accessToken을 받아와서 localStorage에 저장 - const location = useLocation(); - const navigate = useNavigate(); + // redirection 주소로 부터 accessToken을 받아와서 localStorage에 저장 + const location = useLocation(); + const navigate = useNavigate(); - useEffect(() => { - const fetchUserInfo = async () => { - const query = new URLSearchParams(location.search); - let accessToken = useAuthStore.getState().accessToken; + useEffect(() => { + const fetchUserInfo = async () => { + const query = new URLSearchParams(location.search); + let accessToken = useAuthStore.getState().accessToken; + let refreshToken = useAuthStore.getState().refreshToken; + if (!accessToken || !refreshToken) { + const token = query.get("accessToken"); + const refresh = query.get("refreshToken"); - if (!accessToken) { - const token = query.get("accessToken"); - if (token) { - accessToken = token; - } - } + if (token) { + accessToken = token; + useAuthStore.setState({ accessToken: token }); + } - if ( - accessToken && - useAuthStore.getState().userData.id === undefined - ) { - try { - const res = await getUserInfo(accessToken); - if (res.success) { - useAuthStore.setState({ - isLoggedIn: true, - userData: res.data, - accessToken: accessToken, - }); - navigate("/"); // 사용자 정보 설정 후 홈으로 리디렉션 - } - } catch (error) { - console.error("사용자 정보 가져오기 오류:", error); - } - } - }; + if (refresh) { + refreshToken = refresh; + useAuthStore.setState({ refreshToken: refresh }); + } + } - fetchUserInfo(); - }, [location.search, navigate]); + if (accessToken && useAuthStore.getState().userData.id === undefined) { + try { + const res = await getUserInfo(accessToken); + if (res.success) { + useAuthStore.setState({ + isLoggedIn: true, + userData: res.data, + accessToken: accessToken, + }); + navigate("/"); // 사용자 정보 설정 후 홈으로 리디렉션 + } + } catch (error) { + console.error("사용자 정보 가져오기 오류:", error); + } + } + }; - return ( - - - - - - - - - - fledge 플리지에게 흥미가 생기셨나요? - + + + + ) : ( + + 후원이 완료되었습니다. + + {nickname} 후원자님의 보탬이 {data.nickname} 님에게 큰 도움이 + 되었을 거에요! + + + 감사이미지 + + )} + + ); + } else { + return
; + } +} + +export default DonateModal; + +const Overlay = styled.div` + ${tw` + w-full h-full bg-[black] bg-opacity-50 + fixed top-[50%] left-[50%] transform translate-x-[-50%] translate-y-[-50%] + flex justify-center items-center + z-[2] + `} +`; +const Wrapper = styled.div` + ${tw`w-[1220px] h-[660px] [border-radius: 16px] bg-background flex flex-col items-center justify-center m-auto`} + .bold-36 { + ${tw`font-bold text-bold-36 text-fontColor1 mt-0.5`} + } + .medium-20 { + ${tw`font-medium text-medium-20 text-fontColor3 mt-8`} + } +`; +const InputForm = styled.form` + ${tw`w-[1100px] h-[488px] flex flex-col justify-center font-sans m-auto`} + + .d-day { + ${tw`font-bold text-bold-36 text-subColor`} + } + + .desc { + ${tw`font-medium text-medium-15 text-fontColor2 ml-2.5 mt-8`} + } + + .amount-input { + ${tw`w-[257px] bg-white h-11 rounded-full font-medium text-medium-20 text-fontColor1 p-3 mt-5 mb-2`} + &:focus { + ${tw`outline-mainColor`}; + } + } +`; +const Button = styled.button<{ disabled?: boolean }>` + ${tw` px-4 h-11 rounded-full font-bold text-bold-20 bg-subColor text-white mt-5`} + + ${({ disabled }) => + disabled + ? tw`bg-[#D9D9D9] text-fontColor1 cursor-default` + : tw`bg-subColor text-white`} +`; diff --git a/src/components/SponsorDetail/Header.tsx b/src/components/SponsorDetail/Header.tsx index aecdbe6..d22d0a2 100644 --- a/src/components/SponsorDetail/Header.tsx +++ b/src/components/SponsorDetail/Header.tsx @@ -7,11 +7,13 @@ import { useQuery } from "@tanstack/react-query"; import { getCanaryInfo } from "../../apis/sponsor"; import useAuthStore from "../../storage/useAuthStore"; import DeleteModal from "./DeleteModal"; +import DonateModal from "./DonateModal"; type HeaderProps = { memberId: number; }; function Header({ memberId }: HeaderProps) { const [isOpenDelete, setIsOpenDelete] = useState(false); + const [isOpenDonate, setIsOpenDonate] = useState(false); const currentUserId = useAuthStore((state) => state.userData.id); // const { data, isLoading, error } = useQuery({ // queryKey: ["getCanaryInfo"], @@ -35,7 +37,9 @@ function Header({ memberId }: HeaderProps) { {memberId !== currentUserId! ? ( - 후원하기 + setIsOpenDonate(!isOpenDonate)}> + 후원하기 + ) : ( 수정하기 @@ -46,6 +50,7 @@ function Header({ memberId }: HeaderProps) { )} {isOpenDelete && setIsOpenDelete(false)} />} + {isOpenDonate && setIsOpenDonate(false)} />} ); } diff --git a/src/components/SponsorDetail/Progress.tsx b/src/components/SponsorDetail/Progress.tsx index c2f710f..c3a8820 100644 --- a/src/components/SponsorDetail/Progress.tsx +++ b/src/components/SponsorDetail/Progress.tsx @@ -8,49 +8,46 @@ import { useParams } from "react-router-dom"; interface progressProps { modal?: boolean; + progress: number; + supportedPrice: number; + totalPrice: number; } -function Progress({ modal }: progressProps) { - const { supportId } = useParams() as { supportId: string }; +function Progress({ + modal, + progress, + supportedPrice, + totalPrice, +}: progressProps) { + return ( + +
+ {!modal && ( + +
+ {supportedPrice !== 0 ? ( + {supportedPrice}원 달성! + ) : ( + 첫 후원자가 되어 주세요! + )} +
+ +
+ )} - const { data, isLoading, error } = useQuery({ - queryKey: ["getSponsorProgress"], - queryFn: () => getProgressInfo(supportId), - }); - - if (!isLoading && data) { - return ( - -
- {!modal && ( - -
- {data.supportedPrice !== 0 ? ( - {data.supportedPrice}원 달성! - ) : ( - 첫 후원자가 되어 주세요! - )} -
- -
- )} - - -
- - 진행률 {data.progress}% - ₩ {data.totalPrice} - -
- ); - } else { - return
; - } + +
+ + 진행률 {progress}% + ₩ {totalPrice} + +
+ ); } export default Progress; const Container = styled.div` - ${tw`w-full mt-24`} + ${tw`w-full mt-11`} .medium-20 { ${tw`font-medium text-fontColor3 text-medium-20`} diff --git a/src/components/SponsorDetail/SponsorList.tsx b/src/components/SponsorDetail/SponsorList.tsx index 082ed4a..8c3b9cd 100644 --- a/src/components/SponsorDetail/SponsorList.tsx +++ b/src/components/SponsorDetail/SponsorList.tsx @@ -11,6 +11,7 @@ type SponsorListProps = { function SponsorList({ supporters, nickname }: SponsorListProps) { //호버 된 아이템 인덱스를 담을 state const [hoverIndex, setHoverIndex] = useState(null); + console.log(supporters); return ( 후원자 명단 @@ -24,14 +25,16 @@ function SponsorList({ supporters, nickname }: SponsorListProps) { onMouseLeave={() => setHoverIndex(null)} > - + {hoverIndex === index && (
후원 금액 - {supporter.value}원 + + {Object.values(supporter)}원 +
diff --git a/src/components/SponsorRegister/AccountForm.tsx b/src/components/SponsorRegister/AccountForm.tsx index e4facd7..a0ab66b 100644 --- a/src/components/SponsorRegister/AccountForm.tsx +++ b/src/components/SponsorRegister/AccountForm.tsx @@ -35,7 +35,7 @@ function AccountForm({ export default AccountForm; const Container = styled.div` - ${tw`flex flex-row items-center w-[610px] justify-between mt-6`} + ${tw`flex flex-row items-center w-[610px] justify-between mt-5`} label { ${tw`font-medium text-medium-20 text-fontColor3 my-3.5`} } diff --git a/src/pages/SponsorDetail.tsx b/src/pages/SponsorDetail.tsx index 5ebf5e5..9ca78fc 100644 --- a/src/pages/SponsorDetail.tsx +++ b/src/pages/SponsorDetail.tsx @@ -8,7 +8,7 @@ import OtherPosts from "../components/SponsorDetail/OtherPosts"; import Button from "../components/Common/Button"; import { useNavigate, useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; -import { getSupportsInfo } from "../apis/sponsor"; +import { getProgressInfo, getSupportsInfo } from "../apis/sponsor"; import { SponsorDetailData } from "../@types/sponsor"; function SponsorDetail() { const navigate = useNavigate(); @@ -18,13 +18,26 @@ function SponsorDetail() { queryKey: ["getSponsorDetail", supportId], //useQuery는 queryKey가 변경될 때마다 호출됨 queryFn: () => getSupportsInfo(supportId), }); - + const { + data: ProgressData, + isLoading: isProgressLoading, + error: isProgressError, + } = useQuery({ + queryKey: ["getSponsorProgress"], + queryFn: () => getProgressInfo(supportId), + }); return (
{!isLoading && data &&
} {!isLoading && data && } - + {!isProgressLoading && ProgressData && ( + + )} {!isLoading && data && ( Date: Sat, 10 Aug 2024 04:54:39 +0900 Subject: [PATCH 04/25] =?UTF-8?q?feat:=20=EC=B1=8C=EB=A6=B0=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@types/challenge.ts | 2 +- src/apis/challenge.ts | 17 ++++ src/components/Challenge/BestChallenger.tsx | 15 +-- src/components/Challenge/ChallengeGrid.tsx | 17 +++- src/components/Challenge/ChallengeItem.tsx | 14 ++- .../ChallengeDetail/Challengers.tsx | 99 +++++++++++++++++++ src/components/ChallengeDetail/Header.tsx | 86 ++++++++++++++++ src/components/ChallengeDetail/Progress.tsx | 54 ++++++++++ .../ChallengeDetail/RecommendedChallenges.tsx | 84 ++++++++++++++++ src/pages/ChallengeDetail.tsx | 71 +++++++++++-- 10 files changed, 433 insertions(+), 26 deletions(-) create mode 100644 src/components/ChallengeDetail/Challengers.tsx create mode 100644 src/components/ChallengeDetail/Header.tsx create mode 100644 src/components/ChallengeDetail/Progress.tsx create mode 100644 src/components/ChallengeDetail/RecommendedChallenges.tsx diff --git a/src/@types/challenge.ts b/src/@types/challenge.ts index ae30904..84bb197 100644 --- a/src/@types/challenge.ts +++ b/src/@types/challenge.ts @@ -9,7 +9,7 @@ export type ChallengerProps = { export type BestChallengerProps = { memberId: number; memberName: string; - participationCount: number; + totalCount: number; successCount: number; successRate: number; topCategories: string[]; diff --git a/src/apis/challenge.ts b/src/apis/challenge.ts index 689001c..a98901b 100644 --- a/src/apis/challenge.ts +++ b/src/apis/challenge.ts @@ -80,3 +80,20 @@ export const getPartnershipChallenges = async ( } } }; + +export const getRecommendedChallenges = async (challengeId: string) => { + try { + const res = await axiosInstance.get( + `https://www.fledge.site/api/v1/public/challenges/${challengeId}/explore` + ); + return res.data; + } catch (error) { + console.log(error); + if (axios.isAxiosError(error) && error.response) { + const errorCode = error.response.data.errorCode; + const message = error.response.data.message; + console.log(`${errorCode}: ${message}`); + alert(message); + } + } +}; diff --git a/src/components/Challenge/BestChallenger.tsx b/src/components/Challenge/BestChallenger.tsx index afe4955..71b9d32 100644 --- a/src/components/Challenge/BestChallenger.tsx +++ b/src/components/Challenge/BestChallenger.tsx @@ -4,20 +4,10 @@ import ContentHeader from "../Common/ContentHeader"; import Challenger from "./Challenger"; import { getTopParticipants } from "../../apis/challenge"; import { useQuery } from "@tanstack/react-query"; -import useAuthStore from "../../storage/useAuthStore"; import defaultProfile from "../../assets/images/profile-big.png"; import { BestChallengerProps } from "../../@types/challenge"; -type ChallengerProps = { - memberId: number; - memberName: string; - participationCount: number; - successCount: number; - successRate: number; - topCategories: string[]; -}; - -const scroll = keyframes` +export const scroll = keyframes` from { transform: translateX(100%); } @@ -27,7 +17,6 @@ const scroll = keyframes` `; const BestChallenger = () => { - const { accessToken } = useAuthStore(); const { data: topChallengersData, isLoading } = useQuery({ queryKey: ["getTopParticipants"], queryFn: () => getTopParticipants(), @@ -51,7 +40,7 @@ const BestChallenger = () => { desc={ challenger.successCount + "/" + - challenger.participationCount + + challenger.totalCount + "개 챌린지 성공!" } categoryList={challenger.topCategories} diff --git a/src/components/Challenge/ChallengeGrid.tsx b/src/components/Challenge/ChallengeGrid.tsx index 42c05c4..3b12935 100644 --- a/src/components/Challenge/ChallengeGrid.tsx +++ b/src/components/Challenge/ChallengeGrid.tsx @@ -48,7 +48,7 @@ const ChallengeGrid = ({ )} - + {challengeData.data.content.map( (challenge: any, index: number) => ( ` ${tw` - grid grid-cols-4 grid-rows-2 gap-[23px] + grid grid-cols-4 gap-[23px] `} + //size가 4 이상일 때 + ${({ size }) => + size && + size > 4 && + tw` + grid-rows-2 + `} `; diff --git a/src/components/Challenge/ChallengeItem.tsx b/src/components/Challenge/ChallengeItem.tsx index 52ff93a..bc94628 100644 --- a/src/components/Challenge/ChallengeItem.tsx +++ b/src/components/Challenge/ChallengeItem.tsx @@ -9,6 +9,7 @@ import Heart from "../Common/Heart"; import Benefit, { BenefitProps } from "./Benefit"; import { challengeType } from "../../@types/challenge-category"; import { useCallback, useState } from "react"; +import { useNavigate } from "react-router-dom"; interface ChallengeItemProps { title: string; @@ -19,6 +20,7 @@ interface ChallengeItemProps { successRate: number; participants: number; isCategory?: boolean; + challengeId?: string; } const ChallengeItem = ({ @@ -30,7 +32,9 @@ const ChallengeItem = ({ successRate, participants, isCategory, + challengeId = "1", }: ChallengeItemProps) => { + const navigate = useNavigate(); let BubbleType = null; if (!isCategory) { if (bubbleType) { @@ -44,7 +48,14 @@ const ChallengeItem = ({
- {title} + { + navigate(`/challenge/${challengeId}`); + window.scrollTo(0, 0); + }} + > + {title} + { + return ( + +
+ +
+
+ {challenger.map((challenger, index) => ( + + ))} +
+
+ ); +}; + +export default Challengers; + +const Container = styled.div` + ${tw` + flex flex-col gap-[42px] w-[full] + `} + .challengers { + ${tw` + flex justify-between items-center w-[1280px] overflow-y-hidden + `} + } + .challenger-list { + ${tw` + w-full flex justify-between gap-[40px] + `} + animation: ${scroll} 30s linear infinite; + display: flex; + white-space: nowrap; + overflow: hidden; + } +`; diff --git a/src/components/ChallengeDetail/Header.tsx b/src/components/ChallengeDetail/Header.tsx new file mode 100644 index 0000000..e1a0ca9 --- /dev/null +++ b/src/components/ChallengeDetail/Header.tsx @@ -0,0 +1,86 @@ +import styled from "styled-components"; +import tw from "twin.macro"; +import LikeIcon from "../../assets/icons/like-icon"; + +type HeaderProps = { + category: string; + title: string; + desc: string; + likeCount: number; + isClicked?: boolean; + onClick?: () => void; +}; + +const Header = ({ + category, + title, + desc, + likeCount, + isClicked, + onClick, +}: HeaderProps) => { + return ( + + +
{category}
+

{title}

+

{desc}

+
+
+ +

{ + e.stopPropagation(); + }} + > + {likeCount} +

+
+
+ ); +}; + +export default Header; + +const Container = styled.div` + ${tw` + flex justify-between items-center w-[1280px] + `} + .like { + ${tw` + flex flex-col items-center gap-[8.84px] + `} + svg { + ${tw` + cursor-pointer + `} + } + } + p { + ${tw` + text-bold-20 font-bold text-fontColor3 + `} + } +`; + +const ChallengeInfo = styled.div` + ${tw` + w-[1280px] flex flex-col items-start + `} + .category { + ${tw` + text-medium-20 font-medium text-white + bg-mainColor rounded-[37px] p-[1px 13px] + `} + } + .title { + ${tw` + text-bold-64 font-bold mt-[6.5px] text-fontColor1 + `} + } + .desc { + ${tw` + text-medium-20 font-medium text-fontColor3 mt-[14px] + `} + } +`; diff --git a/src/components/ChallengeDetail/Progress.tsx b/src/components/ChallengeDetail/Progress.tsx new file mode 100644 index 0000000..d71ed13 --- /dev/null +++ b/src/components/ChallengeDetail/Progress.tsx @@ -0,0 +1,54 @@ +import styled from "styled-components"; +import tw from "twin.macro"; + +type ProgressProps = { + totalParticipants: number; + successParticipants: number; + successRate: number; +}; + +const Progress = ({ + totalParticipants, + successParticipants, + successRate, +}: ProgressProps) => { + return ( + +
+
+
+
+

+ {totalParticipants}명 중 {successParticipants}명 달성 +

+

도전 성공률 {successRate}%

+
+ + ); +}; + +export default Progress; + +const Container = styled.div` + ${tw` + w-[1280px] flex flex-col gap-[36px] + `} + .text-field { + ${tw` + flex justify-between text-bold-20 font-bold text-fontColor3 + `} + } + .progress-bar { + ${tw` + w-[1280px] h-[15.41px] rounded-[28px] border-[3px] border-subColor shadow-[black] bg-white + `} + } + .progress { + ${tw` + h-full rounded-[28px] bg-subColor + `} + } +`; diff --git a/src/components/ChallengeDetail/RecommendedChallenges.tsx b/src/components/ChallengeDetail/RecommendedChallenges.tsx new file mode 100644 index 0000000..cd8ff4d --- /dev/null +++ b/src/components/ChallengeDetail/RecommendedChallenges.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from "react"; +import styled from "styled-components"; +import tw from "twin.macro"; +import LeftArrowIcon from "../../assets/icons/left-arrow"; +import RightArrowIcon from "../../assets/icons/right-arrow"; +import { getRecommendedChallenges } from "../../apis/challenge"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { ChallengeItem } from "../Challenge/ChallengeItem"; + +type RecommendedChallengesProps = { + challengeId?: string; +}; + +const RecommendedChallenges = ({ challengeId }: RecommendedChallengesProps) => { + const [page, setPage] = useState(0); + const [currentChallenges, setCurrentChallenges] = useState([]); + const { + data: challengeData, + isLoading, + error, + } = useQuery({ + queryKey: ["getRecommendedChallenges", challengeId], + queryFn: () => getRecommendedChallenges(challengeId || ""), + enabled: true, + }); + + useEffect(() => { + if (challengeData && challengeData.data) { + const start = page * 4; + const end = start + 4; + setCurrentChallenges(challengeData.data.slice(start, end)); + } + }, [challengeData, page]); + + if (isLoading) return
; + if (challengeData === undefined) return
; + + console.log(challengeData); + + return ( + + + + {currentChallenges.map((challenge: any, index: number) => ( + + ))} + + + + ); +}; +export default RecommendedChallenges; + +const ChallengerContainer = styled.div` + ${tw` + flex + items-center + justify-between + gap-[40px] + mt-[-80px] + w-[1400px] + `} +`; + +const ChallengeSlider = styled.div` + ${tw` + grid grid-cols-4 gap-[23px] +`} +`; diff --git a/src/pages/ChallengeDetail.tsx b/src/pages/ChallengeDetail.tsx index 16b329c..02d9d84 100644 --- a/src/pages/ChallengeDetail.tsx +++ b/src/pages/ChallengeDetail.tsx @@ -1,18 +1,73 @@ -import Button from "../components/Common/Button"; +import styled from "styled-components"; import DefaultLayout from "../components/Common/DefaultLayout"; +import tw from "twin.macro"; +import { useState } from "react"; +import Header from "../components/ChallengeDetail/Header"; +import Progress from "../components/ChallengeDetail/Progress"; +import ContentHeader from "../components/Common/ContentHeader"; +import Challengers from "../components/ChallengeDetail/Challengers"; +import RecommendedChallenges from "../components/ChallengeDetail/RecommendedChallenges"; +import { useNavigate, useParams } from "react-router-dom"; +import Button from "../components/Common/Button"; const ChallengeDetail = () => { + const [likeCount, setLikeCount] = useState(10); + const [isClicked, setIsClicked] = useState(false); + const navigate = useNavigate(); + + const handleLike = () => { + setIsClicked(!isClicked); + isClicked ? setLikeCount(likeCount - 1) : setLikeCount(likeCount + 1); + }; + + const { challengeId } = useParams(); return ( -
-
자기계발
-

1주 1권 독서하기

-

- 한 달 동안 매주 한 권씩 독서 후 독후감을 작성합니다. -

-
+ +
+ + + + + +
- {challenger.map((challenger, index) => ( - - ))} + {challengerData.data.map( + (challenger: BestChallengerProps, index: number) => ( + + ) + )}
); diff --git a/src/components/ChallengeDetail/Header.tsx b/src/components/ChallengeDetail/Header.tsx index e1a0ca9..0c166ff 100644 --- a/src/components/ChallengeDetail/Header.tsx +++ b/src/components/ChallengeDetail/Header.tsx @@ -1,9 +1,11 @@ import styled from "styled-components"; import tw from "twin.macro"; import LikeIcon from "../../assets/icons/like-icon"; +import { challengeType } from "../../@types/challenge-category"; +import { useState } from "react"; type HeaderProps = { - category: string; + categories?: string[]; title: string; desc: string; likeCount: number; @@ -12,28 +14,39 @@ type HeaderProps = { }; const Header = ({ - category, + categories, title, desc, likeCount, isClicked, onClick, }: HeaderProps) => { + const [isLike, setIsLike] = useState(isClicked); return ( -
{category}
+ {categories?.map((category) => ( +
+ {challengeType.find((type) => type.id === category) + ?.label || category} +
+ ))}

{title}

{desc}

-
- +
{ + setIsLike(!isLike); + }} + > +

{ e.stopPropagation(); }} > - {likeCount} + {likeCount + (isLike ? 1 : 0)}

diff --git a/src/components/ChallengeDetail/RecommendedChallenges.tsx b/src/components/ChallengeDetail/RecommendedChallenges.tsx index cd8ff4d..1a267ca 100644 --- a/src/components/ChallengeDetail/RecommendedChallenges.tsx +++ b/src/components/ChallengeDetail/RecommendedChallenges.tsx @@ -35,8 +35,6 @@ const RecommendedChallenges = ({ challengeId }: RecommendedChallengesProps) => { if (isLoading) return
; if (challengeData === undefined) return
; - console.log(challengeData); - return (
+
+ + ); +}; + +export default ApplyModal; + +const Container = styled.div` + ${tw` + flex items-center justify-center + fixed top-0 left-0 w-full h-full z-[1000] bg-[rgba(0,0,0,0.4)] + `} + .modal { + ${tw` + flex flex-col items-center justify-center bg-[black] + p-[67px 119px] rounded-[16px] bg-background + `} + } + .title-text { + ${tw` + text-bold-36 font-bold text-fontColor1 mb-[16px] + `} + } + .sub-text { + ${tw` + text-medium-20 font-medium text-fontColor3 mb-[30px] + `} + } + .button-container { + ${tw` + flex gap-[23px] + `} + } +`; diff --git a/src/components/ChallengeDetail/Certification.tsx b/src/components/ChallengeDetail/Certification.tsx new file mode 100644 index 0000000..46e6bb9 --- /dev/null +++ b/src/components/ChallengeDetail/Certification.tsx @@ -0,0 +1,102 @@ +import styled from "styled-components"; +import tw from "twin.macro"; +import AddIcon from "../../assets/icons/add-icon"; +import ContentHeader from "../Common/ContentHeader"; +import { useState } from "react"; +import ThumbsUpModal from "./ThumbsUpModal"; +import ProofModal from "./ProofModal"; + +type CertificationProps = { + title: string; +}; + +const Certification = ({ title }: CertificationProps) => { + const [proofModalOpen, setProofModalOpen] = useState(false); + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + + const handleProofSuccess = () => { + setProofModalOpen(false); + setIsSuccessModalOpen(true); + }; + + return ( + <> + + +
+ certification + certification +
{ + setProofModalOpen(true); + }} + > + + 인증 내역 추가 업로드 +
+
{ + setIsSuccessModalOpen(true); + }} + > + + 인증 내역 추가 업로드 +
+
+
+ {proofModalOpen && ( + setProofModalOpen(false)} + /> + )} + + {isSuccessModalOpen && ( + { + setIsSuccessModalOpen(false); + }} + /> + )} + + ); +}; + +export default Certification; + +const Container = styled.div` + ${tw` + mt-[243px] + `} + .certification-list { + ${tw` + flex gap-[23px] mt-[50px] + `} + img { + ${tw` + w-[301px] h-[415px] rounded-[16px] + `} + } + .need-certification { + ${tw` + cursor-pointer w-[301px] h-[415px] rounded-[16px] bg-white flex flex-col items-center justify-center gap-[23px] + `} + span { + ${tw` + text-medium-20 font-medium text-fontColor3 + `} + } + } + } +`; diff --git a/src/components/ChallengeDetail/Challengers.tsx b/src/components/ChallengeDetail/Challengers.tsx index d7c6b8d..47b1f6c 100644 --- a/src/components/ChallengeDetail/Challengers.tsx +++ b/src/components/ChallengeDetail/Challengers.tsx @@ -10,9 +10,17 @@ import { BestChallengerProps } from "../../@types/challenge"; type ChallengerProps = { challengeId?: string; + participating?: boolean; + onClick?: () => void; + onCancle?: () => void; }; -const Challengers = ({ challengeId = "1" }: ChallengerProps) => { +const Challengers = ({ + challengeId = "1", + participating, + onClick, + onCancle, +}: ChallengerProps) => { const { data: challengerData, isLoading } = useQuery({ queryKey: ["getChallengeParticipants", challengeId], queryFn: () => getChallengeParticipants(challengeId), @@ -27,7 +35,30 @@ const Challengers = ({ challengeId = "1" }: ChallengerProps) => { title="함께 참여하는 챌린저들" desc="이 챌린지에 함께 참여하고 있는 챌린저들이에요. 같이 힘내봐요!" /> -
+ ) : ( +