Skip to content
Open
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
21 changes: 14 additions & 7 deletions frontend/src/app/jobs/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,21 @@ export async function getJobs(
const skip = (page - 1) * PAGE_SIZE;
minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors;

// derive sort object from filters.sortBy
const sortMap: Record<string, Record<string, 1 | -1>> = {
recent_desc: { created_at: 1 },
recent_asc: { created_at: -1 },
closing_desc: { close_date: 1 },
closing_asc: { close_date: -1 },
};

const sort = sortMap[(filters.sortBy as string) ?? "posted_desc"] ?? {
created_at: -1,
};

if (minSponsors == 0) {
const [jobs, total] = await Promise.all([
collection
.find(query)
.sort({ created_at: -1 })
.skip(skip)
.limit(PAGE_SIZE)
.toArray(),
collection.find(query).sort(sort).skip(skip).limit(PAGE_SIZE).toArray(),
collection.countDocuments(query),
]);
return {
Expand Down Expand Up @@ -156,7 +163,7 @@ export async function getJobs(
const [otherJobs, total] = await Promise.all([
collection
.find(filteredQuery)
.sort({ created_at: -1 })
.sort(sort)
.skip(skip)
.limit(PAGE_SIZE - sponsoredJobs.length)
.toArray(),
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/filters/dropdown-sort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Select } from "@mantine/core";
import { useFilterContext } from "@/context/filter/filter-context";
import { SortBy } from "@/types/filters";

export default function DropdownSort() {
const { filters, updateFilters } = useFilterContext();

return (
<div>
<Select
data={[
{ value: "recent_asc", label: "Newest" },
{ value: "recent_desc", label: "Oldest" },
{ value: "closing_asc", label: "Closing Soon" },
{ value: "closing_desc", label: "Closing Latest" },
]}
value={filters.filters.sortBy}
onChange={(value) => {
if (value) {
updateFilters({
filters: {
...filters.filters,
sortBy: value as SortBy,
page: 1,
},
});
}
}}
allowDeselect={false}
placeholder="Sort by"
radius={"md"}
className="max-w-36"
/>
</div>
);
}
6 changes: 4 additions & 2 deletions frontend/src/components/filters/filter-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useFilterContext } from "@/context/filter/filter-context";
import { useEffect } from "react";
import FilterModal from "@/components/filters/filter-modal";
import ResetFilters from "@/components/filters/reset-filters";
import DropdownSort from "@/components/filters/dropdown-sort";

interface FilterSectionProps {
_totalJobs: number;
Expand All @@ -23,9 +24,10 @@ export default function FilterSection({ _totalJobs }: FilterSectionProps) {
{isLoading ? "" : totalJobs + " Results"}
</Text>

<div className="flex flex-row items-center">
<ResetFilters className={"pr-4"} />
<div className="flex flex-row items-center gap-4">
<ResetFilters className={"p-0"} />
<FilterModal />
<DropdownSort />
</div>
</div>
);
Expand Down
25 changes: 19 additions & 6 deletions frontend/src/components/jobs/job-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import JobCard from "@/components/jobs/job-card";
import { useFilterContext } from "@/context/filter/filter-context";
import { Job } from "@/types/job";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Modal, ScrollArea } from "@mantine/core";
import JobDetails from "@/components/jobs/job-details";
import JobListLoading from "@/components/layout/job-list-loading";
Expand All @@ -21,11 +21,24 @@ export default function JobList({ jobs }: JobListProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 1024px)");

const sortedJobs = useMemo(() => {
const copy = [...jobs];
copy.sort((a, b) => {
// Primary key → highlighted first
const highDiff = (b.highlight ? 1 : 0) - (a.highlight ? 1 : 0);
return highDiff;
});
return copy;
}, [jobs]);

useEffect(() => {
if (!selectedJob) {
setSelectedJob(jobs[0]);
const selectionMissing =
!selectedJob || !sortedJobs.some((job) => job.id === selectedJob.id);

if (selectionMissing && sortedJobs.length > 0) {
setSelectedJob(sortedJobs[0]);
}
}, [jobs, selectedJob, setSelectedJob]);
}, [selectedJob, sortedJobs, setSelectedJob]);

if (isLoading) return <JobListLoading />;

Expand All @@ -46,7 +59,7 @@ export default function JobList({ jobs }: JobListProps) {
}
>
<div className="space-y-4 pr-1">
{jobs.map((job) => (
{sortedJobs.map((job) => (
<div
key={job.id}
onClick={() => {
Expand All @@ -61,7 +74,7 @@ export default function JobList({ jobs }: JobListProps) {
<JobCard
job={job}
isSelected={selectedJob?.id === job.id}
isSponsor={job.highlight}
isSponsor={!!job.highlight}
/>
</div>
))}
Expand Down
13 changes: 4 additions & 9 deletions frontend/src/context/filter/filter-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ReactNode, useEffect, useState } from "react";
import { FilterContext } from "./filter-context";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { CreateQueryString } from "@/lib/utils";
import { FilterState } from "@/types/filters";
import { FilterState, SortBy } from "@/types/filters";
import {
Job,
IndustryField,
Expand All @@ -26,6 +26,7 @@ const emptyFilterState: FilterState = {
locations: [],
workingRights: [],
page: 1,
sortBy: SortBy.RECENT_ASC,
},
isLoading: false,
error: null,
Expand Down Expand Up @@ -63,7 +64,8 @@ export function FilterProvider({ children }: { children: ReactNode }) {
.filter((field): field is WorkingRight =>
WORKING_RIGHTS.includes(field as WorkingRight),
) || [],
page: Number(searchParams.get("page")) || 1,
page: 1,
sortBy: SortBy.RECENT_ASC,
},
isLoading: false,
error: null,
Expand All @@ -89,13 +91,6 @@ export function FilterProvider({ children }: { children: ReactNode }) {
}
}, [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
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/types/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export interface JobFilters {
workingRights: WorkingRight[];
industryFields: IndustryField[];
page: number;
sortBy: SortBy;
}

export enum SortBy {
RECENT_DESC = "recent_desc",
RECENT_ASC = "recent_asc",
CLOSING_DESC = "closing_desc",
CLOSING_ASC = "closing_asc",
}

export interface FilterState {
Expand Down