Skip to content

Commit

Permalink
Merge pull request #96 from hufs-sports-live/feat/main-match-list-inf…
Browse files Browse the repository at this point in the history
…inite-scroll

[FEAT] 경기 리스트 무한 스크롤 구현
  • Loading branch information
seongminn authored Nov 28, 2023
2 parents e353306 + 90496b4 commit f950673
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 133 deletions.
24 changes: 13 additions & 11 deletions src/api/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MatchCommentType,
MatchLineupType,
MatchListType,
MatchStatus,
MatchTimelineType,
MatchType,
MatchVideoType,
Expand All @@ -13,20 +14,21 @@ import { convertObjectToQueryString } from '@/utils/queryString';
import instance from '.';

export type MatchListParams = {
sportsId: number | number[];
status: 'playing' | 'scheduled' | 'finished';
leagueId: number;
cursor: number;
size: number;
sportsId?: string[];
status: MatchStatus;
leagueId?: string;
cursor?: number;
};

export const getMatchList = async (params: MatchListParams) => {
const queryString = convertObjectToQueryString<
keyof MatchListParams,
MatchListParams[keyof MatchListParams]
>(params);
export const getMatchList = async (
{ cursor, ...params }: MatchListParams,
size = 3,
) => {
const queryString = convertObjectToQueryString(params);

const { data } = await instance.get<MatchListType[]>(`games?${queryString}`);
const { data } = await instance.get<MatchListType[]>(
`games?${queryString}&cursor=${cursor || ''}&size=${size}`,
);

return data;
};
Expand Down
57 changes: 44 additions & 13 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,79 @@
import { Suspense } from 'react';

import SportsList from '@/components/league/SportsList';
import MatchList from '@/components/match/MatchList';
import { QUERY_PARAMS } from '@/constants/queryParams';
import useQueryParams from '@/hooks/useQueryParams';
import MatchListFetcher from '@/queries/useMatchList/Fetcher';
import SportsListFetcher from '@/queries/useSportsListByLeagueId/Fetcher';
import { MatchStatus } from '@/types/match';
import { $ } from '@/utils/core';

export default function Home() {
const { appendToParams, setInParams } = useQueryParams();
const { params, repeatIterator, appendToParams, setInParams } =
useQueryParams();

const paramsObj = repeatIterator(
{} as { status: MatchStatus },
params.entries(),
);

return (
<section className="flex flex-col items-center">
<Suspense>
<SportsListFetcher leagueId="1">
{data => <SportsList sportsList={data} onClick={appendToParams} />}
<SportsListFetcher leagueId={params.get(QUERY_PARAMS.league) || '1'}>
{data => (
<SportsList
selectedId={paramsObj[QUERY_PARAMS.sports] as string[]}
sportsList={data}
onClick={appendToParams}
/>
)}
</SportsListFetcher>
</Suspense>

<div className="mb-8 flex w-fit items-center gap-5 rounded-xl bg-gray-2 text-center">
<button
onClick={() => setInParams('status', 'finished')}
className="text-gary-5 rounded-xl px-5 py-3"
onClick={() => setInParams(QUERY_PARAMS.status, 'finished')}
className={$(
'rounded-xl px-5 py-3 text-gray-5',
params.get(QUERY_PARAMS.status) === 'finished' &&
'bg-primary text-white',
)}
>
종료
</button>
<button
onClick={() => setInParams('status', 'playing')}
className="text-gary-5 rounded-xl px-5 py-3"
onClick={() => setInParams(QUERY_PARAMS.status, 'playing')}
className={$(
'rounded-xl px-5 py-3 text-gray-5',
(params.get(QUERY_PARAMS.status) === 'playing' || null) &&
'bg-primary text-white',
)}
>
진행 중
</button>
<button
onClick={() => setInParams('status', 'scheduled')}
className="text-gary-5 rounded-xl px-5 py-3"
onClick={() => setInParams(QUERY_PARAMS.status, 'scheduled')}
className={$(
'rounded-xl px-5 py-3 text-gray-5',
params.get(QUERY_PARAMS.status) === 'scheduled' &&
'bg-primary text-white',
)}
>
예정
</button>
</div>

{/* <div className="flex flex-col gap-8">
<div className="flex flex-col gap-8">
<Suspense fallback={<div>MatchList 로딩중...</div>}>
<MatchListFetcher {...params} leagueId={1} size={10} cursor={1}>
{data => <MatchList matchList={data} />}
<MatchListFetcher {...paramsObj}>
{({ matchList, ...props }) => (
<MatchList matchList={matchList.pages.flat()} {...props} />
)}
</MatchListFetcher>
</Suspense>
</div> */}
</div>
</section>
);
}
8 changes: 2 additions & 6 deletions src/components/common/Icon/svg/BackgroundLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@ export const BackgroundLogo = ({
...props
}: ComponentProps<'svg'>) => {
return (
<svg
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox={viewBox} xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M85.5606 195.998C77.732 198.97 68.9512 195.108 65.9441 187.377L57.1409 164.737C55.6563 165.736 54.0584 166.581 52.3706 167.248L95.2766 277.598C98.2837 285.337 94.377 294.018 86.5559 296.99L86.5635 296.998C78.7349 299.97 69.9541 296.108 66.947 288.377L2.01711 121.395C-0.989967 113.655 2.91677 104.975 10.7378 102.003C18.5664 99.0299 27.3472 102.892 30.354 110.624L34.2395 120.617C35.9024 119.988 37.6497 119.531 39.4597 119.266L1.01418 20.3946C-1.9929 12.6554 1.91384 3.97519 9.73489 1.00254C17.5635 -1.97012 26.3443 1.89218 29.3511 9.62363L94.2737 176.598C97.2808 184.337 93.374 193.018 85.553 195.99L85.5606 195.998Z"
fill="currentColor"
className="fill-inherit"
/>
</svg>
);
Expand Down
22 changes: 17 additions & 5 deletions src/components/league/SportsList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { Icon } from '@/components/common/Icon';
import { QUERY_PARAMS } from '@/constants/queryParams';
import { SportsType } from '@/types/league';
import { $ } from '@/utils/core';

type SportsListProps = {
selectedId: string[];
sportsList: SportsType[];
onClick: (key: string, value: string) => void;
};

export default function SportsList({ sportsList, onClick }: SportsListProps) {
export default function SportsList({
selectedId = [],
sportsList,
onClick,
}: SportsListProps) {
return (
<ul className="mb-5 flex w-full items-center gap-5">
{sportsList.map(sports => (
<li
key={sports.sportId}
className="text-gary-5 cursor-pointer rounded-xl bg-gray-2"
className={$(
'text-gary-5 cursor-pointer rounded-xl bg-gray-2',
selectedId.includes(sports.sportId + '') && 'bg-primary text-white',
)}
>
<button
onClick={() => onClick('sportsId', String(sports.sportId))}
className="px-3 py-2"
onClick={() => onClick(QUERY_PARAMS.sports, String(sports.sportId))}
className="flex items-center gap-2 px-3 py-2"
>
{sports.name}
<span>{sports.name}</span>
<Icon iconName="cross" width="12" height="12" />
</button>
</li>
))}
Expand Down
88 changes: 55 additions & 33 deletions src/components/match/MatchList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
import Link from 'next/link';

import { MatchCard } from '@/components/common/MatchCard';
import useIntersect from '@/hooks/useInfiniteObserver';
import { MatchListType } from '@/types/match';

type MatchListProps = {
matchList: MatchListType[];
hasNextPage: boolean;
fetchNextPage: () => void;
isFetching: boolean;
};

export default function MatchList({
matchList,
}: {
matchList: MatchListType[];
}) {
fetchNextPage,
hasNextPage,
isFetching,
}: MatchListProps) {
const { ref } = useIntersect<HTMLDivElement>(async (entry, observer) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);

if (hasNextPage && !isFetching) {
fetchNextPage();
}
}
});

return (
<ul>
{matchList.map(({ gameId, ...match }) => (
<li key={gameId} className="mb-14">
<Link href={`match/${gameId}`}>
<MatchCard {...match} className="flex flex-col">
<MatchCard.Label className="mb-2 grid w-full grid-cols-3 border-b-2 border-b-gray-5 px-1 pb-1" />
<div className="flex h-full min-h-[180px] items-center justify-around rounded-xl bg-gray-1 shadow-lg">
<MatchCard.Background
viewBox="-13 117 120 50"
width={150}
height={170}
className="h-[180px]"
/>
<>
<ul>
{matchList.map(({ id, ...match }) => (
<li key={id} className="mb-14">
<Link href={`match/${id}`}>
<MatchCard {...match} className="flex flex-col">
<MatchCard.Label className="mb-2 grid w-full grid-cols-3 border-b-2 border-b-gray-5 px-1 pb-1" />
<div className="flex h-full min-h-[180px] items-center justify-around rounded-xl bg-gray-1 shadow-lg">
<MatchCard.Background
viewBox="-13 117 120 50"
width={150}
height={170}
className="h-[180px] fill-primary"
/>

<MatchCard.Team
teamIndex={1}
className="flex flex-col items-center"
/>
<MatchCard.Score teamIndex={1} />
<MatchCard.Status />
<MatchCard.Score teamIndex={2} />
<MatchCard.Team
teamIndex={2}
className="flex flex-col items-center"
/>
</div>
</MatchCard>
</Link>
</li>
))}
</ul>
<MatchCard.Team
teamIndex={1}
className="flex flex-col items-center"
/>
<MatchCard.Score teamIndex={1} />
<MatchCard.Status />
<MatchCard.Score teamIndex={2} />
<MatchCard.Team
teamIndex={2}
className="flex flex-col items-center"
/>
</div>
</MatchCard>
</Link>
</li>
))}
</ul>
<div ref={ref}></div>
</>
);
}
5 changes: 5 additions & 0 deletions src/constants/queryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const QUERY_PARAMS = {
league: 'league_id',
sports: 'sports_id',
status: 'status',
};
34 changes: 33 additions & 1 deletion src/hooks/useQueryParams.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';

import { MatchStatus } from '@/types/match';

export default function useQueryParams() {
const params = useSearchParams();
const pathname = usePathname();
Expand All @@ -26,5 +28,35 @@ export default function useQueryParams() {
router.push(`${pathname}?${newParams.toString()}`);
};

return { params, appendToParams, setInParams };
type StoreWithStatus<T> = T & { status: MatchStatus };

const repeatIterator = <T extends { [key: string]: string | string[] }>(
store: StoreWithStatus<T>,
iterator: IterableIterator<[string, string]>,
): StoreWithStatus<T> => {
const { value, done } = iterator.next();

if (!done) {
const [iterableKey, iterableValue] = value;

if (iterableKey in store) {
if (Array.isArray(store[iterableKey])) {
(store[iterableKey] as string[]).push(iterableValue);
} else {
(store[iterableKey] as string[]) = [
store[iterableKey] as string,
iterableValue,
];
}
} else {
(store[iterableKey] as string) = iterableValue as string;
}

return repeatIterator(store, iterator);
}

return store;
};

return { params, repeatIterator, appendToParams, setInParams };
}
Loading

0 comments on commit f950673

Please sign in to comment.