Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 62 additions & 27 deletions frontend/src/app/jobs/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,41 +97,76 @@ async function withDbConnection<T>(
*/
export async function getJobs(
filters: Partial<JobFilters>,
minSponsors: number = -1,
prioritySponsors: Array<string> = ["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;

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,
};
});
}
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 };

/**
* Fetches all sponsored job listings from MongoDB that match the given filters.
* This function does not paginate results.
*/
export async function getSponsoredJobs(
filters: Partial<JobFilters>,
): 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,
};
// 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,
};
}
});
}

Expand Down
23 changes: 2 additions & 21 deletions frontend/src/app/jobs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 (
<>
<FilterSection _totalJobs={total} />
Expand All @@ -53,10 +37,7 @@ export default async function JobsPage({
<div className="mt-4 flex flex-col lg:flex-row">
<div id="job-list-container" className="lg:pr-1 w-full lg:w-[35%]">
<Suspense fallback={<JobListLoading />}>
<JobList
jobs={jobsWithoutDuplicates}
sponsoredJobs={sponsoredSlots}
/>
<JobList jobs={jobs} />
</Suspense>
</div>

Expand Down
19 changes: 5 additions & 14 deletions frontend/src/components/jobs/job-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
setSelectedJob(jobs[0]);
}
}, [jobs, sponsoredJobs, selectedJob, setSelectedJob]);
}, [jobs, selectedJob, setSelectedJob]);

if (isLoading) return <JobListLoading />;

Expand All @@ -50,10 +45,6 @@ export default function JobList({ jobs, sponsoredJobs }: JobListProps) {
: undefined
}
>
<SponsorSection
selectedJobID={selectedJob?.id}
sponsoredJobs={sponsoredJobs}
></SponsorSection>
<div className="space-y-4 pr-1">
{jobs.map((job) => (
<div
Expand All @@ -70,7 +61,7 @@ export default function JobList({ jobs, sponsoredJobs }: JobListProps) {
<JobCard
job={job}
isSelected={selectedJob?.id === job.id}
isSponsor={false}
isSponsor={job.highlight}
/>
</div>
))}
Expand Down
67 changes: 0 additions & 67 deletions frontend/src/components/jobs/sponsor-section.tsx

This file was deleted.

112 changes: 1 addition & 111 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -174,114 +175,3 @@ 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<string>();

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;
}
Loading