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
5 changes: 5 additions & 0 deletions frontend/.claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"permissions": {
"allow": ["Bash(npm ls *)", "Bash(npx tsc *)"]
}
}
16 changes: 3 additions & 13 deletions frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
const eslintConfig = [...nextCoreWebVitals, ...nextTypescript];

export default eslintConfig;
2 changes: 1 addition & 1 deletion frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"format": "npx prettier . --write"
},
"dependencies": {
Expand Down
38 changes: 27 additions & 11 deletions frontend/src/actions/jobs.fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,32 @@ async function withDbConnection<T>(
}
}

const SPONSOR_TIERS: Record<string, number> = {
// Platinum (rank 0) — highest priority
"Jane Street": 0,
Atlassian: 0,
// Gold (rank 1)
"Citadel Securities": 1,
Canva: 1,
"Lyra Technologies": 1,
Susquehanna: 1,
IMC: 1,
Vivcourt: 1,
// Silver (rank 2)
"January Capital": 2,
Optiver: 2,
};

/**
* Fetches paginated and filtered job listings from MongoDB.
*/
export async function getJobs(
filters: Partial<JobFilters>,
minSponsors: number = -1,
prioritySponsors: Array<string> = ["IMC", "Atlassian"],
): Promise<{ jobs: Job[]; total: number }> {
const page = filters.page || 1;
const normalizedFilters = normalizeFiltersForKey(filters);
const priorityStr = prioritySponsors.sort().join(",");
const cacheKey = `jobs:${JSON.stringify(normalizedFilters)}:${page}:${minSponsors}:${priorityStr}`;
const cacheKey = `jobs:${JSON.stringify(normalizedFilters)}:${page}:${minSponsors}`;

// Check cache first
const cached = jobCache.get(cacheKey);
Expand All @@ -156,10 +170,7 @@ export async function getJobs(
return cached as { jobs: Job[]; total: number };
}

logger.info(
{ filters, minSponsors, prioritySponsors },
"Fetching jobs with filters",
);
logger.info({ filters, minSponsors }, "Fetching jobs with filters");

return await withDbConnection(async (client) => {
const collection = client.db("default").collection("active_jobs");
Expand Down Expand Up @@ -196,11 +207,16 @@ export async function getJobs(
])
.toArray();

// Sort by tier (platinum=0 first, gold=1, silver=2, unknown=3),
// shuffling randomly within each tier.
sponsoredJobs = sponsoredJobs
.filter((job) => {
const isPriority = prioritySponsors.includes(job.company.name);
return isPriority ? Math.random() < 0.65 : Math.random() >= 0.35;
})
.map((job) => ({
job,
rank: SPONSOR_TIERS[job.company?.name as string] ?? 3,
rand: Math.random(),
}))
.sort((a, b) => a.rank - b.rank || a.rand - b.rand)
.map(({ job }) => job)
.slice(0, minSponsors)
.map((job) => ({ ...job, highlight: true }));

Expand Down
30 changes: 10 additions & 20 deletions frontend/src/components/filters/dropdown-filter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// frontend/src/components/jobs/filters/dropdown-filter.tsx
import { useEffect, useState } from "react";
import {
Checkbox,
Combobox,
Expand Down Expand Up @@ -30,22 +29,13 @@ export default function DropdownFilter({
});

const { filters, updateFilters } = useFilterContext();
const [localSelected, setLocalSelected] = useState<string[]>(
(filters.filters[filterKey] as string[]) || [],
);

// Sync when filters change
useEffect(() => {
setLocalSelected((filters.filters[filterKey] as string[]) || []);
}, [filters.filters, filterKey]);
const selected = (filters.filters[filterKey] as string[]) || [];

// Updates locally selected value & filters
const handleValueSelect = (value: string) => {
const newValues = localSelected.includes(value)
? localSelected.filter((item) => item !== value)
: [...localSelected, value];
const newValues = selected.includes(value)
? selected.filter((item) => item !== value)
: [...selected, value];

setLocalSelected(newValues);
updateFilters({
filters: {
...filters.filters,
Expand All @@ -56,9 +46,9 @@ export default function DropdownFilter({
};

const getDisplayText = () => {
if (localSelected.length === 0) return label;
if (localSelected.length === 1) return formatCapString(localSelected[0]);
return `${localSelected.length} ${getPluralLabel(label)}`;
if (selected.length === 0) return label;
if (selected.length === 1) return formatCapString(selected[0]);
return `${selected.length} ${getPluralLabel(label)}`;
};

return (
Expand All @@ -77,7 +67,7 @@ export default function DropdownFilter({
onClick={() => combobox.toggleDropdown()}
className={`min-w-32`}
>
<Text size="sm" color={localSelected.length > 0 ? "light" : "dimmed"}>
<Text size="sm" color={selected.length > 0 ? "light" : "dimmed"}>
{getDisplayText()}
</Text>
</Input>
Expand All @@ -89,11 +79,11 @@ export default function DropdownFilter({
<Combobox.Option
value={option}
key={option}
active={localSelected.includes(option)}
active={selected.includes(option)}
>
<Group gap="sm">
<Checkbox
checked={localSelected.includes(option)}
checked={selected.includes(option)}
onChange={() => {}}
aria-hidden
tabIndex={-1}
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/components/jobs/job-pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// frontend/src/components/jobs/pagination.tsx
"use client";

import { useEffect, useState } from "react";
import { Pagination } from "@mantine/core";
import { useFilterContext } from "@/context/filter/filter-context";

Expand All @@ -10,18 +9,11 @@ interface JobPaginationProps {
}

export default function JobPagination({ pageSize = 20 }: JobPaginationProps) {
const [isReady, setIsReady] = useState(false);
const { filters, updateFilters, totalJobs, isLoading } = useFilterContext();

useEffect(() => {
if (totalJobs !== undefined) {
setIsReady(true);
}
}, [totalJobs]);

const totalPages = Math.ceil(totalJobs / pageSize);

if (!isReady || totalPages <= 1 || isLoading) return null;
if (totalJobs === undefined || totalPages <= 1 || isLoading) return null;

const handlePageChange = (page: number) => {
const scrollContainer = document.querySelector("#job-list-container");
Expand All @@ -48,7 +40,7 @@ export default function JobPagination({ pageSize = 20 }: JobPaginationProps) {
size="md"
gap={12}
boundaries={1}
siblings={0}
siblings={1}
radius="lg"
color="accent"
getItemProps={(page) => ({
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/search/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { Input } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useFilterContext } from "@/context/filter/filter-context";
import { useDebouncedCallback } from "@mantine/hooks";
import { useEffect, useState } from "react";
import { useState } from "react";

export default function SearchBar() {
const { filters, updateFilters } = useFilterContext();
const [searchValue, setSearchValue] = useState(filters.filters.search || "");
const externalSearch = filters.filters.search || "";
const [searchValue, setSearchValue] = useState(externalSearch);
const [prevExternalSearch, setPrevExternalSearch] = useState(externalSearch);

useEffect(() => {
setSearchValue(filters.filters.search || "");
}, [filters.filters.search]);
if (prevExternalSearch !== externalSearch) {
setPrevExternalSearch(externalSearch);
setSearchValue(externalSearch);
}

const handleSearch = useDebouncedCallback((value: string) => {
updateFilters({
Expand Down
31 changes: 15 additions & 16 deletions frontend/src/context/filter/filter-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// frontend/src/context/jobs/jobs-provider.tsx
"use client";

import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useState } from "react";
import { FilterContext } from "./filter-context";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { CreateQueryString } from "@/lib/utils";
Expand Down Expand Up @@ -74,6 +74,15 @@ export function FilterProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(false);
const [totalJobs, setTotalJobs] = useState<number>(0);

// Wrapper for SelectedJob to validate attributes first
const setSelectedJob = (job: Job | null) => {
// Remove duplicates from working_rights
if (job?.working_rights && job.working_rights.length > 0) {
job.working_rights = [...new Set(job.working_rights)];
}
setSelectedJobInternal(job);
};

const updateFilters = (newFilters: Partial<FilterState>) => {
setIsLoading(true);
setFilters((curr) => ({ ...curr, ...newFilters }));
Expand All @@ -82,28 +91,18 @@ export function FilterProvider({ children }: { children: ReactNode }) {
router.push(`/jobs?${params}`);
};

useEffect(() => {
const navKey = `${pathname}|${searchParams.toString()}`;
const [prevNavKey, setPrevNavKey] = useState(navKey);
if (prevNavKey !== navKey) {
setPrevNavKey(navKey);
if (pathname === "/jobs") {
setIsLoading(false);
setSelectedJob(null);
}
}, [pathname, searchParams]);

useEffect(() => {
// clear filters on return to homepage
if (pathname === "/") {
setFilters(emptyFilterState);
}
}, [pathname]);

// Wrapper for SelectedJob to validate attributes first
const setSelectedJob = (job: Job | null) => {
// Remove duplicates from working_rights
if (job?.working_rights && job.working_rights.length > 0) {
job.working_rights = [...new Set(job.working_rights)];
}
setSelectedJobInternal(job);
};
}

const clearFilters = () => {
setIsLoading(true);
Expand Down
14 changes: 3 additions & 11 deletions frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
Expand All @@ -23,9 +19,7 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
Expand All @@ -35,7 +29,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}
Loading