Skip to content

Commit

Permalink
feat: add blog search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
aulianza committed Nov 12, 2023
1 parent 832dc67 commit 7ab14c2
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/common/components/elements/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type EmptyStatePageProps = {

const EmptyState = ({ message }: EmptyStatePageProps) => {
return (
<div className='flex flex-col items-center justify-center space-y-1 text-neutral-400 dark:text-neutral-500 py-3'>
<div className='flex flex-col items-center justify-center space-y-3 text-neutral-400 dark:text-neutral-500 py-5'>
<MoodIcon size={48} />
<p>{message}</p>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/common/components/elements/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ const Pagination: React.FC<PaginationProps> = ({
));
};

if (!totalPages) {
return null;
}

return (
<div className='flex justify-center pt-5 font-sora'>
{currentPage !== 1 && (
Expand Down
40 changes: 40 additions & 0 deletions src/common/components/elements/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { BiSearchAlt as SearchIcon, BiX as ClearIcon } from 'react-icons/bi';

interface SearchBarProps {
searchTerm: string;
onSearchChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClearSearch: () => void;
}

const SearchBar: React.FC<SearchBarProps> = ({
searchTerm,
onSearchChange,
onClearSearch,
}) => {
return (
<div className='flex items-center w-full sm:w-auto '>
<div className='relative w-full'>
<SearchIcon
size={18}
className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400'
/>
<input
type='text'
placeholder='Search...'
className='w-full py-2 px-10 border-2 dark:border-neutral-600 rounded-lg transition-all duration-300 text-sm font-sora'
value={searchTerm}
onChange={onSearchChange}
/>
{searchTerm && (
<ClearIcon
size={20}
className='absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 cursor-pointer'
onClick={onClearSearch}
/>
)}
</div>
</div>
);
};

export default SearchBar;
2 changes: 1 addition & 1 deletion src/common/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ a {

#nprogress .bar {
background: #15b8a6 !important;
height: 2px !important;
height: 1px !important;
z-index: 9999999 !important;
}

Expand Down
4 changes: 4 additions & 0 deletions src/modules/blog/components/BlogFeaturedSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const BlogFeaturedSection = () => {
return [];
}, [data]);

if (!featuredData || featuredData.length === 0) {
return null;
}

return (
<>
{!isLoading ? (
Expand Down
90 changes: 78 additions & 12 deletions src/modules/blog/components/BlogListNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { useDebounce } from 'usehooks-ts';

import EmptyState from '@/common/components/elements/EmptyState';
import Pagination from '@/common/components/elements/Pagination';
import SearchBar from '@/common/components/elements/SearchBar';
import BlogCardNewSkeleton from '@/common/components/skeleton/BlogCardNewSkeleton';
import { BlogItemProps } from '@/common/types/blog';
import { fetcher } from '@/services/fetcher';
Expand All @@ -14,41 +16,105 @@ import BlogFeaturedSection from './BlogFeaturedSection';

const BlogListNew = () => {
const [page, setPage] = useState<number>(1);
const [searchTerm, setSearchTerm] = useState<string>('');
const router = useRouter();

const debouncedSearchTerm = useDebounce(searchTerm, 500);

const { data, mutate, isLoading } = useSWR(
`/api/blog?page=${page}&per_page=6`,
`/api/blog?page=${page}&per_page=6&search=${debouncedSearchTerm}`,
fetcher
);

const { posts: blogData = [], total_pages: totalPages = 1 } =
data?.data ?? {};
const {
posts: blogData = [],
total_pages: totalPages = 1,
total_posts = 0,
} = data?.data || {};

const handlePageChangeAndLoadData = async (newPage: number) => {
const handlePageChange = async (newPage: number) => {
await mutate();
router.push(`/blog?page=${newPage}`, undefined, { shallow: true });
router.push(
{
pathname: '/blog',
query: { page: newPage, search: debouncedSearchTerm },
},
undefined,
{ shallow: true }
);
setPage(newPage);
};

const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = event?.target?.value;
setSearchTerm(searchValue);
setPage(1);

router.push(
{
pathname: '/blog',
query: searchValue ? { page: 1, search: searchValue } : { page: 1 },
},
undefined,
{ shallow: true }
);
};

const handleClearSearch = () => {
setSearchTerm('');
setPage(1);

router.push(
{
pathname: '/blog',
query: { page: 1 },
},
undefined,
{ shallow: true }
);
};

useEffect(() => {
const queryPage = Number(router.query.page);
if (!isNaN(queryPage) && queryPage !== page) {
setPage(queryPage);
}
}, [page, router.query.page]);
}, [page, router.query.page, searchTerm]);

const renderEmptyState = () =>
!isLoading && data?.status === false && <EmptyState message='No Post' />;
!isLoading &&
(!data?.status || blogData.length === 0) && (
<EmptyState message='No Post Found.' />
);

return (
<div className='space-y-10'>
<BlogFeaturedSection />

<div className='space-y-5'>
<div className='flex justify-between items-center'>
<h2 className='text-xl font-sora font-medium px-1'>
Latest Articles
</h2>
<div className='flex flex-col sm:flex-row gap-3 justify-between items-center mb-6'>
<div className='flex items-center gap-2 text-xl font-sora font-medium px-1'>
{searchTerm ? (
<div>
<span className='text-neutral-600 dark:text-neutral-400 mr-2'>
Search:
</span>
<span className='italic'>{searchTerm}</span>
</div>
) : (
<h4 className='text-neutral-800 dark:text-neutral-200'>
Latest Articles
</h4>
)}
<span className='rounded-full py-1 px-2 bg-neutral-300 text-neutral-900 dark:bg-neutral-700 dark:text-neutral-50 text-xs font-sora'>
{total_posts}
</span>
</div>
<SearchBar
searchTerm={searchTerm}
onSearchChange={handleSearch}
onClearSearch={handleClearSearch}
/>
</div>

<div className='grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5'>
Expand Down Expand Up @@ -78,7 +144,7 @@ const BlogListNew = () => {
<Pagination
totalPages={totalPages}
currentPage={page}
onPageChange={handlePageChangeAndLoadData}
onPageChange={handlePageChange}
/>
)}

Expand Down
3 changes: 2 additions & 1 deletion src/pages/api/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ export default async function handler(
'public, s-maxage=60, stale-while-revalidate=30'
);

const { page, per_page, categories } = req.query;
const { page, per_page, categories, search } = req.query;

const responseData = await getBlogList({
page: Number(page) || 1,
per_page: Number(per_page) || 9,
categories: categories ? Number(categories) : undefined,
search: search ? String(search) : undefined,
});

const blogItemsWithViews = await Promise.all(
Expand Down
4 changes: 3 additions & 1 deletion src/services/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type BlogParamsProps = {
page?: number;
per_page?: number;
categories?: number | undefined;
search?: string;
};

interface BlogDetailResponseProps {
Expand Down Expand Up @@ -51,9 +52,10 @@ export const getBlogList = async ({
page = 1,
per_page = 6,
categories,
search,
}: BlogParamsProps): Promise<{ status: number; data: any }> => {
try {
const params = { page, per_page, categories };
const params = { page, per_page, categories, search };
const response = await axios.get(`${BLOG_URL}posts`, { params });
return { status: response?.status, data: extractData(response) };
} catch (error) {
Expand Down

0 comments on commit 7ab14c2

Please sign in to comment.