Skip to content

Commit c45413c

Browse files
authored
Merge pull request #131 from CodeForStartup/feat/upload-image
feat: add image banner for post
2 parents 8154d2f + 9e98ccd commit c45413c

File tree

47 files changed

+1918
-173
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1918
-173
lines changed

apps/web/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/.pnp
66
.pnp.js
77

8+
/public/uploads/
9+
810
# testing
911
/coverage
1012

apps/web/@/actions/public/posts.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

apps/web/@/actions/public/tags/index.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

apps/web/@/actions/user/user.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

apps/web/@/constants/upload.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const OrderByField = {
2+
newest: "newest",
3+
oldest: "oldest",
4+
nameAsc: "name_asc",
5+
nameDesc: "name_desc",
6+
}

apps/web/@/hooks/useGetImages.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useMemo } from "react"
2+
3+
import { IImageFilter, IListImageResponse } from "database"
4+
import useSWRInfinite from "swr/infinite"
5+
6+
const getImages = async (url): Promise<IListImageResponse> => {
7+
const response = await fetch(url, {
8+
method: "GET",
9+
headers: {
10+
"Content-Type": "application/json",
11+
},
12+
})
13+
14+
if (!response.ok) {
15+
throw new Error("Failed to fetch images")
16+
}
17+
18+
return response.json()
19+
}
20+
21+
export function useGetImages(filter: IImageFilter) {
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+
queryParams.append("page", (index + 1).toString())
29+
30+
return `/api/protected/images?${queryParams.toString()}`
31+
},
32+
(url) => getImages(url)
33+
)
34+
35+
const images = useMemo(() => (data || []).flatMap((page) => page?.data?.data?.data), [data])
36+
const totalPages = useMemo(() => data?.[0]?.data?.data?.totalPages, [data])
37+
const total = useMemo(() => data?.[0]?.data?.data?.total, [data])
38+
39+
const fetchMore = () => {
40+
if (size >= totalPages) {
41+
return
42+
}
43+
44+
setSize(size + 1)
45+
}
46+
47+
return {
48+
images,
49+
total,
50+
isLoading,
51+
isError: error,
52+
mutate,
53+
fetchMore,
54+
}
55+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
callback?.()
12+
}
13+
},
14+
[callback, isFetching]
15+
)
16+
17+
useEffect(() => {
18+
if (!root || !node || isFetching) return
19+
20+
if (observer.current) {
21+
observer.current.disconnect()
22+
}
23+
24+
observer.current = new IntersectionObserver(handleIntersection, {
25+
root,
26+
rootMargin: "100px",
27+
threshold: 0.1,
28+
})
29+
30+
observer.current.observe(node)
31+
32+
return () => {
33+
if (observer.current) {
34+
observer.current.disconnect()
35+
}
36+
}
37+
}, [handleIntersection, root, node])
38+
39+
return { setNode }
40+
}
41+
42+
export default useInfiniteScroll

apps/web/@/hooks/useUploadImage.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from "react"
2+
3+
import { toast } from "react-toastify"
4+
import { useSWRConfig } from "swr"
5+
import useSWRMutation from "swr/mutation"
6+
import useSWRImmutable from "swr/mutation"
7+
8+
// upload image to server API
9+
const uploadImage = async (file: File) => {
10+
try {
11+
const formData = new FormData()
12+
13+
formData.append("file", file)
14+
15+
// Todo: replace with hook
16+
const response = await fetch("/api/protected/images", {
17+
method: "POST",
18+
body: formData,
19+
headers: {
20+
Authorization: `Bearer ${localStorage.getItem("token")}`,
21+
},
22+
})
23+
24+
toast.success("Image uploaded successfully")
25+
26+
return response.json()
27+
} catch (error) {
28+
toast.error("Error uploading image")
29+
// throw error
30+
}
31+
}
32+
33+
// upload image hook
34+
export const useUploadImage = () => {
35+
const { mutate } = useSWRConfig()
36+
37+
const { trigger, isMutating, error, data } = useSWRMutation(
38+
"/api/protected/images",
39+
async (url, { arg }: { arg: File }) => {
40+
const result = await uploadImage(arg)
41+
mutate(["/api/protected/images"])
42+
return result
43+
}
44+
)
45+
46+
return { uploadImage: trigger, isMutating, error, data }
47+
}

apps/web/@/messages/en.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"register": "Register",
2929
"profile": "Profile",
3030
"search": "Search",
31+
"clear": "Clear",
3132
"searchPlaceholder": "Enter your search here...",
3233
"searchResults": "Search Results",
3334
"searchNoResults": "No results found",
@@ -110,6 +111,30 @@
110111
"turn_draft": "Turn draft",
111112
"turn_publish": "Turn publish",
112113
"post_created": "Post created successfully",
113-
"post_updated": "Post updated successfully"
114+
"post_updated": "Post updated successfully",
115+
"order_by": "Order by",
116+
"order_by_asc": "Ascending",
117+
"order_by_desc": "Descending",
118+
"order_by_created_at": "Created at",
119+
"order_by_name": "Name",
120+
"order_by_size": "Size",
121+
"order_by_type": "Type",
122+
"order_by_uploaded_at": "Uploaded at"
123+
},
124+
"uploads": {
125+
"asset_management": "Asset Management",
126+
"upload_image": "Upload Image",
127+
"select": "Select",
128+
"upload": "Upload",
129+
"has_been_selected": "has been selected",
130+
"image_uploaded_successfully": "Image uploaded successfully",
131+
"error_uploading_image": "Error uploading image",
132+
"total_images": "{total, plural, =0 {No image found} =1 {Total 1 image} other {Total # images}}",
133+
"order_by": {
134+
"newest": "Newest",
135+
"oldest": "Oldest",
136+
"name_asc": "Name (A → Z)",
137+
"name_desc": "Name (Z → A)"
138+
}
114139
}
115140
}

apps/web/@/molecules/home/filter/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
66
import { cn } from "ui"
77

88
import { FilterValues, PeriodValues } from "@/types/filter"
9-
import { capitalizeFirstLetter } from "@/utils/capitalize"
9+
import { capitalizeFirstLetter } from "@/utils/text"
1010

1111
import { FilterItem } from "./filter-item"
1212

0 commit comments

Comments
 (0)