Skip to content

Commit 8a43556

Browse files
Sponsor section (#106)
Co-authored-by: ed <[email protected]>
1 parent 89c5dcf commit 8a43556

File tree

12 files changed

+345
-140
lines changed

12 files changed

+345
-140
lines changed

frontend/src/app/jobs/actions.ts

Lines changed: 105 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,89 +4,103 @@
44
import { MongoClient, ObjectId } from "mongodb";
55
import { JobFilters } from "@/types/filters";
66
import { Job } from "@/types/job";
7-
87
import serializeJob from "@/lib/utils";
98

109
const PAGE_SIZE = 20;
1110

11+
// Define the MongoJob interface with the correct DB field names.
1212
export interface MongoJob extends Omit<Job, "id"> {
1313
_id: ObjectId;
14+
is_sponsored: boolean;
1415
}
1516

1617
/**
17-
* Fetches paginated and filtered job listings from MongoDB.
18-
*
19-
* @param filters - Partial JobFilters object containing:
20-
* - workingRights?: Array of required working rights
21-
* - jobTypes?: Array of job types to include
22-
* - industryFields?: Array of industry fields
23-
* - search?: Full-text search on job titles and company names (case-insensitive)
24-
* - page?: Page number (defaults to 1)
25-
*
26-
* @returns Promise containing:
27-
* - jobs: Array of serialized Job objects
28-
* - total: Total count of jobs matching the filters
29-
*
30-
* @throws Error if MongoDB connection fails or if MONGODB_URI is not configured
18+
* Helper function to build a query object from filters.
19+
* @param filters - The job filters from the client.
20+
* @param additional - Additional query overrides (e.g. { is_sponsor: true }).
21+
* @returns The query object to use with MongoDB.
3122
*/
32-
export async function getJobs(
23+
function buildJobQuery(
3324
filters: Partial<JobFilters>,
34-
): Promise<{ jobs: Job[]; total: number }> {
25+
additional?: Record<string, unknown>,
26+
) {
27+
const array_jobs = JSON.parse(JSON.stringify(filters, null, 2));
28+
const query = {
29+
outdated: false,
30+
...(array_jobs["workingRights[]"] !== undefined &&
31+
array_jobs["workingRights[]"].length && {
32+
working_rights: {
33+
$in: Array.isArray(array_jobs["workingRights[]"])
34+
? array_jobs["workingRights[]"]
35+
: [array_jobs["workingRights[]"]],
36+
},
37+
}),
38+
...(array_jobs["locations[]"] !== undefined &&
39+
array_jobs["locations[]"].length && {
40+
locations: {
41+
$in: Array.isArray(array_jobs["locations[]"])
42+
? array_jobs["locations[]"]
43+
: [array_jobs["locations[]"]],
44+
},
45+
}),
46+
...(array_jobs["industryFields[]"] !== undefined &&
47+
array_jobs["industryFields[]"].length && {
48+
industry_field: {
49+
$in: Array.isArray(array_jobs["industryFields[]"])
50+
? array_jobs["industryFields[]"]
51+
: [array_jobs["industryFields[]"]],
52+
},
53+
}),
54+
...(array_jobs["jobTypes[]"] !== undefined &&
55+
array_jobs["jobTypes[]"].length && {
56+
type: {
57+
$in: Array.isArray(array_jobs["jobTypes[]"])
58+
? array_jobs["jobTypes[]"]
59+
: [array_jobs["jobTypes[]"]],
60+
},
61+
}),
62+
...(filters.search && {
63+
$or: [
64+
{ title: { $regex: filters.search, $options: "i" } },
65+
{ "company.name": { $regex: filters.search, $options: "i" } },
66+
],
67+
}),
68+
...additional,
69+
};
70+
return query;
71+
}
72+
73+
/**
74+
* Helper function to manage a MongoDB connection.
75+
* @param callback - The function that uses the connected MongoClient.
76+
* @returns The result from the callback.
77+
*/
78+
async function withDbConnection<T>(
79+
callback: (client: MongoClient) => Promise<T>,
80+
): Promise<T> {
3581
if (!process.env.MONGODB_URI) {
3682
throw new Error(
3783
"MongoDB URI is not configured. Please check environment variables.",
3884
);
3985
}
40-
41-
const client = new MongoClient(process.env.MONGODB_URI ?? "");
42-
86+
const client = new MongoClient(process.env.MONGODB_URI);
4387
try {
4488
await client.connect();
45-
const collection = client.db("default").collection("active_jobs");
46-
const array_jobs = JSON.parse(JSON.stringify(filters, null, 2));
47-
// Build the query object with proper typing
48-
const query = {
49-
outdated: false,
50-
...(array_jobs["workingRights[]"] !== undefined &&
51-
array_jobs["workingRights[]"].length && {
52-
working_rights: {
53-
$in: Array.isArray(array_jobs["workingRights[]"])
54-
? array_jobs["workingRights[]"]
55-
: [array_jobs["workingRights[]"]],
56-
},
57-
}),
58-
...(array_jobs["locations[]"] !== undefined &&
59-
array_jobs["locations[]"].length && {
60-
locations: {
61-
$in: Array.isArray(array_jobs["locations[]"])
62-
? array_jobs["locations[]"]
63-
: [array_jobs["locations[]"]],
64-
},
65-
}),
66-
...(array_jobs["industryFields[]"] !== undefined &&
67-
array_jobs["industryFields[]"].length && {
68-
industry_field: {
69-
$in: Array.isArray(array_jobs["industryFields[]"])
70-
? array_jobs["industryFields[]"]
71-
: [array_jobs["industryFields[]"]],
72-
},
73-
}),
74-
...(array_jobs["jobTypes[]"] !== undefined &&
75-
array_jobs["jobTypes[]"].length && {
76-
type: {
77-
$in: Array.isArray(array_jobs["jobTypes[]"])
78-
? array_jobs["jobTypes[]"]
79-
: [array_jobs["jobTypes[]"]],
80-
},
81-
}),
82-
...(filters.search && {
83-
$or: [
84-
{ title: { $regex: filters.search, $options: "i" } },
85-
{ "company.name": { $regex: filters.search, $options: "i" } },
86-
],
87-
}),
88-
};
89+
return await callback(client);
90+
} finally {
91+
await client.close();
92+
}
93+
}
8994

95+
/**
96+
* Fetches paginated and filtered job listings from MongoDB.
97+
*/
98+
export async function getJobs(
99+
filters: Partial<JobFilters>,
100+
): Promise<{ jobs: Job[]; total: number }> {
101+
return await withDbConnection(async (client) => {
102+
const collection = client.db("default").collection("active_jobs");
103+
const query = buildJobQuery(filters);
90104
const page = filters.page || 1;
91105
const skip = (page - 1) * PAGE_SIZE;
92106

@@ -98,48 +112,42 @@ export async function getJobs(
98112
jobs: (jobs as MongoJob[]).map(serializeJob),
99113
total,
100114
};
101-
} catch (error) {
102-
console.error("Server Error:", {
103-
error,
104-
timestamp: new Date().toISOString(),
105-
filters,
106-
});
107-
throw new Error(
108-
"Failed to fetch jobs from the server. Check the server console for more details.",
109-
);
110-
} finally {
111-
await client.close();
112-
}
115+
});
113116
}
114117

115-
export async function getJobById(id: string): Promise<Job | null> {
116-
if (!process.env.MONGODB_URI) {
117-
throw new Error(
118-
"MongoDB URI is not configured. Please check environment variables.",
119-
);
120-
}
121-
122-
const client = new MongoClient(process.env.MONGODB_URI);
123-
124-
try {
125-
await client.connect();
118+
/**
119+
* Fetches all sponsored job listings from MongoDB that match the given filters.
120+
* This function does not paginate results.
121+
*/
122+
export async function getSponsoredJobs(
123+
filters: Partial<JobFilters>,
124+
): Promise<{ jobs: Job[]; total: number }> {
125+
return await withDbConnection(async (client) => {
126126
const collection = client.db("default").collection("active_jobs");
127+
// Add an override to filter only sponsored jobs.
128+
const query = buildJobQuery(filters, { is_sponsored: true });
129+
const jobs = await collection.find(query).toArray();
130+
const total = jobs.length;
131+
return {
132+
jobs: (jobs as MongoJob[]).map(serializeJob),
133+
total,
134+
};
135+
});
136+
}
127137

128-
// Convert the string ID to an ObjectId
138+
/**
139+
* Fetches a single job by its id.
140+
*/
141+
export async function getJobById(id: string): Promise<Job | null> {
142+
return await withDbConnection(async (client) => {
143+
const collection = client.db("default").collection("active_jobs");
129144
const job = await collection.findOne({
130145
_id: new ObjectId(id),
131146
outdated: false,
132147
});
133148
if (!job) {
134149
return null;
135150
}
136-
137-
const typedJob = job as MongoJob;
138-
return serializeJob(typedJob);
139-
} catch (error) {
140-
console.error("Server Error in getJobById:", error);
141-
throw new Error("Failed to fetch job from the server.");
142-
} finally {
143-
await client.close();
144-
}
151+
return serializeJob(job as MongoJob);
152+
});
145153
}

frontend/src/app/jobs/page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import FilterSection from "@/components/filters/filter-section";
33
import JobList from "@/components/jobs/job-list";
44
import JobDetails from "@/components/jobs/job-details";
55
import { JobFilters } from "@/types/filters";
6-
import { getJobs } from "@/app/jobs/actions";
6+
import { getJobs, getSponsoredJobs } from "@/app/jobs/actions";
77
import NoResults from "@/components/ui/no-results";
88
import { Suspense } from "react";
99
import JobListLoading from "@/components/layout/job-list-loading";
1010
import JobDetailsLoading from "@/components/layout/job-details-loading";
11+
import { getSponsoredSlots } from "@/lib/utils";
1112

1213
export const metadata = {
1314
title: "Jobs",
@@ -22,7 +23,25 @@ export default async function JobsPage({
2223
// searchParams is a promise that resolves to an object containing the search
2324
// parameters of the current URL.
2425

25-
const { jobs, total } = await getJobs(await searchParams);
26+
const filters = await searchParams;
27+
28+
// Fetch regular jobs with pagination.
29+
const { jobs, total } = await getJobs(filters);
30+
31+
// Fetch all sponsored jobs (non-paginated) that match the filter.
32+
const { jobs: sponsoredJobs } = await getSponsoredJobs(filters);
33+
34+
// Hardcode the platinum sponsor companies.
35+
const platinumSponsors = ["IMC", "Atlassian"];
36+
37+
// Use our helper to get a set of sponsored slots.
38+
const sponsoredSlots = getSponsoredSlots(sponsoredJobs, platinumSponsors, 4);
39+
40+
// Remove any duplicate jobs from the regular list that appear in the sponsored slots.
41+
const sponsoredJobIds = new Set(sponsoredSlots.map((job) => job.id));
42+
const jobsWithoutDuplicates = jobs.filter(
43+
(job) => !sponsoredJobIds.has(job.id),
44+
);
2645

2746
return (
2847
<>
@@ -34,7 +53,10 @@ export default async function JobsPage({
3453
<div className="mt-4 flex flex-col lg:flex-row">
3554
<div id="job-list-container" className="lg:pr-1 w-full lg:w-[35%]">
3655
<Suspense fallback={<JobListLoading />}>
37-
<JobList jobs={jobs} />
56+
<JobList
57+
jobs={jobsWithoutDuplicates}
58+
sponsoredJobs={sponsoredSlots}
59+
/>
3860
</Suspense>
3961
</div>
4062

frontend/src/app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ export default function Page() {
1515
}, []);
1616

1717
const handleGradJobsClick = () => {
18-
router.push(`/jobs?jobTypes%5B%5D=GRADUATE&page=1&sortBy=recent`);
18+
router.push(`/jobs?jobTypes%5B%5D=GRADUATE&page=1`);
1919
};
2020

2121
const handleInternJobsClick = () => {
22-
router.push(`/jobs?jobTypes%5B%5D=INTERN&page=1&sortBy=recent`);
22+
router.push(`/jobs?jobTypes%5B%5D=INTERN&page=1`);
2323
};
2424

2525
return (

frontend/src/components/filters/dropdown-sort.tsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

frontend/src/components/jobs/job-card.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import CompanyLogo from "@/components/jobs/company-logo";
99
interface JobCardProps {
1010
job: Job;
1111
isSelected?: boolean;
12+
isSponsor?: boolean;
1213
}
1314
const removeImageTags = (content: string): string => {
1415
return content.replace(/<img[^>]*>/g, "");
1516
};
1617

17-
export default function JobCard({ job, isSelected }: JobCardProps) {
18+
export default function JobCard({ job, isSelected, isSponsor }: JobCardProps) {
1819
const washedDescription = job.one_liner ? removeImageTags(job.one_liner) : "";
1920
return (
2021
<Box
@@ -57,6 +58,8 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
5758

5859
{/* Bottom section - badges */}
5960
<div className="flex gap-2 mt-auto">
61+
{/* Show a yellow "Sponsored" badge if this is a sponsor card */}
62+
{isSponsor && <Badge text="Sponsored" color="accent"></Badge>}
6063
{job.type && <Badge text={formatCapString(job.type)} />}
6164
{job.working_rights?.[0] && (
6265
<Badge
@@ -67,7 +70,7 @@ export default function JobCard({ job, isSelected }: JobCardProps) {
6770
}
6871
/>
6972
)}
70-
{job.industry_field && (
73+
{!isSponsor && job.industry_field && (
7174
<Badge text={formatCapString(job.industry_field)} />
7275
)}
7376
{job.locations && job.locations.length > 0 && (

0 commit comments

Comments
 (0)