CoastalCommitsPastes/client/components/post-list/index.tsx

171 lines
4 KiB
TypeScript

import { Button, Input, Select, Text } from "@geist-ui/core"
import NextLink from "next/link"
import Link from "../Link"
import styles from "./post-list.module.css"
import ListItemSkeleton from "./list-item-skeleton"
import ListItem from "./list-item"
import { Post } from "@lib/types"
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"
import Cookies from "js-cookie"
import useDebounce from "@lib/hooks/use-debounce"
type Props = {
initialPosts: Post[]
error: boolean
morePosts: boolean
}
const PostList = ({ morePosts, initialPosts, error }: Props) => {
const [search, setSearchValue] = useState("")
const [posts, setPosts] = useState<Post[]>(initialPosts)
const [searching, setSearching] = useState(false)
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
const debouncedSearchValue = useDebounce(search, 200)
const loadMoreClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (hasMorePosts) {
async function fetchPosts() {
const res = await fetch(`/server-api/posts/mine`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`,
"x-page": `${posts.length / 10 + 1}`
}
})
const json = await res.json()
setPosts([...posts, ...json.posts])
setHasMorePosts(json.morePosts)
}
fetchPosts()
}
},
[posts, hasMorePosts]
)
// update posts on search
useEffect(() => {
if (debouncedSearchValue) {
// fetch results from /server-api/posts/search
const fetchResults = async () => {
setSearching(true)
//encode search
const res = await fetch(
`/server-api/posts/search?q=${encodeURIComponent(
debouncedSearchValue
)}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
// "tok": process.env.SECRET_KEY || ''
}
}
)
const data = await res.json()
setPosts(data)
setSearching(false)
}
fetchResults()
} else {
setPosts(initialPosts)
}
}, [initialPosts, debouncedSearchValue])
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value)
}
// const debouncedSearchHandler = useMemo(
// () => debounce(handleSearchChange, 300),
// []
// )
// useEffect(() => {
// return () => {
// debouncedSearchHandler.cancel()
// }
// }, [debouncedSearchHandler])
const deletePost = useCallback(
(postId: string) => async () => {
const res = await fetch(`/server-api/posts/${postId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
}
})
if (!res.ok) {
console.error(res)
return
} else {
setPosts((posts) => posts.filter((post) => post.id !== postId))
}
},
[]
)
return (
<div className={styles.container}>
<div className={styles.searchContainer}>
<Input
scale={3 / 2}
clearable
placeholder="Search..."
onChange={handleSearchChange}
/>
</div>
{error && <Text type="error">Failed to load.</Text>}
{!posts.length && searching && (
<ul>
<li>
<ListItemSkeleton />
</li>
<li>
<ListItemSkeleton />
</li>
</ul>
)}
{posts?.length === 0 && !error && (
<Text type="secondary">
No posts found. Create one{" "}
<NextLink passHref={true} href="/new">
<Link color>here</Link>
</NextLink>
.
</Text>
)}
{posts?.length > 0 && (
<div>
<ul>
{posts.map((post) => {
return (
<ListItem
deletePost={deletePost(post.id)}
post={post}
key={post.id}
/>
)
})}
</ul>
</div>
)}
{hasMorePosts && !setSearchValue && (
<div className={styles.moreContainer}>
<Button width={"100%"} onClick={loadMoreClick}>
Load more
</Button>
</div>
)}
</div>
)
}
export default PostList