diff --git a/.gitignore b/.gitignore index 8bf0160c..ba44e4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ next-env.d.ts pnpm-lock.yaml bun.lockb package-lock.json -yarn.lock \ No newline at end of file +yarn.lock + diff --git a/package.json b/package.json index 940ac277..64ba50e1 100644 --- a/package.json +++ b/package.json @@ -24,20 +24,22 @@ "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", + "dayjs": "^1.11.13", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@types/lodash": "^4.17.7", "@uidotdev/usehooks": "^2.4.1", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "dayjs": "^1.11.13", + "lodash": "^4.17.21", "lucide-react": "^0.426.0", "next": "14.2.5", "next-auth": "^4.24.7", diff --git a/src/actions/job.action.ts b/src/actions/job.action.ts index 7d5ae5dd..bdfbb921 100644 --- a/src/actions/job.action.ts +++ b/src/actions/job.action.ts @@ -15,7 +15,6 @@ import { import { getJobFilters } from '@/services/jobs.services'; import { ServerActionReturnType } from '@/types/api.types'; import { getAllJobsAdditonalType, getJobType } from '@/types/jobs.types'; -import { redirect } from 'next/navigation'; type additional = { isVerifiedJob: boolean; @@ -132,20 +131,3 @@ export const getJobById = withServerActionAsyncCatcher< job, }).serialize(); }); - -export const jobFilterQuery = async ( - queries: JobQuerySchemaType, - baseUrl: string -) => { - const { page, sortby, location, salaryrange, search, workmode } = - JobQuerySchema.parse(queries); - const searchParams = new URLSearchParams({ - page: page.toString(), - sortby, - ...(search && { search: search.trim() }), - }); - location?.map((location) => searchParams.append('location', location)); - salaryrange?.map((range) => searchParams.append('salaryrange', range)); - workmode?.map((mode) => searchParams.append('workmode', mode)); - redirect(`${baseUrl}?${searchParams.toString()}`); -}; diff --git a/src/app/jobs/page.tsx b/src/app/jobs/page.tsx index 44a3290d..ee41d31e 100644 --- a/src/app/jobs/page.tsx +++ b/src/app/jobs/page.tsx @@ -1,36 +1,35 @@ import AllJobs from '@/components/all-jobs'; import Loader from '@/components/loader'; -import APP_PATHS from '@/config/path.config'; import JobFilters from '@/layouts/job-filters'; import JobsHeader from '@/layouts/jobs-header'; import { JobQuerySchema, JobQuerySchemaType, } from '@/lib/validators/jobs.validator'; +import { redirect } from 'next/navigation'; import { Suspense } from 'react'; const page = async ({ searchParams }: { searchParams: JobQuerySchemaType }) => { - const validatedSearchParams = JobQuerySchema.parse(searchParams); - + const parsedData = JobQuerySchema.safeParse(searchParams); + if (!(parsedData.success && parsedData.data)) { + console.error(parsedData.error); + redirect('/jobs'); + } + const parsedSearchParams = parsedData.data; return (
- +
- +
} > - +
diff --git a/src/components/job-landing.tsx b/src/components/job-landing.tsx index e986c8a9..5f25702c 100644 --- a/src/components/job-landing.tsx +++ b/src/components/job-landing.tsx @@ -31,7 +31,7 @@ export const JobLanding = async ({ return (
- + }> diff --git a/src/components/pagination-client.tsx b/src/components/pagination-client.tsx index c90f56c5..b8571c48 100644 --- a/src/components/pagination-client.tsx +++ b/src/components/pagination-client.tsx @@ -1,28 +1,23 @@ 'use client'; -import { jobFilterQuery } from '@/actions/job.action'; import { PaginationNext, PaginationPrevious } from './ui/pagination'; import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; +import useSetQueryParams from '@/hooks/useSetQueryParams'; const PAGE_INCREMENT = 1; const PaginationPreviousButton = ({ - searchParams, currentPage, - baseUrl, }: { searchParams: JobQuerySchemaType; currentPage: number; baseUrl: string; }) => { + const setQueryParams = useSetQueryParams(); return ( - jobFilterQuery( - { - ...searchParams, - page: currentPage - PAGE_INCREMENT, - }, - baseUrl - ) + setQueryParams({ + page: (currentPage - PAGE_INCREMENT).toString(), + }) } aria-disabled={currentPage - PAGE_INCREMENT < PAGE_INCREMENT} role="button" @@ -31,27 +26,22 @@ const PaginationPreviousButton = ({ ); }; const PaginationNextButton = ({ - searchParams, currentPage, totalPages, - baseUrl, }: { searchParams: JobQuerySchemaType; currentPage: number; totalPages: number; baseUrl: string; }) => { + const setQueryParams = useSetQueryParams(); return ( - jobFilterQuery( - { - ...searchParams, - page: currentPage + PAGE_INCREMENT, - }, - baseUrl - ) + setQueryParams({ + page: (currentPage + PAGE_INCREMENT).toString(), + }) } aria-disabled={currentPage > totalPages - PAGE_INCREMENT} className="aria-disabled:pointer-events-none dark:bg-neutral-900 rounded-full bg-neutral-100" diff --git a/src/components/ui/paginator.tsx b/src/components/ui/paginator.tsx index b2aa6446..604a6414 100644 --- a/src/components/ui/paginator.tsx +++ b/src/components/ui/paginator.tsx @@ -1,26 +1,25 @@ 'use client'; -import { jobFilterQuery } from '@/actions/job.action'; import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; import { PaginationEllipsis, PaginationItem, PaginationLink, } from './pagination'; +import useSetQueryParams from '@/hooks/useSetQueryParams'; import { cn } from '@/lib/utils'; export const PaginationPages = ({ currentPage, totalPages, - searchParams, - baseUrl, }: { currentPage: number; totalPages: number; searchParams: JobQuerySchemaType; baseUrl: string; }) => { + const setQueryParams = useSetQueryParams(); function paginationHandler(page: number) { - jobFilterQuery({ ...searchParams, page: page }, baseUrl); + setQueryParams({ page: page.toString() }); } const pages: JSX.Element[] = []; if (totalPages <= 5) { diff --git a/src/hooks/useSetQueryParams.ts b/src/hooks/useSetQueryParams.ts new file mode 100644 index 00000000..06f5b1da --- /dev/null +++ b/src/hooks/useSetQueryParams.ts @@ -0,0 +1,29 @@ +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import _ from 'lodash'; +import { useCallback } from 'react'; +import { debounce } from 'lodash'; + +//pass in key value pairs to update query params +export default function useSetQueryParams() { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathName = usePathname(); + + const updateQueryParams = useCallback( + debounce((params) => { + const newSearchParams = new URLSearchParams(searchParams?.toString()); + for (const [key, value] of Object.entries(params)) { + //isEmpty reads number as empty too + if (_.isEmpty(value) && typeof value !== 'number') { + newSearchParams.delete(key); + } else { + newSearchParams.set(key, String(value)); + } + } + router.push(`${pathName}?${newSearchParams}`, { scroll: false }); + }, 300), // 300ms debounce + [router, searchParams, pathName] + ); + + return updateQueryParams; +} diff --git a/src/layouts/job-filters.tsx b/src/layouts/job-filters.tsx index a2784617..52f0675a 100644 --- a/src/layouts/job-filters.tsx +++ b/src/layouts/job-filters.tsx @@ -1,5 +1,4 @@ 'use client'; -import { jobFilterQuery } from '@/actions/job.action'; import { filters, WorkModeEnums } from '@/lib/constant/jobs.constant'; import { JobQuerySchema, @@ -24,55 +23,38 @@ import { } from '../components/ui/form'; import { Separator } from '../components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { cn, formatFilterSearchParams } from '@/lib/utils'; -import { usePathname } from 'next/navigation'; -import APP_PATHS from '@/config/path.config'; +import { cn } from '@/lib/utils'; +import useSetQueryParams from '@/hooks/useSetQueryParams'; +import { useEffect } from 'react'; -const JobFilters = ({ - searchParams, - baseUrl, -}: { - searchParams: JobQuerySchemaType; - baseUrl: string; -}) => { - const pathname = usePathname(); - const isHome = pathname === APP_PATHS.HOME; +const JobFilters = ({ searchParams }: { searchParams: JobQuerySchemaType }) => { + const setQueryParams = useSetQueryParams(); const form = useForm({ resolver: zodResolver(JobQuerySchema), defaultValues: { - workmode: - searchParams.workmode && - (formatFilterSearchParams(searchParams.workmode) as WorkModeEnums[]), - salaryrange: - searchParams.salaryrange && - formatFilterSearchParams(searchParams.salaryrange), - location: - searchParams.location && - formatFilterSearchParams(searchParams.location), + workmode: searchParams.workmode, + salaryrange: searchParams.salaryrange, + location: searchParams.location, }, }); - async function handleFormSubmit(data: JobQuerySchemaType) { - await jobFilterQuery( - { - ...data, - search: searchParams.search, - sortby: searchParams.sortby, - }, - baseUrl - ); - } + + const formValues = form.watch(); + + useEffect(() => { + if (formValues) { + setQueryParams(formValues); + } + }, [formValues, setQueryParams, searchParams]); + return ( -