Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/usequery #568

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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