From 35d19f6f75af028bbf69e76baa02cf8dc2032676 Mon Sep 17 00:00:00 2001 From: kyranaus Date: Sun, 9 Mar 2025 20:38:39 +1100 Subject: [PATCH 1/2] Optimized sponsor selection and presentation --- frontend/src/app/jobs/actions.ts | 237 ++++++++++-------- frontend/src/app/jobs/page.tsx | 21 +- frontend/src/components/jobs/job-list.tsx | 17 +- .../src/components/jobs/sponsor-section.tsx | 67 ----- frontend/src/lib/utils.ts | 109 +------- frontend/src/types/job.ts | 1 + 6 files changed, 139 insertions(+), 313 deletions(-) delete mode 100644 frontend/src/components/jobs/sponsor-section.tsx diff --git a/frontend/src/app/jobs/actions.ts b/frontend/src/app/jobs/actions.ts index 295b31f..2e604c4 100644 --- a/frontend/src/app/jobs/actions.ts +++ b/frontend/src/app/jobs/actions.ts @@ -10,8 +10,8 @@ const PAGE_SIZE = 20; // Define the MongoJob interface with the correct DB field names. export interface MongoJob extends Omit { - _id: ObjectId; - is_sponsored: boolean; + _id: ObjectId; + is_sponsored: boolean; } /** @@ -21,53 +21,53 @@ export interface MongoJob extends Omit { * @returns The query object to use with MongoDB. */ function buildJobQuery( - filters: Partial, - additional?: Record, + filters: Partial, + additional?: Record, ) { - const array_jobs = JSON.parse(JSON.stringify(filters, null, 2)); - const query = { - outdated: false, - ...(array_jobs["workingRights[]"] !== undefined && - array_jobs["workingRights[]"].length && { - working_rights: { - $in: Array.isArray(array_jobs["workingRights[]"]) - ? array_jobs["workingRights[]"] - : [array_jobs["workingRights[]"]], - }, - }), - ...(array_jobs["locations[]"] !== undefined && - array_jobs["locations[]"].length && { - locations: { - $in: Array.isArray(array_jobs["locations[]"]) - ? array_jobs["locations[]"] - : [array_jobs["locations[]"]], - }, - }), - ...(array_jobs["industryFields[]"] !== undefined && - array_jobs["industryFields[]"].length && { - industry_field: { - $in: Array.isArray(array_jobs["industryFields[]"]) - ? array_jobs["industryFields[]"] - : [array_jobs["industryFields[]"]], - }, - }), - ...(array_jobs["jobTypes[]"] !== undefined && - array_jobs["jobTypes[]"].length && { - type: { - $in: Array.isArray(array_jobs["jobTypes[]"]) - ? array_jobs["jobTypes[]"] - : [array_jobs["jobTypes[]"]], - }, - }), - ...(filters.search && { - $or: [ - { title: { $regex: filters.search, $options: "i" } }, - { "company.name": { $regex: filters.search, $options: "i" } }, - ], - }), - ...additional, - }; - return query; + const array_jobs = JSON.parse(JSON.stringify(filters, null, 2)); + const query = { + outdated: false, + ...(array_jobs["workingRights[]"] !== undefined && + array_jobs["workingRights[]"].length && { + working_rights: { + $in: Array.isArray(array_jobs["workingRights[]"]) + ? array_jobs["workingRights[]"] + : [array_jobs["workingRights[]"]], + }, + }), + ...(array_jobs["locations[]"] !== undefined && + array_jobs["locations[]"].length && { + locations: { + $in: Array.isArray(array_jobs["locations[]"]) + ? array_jobs["locations[]"] + : [array_jobs["locations[]"]], + }, + }), + ...(array_jobs["industryFields[]"] !== undefined && + array_jobs["industryFields[]"].length && { + industry_field: { + $in: Array.isArray(array_jobs["industryFields[]"]) + ? array_jobs["industryFields[]"] + : [array_jobs["industryFields[]"]], + }, + }), + ...(array_jobs["jobTypes[]"] !== undefined && + array_jobs["jobTypes[]"].length && { + type: { + $in: Array.isArray(array_jobs["jobTypes[]"]) + ? array_jobs["jobTypes[]"] + : [array_jobs["jobTypes[]"]], + }, + }), + ...(filters.search && { + $or: [ + { title: { $regex: filters.search, $options: "i" } }, + { "company.name": { $regex: filters.search, $options: "i" } }, + ], + }), + ...additional, + }; + return query; } /** @@ -76,78 +76,103 @@ function buildJobQuery( * @returns The result from the callback. */ async function withDbConnection( - callback: (client: MongoClient) => Promise, + callback: (client: MongoClient) => Promise, ): Promise { - if (!process.env.MONGODB_URI) { - throw new Error( - "MongoDB URI is not configured. Please check environment variables.", - ); - } - const client = new MongoClient(process.env.MONGODB_URI); - try { - await client.connect(); - return await callback(client); - } finally { - await client.close(); - } + if (!process.env.MONGODB_URI) { + throw new Error( + "MongoDB URI is not configured. Please check environment variables.", + ); + } + const client = new MongoClient(process.env.MONGODB_URI); + try { + await client.connect(); + return await callback(client); + } finally { + await client.close(); + } } /** * Fetches paginated and filtered job listings from MongoDB. */ export async function getJobs( - filters: Partial, -): Promise<{ jobs: Job[]; total: number }> { - return await withDbConnection(async (client) => { - const collection = client.db("default").collection("active_jobs"); - const query = buildJobQuery(filters); - const page = filters.page || 1; - const skip = (page - 1) * PAGE_SIZE; - - const [jobs, total] = await Promise.all([ - collection.find(query).skip(skip).limit(PAGE_SIZE).toArray(), - collection.countDocuments(query), - ]); - return { - jobs: (jobs as MongoJob[]).map(serializeJob), - total, - }; - }); -} + filters: Partial, + minSponsors: number = -1, + prioritySponsors: Array = ["IMC", "Atlassian"] -/** - * Fetches all sponsored job listings from MongoDB that match the given filters. - * This function does not paginate results. - */ -export async function getSponsoredJobs( - filters: Partial, ): Promise<{ jobs: Job[]; total: number }> { - return await withDbConnection(async (client) => { - const collection = client.db("default").collection("active_jobs"); - // Add an override to filter only sponsored jobs. - const query = buildJobQuery(filters, { is_sponsored: true }); - const jobs = await collection.find(query).toArray(); - const total = jobs.length; - return { - jobs: (jobs as MongoJob[]).map(serializeJob), - total, - }; - }); + return await withDbConnection(async (client) => { + const collection = client.db("default").collection("active_jobs"); + const query = buildJobQuery(filters); + const page = filters.page || 1; + const skip = (page - 1) * PAGE_SIZE; + minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors + + if (minSponsors == 0) { + const [jobs, total] = await Promise.all([ + collection.find(query).skip(skip).limit(PAGE_SIZE).toArray(), + collection.countDocuments(query), + ]); + return { + // Serialize Job and set highlight to false + jobs: (jobs as MongoJob[]).map(serializeJob).map((job) => ({ ...job, highlight: false })), + total, + }; + + } else { + // Modify query to include sponsored job filtering + const sponsoredQuery = { ...query, is_sponsored: true }; + + // Fetch sponsored jobs (without priority filtering) + let sponsoredJobs = await collection.aggregate([{ $match: sponsoredQuery }, { $sample: { size: minSponsors * 8 } }]).toArray(); + + // Apply 65% chance selection for priority sponsors + sponsoredJobs = sponsoredJobs + .filter((job) => { + const isPriority = prioritySponsors.includes(job.company.name); + return isPriority ? Math.random() < 0.65 : Math.random() >= 0.35; // 65% chance for priority, 35% for others + }) + .slice(0, minSponsors) // Ensure we only take the required number + + .map((job) => ({ ...job, highlight: true })); // Add highlight property + + // Get IDs of selected sponsored jobs to exclude them from regular jobs + const sponsoredJobIds = sponsoredJobs.map((job) => job._id); + + // Modify the main query to exclude sponsored jobs we already fetched + const filteredQuery = { ...query, _id: { $nin: sponsoredJobIds } }; + + // Fetch remaining jobs with pagination + const [otherJobs, total] = await Promise.all([ + collection.find(filteredQuery).skip(skip).limit(PAGE_SIZE - sponsoredJobs.length).toArray(), + collection.countDocuments(query), // Total should still include all jobs matching the original query + ]); + // Merge jobs and make sure we don’t exceed PAGE_SIZE also add highlight property + const mergedJobs = [...sponsoredJobs.map((job) => ({ ...job, highlight: true })), ...otherJobs.map((job) => ({ ...job, highlight: false }))].slice(0, PAGE_SIZE); + + return { + jobs: (mergedJobs as MongoJob[]).map(serializeJob), + total, + }; + } + + }); } + /** * Fetches a single job by its id. */ export async function getJobById(id: string): Promise { - return await withDbConnection(async (client) => { - const collection = client.db("default").collection("active_jobs"); - const job = await collection.findOne({ - _id: new ObjectId(id), - outdated: false, + return await withDbConnection(async (client) => { + const collection = client.db("default").collection("active_jobs"); + const job = await collection.findOne({ + _id: new ObjectId(id), + outdated: false, + }); + if (!job) { + return null; + } + return serializeJob(job as MongoJob); }); - if (!job) { - return null; - } - return serializeJob(job as MongoJob); - }); } diff --git a/frontend/src/app/jobs/page.tsx b/frontend/src/app/jobs/page.tsx index a206962..10fb5fd 100644 --- a/frontend/src/app/jobs/page.tsx +++ b/frontend/src/app/jobs/page.tsx @@ -3,12 +3,11 @@ import FilterSection from "@/components/filters/filter-section"; import JobList from "@/components/jobs/job-list"; import JobDetails from "@/components/jobs/job-details"; import { JobFilters } from "@/types/filters"; -import { getJobs, getSponsoredJobs } from "@/app/jobs/actions"; +import { getJobs } from "@/app/jobs/actions"; import NoResults from "@/components/ui/no-results"; import { Suspense } from "react"; import JobListLoading from "@/components/layout/job-list-loading"; import JobDetailsLoading from "@/components/layout/job-details-loading"; -import { getSponsoredSlots } from "@/lib/utils"; export const metadata = { title: "Jobs", @@ -28,21 +27,6 @@ export default async function JobsPage({ // Fetch regular jobs with pagination. const { jobs, total } = await getJobs(filters); - // Fetch all sponsored jobs (non-paginated) that match the filter. - const { jobs: sponsoredJobs } = await getSponsoredJobs(filters); - - // Hardcode the platinum sponsor companies. - const platinumSponsors = ["IMC", "Atlassian"]; - - // Use our helper to get a set of sponsored slots. - const sponsoredSlots = getSponsoredSlots(sponsoredJobs, platinumSponsors, 4); - - // Remove any duplicate jobs from the regular list that appear in the sponsored slots. - const sponsoredJobIds = new Set(sponsoredSlots.map((job) => job.id)); - const jobsWithoutDuplicates = jobs.filter( - (job) => !sponsoredJobIds.has(job.id), - ); - return ( <> @@ -54,8 +38,7 @@ export default async function JobsPage({
}>
diff --git a/frontend/src/components/jobs/job-list.tsx b/frontend/src/components/jobs/job-list.tsx index bbd9efe..afb745f 100644 --- a/frontend/src/components/jobs/job-list.tsx +++ b/frontend/src/components/jobs/job-list.tsx @@ -10,27 +10,22 @@ import JobDetails from "@/components/jobs/job-details"; import JobListLoading from "@/components/layout/job-list-loading"; import JobPagination from "@/components/jobs/job-pagination"; import { useMediaQuery } from "@mantine/hooks"; -import SponsorSection from "./sponsor-section"; interface JobListProps { jobs: Job[]; // Regular jobs - sponsoredJobs: Job[]; // Sponsored jobs } -export default function JobList({ jobs, sponsoredJobs }: JobListProps) { +export default function JobList({ jobs}: JobListProps) { +//export default function JobList({ jobs, sponsoredJobs }: JobListProps) { const { selectedJob, setSelectedJob, isLoading } = useFilterContext(); const [isModalOpen, setIsModalOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 1024px)"); useEffect(() => { if (!selectedJob) { - if (sponsoredJobs.length > 0) { - setSelectedJob(sponsoredJobs[0]); - } else if (jobs.length > 0) { setSelectedJob(jobs[0]); } - } - }, [jobs, sponsoredJobs, selectedJob, setSelectedJob]); + }, [jobs, selectedJob, setSelectedJob]); if (isLoading) return ; @@ -50,10 +45,6 @@ export default function JobList({ jobs, sponsoredJobs }: JobListProps) { : undefined } > -
{jobs.map((job) => (
))} diff --git a/frontend/src/components/jobs/sponsor-section.tsx b/frontend/src/components/jobs/sponsor-section.tsx deleted file mode 100644 index 6595bc4..0000000 --- a/frontend/src/components/jobs/sponsor-section.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// src/components/jobs/sponsor-section.tsx -"use client"; - -import { Job } from "@/types/job"; -import JobCard from "@/components/jobs/job-card"; -import { useFilterContext } from "@/context/filter/filter-context"; -import { useState } from "react"; -import { Modal, ScrollArea } from "@mantine/core"; -import JobDetails from "./job-details"; - -interface SponsorSectionProps { - sponsoredJobs: Job[]; - selectedJobID?: string; -} - -export default function SponsorSection({ - sponsoredJobs, - selectedJobID, -}: SponsorSectionProps) { - const { setSelectedJob } = useFilterContext(); - const [isModalOpen, setIsModalOpen] = useState(false); - - if (sponsoredJobs.length === 0) return null; - - return ( - <> -
-
- {sponsoredJobs.map((job) => ( -
{ - setSelectedJob(job); - // Only open modal on mobile - if (window.innerWidth < 1024) { - setIsModalOpen(true); - } - }} - className="cursor-pointer" - > - -
- ))} -
-
- setIsModalOpen(false)} - size="lg" - scrollAreaComponent={ScrollArea} - className="lg:hidden" - fullScreen - styles={{ - body: { - height: "calc(100svh - 100px)", - }, - }} - > - - - - ); -} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index d2fcdb1..5d4a137 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -77,6 +77,7 @@ export default function serializeJob(job: MongoJob): Job { application_url: job.application_url, close_date: serializeDate(job.close_date), is_sponsored: job.is_sponsored, + highlight: job.highlight }; } @@ -175,113 +176,5 @@ export const formatWorkingRights = (rights: WorkingRight[]): string => { .join(", "); }; -// Sponsor util -export interface SponsorCompany { - companyName: string; - isPlatinum: boolean; - jobs: Job[]; - companyLogo: string; - website: string; -} - -/** - * Groups sponsored jobs by company. - * @param sponsoredJobs - Array of sponsored Job objects. - * @param platinumSponsors - Array of company names that are platinum sponsors. - * @returns An array of SponsorCompany objects. - */ -export function groupSponsoredJobsByCompany( - sponsoredJobs: Job[], - platinumSponsors: string[], -): SponsorCompany[] { - const sponsorsByCompany: { - [companyName: string]: { - jobs: Job[]; - companyLogo: string; - website: string; - }; - } = {}; - - sponsoredJobs.forEach((job) => { - const companyName = job.company.name; - if (!sponsorsByCompany[companyName]) { - sponsorsByCompany[companyName] = { - jobs: [], - companyLogo: job.company.logo ?? "", - website: job.company.website ?? "", - }; - } - sponsorsByCompany[companyName].jobs.push(job); - }); - - return Object.entries(sponsorsByCompany).map(([companyName, data]) => ({ - companyName, - isPlatinum: platinumSponsors.includes(companyName), - jobs: data.jobs, - companyLogo: data.companyLogo, - website: data.website, - })); -} - -/** - * Picks a random company from an array using weighted selection. - * @param sponsorCompanies - Array of SponsorCompany objects. - * @returns A randomly selected SponsorCompany or null. - */ -function pickRandomCompany( - sponsorCompanies: SponsorCompany[], -): SponsorCompany | null { - const platinum = sponsorCompanies.filter((c) => c.isPlatinum); - const nonPlatinum = sponsorCompanies.filter((c) => !c.isPlatinum); - const choosePlatinum = Math.random() < 0.65 && platinum.length > 0; - const pool = choosePlatinum - ? platinum - : nonPlatinum.length > 0 - ? nonPlatinum - : platinum; - if (pool.length === 0) return null; - return pool[Math.floor(Math.random() * pool.length)]; -} -/** - * Returns a specified number of unique sponsored jobs (slots). - * Allows the same company to be chosen multiple times if different jobs are available. - * @param sponsoredJobs - Array of sponsored Job objects. - * @param platinumSponsors - Array of company names that are platinum sponsors. - * @param slots - Number of sponsored slots to pick. - * @returns An array of Job objects to be used as sponsored slots. - */ -export function getSponsoredSlots( - sponsoredJobs: Job[], - platinumSponsors: string[], - slots: number, -): Job[] { - const sponsorCompanies = groupSponsoredJobsByCompany( - sponsoredJobs, - platinumSponsors, - ); - const sponsoredSlots: Job[] = []; - const usedJobIds = new Set(); - for (let i = 0; i < slots; i++) { - let company = pickRandomCompany(sponsorCompanies); - let attempts = 0; - let randomJob: Job | undefined; - while (attempts < 20) { - if (!company) break; - const candidate = - company.jobs[Math.floor(Math.random() * company.jobs.length)]; - if (!usedJobIds.has(candidate.id)) { - randomJob = candidate; - break; - } - attempts++; - company = pickRandomCompany(sponsorCompanies); - } - if (company && randomJob && !usedJobIds.has(randomJob.id)) { - usedJobIds.add(randomJob.id); - sponsoredSlots.push(randomJob); - } - } - return sponsoredSlots; -} diff --git a/frontend/src/types/job.ts b/frontend/src/types/job.ts index 2dfb5b3..14a8f98 100644 --- a/frontend/src/types/job.ts +++ b/frontend/src/types/job.ts @@ -67,4 +67,5 @@ export interface Job { created_at: string; updated_at: string; is_sponsored: boolean; + highlight: boolean; } From f36f9e8e87ead4ebd1c7347012400f2c3df75cce Mon Sep 17 00:00:00 2001 From: kyranaus Date: Sun, 9 Mar 2025 20:45:07 +1100 Subject: [PATCH 2/2] format & lint --- frontend/src/app/jobs/actions.ts | 274 +++++++++++----------- frontend/src/app/jobs/page.tsx | 4 +- frontend/src/components/jobs/job-list.tsx | 8 +- frontend/src/lib/utils.ts | 5 +- 4 files changed, 148 insertions(+), 143 deletions(-) diff --git a/frontend/src/app/jobs/actions.ts b/frontend/src/app/jobs/actions.ts index 2e604c4..10e16dc 100644 --- a/frontend/src/app/jobs/actions.ts +++ b/frontend/src/app/jobs/actions.ts @@ -10,8 +10,8 @@ const PAGE_SIZE = 20; // Define the MongoJob interface with the correct DB field names. export interface MongoJob extends Omit { - _id: ObjectId; - is_sponsored: boolean; + _id: ObjectId; + is_sponsored: boolean; } /** @@ -21,53 +21,53 @@ export interface MongoJob extends Omit { * @returns The query object to use with MongoDB. */ function buildJobQuery( - filters: Partial, - additional?: Record, + filters: Partial, + additional?: Record, ) { - const array_jobs = JSON.parse(JSON.stringify(filters, null, 2)); - const query = { - outdated: false, - ...(array_jobs["workingRights[]"] !== undefined && - array_jobs["workingRights[]"].length && { - working_rights: { - $in: Array.isArray(array_jobs["workingRights[]"]) - ? array_jobs["workingRights[]"] - : [array_jobs["workingRights[]"]], - }, - }), - ...(array_jobs["locations[]"] !== undefined && - array_jobs["locations[]"].length && { - locations: { - $in: Array.isArray(array_jobs["locations[]"]) - ? array_jobs["locations[]"] - : [array_jobs["locations[]"]], - }, - }), - ...(array_jobs["industryFields[]"] !== undefined && - array_jobs["industryFields[]"].length && { - industry_field: { - $in: Array.isArray(array_jobs["industryFields[]"]) - ? array_jobs["industryFields[]"] - : [array_jobs["industryFields[]"]], - }, - }), - ...(array_jobs["jobTypes[]"] !== undefined && - array_jobs["jobTypes[]"].length && { - type: { - $in: Array.isArray(array_jobs["jobTypes[]"]) - ? array_jobs["jobTypes[]"] - : [array_jobs["jobTypes[]"]], - }, - }), - ...(filters.search && { - $or: [ - { title: { $regex: filters.search, $options: "i" } }, - { "company.name": { $regex: filters.search, $options: "i" } }, - ], - }), - ...additional, - }; - return query; + const array_jobs = JSON.parse(JSON.stringify(filters, null, 2)); + const query = { + outdated: false, + ...(array_jobs["workingRights[]"] !== undefined && + array_jobs["workingRights[]"].length && { + working_rights: { + $in: Array.isArray(array_jobs["workingRights[]"]) + ? array_jobs["workingRights[]"] + : [array_jobs["workingRights[]"]], + }, + }), + ...(array_jobs["locations[]"] !== undefined && + array_jobs["locations[]"].length && { + locations: { + $in: Array.isArray(array_jobs["locations[]"]) + ? array_jobs["locations[]"] + : [array_jobs["locations[]"]], + }, + }), + ...(array_jobs["industryFields[]"] !== undefined && + array_jobs["industryFields[]"].length && { + industry_field: { + $in: Array.isArray(array_jobs["industryFields[]"]) + ? array_jobs["industryFields[]"] + : [array_jobs["industryFields[]"]], + }, + }), + ...(array_jobs["jobTypes[]"] !== undefined && + array_jobs["jobTypes[]"].length && { + type: { + $in: Array.isArray(array_jobs["jobTypes[]"]) + ? array_jobs["jobTypes[]"] + : [array_jobs["jobTypes[]"]], + }, + }), + ...(filters.search && { + $or: [ + { title: { $regex: filters.search, $options: "i" } }, + { "company.name": { $regex: filters.search, $options: "i" } }, + ], + }), + ...additional, + }; + return query; } /** @@ -76,103 +76,113 @@ function buildJobQuery( * @returns The result from the callback. */ async function withDbConnection( - callback: (client: MongoClient) => Promise, + callback: (client: MongoClient) => Promise, ): Promise { - if (!process.env.MONGODB_URI) { - throw new Error( - "MongoDB URI is not configured. Please check environment variables.", - ); - } - const client = new MongoClient(process.env.MONGODB_URI); - try { - await client.connect(); - return await callback(client); - } finally { - await client.close(); - } + if (!process.env.MONGODB_URI) { + throw new Error( + "MongoDB URI is not configured. Please check environment variables.", + ); + } + const client = new MongoClient(process.env.MONGODB_URI); + try { + await client.connect(); + return await callback(client); + } finally { + await client.close(); + } } /** * Fetches paginated and filtered job listings from MongoDB. */ export async function getJobs( - filters: Partial, - minSponsors: number = -1, - prioritySponsors: Array = ["IMC", "Atlassian"] - + filters: Partial, + minSponsors: number = -1, + prioritySponsors: Array = ["IMC", "Atlassian"], ): Promise<{ jobs: Job[]; total: number }> { - return await withDbConnection(async (client) => { - const collection = client.db("default").collection("active_jobs"); - const query = buildJobQuery(filters); - const page = filters.page || 1; - const skip = (page - 1) * PAGE_SIZE; - minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors - - if (minSponsors == 0) { - const [jobs, total] = await Promise.all([ - collection.find(query).skip(skip).limit(PAGE_SIZE).toArray(), - collection.countDocuments(query), - ]); - return { - // Serialize Job and set highlight to false - jobs: (jobs as MongoJob[]).map(serializeJob).map((job) => ({ ...job, highlight: false })), - total, - }; - - } else { - // Modify query to include sponsored job filtering - const sponsoredQuery = { ...query, is_sponsored: true }; - - // Fetch sponsored jobs (without priority filtering) - let sponsoredJobs = await collection.aggregate([{ $match: sponsoredQuery }, { $sample: { size: minSponsors * 8 } }]).toArray(); - - // Apply 65% chance selection for priority sponsors - sponsoredJobs = sponsoredJobs - .filter((job) => { - const isPriority = prioritySponsors.includes(job.company.name); - return isPriority ? Math.random() < 0.65 : Math.random() >= 0.35; // 65% chance for priority, 35% for others - }) - .slice(0, minSponsors) // Ensure we only take the required number - - .map((job) => ({ ...job, highlight: true })); // Add highlight property - - // Get IDs of selected sponsored jobs to exclude them from regular jobs - const sponsoredJobIds = sponsoredJobs.map((job) => job._id); - - // Modify the main query to exclude sponsored jobs we already fetched - const filteredQuery = { ...query, _id: { $nin: sponsoredJobIds } }; - - // Fetch remaining jobs with pagination - const [otherJobs, total] = await Promise.all([ - collection.find(filteredQuery).skip(skip).limit(PAGE_SIZE - sponsoredJobs.length).toArray(), - collection.countDocuments(query), // Total should still include all jobs matching the original query - ]); - // Merge jobs and make sure we don’t exceed PAGE_SIZE also add highlight property - const mergedJobs = [...sponsoredJobs.map((job) => ({ ...job, highlight: true })), ...otherJobs.map((job) => ({ ...job, highlight: false }))].slice(0, PAGE_SIZE); - - return { - jobs: (mergedJobs as MongoJob[]).map(serializeJob), - total, - }; - } - - }); + return await withDbConnection(async (client) => { + const collection = client.db("default").collection("active_jobs"); + const query = buildJobQuery(filters); + const page = filters.page || 1; + const skip = (page - 1) * PAGE_SIZE; + minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors; + + if (minSponsors == 0) { + const [jobs, total] = await Promise.all([ + collection.find(query).skip(skip).limit(PAGE_SIZE).toArray(), + collection.countDocuments(query), + ]); + return { + // Serialize Job and set highlight to false + jobs: (jobs as MongoJob[]) + .map(serializeJob) + .map((job) => ({ ...job, highlight: false })), + total, + }; + } else { + // Modify query to include sponsored job filtering + const sponsoredQuery = { ...query, is_sponsored: true }; + + // Fetch sponsored jobs (without priority filtering) + let sponsoredJobs = await collection + .aggregate([ + { $match: sponsoredQuery }, + { $sample: { size: minSponsors * 8 } }, + ]) + .toArray(); + + // Apply 65% chance selection for priority sponsors + sponsoredJobs = sponsoredJobs + .filter((job) => { + const isPriority = prioritySponsors.includes(job.company.name); + return isPriority ? Math.random() < 0.65 : Math.random() >= 0.35; // 65% chance for priority, 35% for others + }) + .slice(0, minSponsors) // Ensure we only take the required number + + .map((job) => ({ ...job, highlight: true })); // Add highlight property + + // Get IDs of selected sponsored jobs to exclude them from regular jobs + const sponsoredJobIds = sponsoredJobs.map((job) => job._id); + + // Modify the main query to exclude sponsored jobs we already fetched + const filteredQuery = { ...query, _id: { $nin: sponsoredJobIds } }; + + // Fetch remaining jobs with pagination + const [otherJobs, total] = await Promise.all([ + collection + .find(filteredQuery) + .skip(skip) + .limit(PAGE_SIZE - sponsoredJobs.length) + .toArray(), + collection.countDocuments(query), // Total should still include all jobs matching the original query + ]); + // Merge jobs and make sure we don’t exceed PAGE_SIZE also add highlight property + const mergedJobs = [ + ...sponsoredJobs.map((job) => ({ ...job, highlight: true })), + ...otherJobs.map((job) => ({ ...job, highlight: false })), + ].slice(0, PAGE_SIZE); + + return { + jobs: (mergedJobs as MongoJob[]).map(serializeJob), + total, + }; + } + }); } - /** * Fetches a single job by its id. */ export async function getJobById(id: string): Promise { - return await withDbConnection(async (client) => { - const collection = client.db("default").collection("active_jobs"); - const job = await collection.findOne({ - _id: new ObjectId(id), - outdated: false, - }); - if (!job) { - return null; - } - return serializeJob(job as MongoJob); + return await withDbConnection(async (client) => { + const collection = client.db("default").collection("active_jobs"); + const job = await collection.findOne({ + _id: new ObjectId(id), + outdated: false, }); + if (!job) { + return null; + } + return serializeJob(job as MongoJob); + }); } diff --git a/frontend/src/app/jobs/page.tsx b/frontend/src/app/jobs/page.tsx index 10fb5fd..99c5f88 100644 --- a/frontend/src/app/jobs/page.tsx +++ b/frontend/src/app/jobs/page.tsx @@ -37,9 +37,7 @@ export default async function JobsPage({
}> - +
diff --git a/frontend/src/components/jobs/job-list.tsx b/frontend/src/components/jobs/job-list.tsx index afb745f..3871b56 100644 --- a/frontend/src/components/jobs/job-list.tsx +++ b/frontend/src/components/jobs/job-list.tsx @@ -15,16 +15,16 @@ interface JobListProps { jobs: Job[]; // Regular jobs } -export default function JobList({ jobs}: JobListProps) { -//export default function JobList({ jobs, sponsoredJobs }: JobListProps) { +export default function JobList({ jobs }: JobListProps) { + //export default function JobList({ jobs, sponsoredJobs }: JobListProps) { const { selectedJob, setSelectedJob, isLoading } = useFilterContext(); const [isModalOpen, setIsModalOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 1024px)"); useEffect(() => { if (!selectedJob) { - setSelectedJob(jobs[0]); - } + setSelectedJob(jobs[0]); + } }, [jobs, selectedJob, setSelectedJob]); if (isLoading) return ; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 5d4a137..e8e9085 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -77,7 +77,7 @@ export default function serializeJob(job: MongoJob): Job { application_url: job.application_url, close_date: serializeDate(job.close_date), is_sponsored: job.is_sponsored, - highlight: job.highlight + highlight: job.highlight, }; } @@ -175,6 +175,3 @@ export const formatWorkingRights = (rights: WorkingRight[]): string => { }) .join(", "); }; - - -