diff --git a/.github/workflow/ci.yml b/.github/workflow/ci.yml new file mode 100644 index 0000000..5cc3024 --- /dev/null +++ b/.github/workflow/ci.yml @@ -0,0 +1,26 @@ +name: ESLint test + +on: + pull_request: + branches: + - main + - develop + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Pnpm package manager + run: | + npm install -g pnpm + + - name: Install Dependencies + run: pnpm install + + - name: Lint Code + run: pnpm lint diff --git a/src/apis/challenge-list/getChallengeList.api.ts b/src/apis/challenge-list/getChallengeList.api.ts new file mode 100644 index 0000000..84ec38f --- /dev/null +++ b/src/apis/challenge-list/getChallengeList.api.ts @@ -0,0 +1,27 @@ +import { ChallengeListResponse } from './getChallengeList.response'; +import { axiosClient } from '@/apis/AxiosClient'; +import { useQuery } from '@tanstack/react-query'; + +const getChallengeListPath = () => '/api/challengeGroups/shorts'; + +const challengeListQueryKey = [getChallengeListPath()]; + +const getChallengeList = async ( + page: number, + size: number +): Promise => { + const response = await axiosClient.get(getChallengeListPath(), { + params: { + page, + size, + }, + }); + return response.data; +}; + +export const useGetChallengeList = (page: number, size: number) => { + return useQuery({ + queryKey: [challengeListQueryKey, page, size], + queryFn: () => getChallengeList(page, size), + }); +}; diff --git a/src/apis/challenge-list/getChallengeList.response.ts b/src/apis/challenge-list/getChallengeList.response.ts new file mode 100644 index 0000000..7e04e68 --- /dev/null +++ b/src/apis/challenge-list/getChallengeList.response.ts @@ -0,0 +1,17 @@ +import ApiResponse from '@/apis/ApiResponse'; + +export type ChallengeListData = { + totalPage: number; + hasNext: boolean; + data: { + id: number; + title: string; + content: string; + participantCount: number; + startDate: string; + endDate: string; + category: 'HEALTH' | 'ECHO' | 'SHARE' | 'VOLUNTEER' | 'ETC'; + }[]; +}; + +export type ChallengeListResponse = ApiResponse; diff --git a/src/pages/challenge-list/components/contents/index.tsx b/src/pages/challenge-list/components/contents/index.tsx new file mode 100644 index 0000000..2aa0b64 --- /dev/null +++ b/src/pages/challenge-list/components/contents/index.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; + +import { Box, Text } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +type Props = { + title: string; + content: string; + startDate: string; + endDate: string; + participantCount: number; +}; + +const Contents = ({ + title, + content, + startDate, + endDate, + participantCount, +}: Props) => { + const [isClicked, setIsClicked] = useState(false); + + const handleBoxClick = () => { + setIsClicked(!isClicked); + }; + + const date = `${startDate} ~ ${endDate}`; + + return ( + + + + {title} + + + + {content} + + 누적 참여자 수 : {participantCount}명 + + + + + 참여 가능 기간 + + {date} + + + ); +}; + +export default Contents; + +const ContentsBox = styled(Box)<{ isClicked: boolean }>` + width: 100%; + height: 100%; + background-color: ${({ isClicked }) => + isClicked ? 'var(--color-green-06)' : 'var(--color-green-01)'}; + border-radius: 1.2rem; + padding: 1rem; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + text-align: left; + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 1.5rem; + cursor: pointer; +`; + +const FlexBox = styled(Box)` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const TextItem = styled(Text)<{ isClicked: boolean }>` + color: ${({ isClicked }) => (isClicked ? '#000' : '#fff')}; + font-size: 1rem; +`; diff --git a/src/pages/challenge-list/index.tsx b/src/pages/challenge-list/index.tsx new file mode 100644 index 0000000..5452826 --- /dev/null +++ b/src/pages/challenge-list/index.tsx @@ -0,0 +1,116 @@ +import { useCallback, useEffect, useState } from 'react'; + +import Contents from './components/contents'; +import { useGetChallengeList } from '@/apis/challenge-list/getChallengeList.api'; +import { Tab, Tabs } from '@/components/common/tabs'; +import { TabPanel } from '@/components/common/tabs/tap-panels'; +import TopBar from '@/components/features/layout/top-bar'; +import { Box, Spinner } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +type Challenge = { + id: number; + title: string; + content: string; + participantCount: number; + category: 'HEALTH' | 'ECHO' | 'SHARE' | 'VOLUNTEER' | 'ETC'; + startDate: string; + endDate: string; +}; + +const ChallengeList = () => { + const [activeTab, setActiveTab] = useState(0); + const [allData, setAllData] = useState([]); + const [page, setPage] = useState(0); + + const categoryList = [ + { label: '건강', data: 'HEALTH' }, + { label: '에코', data: 'ECHO' }, + { label: '나눔', data: 'SHARE' }, + { label: '봉사', data: 'VOLUNTEER' }, + ]; + + const { data, isLoading } = useGetChallengeList(page, 20); + + useEffect(() => { + if (data) { + setAllData((prevData) => [...prevData, ...data.data.data]); + } + }, [data]); + + const handleSelectedTab = (value: number) => { + setActiveTab(value); + sessionStorage.setItem('activeTab', String(value)); + }; + + const filteredData = allData.filter( + (item) => item.category === categoryList[activeTab].data + ); + + const loadNextPage = useCallback(() => { + if (data?.data.hasNext && !isLoading) { + setPage((prevPage) => prevPage + 1); + } + }, [data, isLoading]); + + useEffect(() => { + const handleScroll = () => { + if ( + window.innerHeight + document.documentElement.scrollTop !== + document.documentElement.offsetHeight + ) + return; + loadNextPage(); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [loadNextPage]); + + return ( + <> + + + + {categoryList.map((category, index) => ( + + ))} + + + {categoryList.map((_, index) => ( + + <> + {filteredData.map((challenge) => ( + + ))} + + + ))} + + {isLoading && } + + + ); +}; + +export default ChallengeList; + +const ChallengeListLayout = styled.div` + display: block; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + margin: 0 1.5rem; + padding-bottom: 6rem; +`; + +const TabPanelsLayout = styled(Box)` + margin: 1rem 0; +`; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 91efc7a..a16cf8f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,6 +4,7 @@ import { ProtectedRoute } from './protected-route'; import NavBar from '@/components/features/layout/nav-bar'; import ErrorPage from '@/pages/ErrorPage'; import ChallengeDetailPage from '@/pages/challenge-detail'; +import ChallengeList from '@/pages/challenge-list'; import ChallengeRecord from '@/pages/challenge-record'; import DashBoardPage from '@/pages/dashboard'; import LoginPage from '@/pages/login'; @@ -58,6 +59,14 @@ const router = createBrowserRouter([ ), }, + { + path: RouterPath.challengeList, + element: ( + + + + ), + }, { path: `:id/${RouterPath.detail}`, element: ( diff --git a/src/routes/path.ts b/src/routes/path.ts index fd20627..b55e80a 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -15,4 +15,5 @@ export const RouterPath = { register: 'register', review: 'review', write: 'write', + challengeList: 'list', };