Skip to content

Commit b3fde42

Browse files
committed
feat: add scroll to load more
1 parent a41548b commit b3fde42

File tree

7 files changed

+96
-28
lines changed

7 files changed

+96
-28
lines changed

apps/web/@/hooks/useGetImages.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
import { TImageFilter } from "database"
2-
import useSWR from "swr"
1+
import { useMemo } from "react"
32

4-
const getImages = async (filter: TImageFilter) => {
5-
const queryParams = new URLSearchParams()
6-
7-
// TODO: will update filter params later
8-
// Add filter parameters to the query string
9-
if (filter.search) queryParams.append("search", filter.search)
10-
if (filter.order) queryParams.append("order", filter.order)
11-
if (filter.orderBy) queryParams.append("orderBy", filter.orderBy)
12-
// Add any other filter parameters as needed
13-
14-
const url = `/api/protected/images?${queryParams.toString()}`
3+
import { TImageFilter, TListImageResponse } from "database"
4+
import useSWRInfinite from "swr/infinite"
155

6+
const getImages = async (url): Promise<TListImageResponse> => {
167
const response = await fetch(url, {
178
method: "GET",
189
headers: {
@@ -28,14 +19,36 @@ const getImages = async (filter: TImageFilter) => {
2819
}
2920

3021
export function useGetImages(filter: TImageFilter) {
31-
const { data, error, isLoading, mutate } = useSWR(["/api/protected/images", filter], () =>
32-
getImages(filter)
22+
const { data, mutate, size, setSize, isLoading, error } = useSWRInfinite(
23+
(index) => {
24+
const queryParams = new URLSearchParams()
25+
26+
if (filter.search) queryParams.append("search", filter.search)
27+
// if (filter.order) queryParams.append("order", filter.order)
28+
// if (filter.orderBy) queryParams.append("orderBy", filter.orderBy)
29+
queryParams.append("page", (index + 1).toString())
30+
31+
return `/api/protected/images?${queryParams.toString()}`
32+
},
33+
(url) => getImages(url)
3334
)
3435

36+
const images = useMemo(() => (data || []).flatMap((page) => page.data.data.data), [data])
37+
const totalPages = useMemo(() => data?.[0]?.data?.data?.totalPages, [data])
38+
39+
const fetchMore = () => {
40+
if (size >= totalPages) {
41+
return
42+
}
43+
44+
setSize(size + 1)
45+
}
46+
3547
return {
36-
images: data?.data?.data?.data,
48+
images,
3749
isLoading,
3850
isError: error,
3951
mutate,
52+
fetchMore,
4053
}
4154
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useCallback, useEffect, useRef, useState } from "react"
2+
3+
const useInfiniteScroll = (callback: () => void, root: HTMLElement | null, isFetching: boolean) => {
4+
// const [isFetching, setIsFetching] = useState(false)
5+
const observer = useRef<IntersectionObserver | null>(null)
6+
const [node, setNode] = useState<HTMLElement | null>(null)
7+
8+
const handleIntersection = useCallback(
9+
(entries: IntersectionObserverEntry[]) => {
10+
if (entries[0].isIntersecting && !isFetching) {
11+
console.log("isIntersecting", entries[0])
12+
callback?.()
13+
}
14+
},
15+
[callback, isFetching]
16+
)
17+
18+
useEffect(() => {
19+
if (!root || !node || isFetching) return
20+
21+
if (observer.current) {
22+
observer.current.disconnect()
23+
}
24+
25+
observer.current = new IntersectionObserver(handleIntersection, {
26+
root,
27+
rootMargin: "100px",
28+
threshold: 0.1,
29+
})
30+
31+
observer.current.observe(node)
32+
33+
return () => {
34+
if (observer.current) {
35+
observer.current.disconnect()
36+
}
37+
}
38+
}, [handleIntersection, root, node])
39+
40+
return { setNode }
41+
}
42+
43+
export default useInfiniteScroll

apps/web/@/molecules/upload/AssetsManagement.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1-
import { useMemo } from "react"
1+
import { useMemo, useRef } from "react"
22

33
import { useGetImages } from "@/hooks/useGetImages"
4+
import useInfiniteScroll from "@/hooks/useInfinityScroll"
45

56
import { useFileManager } from "./FileManagerContainer"
67
import ImageList from "./ImageList"
78
import ImageSearchBar from "./ImageSearchBar"
89

910
const AssetManagement = () => {
1011
const { search } = useFileManager()
12+
const imageListRef = useRef<HTMLDivElement>(null)
1113

1214
const filterParams = useMemo(() => {
1315
return {
1416
search,
1517
}
1618
}, [search])
1719

18-
const { images, isLoading, isError } = useGetImages(filterParams)
20+
const { images, isLoading, fetchMore } = useGetImages(filterParams)
21+
const { setNode } = useInfiniteScroll(fetchMore, imageListRef.current, isLoading)
1922

2023
return (
21-
<div className="h-[400px] overflow-scroll px-4 py-4">
22-
<ImageSearchBar />
23-
24+
<div
25+
ref={imageListRef}
26+
className="h-[400px] overflow-scroll px-4 py-4"
27+
>
2428
<ImageList
2529
isLoading={isLoading}
2630
images={images}
2731
/>
32+
33+
<div ref={setNode}>
34+
<div className="h-10 w-full bg-transparent" />
35+
</div>
2836
</div>
2937
)
3038
}

apps/web/@/molecules/upload/ImageSearchBar.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function ImageSearchBar() {
1414
}
1515

1616
return (
17-
<div className="flex gap-4">
17+
<div className="flex gap-4 px-4 py-2">
1818
<Input
1919
value={search}
2020
onChange={(e) => setSearch(e.target.value)}
@@ -23,14 +23,13 @@ export default function ImageSearchBar() {
2323

2424
<Select>
2525
<SelectTrigger className="w-[180px]">
26-
<SelectValue placeholder="Select a fruit" />
26+
<SelectValue placeholder={t("common.sort_by")} />
2727
</SelectTrigger>
2828
<SelectContent>
29-
<SelectItem value="apple">Apple</SelectItem>
30-
<SelectItem value="banana">Banana</SelectItem>
31-
<SelectItem value="blueberry">Blueberry</SelectItem>
32-
<SelectItem value="grapes">Grapes</SelectItem>
33-
<SelectItem value="pineapple">Pineapple</SelectItem>
29+
<SelectItem value="apple">Name A → Z</SelectItem>
30+
<SelectItem value="banana">Name Z → A</SelectItem>
31+
<SelectItem value="blueberry">Recent created</SelectItem>
32+
<SelectItem value="grapes">Last created</SelectItem>
3433
</SelectContent>
3534
</Select>
3635

apps/web/@/molecules/upload/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { truncateFileName } from "@/utils/text"
1818

1919
import AssetManagement from "./AssetsManagement"
2020
import FileManagerContainer, { useFileManager } from "./FileManagerContainer"
21+
import ImageSearchBar from "./ImageSearchBar"
2122
import UploadImageButton from "./UploadImageButton"
2223

2324
interface UploadProps {
@@ -103,6 +104,8 @@ const Upload: React.FC<UploadProps> = ({ children, onSelect }) => {
103104
<UploadImageButton />
104105
</DialogHeader>
105106

107+
<ImageSearchBar />
108+
106109
<AssetManagement />
107110

108111
<DialogFooter className="mt-0 flex flex-row items-center justify-between border-t px-4 py-2">

packages/database/src/images/queries.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const getImages = async (
5757
data: {
5858
data,
5959
total,
60+
totalPages: Math.ceil(total / (options.limit ?? DEFAULT_LIMIT)),
6061
page: options.page ?? DEFAULT_PAGE,
6162
limit: options.limit ?? DEFAULT_LIMIT,
6263
},

packages/database/src/shared/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ export type TGetListResponse<T> = {
2525
data: T[]
2626
total: number
2727
page: number
28+
totalPages: number
2829
limit: number
2930
}

0 commit comments

Comments
 (0)