Skip to content

Commit 1ba6f09

Browse files
authored
Merge branch 'production' into development
2 parents 4410ca3 + b768bbc commit 1ba6f09

File tree

6 files changed

+326
-0
lines changed

6 files changed

+326
-0
lines changed

backend/start.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Start spring boot devtools process in the background.
2+
# This is necessary for hot reload.
3+
(./gradlew -t :bootJar) &
4+
# Next, start the app.
5+
# The "PskipDownload" option ensures dependencies are not downloaded again.
6+
./gradlew bootRun -PskipDownload=true --parallel --build-cache --continuous

frontend/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/app/actions.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// frontend/src/app/actions/feedback.ts
2+
"use server";
3+
4+
import logger from "@/lib/logger";
5+
6+
// Type for our form data
7+
export interface FeedbackFormData {
8+
email?: string;
9+
message: string;
10+
}
11+
12+
export async function submitFeedback(data: FeedbackFormData) {
13+
const databaseId = process.env.NOTION_DATABASE_ID;
14+
const notionApiKey = process.env.NOTION_API_KEY;
15+
16+
if (!databaseId) {
17+
logger.error("NOTION_DATABASE_ID environment variable is not set");
18+
throw new Error("NOTION_DATABASE_ID environment variable is not set");
19+
}
20+
21+
if (!notionApiKey) {
22+
logger.error("NOTION_API_KEY environment variable is not set");
23+
throw new Error("NOTION_API_KEY environment variable is not set");
24+
}
25+
26+
try {
27+
const response = await fetch("https://api.notion.com/v1/pages", {
28+
method: "POST",
29+
headers: {
30+
Authorization: `Bearer ${notionApiKey}`,
31+
"Content-Type": "application/json",
32+
"Notion-Version": "2022-06-28",
33+
},
34+
body: JSON.stringify({
35+
parent: {
36+
database_id: databaseId,
37+
},
38+
properties: {
39+
Email: {
40+
title: [
41+
{
42+
text: {
43+
content: data.email || "Anonymous",
44+
},
45+
},
46+
],
47+
},
48+
Feedback: {
49+
rich_text: [
50+
{
51+
text: {
52+
content: data.message,
53+
},
54+
},
55+
],
56+
},
57+
Date: {
58+
date: {
59+
start: new Date().toISOString(),
60+
},
61+
},
62+
},
63+
}),
64+
});
65+
66+
if (!response.ok) {
67+
const errorData = await response.json();
68+
logger.error(
69+
{ error: errorData, status: response.status },
70+
"Notion API error during feedback submission",
71+
);
72+
return {
73+
success: false,
74+
message: `We're sorry, but there was an issue submitting your feedback. Please try again later.`,
75+
};
76+
}
77+
78+
logger.info(
79+
{ email: data.email || "Anonymous" },
80+
"Feedback submitted successfully",
81+
);
82+
return {
83+
success: true,
84+
message: "Thank you! Your feedback has been submitted successfully.",
85+
};
86+
} catch (error) {
87+
logger.error(error, "Unexpected error during feedback submission");
88+
return {
89+
success: false,
90+
message: `An unexpected error occurred while submitting your feedback. Please try again.`,
91+
};
92+
}
93+
}

frontend/src/app/jobs/actions.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// /src/app/jobs/actions.ts
2+
"use server";
3+
4+
import { MongoClient, ObjectId } from "mongodb";
5+
import { JobFilters } from "@/types/filters";
6+
import { Job } from "@/types/job";
7+
import serializeJob from "@/lib/utils";
8+
import logger from "@/lib/logger";
9+
10+
const PAGE_SIZE = 20;
11+
12+
// Define the MongoJob interface with the correct DB field names.
13+
export interface MongoJob extends Omit<Job, "id"> {
14+
_id: ObjectId;
15+
is_sponsored: boolean;
16+
}
17+
18+
/**
19+
* Helper function to build a query object from filters.
20+
* @param filters - The job filters from the client.
21+
* @param additional - Additional query overrides (e.g. { is_sponsor: true }).
22+
* @returns The query object to use with MongoDB.
23+
*/
24+
function buildJobQuery(
25+
filters: Partial<JobFilters>,
26+
additional?: Record<string, unknown>,
27+
) {
28+
const array_jobs = JSON.parse(JSON.stringify(filters, null, 2));
29+
const query = {
30+
outdated: false,
31+
...(array_jobs["workingRights[]"] !== undefined &&
32+
array_jobs["workingRights[]"].length && {
33+
working_rights: {
34+
$in: Array.isArray(array_jobs["workingRights[]"])
35+
? array_jobs["workingRights[]"]
36+
: [array_jobs["workingRights[]"]],
37+
},
38+
}),
39+
...(array_jobs["locations[]"] !== undefined &&
40+
array_jobs["locations[]"].length && {
41+
locations: {
42+
$in: Array.isArray(array_jobs["locations[]"])
43+
? array_jobs["locations[]"]
44+
: [array_jobs["locations[]"]],
45+
},
46+
}),
47+
...(array_jobs["industryFields[]"] !== undefined &&
48+
array_jobs["industryFields[]"].length && {
49+
industry_field: {
50+
$in: Array.isArray(array_jobs["industryFields[]"])
51+
? array_jobs["industryFields[]"]
52+
: [array_jobs["industryFields[]"]],
53+
},
54+
}),
55+
...(array_jobs["jobTypes[]"] !== undefined &&
56+
array_jobs["jobTypes[]"].length && {
57+
type: {
58+
$in: Array.isArray(array_jobs["jobTypes[]"])
59+
? array_jobs["jobTypes[]"]
60+
: [array_jobs["jobTypes[]"]],
61+
},
62+
}),
63+
...(filters.search && {
64+
$or: [
65+
{ title: { $regex: filters.search, $options: "i" } },
66+
{ "company.name": { $regex: filters.search, $options: "i" } },
67+
],
68+
}),
69+
...additional,
70+
};
71+
return query;
72+
}
73+
74+
/**
75+
* Helper function to manage a MongoDB connection.
76+
* @param callback - The function that uses the connected MongoClient.
77+
* @returns The result from the callback.
78+
*/
79+
async function withDbConnection<T>(
80+
callback: (client: MongoClient) => Promise<T>,
81+
): Promise<T> {
82+
if (!process.env.MONGODB_URI) {
83+
logger.error("MONGODB_URI environment variable is not set");
84+
throw new Error(
85+
"MongoDB URI is not configured. Please check environment variables.",
86+
);
87+
}
88+
const client = new MongoClient(process.env.MONGODB_URI);
89+
try {
90+
await client.connect();
91+
logger.debug("MongoDB connected successfully");
92+
return await callback(client);
93+
} catch (error) {
94+
logger.error(error, "Failed to connect to MongoDB or execute callback");
95+
throw error;
96+
} finally {
97+
await client.close();
98+
}
99+
}
100+
101+
/**
102+
* Fetches paginated and filtered job listings from MongoDB.
103+
*/
104+
export async function getJobs(
105+
filters: Partial<JobFilters>,
106+
minSponsors: number = -1,
107+
prioritySponsors: Array<string> = ["IMC", "Atlassian"],
108+
): Promise<{ jobs: Job[]; total: number }> {
109+
logger.info(
110+
{ filters, minSponsors, prioritySponsors },
111+
"Fetching jobs with filters",
112+
);
113+
return await withDbConnection(async (client) => {
114+
const collection = client.db("default").collection("active_jobs");
115+
const query = buildJobQuery(filters);
116+
const page = filters.page || 1;
117+
const skip = (page - 1) * PAGE_SIZE;
118+
minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors;
119+
120+
try {
121+
if (minSponsors == 0) {
122+
const [jobs, total] = await Promise.all([
123+
collection
124+
.find(query)
125+
.sort({ created_at: -1 })
126+
.skip(skip)
127+
.limit(PAGE_SIZE)
128+
.toArray(),
129+
collection.countDocuments(query),
130+
]);
131+
logger.debug({ total }, "Fetched non-sponsored jobs");
132+
return {
133+
jobs: (jobs as MongoJob[])
134+
.map(serializeJob)
135+
.map((job) => ({ ...job, highlight: false })),
136+
total,
137+
};
138+
} else {
139+
const sponsoredQuery = { ...query, is_sponsored: true };
140+
141+
let sponsoredJobs = await collection
142+
.aggregate([
143+
{ $match: sponsoredQuery },
144+
{ $sample: { size: minSponsors * 8 } },
145+
])
146+
.toArray();
147+
148+
sponsoredJobs = sponsoredJobs
149+
.filter((job) => {
150+
const isPriority = prioritySponsors.includes(job.company.name);
151+
return isPriority ? Math.random() < 0.65 : Math.random() >= 0.35;
152+
})
153+
.slice(0, minSponsors)
154+
.map((job) => ({ ...job, highlight: true }));
155+
156+
const sponsoredJobIds = sponsoredJobs.map((job) => job._id);
157+
158+
const filteredQuery = { ...query, _id: { $nin: sponsoredJobIds } };
159+
160+
const [otherJobs, total] = await Promise.all([
161+
collection
162+
.find(filteredQuery)
163+
.sort({ created_at: -1 })
164+
.skip(skip)
165+
.limit(PAGE_SIZE - sponsoredJobs.length)
166+
.toArray(),
167+
collection.countDocuments(query),
168+
]);
169+
170+
const mergedJobs = [
171+
...sponsoredJobs.map((job) => ({ ...job, highlight: true })),
172+
...otherJobs.map((job) => ({ ...job, highlight: false })),
173+
].slice(0, PAGE_SIZE);
174+
175+
logger.debug(
176+
{
177+
sponsoredCount: sponsoredJobs.length,
178+
otherCount: otherJobs.length,
179+
total,
180+
},
181+
"Fetched sponsored and other jobs",
182+
);
183+
return {
184+
jobs: (mergedJobs as MongoJob[]).map(serializeJob),
185+
total,
186+
};
187+
}
188+
} catch (error) {
189+
logger.error({ query, filters }, "Error fetching jobs");
190+
throw error;
191+
}
192+
});
193+
}
194+
195+
/**
196+
* Fetches a single job by its id.
197+
*/
198+
export async function getJobById(id: string): Promise<Job | null> {
199+
logger.info({ id }, "Fetching job by ID");
200+
return await withDbConnection(async (client) => {
201+
const collection = client.db("default").collection("active_jobs");
202+
const job = await collection.findOne({
203+
_id: new ObjectId(id),
204+
outdated: false,
205+
});
206+
if (!job) {
207+
logger.warn({ id }, "Job not found");
208+
return null;
209+
}
210+
logger.debug({ id }, "Job fetched successfully");
211+
return serializeJob(job as MongoJob);
212+
});
213+
}

frontend/src/assets/OgImage.png

22.8 KB
Loading

frontend/src/assets/mac.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)