Skip to content
Open
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.59.17",
"@types/lodash": "^4.17.7",
"@types/uuid": "^10.0.0",
"@uidotdev/usehooks": "^2.4.1",
Expand Down
19 changes: 16 additions & 3 deletions src/actions/job.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,22 @@ export const getRecommendedJobs = withServerActionAsyncCatcher<
ServerActionReturnType<getAllRecommendedJobs>
>(async (data) => {
const result = RecommendedJobSchema.parse(data);
const { id, category } = result;
const { id } = result;

const job = await prisma.job.findUnique({
where: { id },
select: {
id: true,
category: true,
},
});

if (!job) {
throw new Error('Job not found');
}

const { category } = job;

// fettching the latest three jobs excluding the current job and in the same category
const jobs = await prisma.job.findMany({
where: {
category: category,
Expand Down Expand Up @@ -236,7 +249,7 @@ export const getRecommendedJobs = withServerActionAsyncCatcher<
orderBy: {
postedAt: 'desc',
},
take: 3, // Fallback to showing latest 3 jobs from other categories
take: 3,
select: {
id: true,
type: true,
Expand Down
88 changes: 41 additions & 47 deletions src/app/jobs/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,56 @@
import { getJobById, getRecommendedJobs } from '@/actions/job.action';
import { Job } from '@/components/job';
import JobCard from '@/components/job-card-rec';
import { JobByIdSchemaType } from '@/lib/validators/jobs.validator';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import getQueryClient from '../../../providers/queryClient';
import { RecommendJobs } from '@/components/RecommendJobs';

const page = async ({ params }: { params: JobByIdSchemaType }) => {
const job = await getJobById(params);
if (!job.status) {
return;
}
const queryClient = getQueryClient();

const jobDetail = job.additional?.job;
if (!jobDetail) {
return redirect('/jobs');
}

const curatedJobs = await getRecommendedJobs({
id: jobDetail.id,
category: jobDetail.category,
await queryClient.prefetchQuery({
queryKey: ['jobs', params],
queryFn: () => getJobById({ id: params.id }),
staleTime: 1000 * 60 * 5,
});

if (!curatedJobs.status) {
return;
}

const recommendedJobs = curatedJobs.additional?.jobs;
await queryClient.prefetchQuery({
queryKey: ['RecommendedJobs', params],
queryFn: () => getRecommendedJobs({ id: params.id }),
staleTime: 1000 * 60 * 5,
});

return (
<div className="container max-w-8xl h-fit mx-auto my-8">
<section className="flex h-fit py-4">
<Link
href="/jobs"
className="flex border-2 border-transparent cursor-pointer h-fit p-2 rounded-full px-4 transition-all duration-450 ease-linear hover:border-2 hover:bg-[#F1F5F9] dark:hover:bg-[#0F172A] items-center gap-2"
>
<ArrowLeft size={18} />
<p className="text-xs">Back to All Jobs</p>
</Link>
</section>
<main className="grid grid-cols-1 lg:grid-cols-6 gap-8">
{/* the particular job details */}
<Job job={jobDetail} />

{/* job recommendations */}
<aside className="col-span-1 rounded-md lg:col-span-2">
<div className="sticky top-4">
<h2 className="text-xl font-semibold mb-4">Recommended for you</h2>
<main className="my-2 flex flex-col gap-4">
{recommendedJobs &&
recommendedJobs.map((job, index) => {
return <JobCard key={`recommended_job_${index}`} job={job} />;
})}
</main>
</div>
</aside>
</main>
</div>
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container max-w-8xl h-fit mx-auto my-8">
<section className="flex h-fit py-4">
<Link
href="/jobs"
className="flex border-2 border-transparent cursor-pointer h-fit p-2 rounded-full px-4 transition-all duration-450 ease-linear hover:border-2 hover:bg-[#F1F5F9] dark:hover:bg-[#0F172A] items-center gap-2"
>
<ArrowLeft size={18} />
<p className="text-xs">Back to All Jobs</p>
</Link>
</section>
<main className="grid grid-cols-1 lg:grid-cols-6 gap-8">
{/* the particular job details */}

<Job />

{/* job recommendations */}
<aside className="col-span-1 rounded-md lg:col-span-2">
<div className="sticky top-4">
<h2 className="text-xl font-semibold mb-4">
Recommended for you
</h2>
<RecommendJobs jobId={params.id} />
</div>
</aside>
</main>
</div>
</HydrationBoundary>
);
};

Expand Down
81 changes: 52 additions & 29 deletions src/app/jobs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,74 @@ import {
} from '@/lib/validators/jobs.validator';
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
import getQueryClient from '../../providers/queryClient';
import { GetUserBookmarksId, getAllJobs } from '@/actions/job.action';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getServerSession } from 'next-auth';

const page = async ({ searchParams }: { searchParams: JobQuerySchemaType }) => {
const session = await getServerSession();
if (!session) {
redirect('/auth/signin');
}
const parsedData = JobQuerySchema.safeParse(searchParams);
if (!(parsedData.success && parsedData.data)) {
console.error(parsedData.error);
redirect('/jobs');
}
const parsedSearchParams = parsedData.data;
return (
<div className="container relative grid sm:gap-6 gap-4 mt-12">
<div className="grid gap-2">
<h1 className="text-3xl sm:text-4xl font-bold">Explore Jobs</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
Explore thousands of remote and onsite jobs that match your skills and
aspirations.
</p>
</div>

<div className="flex gap-6">
<div className="hidden w-[310px] rounded-lg sm:block border h-[calc(100vh-100px)] overflow-y-auto scrollbar-custom sticky top-[5.5rem]">
<div className=" ">
<JobFilters searchParams={parsedSearchParams} />
</div>
const queryClient = getQueryClient();

await queryClient.prefetchQuery({
queryKey: ['jobs', parsedSearchParams],
queryFn: () => getAllJobs(parsedSearchParams),
staleTime: 1000 * 60 * 5,
});
await queryClient.prefetchQuery({
queryKey: ['UserBookmarksId', session?.user?.id],
queryFn: () => GetUserBookmarksId(),
});

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container relative grid sm:gap-6 gap-4 mt-12">
<div className="grid gap-2">
<h1 className="text-3xl sm:text-4xl font-bold">Explore Jobs</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
Explore thousands of remote and onsite jobs that match your skills
and aspirations.
</p>
</div>

<div className="grow flex flex-col bg-background">
<div className="sticky top-[4.5rem] bg-background dark:bg-background z-10 py-4">
<JobsHeader searchParams={parsedSearchParams} />
<div className="flex gap-6">
<div className="hidden w-[310px] rounded-lg sm:block border h-[calc(100vh-100px)] overflow-y-auto scrollbar-custom sticky top-[5.5rem]">
<div className=" ">
<JobFilters searchParams={parsedSearchParams} />
</div>
</div>

<div className="grow bg-background">
<Suspense
key={JSON.stringify(parsedSearchParams)}
fallback={
<div className="flex justify-center items-center h-full gap-5">
<Loader />
</div>
}
>
<AllJobs searchParams={parsedSearchParams} />
</Suspense>
<div className="grow flex flex-col bg-background">
<div className="sticky top-[4.5rem] bg-background dark:bg-background z-10 py-4">
<JobsHeader searchParams={parsedSearchParams} />
</div>

<div className="grow bg-background">
<Suspense
key={JSON.stringify(parsedSearchParams)}
fallback={
<div className="flex justify-center items-center h-full gap-5">
<Loader />
</div>
}
>
<AllJobs searchParams={parsedSearchParams} />
</Suspense>
</div>
</div>
</div>
</div>
</div>
</HydrationBoundary>
);
};

Expand Down
27 changes: 21 additions & 6 deletions src/components/RecentJobs.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
'use client';
import { getRecentJobs, GetUserBookmarksId } from '@/actions/job.action';
import JobCard from './Jobcard';
import { useQuery } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';

export default async function RecentJobs() {
const [recentJobs, getUserBookmarks] = await Promise.all([
await getRecentJobs(),
await GetUserBookmarksId(),
]);
export default function RecentJobs() {
const session = useSession();

const userbookmarkArr: { jobId: string }[] | null = getUserBookmarks.data;
const { data } = useQuery({
queryKey: ['recentJobs'],
queryFn: () => getRecentJobs(),
staleTime: 1000 * 60 * 5,
});
const bookmarks = useQuery({
queryKey: ['UserBookmarksId', session?.data?.user?.id],
queryFn: () => GetUserBookmarksId(),
});
if (!data?.status || !data?.additional) {
return <div>Error {data?.message}</div>;
}
const recentJobs = data;

const userbookmarkArr: { jobId: string }[] | null =
bookmarks.data?.data || null;

if (!recentJobs.status) {
return <div>{recentJobs.message}</div>;
Expand Down
34 changes: 34 additions & 0 deletions src/components/RecommendJobs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';
import { getRecommendedJobs } from '@/actions/job.action';
import { useQuery } from '@tanstack/react-query';
import JobCard from './Jobcard';
import { useParams } from 'next/navigation';

export const RecommendJobs = ({ jobId }: { jobId: string }) => {
const params = useParams();
const { data } = useQuery({
queryKey: ['RecommendedJobs', params],
queryFn: () => getRecommendedJobs({ id: jobId }),
staleTime: 1000 * 60 * 5,
});

if (!data?.status) {
return <div>Error </div>;
}
const recommendedJobs = data?.additional?.jobs;

return (
<main className="my-2 flex flex-col gap-4">
{recommendedJobs &&
recommendedJobs.map((job, index) => {
return (
<JobCard
isBookmarked={false}
key={`recommended_job_${index}`}
job={job}
/>
);
})}
</main>
);
};
34 changes: 23 additions & 11 deletions src/components/all-jobs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getAllJobs, GetUserBookmarksId } from '@/actions/job.action';
'use client';
import { GetUserBookmarksId, getAllJobs } from '@/actions/job.action';
import { DEFAULT_PAGE, JOBS_PER_PAGE } from '@/config/app.config';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
Expand All @@ -10,31 +11,42 @@ import { Pagination, PaginationContent, PaginationItem } from './ui/pagination';
import { PaginationPages } from './ui/paginator';
import JobCard from './Jobcard';
import APP_PATHS from '@/config/path.config';
import { useQuery } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';

type PaginatorProps = {
searchParams: JobQuerySchemaType;
};

const AllJobs = async ({ searchParams }: PaginatorProps) => {
const [jobs, getUserBookmarks] = await Promise.all([
await getAllJobs(searchParams),
await GetUserBookmarksId(),
]);
const AllJobs = ({ searchParams }: PaginatorProps) => {
// const userbookmarkArr: { jobId: string }[] | null = userbookmarks;
const session = useSession();

const userbookmarkArr: { jobId: string }[] | null = getUserBookmarks.data;
const userbookmark = useQuery({
queryKey: ['UserBookmarksId', session?.data?.user?.id],
queryFn: () => GetUserBookmarksId(),
});

if (!jobs.status || !jobs.additional) {
return <div>Error {jobs.message}</div>;
const { data } = useQuery({
queryKey: ['jobs', searchParams],
queryFn: () => getAllJobs(searchParams),
staleTime: 1000 * 60 * 5,
});
if (!data?.status || !data?.additional) {
return <div>Error {data?.message}</div>;
}
const jobs = data;
const userbookmarkArr: { jobId: string }[] | null =
userbookmark.data?.data || null;

const totalPages =
Math.ceil((jobs.additional?.totalJobs || 0) / JOBS_PER_PAGE) ||
DEFAULT_PAGE;
const currentPage = parseInt(searchParams.page?.toString()) || DEFAULT_PAGE;

const jobsLength = jobs.additional?.jobs.length || 0; // Defaults to 0 if undefined
return (
<div className="bg-background py-4 grid gap-3 w-full">
{jobs.additional.jobs.length > 0 ? (
{jobsLength > 0 ? (
jobs.additional?.jobs.map((job, index) => (
<JobCard
job={job}
Expand Down
Loading
Loading