Skip to content

Commit

Permalink
✨ Add search posts feature
Browse files Browse the repository at this point in the history
  • Loading branch information
anxiubin committed Dec 13, 2023
1 parent 734ca26 commit a036fd7
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 30 deletions.
18 changes: 13 additions & 5 deletions src/features/post/components/search-item/SearchItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { IPostItem } from "@/features/post/types"
import { ISearchPostItem } from "@/features/post/types"
import { Badge } from "@/components/ui/badge"

const SearchItem = ({
id,
Expand All @@ -15,8 +16,9 @@ const SearchItem = ({
commentsCount,
content,
createdDate,
categoryName,
onClickPost,
}: IPostItem) => {
}: ISearchPostItem) => {
const handleClickPost = (id: string) => {
onClickPost && id && onClickPost(id)
}
Expand All @@ -34,13 +36,19 @@ const SearchItem = ({
{title}
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<CardContent>
<p
className="overflow-hidden whitespace-normal text-ellipsis break-words line-clamp-3"
style={{ height: 72 }}
className="overflow-hidden whitespace-normal text-ellipsis break-words line-clamp-2"
style={{ height: 48 }}
>
{content}
</p>

{categoryName && (
<div className="mt-2">
<Badge className="bg-green-700 text-sm">{categoryName}</Badge>
</div>
)}
</CardContent>
<CardFooter className="flex flex-wrap gap-2 text-sm text-gray-400 border-b-2 border-b-gray-100">
<p>{createdDate}</p>
Expand Down
33 changes: 29 additions & 4 deletions src/features/post/operations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export const LIST_POST = gql(`
id
}
createdAt
category {
id
name
}
}
}
}
Expand Down Expand Up @@ -75,6 +71,35 @@ export const UPDATE_POST = gql(`
}
`)

export const SEARCH_POSTS = gql(`
query SearchPosts($searchCriteria: PostSearchInput!,$pagination:PaginationInput!) {
searchPosts(searchCriteria: $searchCriteria, pagination:$pagination) {
pagination {
page
pageSize
totalItems
}
posts {
id
title
content
author {
id
name
email
}
comments {
id
}
category {
name
}
createdAt
}
}
}
`)

/** COMMENT OPERATION */
export const LIST_COMMENTS = gql(`
query ListComments($postId: Int!) {
Expand Down
6 changes: 3 additions & 3 deletions src/features/post/pages/posts/Posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ const PAGE_SIZE = 20
const Posts = () => {
const {
getPosts,
postResults: { data, fetchMore, loading },
postResult: { data, fetchMore, loading },
} = usePost()

const [page, setPage] = useState(1)

const postsCount = data?.listPosts.posts.length
const totalCount = data?.listPosts.pagination.totalItems
const postsCount = data?.listPosts.posts.length || 0
const totalCount = data?.listPosts.pagination.totalItems || 0
const isReachingEnd = Boolean(!postsCount || postsCount === totalCount)

const posts = useMemo((): IPostItem[] => {
Expand Down
116 changes: 111 additions & 5 deletions src/features/post/pages/search/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,131 @@
import { useDebounce } from "@/hooks/useDebounce"
import useIntersectionObserver from "@/hooks/useIntersectionObserver"
import { SearchOutlined } from "@ant-design/icons"
// import { useSearchParams } from "react-router-dom"
// import SearchList from "../../widgets/search-list/SearchList"
import { ChangeEvent, useEffect, useMemo, useState } from "react"
import { useSearchParams } from "react-router-dom"
import usePost from "../../service/usePost"
import { ISearchPostItem } from "../../types"
import { formatYYMMDD } from "@/lib/formatDate"
import SearchList from "../../widgets/search-list/SearchList"

const PAGE_SIZE = 10

const Search = () => {
// const [searchParams, setSearchParams] = useSearchParams()
const [_, setSearchParams] = useSearchParams()

const [value, setValue] = useState("")
const debouncedValue = useDebounce<string>(value, 500)
const [page, setPage] = useState(1)
const {
searchPosts,
searchPostsResult: { data, fetchMore, loading },
} = usePost()

const postsCount = data?.searchPosts.posts.length || 0
const totalCount = data?.searchPosts.pagination.totalItems || 0
const isReachingEnd = Boolean(!postsCount || postsCount === totalCount)

const posts = useMemo((): ISearchPostItem[] => {
const postsList = data?.searchPosts.posts

if (!postsList?.length) {
return []
} else {
return postsList.map(
({ id, title, content, createdAt, comments, author, category }) =>
({
id,
title,
content,
authorName: author.name || author.email,
commentsCount: comments?.length ?? 0,
categoryName: category ? category?.name : "",
createdDate: formatYYMMDD(createdAt),
} as ISearchPostItem)
)
}
}, [data])

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
}

useEffect(() => {
setSearchParams({
q: debouncedValue,
})

if (debouncedValue.length) {
searchPosts({
variables: {
pagination: {
page: 1,
pageSize: PAGE_SIZE,
},
searchCriteria: {
title: debouncedValue,
content: debouncedValue,
},
},
})
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue])

const { setTarget } = useIntersectionObserver({
rootMargin: "50px",
onIntersect: (entries) => {
if (entries[0].isIntersecting && !loading && !isReachingEnd) {
setPage((prevPage) => prevPage + 1)
}
},
})

useEffect(() => {
if (page > 1 && !isReachingEnd) {
fetchMore({
variables: {
pagination: { page, pageSize: PAGE_SIZE },
},
updateQuery(previousResult, { fetchMoreResult }) {
const prevPosts = previousResult.searchPosts.posts
const newPosts = fetchMoreResult.searchPosts.posts
if (!fetchMoreResult.searchPosts.posts.length) {
return previousResult
} else {
fetchMoreResult.searchPosts.posts = [...prevPosts, ...newPosts]
return {
...fetchMoreResult,
}
}
},
})
}
}, [page, isReachingEnd, fetchMore])

return (
<div className="w-full max-w-7xl h-full py-16 flex flex-col items-center ">
<div className="w-full sm:w-3/4 ">
{/** SEARCH INPUT */}
<div className="border-2 border-gray-400 flex items-center gap-4 p-4 mb-8">
<div className="border-2 border-gray-400 flex items-center gap-4 p-4 mb-4">
<SearchOutlined style={{ fontSize: 30 }} />
<input
type="text"
placeholder="Search..."
className="text-lg w-full focus-visible:outline-none placeholder:text-lg"
onChange={handleChange}
/>
</div>

{/** SEARCH RESULT */}
{/* <SearchList posts={posts} /> */}
<section className="flex flex-col">
{Boolean(!!debouncedValue.length && totalCount > 0) && (
<div className="mb-8 font-semibold">{totalCount} results</div>
)}
{!!debouncedValue.length && <SearchList posts={posts} />}
<div ref={setTarget} className="w-full h-1"></div>
</section>
</div>
</div>
)
Expand Down
12 changes: 10 additions & 2 deletions src/features/post/service/usePost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
DELETE_POST,
FIND_CATEGORY,
LIST_POST,
SEARCH_POSTS,
UPDATE_POST,
VIEW_POST,
} from "../operations"

const usePost = () => {
const [getPosts, postResults] = useLazyQuery(LIST_POST, {
const [getPosts, postResult] = useLazyQuery(LIST_POST, {
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
})
Expand All @@ -24,6 +25,11 @@ const usePost = () => {

const [updatePost, updatePostResult] = useMutation(UPDATE_POST)

const [searchPosts, searchPostsResult] = useLazyQuery(SEARCH_POSTS, {
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
})

const [createCategory, createCategoryResult] = useMutation(CREATE_CATEGORY)

const [assignCategory, assignCategoryResult] = useMutation(ASSIGN_CATEGORY)
Expand All @@ -32,7 +38,7 @@ const usePost = () => {

return {
getPosts,
postResults,
postResult,
createPost,
createPostResult,
deletePost,
Expand All @@ -41,6 +47,8 @@ const usePost = () => {
viewPostResult,
updatePost,
updatePostResult,
searchPosts,
searchPostsResult,
createCategory,
createCategoryResult,
assignCategory,
Expand Down
4 changes: 4 additions & 0 deletions src/features/post/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export interface IPostItem {
createdDate: string
onClickPost?: (id: string) => void
}

export interface ISearchPostItem extends IPostItem {
categoryName?: string
}
10 changes: 5 additions & 5 deletions src/features/post/widgets/search-list/SearchList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useNavigate } from "react-router-dom"
import { IPostItem } from "../../types"
import { ISearchPostItem } from "../../types"
import SearchItem from "../../components/search-item/SearchItem"

interface IPostListProps {
posts: IPostItem[]
interface ISearchListProps {
posts: ISearchPostItem[]
}

const SearchList = ({ posts }: IPostListProps) => {
const SearchList = ({ posts }: ISearchListProps) => {
const navigate = useNavigate()

const handleClickPost = (id: string) => {
Expand All @@ -15,7 +15,7 @@ const SearchList = ({ posts }: IPostListProps) => {

return (
<ul className="flex flex-col gap-8 w-full">
{posts.map((post: IPostItem) => (
{posts.map((post: ISearchPostItem) => (
<SearchItem key={post.id} {...post} onClickPost={handleClickPost} />
))}
</ul>
Expand Down
4 changes: 2 additions & 2 deletions src/features/post/widgets/update-post-form/UpdatePostForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const WritePostForm = () => {
},
})

const handleFindCategoryAndCreatePost = () => {
const handleFindCategoryAndUpdatePost = () => {
findCategory({
variables: {
name: form.getValues("category"),
Expand All @@ -63,7 +63,7 @@ const WritePostForm = () => {
const errorMessage = error.graphQLErrors[0].message

if (errorMessage.includes("exists")) {
handleFindCategoryAndCreatePost()
handleFindCategoryAndUpdatePost()
} else {
form.setError("category", {
type: "validate",
Expand Down
15 changes: 15 additions & 0 deletions src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect, useState } from 'react'

export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500)

return () => {
clearTimeout(timer)
}
}, [value, delay])

return debouncedValue
}
9 changes: 7 additions & 2 deletions src/lib/graphql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
const documents = {
"\nmutation RegisterUser($createUserInput: CreateUserInput!) {\n registerUser(createUserInput: $createUserInput) {\n id\n email\n name\n role {\n id\n }\n }\n}\n": types.RegisterUserDocument,
"\nmutation LoginUser($credentials: LoginInput!) {\n loginUser(credentials: $credentials) {\n token\n user {\n id\n email\n name\n role {\n id\n }\n }\n }\n}\n": types.LoginUserDocument,
"\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n category {\n id\n name\n }\n }\n\t\t}\n\t}\n\t": types.ListPostsDocument,
"\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n }\n\t\t}\n\t}\n\t": types.ListPostsDocument,
"\n mutation CreatePost($createPostInput: CreatePostInput!) {\n createPost(createPostInput: $createPostInput) {\n id\n }\n }\n": types.CreatePostDocument,
"\n mutation DeletePost($postId: Int!) {\n deletePost(postId: $postId) {\n id\n }\n }\n": types.DeletePostDocument,
"\nquery ViewPost($id: Int!) {\n viewPost(id: $id) {\n id\n title\n content\n author {\n id\n name\n email\n }\n createdAt\n category {\n id\n name\n }\n }\n}\n": types.ViewPostDocument,
"\n mutation UpdatePost($postId: Int!, $updateData: UpdatePostInput!) {\n updatePost(postId: $postId, updateData: $updateData) {\n id\n }\n }\n": types.UpdatePostDocument,
"\nquery SearchPosts($searchCriteria: PostSearchInput!,$pagination:PaginationInput!) {\n searchPosts(searchCriteria: $searchCriteria, pagination:$pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n category {\n name\n }\n createdAt\n }\n }\n}\n": types.SearchPostsDocument,
"\n query ListComments($postId: Int!) {\n listComments(postId: $postId) { \n id\n content\n author {\n id\n name\n email\n }\n createdAt\n }\n }\n": types.ListCommentsDocument,
"\n mutation AddComment($createCommentInput: CreateCommentInput!) {\n addComment(createCommentInput: $createCommentInput) {\n id\n }\n }\n": types.AddCommentDocument,
"\n mutation UpdateComment($updateCommentInput: UpdateCommentInput!) {\n updateComment(updateCommentInput: $updateCommentInput) {\n id\n }\n }\n": types.UpdateCommentDocument,
Expand Down Expand Up @@ -58,7 +59,7 @@ export function gql(source: "\nmutation LoginUser($credentials: LoginInput!) {\n
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n category {\n id\n name\n }\n }\n\t\t}\n\t}\n\t"): (typeof documents)["\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n category {\n id\n name\n }\n }\n\t\t}\n\t}\n\t"];
export function gql(source: "\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n }\n\t\t}\n\t}\n\t"): (typeof documents)["\n\tquery ListPosts($pagination: PaginationInput!) {\n\t\tlistPosts(pagination: $pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n createdAt\n }\n\t\t}\n\t}\n\t"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand All @@ -75,6 +76,10 @@ export function gql(source: "\nquery ViewPost($id: Int!) {\n viewPost(id: $id)
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\n mutation UpdatePost($postId: Int!, $updateData: UpdatePostInput!) {\n updatePost(postId: $postId, updateData: $updateData) {\n id\n }\n }\n"): (typeof documents)["\n mutation UpdatePost($postId: Int!, $updateData: UpdatePostInput!) {\n updatePost(postId: $postId, updateData: $updateData) {\n id\n }\n }\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\nquery SearchPosts($searchCriteria: PostSearchInput!,$pagination:PaginationInput!) {\n searchPosts(searchCriteria: $searchCriteria, pagination:$pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n category {\n name\n }\n createdAt\n }\n }\n}\n"): (typeof documents)["\nquery SearchPosts($searchCriteria: PostSearchInput!,$pagination:PaginationInput!) {\n searchPosts(searchCriteria: $searchCriteria, pagination:$pagination) {\n pagination {\n page\n pageSize\n totalItems\n }\n posts {\n id\n title\n content\n author {\n id \n name\n email\n }\n comments {\n id\n }\n category {\n name\n }\n createdAt\n }\n }\n}\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit a036fd7

Please sign in to comment.