-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(fe): implement infinite scroll (#1532)
* feat: set up providers and install react query * feat: add useInview, edit fetch method * feat: implement infinite scroll with DataSet Type * feat: add isLoadButton state * feat: convert to suspenseinfinitequery * fix: fix return value to apply suspense * fix: return isFetchingNextPage * fix: fix take value * feat: add react-query * feat: implement problem infinite table * feat: replace problemtable with probleminfinitetable * chore: change provider location * chore: remove previous problem table
- Loading branch information
Showing
6 changed files
with
248 additions
and
91 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
apps/frontend/app/(main)/problem/_components/ProblemInfiniteTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
'use client' | ||
|
||
import DataTable from '@/components/DataTable' | ||
import SearchBar from '@/components/SearchBar' | ||
import { Skeleton } from '@/components/ui/skeleton' | ||
import { useInfiniteScroll } from '@/lib/useInfiniteScroll' | ||
import type { Problem } from '@/types/type' | ||
import { useSearchParams } from 'next/navigation' | ||
import { columns } from './Columns' | ||
|
||
export default function ProblemInfiniteTable() { | ||
const searchParams = useSearchParams() | ||
const search = searchParams.get('search') ?? '' | ||
const order = searchParams.get('order') ?? 'id-asc' | ||
const newSearchParams = new URLSearchParams() | ||
newSearchParams.set('search', search) | ||
newSearchParams.set('order', order) | ||
|
||
const { items, total, ref, isFetchingNextPage } = useInfiniteScroll<Problem>({ | ||
pathname: 'problem', | ||
query: newSearchParams | ||
}) | ||
|
||
return ( | ||
<> | ||
<div className="flex items-center justify-between text-gray-500"> | ||
<div className="flex gap-1"> | ||
<p className="text-2xl font-bold text-gray-500">All</p> | ||
<p className="text-2xl font-bold text-blue-500">{total}</p> | ||
</div> | ||
<SearchBar /> | ||
</div> | ||
<div className="flex flex-col items-center"> | ||
<DataTable | ||
data={items} | ||
columns={columns} | ||
headerStyle={{ | ||
title: 'text-left w-5/12', | ||
difficulty: 'w-2/12', | ||
submissionCount: 'w-2/12', | ||
acceptedRate: 'w-2/12', | ||
info: 'w-1/12' | ||
}} | ||
linked | ||
/> | ||
{isFetchingNextPage && ( | ||
<> | ||
{[...Array(5)].map((_, i) => ( | ||
<Skeleton key={i} className="my-2 flex h-12 w-full rounded-xl" /> | ||
))} | ||
</> | ||
)} | ||
<div ref={ref} /> | ||
</div> | ||
</> | ||
) | ||
} |
40 changes: 0 additions & 40 deletions
40
apps/frontend/app/(main)/problem/_components/ProblemTable.tsx
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,48 @@ | ||
import SearchBar from '@/components/SearchBar' | ||
'use client' | ||
|
||
import { Skeleton } from '@/components/ui/skeleton' | ||
import type { Problem } from '@/types/type' | ||
import { QueryClientProvider, QueryClient } from '@tanstack/react-query' | ||
import { Suspense } from 'react' | ||
import ProblemTable from './_components/ProblemTable' | ||
|
||
interface ProblemProps { | ||
searchParams: { search: string; tag: string; order: string } | ||
} | ||
|
||
export default function Problem({ searchParams }: ProblemProps) { | ||
const search = searchParams?.search ?? '' | ||
const order = searchParams?.order ?? 'id-asc' | ||
import ProblemInfiniteTable from './_components/ProblemInfiniteTable' | ||
|
||
export default function Problem() { | ||
const queryClient = new QueryClient() | ||
return ( | ||
<> | ||
<div className="flex justify-end text-gray-500"> | ||
<SearchBar /> | ||
</div> | ||
<Suspense | ||
fallback={ | ||
<> | ||
<div className="mt-4 flex"> | ||
<span className="w-5/12"> | ||
<Skeleton className="h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-1/12"> | ||
<Skeleton className="mx-auto h-6 w-12" /> | ||
</span> | ||
</div> | ||
{[...Array(5)].map((_, i) => ( | ||
<Skeleton key={i} className="my-2 flex h-12 w-full rounded-xl" /> | ||
))} | ||
</> | ||
} | ||
> | ||
<ProblemTable search={search} order={order} /> | ||
</Suspense> | ||
<QueryClientProvider client={queryClient}> | ||
<Suspense | ||
fallback={ | ||
<> | ||
<div className="mt-4 flex"> | ||
<span className="w-5/12"> | ||
<Skeleton className="h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-2/12"> | ||
<Skeleton className="mx-auto h-6 w-20" /> | ||
</span> | ||
<span className="w-1/12"> | ||
<Skeleton className="mx-auto h-6 w-12" /> | ||
</span> | ||
</div> | ||
{[...Array(5)].map((_, i) => ( | ||
<Skeleton | ||
key={i} | ||
className="my-2 flex h-12 w-full rounded-xl" | ||
/> | ||
))} | ||
</> | ||
} | ||
> | ||
<ProblemInfiniteTable /> | ||
</Suspense> | ||
</QueryClientProvider> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query' | ||
import { useEffect } from 'react' | ||
import { useInView } from 'react-intersection-observer' | ||
import type { URLSearchParams } from 'url' | ||
import { fetcher, fetcherWithAuth } from './utils' | ||
|
||
interface Item { | ||
id: number | ||
} | ||
|
||
interface DataSet<T> { | ||
problems: T[] //TODO: 백엔드 get류 api return type 통일되면 problems -> data로 고치기 | ||
total: number | ||
} | ||
|
||
interface UseInfiniteScrollProps { | ||
pathname: string | ||
query: URLSearchParams | ||
itemsPerPage?: number | ||
withAuth?: boolean | ||
} | ||
|
||
/** | ||
* | ||
* @param pathname | ||
* The path or endpoint from which data will be fetched. | ||
* | ||
* @param query | ||
* The URLSearchParams object representing the query parameters for data fetching. | ||
* | ||
* @param itemsPerPage | ||
* The number of items to fetch per page. Default is 5. | ||
* | ||
* @param withAuth | ||
* A boolean indicating whether authentication is required for data fetching. Default is false. | ||
* | ||
*/ | ||
|
||
export const useInfiniteScroll = <T extends Item>({ | ||
pathname, | ||
query, | ||
itemsPerPage = 10, | ||
withAuth = false | ||
}: UseInfiniteScrollProps) => { | ||
//fetch datas with pageParams and url | ||
const getInfiniteData = async ({ | ||
pageParam | ||
}: { | ||
pageParam?: number | ||
}): Promise<DataSet<T>> => { | ||
if (!query.has('take')) query.append('take', String(itemsPerPage)) | ||
pageParam && pageParam > 0 && query.set('cursor', pageParam.toString()) | ||
let dataSet: DataSet<T> | ||
withAuth | ||
? (dataSet = await fetcherWithAuth | ||
.get(pathname, { | ||
searchParams: query | ||
}) | ||
.json()) | ||
: (dataSet = await fetcher | ||
.get(pathname, { | ||
searchParams: query | ||
}) | ||
.json()) | ||
return dataSet | ||
} | ||
|
||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = | ||
useSuspenseInfiniteQuery({ | ||
queryKey: [pathname, query.toString()], | ||
staleTime: 0, | ||
queryFn: getInfiniteData, | ||
initialPageParam: 0, | ||
getNextPageParam: (lastPage: DataSet<T>) => { | ||
return lastPage?.problems.length === 0 || //TODO: fix problem to data | ||
lastPage?.problems.length < itemsPerPage | ||
? undefined //페이지에 있는 아이템 수가 0이거나 itemsPerPage보다 작으면 undefined를 반환합니다. | ||
: lastPage.problems.at(-1)?.id //cursor를 getData의 params로 넘겨줍니다. | ||
} | ||
}) | ||
|
||
const { ref, inView } = useInView() | ||
|
||
useEffect(() => { | ||
if (inView && !isFetchingNextPage && hasNextPage) { | ||
fetchNextPage() | ||
} | ||
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage, data]) | ||
|
||
/* | ||
5번 이상 바닥에 닿으면 자동 페칭을 멈추고, loadmore 버튼을 보이게 합니다. | ||
const scrollCounter = useRef(0) // 바닥에 닿은 횟수를 세는 카운터 | ||
const [isLoadButton, setIsLoadButton] = useState(false) | ||
useEffect(() => { | ||
if (inView && !isFetchingNextPage) { | ||
if (hasNextPage) { | ||
if (scrollCounter.current < 5) { | ||
setIsLoadButton(false) | ||
fetchNextPage() | ||
scrollCounter.current += 1 | ||
} else { | ||
setIsLoadButton(true) | ||
} | ||
} | ||
} else setIsLoadButton(false) | ||
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage, data]) | ||
*/ | ||
|
||
return { | ||
items: data.pages.flat().flatMap((page) => page.problems), | ||
total: data.pages.at(0)?.total, | ||
fetchNextPage, | ||
ref, | ||
isFetchingNextPage | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.