diff --git a/frontend/.claude/settings.local.json b/frontend/.claude/settings.local.json
new file mode 100644
index 0000000..d0f83a5
--- /dev/null
+++ b/frontend/.claude/settings.local.json
@@ -0,0 +1,5 @@
+{
+ "permissions": {
+ "allow": ["Bash(npm ls *)", "Bash(npx tsc *)"]
+ }
+}
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index c85fb67..9aaa7a1 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -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;
diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/frontend/next-env.d.ts
+++ b/frontend/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-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.
diff --git a/frontend/package.json b/frontend/package.json
index f07d174..46042c4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,7 +6,7 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
- "lint": "next lint",
+ "lint": "eslint .",
"format": "npx prettier . --write"
},
"dependencies": {
diff --git a/frontend/src/actions/jobs.fetch.ts b/frontend/src/actions/jobs.fetch.ts
index 7ac1f23..2413881 100644
--- a/frontend/src/actions/jobs.fetch.ts
+++ b/frontend/src/actions/jobs.fetch.ts
@@ -136,18 +136,32 @@ async function withDbConnection(
}
}
+const SPONSOR_TIERS: Record = {
+ // 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,
minSponsors: number = -1,
- prioritySponsors: Array = ["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);
@@ -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");
@@ -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 }));
diff --git a/frontend/src/components/filters/dropdown-filter.tsx b/frontend/src/components/filters/dropdown-filter.tsx
index da28946..0421474 100644
--- a/frontend/src/components/filters/dropdown-filter.tsx
+++ b/frontend/src/components/filters/dropdown-filter.tsx
@@ -1,5 +1,4 @@
// frontend/src/components/jobs/filters/dropdown-filter.tsx
-import { useEffect, useState } from "react";
import {
Checkbox,
Combobox,
@@ -30,22 +29,13 @@ export default function DropdownFilter({
});
const { filters, updateFilters } = useFilterContext();
- const [localSelected, setLocalSelected] = useState(
- (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,
@@ -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 (
@@ -77,7 +67,7 @@ export default function DropdownFilter({
onClick={() => combobox.toggleDropdown()}
className={`min-w-32`}
>
- 0 ? "light" : "dimmed"}>
+ 0 ? "light" : "dimmed"}>
{getDisplayText()}
@@ -89,11 +79,11 @@ export default function DropdownFilter({
{}}
aria-hidden
tabIndex={-1}
diff --git a/frontend/src/components/jobs/job-pagination.tsx b/frontend/src/components/jobs/job-pagination.tsx
index 8950489..bb7aa9e 100644
--- a/frontend/src/components/jobs/job-pagination.tsx
+++ b/frontend/src/components/jobs/job-pagination.tsx
@@ -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";
@@ -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");
@@ -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) => ({
diff --git a/frontend/src/components/search/search-bar.tsx b/frontend/src/components/search/search-bar.tsx
index a69a07f..18def8c 100644
--- a/frontend/src/components/search/search-bar.tsx
+++ b/frontend/src/components/search/search-bar.tsx
@@ -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({
diff --git a/frontend/src/context/filter/filter-provider.tsx b/frontend/src/context/filter/filter-provider.tsx
index e51cdc7..7d65154 100644
--- a/frontend/src/context/filter/filter-provider.tsx
+++ b/frontend/src/context/filter/filter-provider.tsx
@@ -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";
@@ -74,6 +74,15 @@ export function FilterProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(false);
const [totalJobs, setTotalJobs] = useState(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) => {
setIsLoading(true);
setFilters((curr) => ({ ...curr, ...newFilters }));
@@ -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);
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index b575f7d..19c51c8 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,9 +19,7 @@
}
],
"paths": {
- "@/*": [
- "./src/*"
- ]
+ "@/*": ["./src/*"]
}
},
"include": [
@@ -35,7 +29,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
- "exclude": [
- "node_modules"
- ]
+ "exclude": ["node_modules"]
}