job list source filter
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { ArrowUpDown, LayoutGrid, Search, Table2, X } from "lucide-react";
|
import { ArrowUpDown, Filter, LayoutGrid, Search, Table2, X } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Job, JobStatus } from "../../shared/types";
|
import type { Job, JobStatus, JobSource } from "../../shared/types";
|
||||||
import { JobCard } from "./JobCard";
|
import { JobCard } from "./JobCard";
|
||||||
import { JobTable, type JobSort } from "./JobTable";
|
import { JobTable, type JobSort } from "./JobTable";
|
||||||
|
|
||||||
@@ -51,6 +51,13 @@ const sortLabels: Record<JobSort["key"], string> = {
|
|||||||
status: "Status",
|
status: "Status",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sourceLabels: Record<JobSource, string> = {
|
||||||
|
gradcracker: "Gradcracker",
|
||||||
|
indeed: "Indeed",
|
||||||
|
linkedin: "LinkedIn",
|
||||||
|
ukvisajobs: "UK Visa Jobs",
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: Array<{ id: FilterTab; label: string; statuses: JobStatus[] }> = [
|
const tabs: Array<{ id: FilterTab; label: string; statuses: JobStatus[] }> = [
|
||||||
{ id: "ready", label: "Ready", statuses: ["ready"] },
|
{ id: "ready", label: "Ready", statuses: ["ready"] },
|
||||||
{ id: "discovered", label: "Discovered", statuses: ["discovered", "processing"] },
|
{ id: "discovered", label: "Discovered", statuses: ["discovered", "processing"] },
|
||||||
@@ -183,6 +190,7 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<FilterTab>("ready");
|
const [activeTab, setActiveTab] = useState<FilterTab>("ready");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [sourceFilter, setSourceFilter] = useState<JobSource | "all">("all");
|
||||||
const [sort, setSort] = useState<JobSort>(DEFAULT_SORT);
|
const [sort, setSort] = useState<JobSort>(DEFAULT_SORT);
|
||||||
const [selectedJobIds, setSelectedJobIds] = useState<Set<string>>(() => new Set());
|
const [selectedJobIds, setSelectedJobIds] = useState<Set<string>>(() => new Set());
|
||||||
const [batchAction, setBatchAction] = useState<null | "process" | "reject" | "apply">(null);
|
const [batchAction, setBatchAction] = useState<null | "process" | "reject" | "apply">(null);
|
||||||
@@ -267,14 +275,22 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
const normalizedQuery = searchQuery.trim().toLowerCase();
|
const normalizedQuery = searchQuery.trim().toLowerCase();
|
||||||
|
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
const base = jobsForTab.get(tab.id) ?? [];
|
let filtered = jobsForTab.get(tab.id) ?? [];
|
||||||
const filtered = normalizedQuery ? base.filter((job) => jobMatchesQuery(job, normalizedQuery)) : base;
|
|
||||||
|
if (sourceFilter !== "all") {
|
||||||
|
filtered = filtered.filter((job) => job.source === sourceFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedQuery) {
|
||||||
|
filtered = filtered.filter((job) => jobMatchesQuery(job, normalizedQuery));
|
||||||
|
}
|
||||||
|
|
||||||
const sorted = [...filtered].sort((a, b) => compareJobs(a, b, sort));
|
const sorted = [...filtered].sort((a, b) => compareJobs(a, b, sort));
|
||||||
map.set(tab.id, sorted);
|
map.set(tab.id, sorted);
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, [jobsForTab, searchQuery, sort]);
|
}, [jobsForTab, searchQuery, sourceFilter, sort]);
|
||||||
|
|
||||||
const activeTabJobs = visibleJobsForTab.get(activeTab) ?? [];
|
const activeTabJobs = visibleJobsForTab.get(activeTab) ?? [];
|
||||||
const highlightedJob = useMemo(
|
const highlightedJob = useMemo(
|
||||||
@@ -303,6 +319,7 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
const activeResultsCount = visibleJobsForTab.get(activeTab)?.length ?? 0;
|
const activeResultsCount = visibleJobsForTab.get(activeTab)?.length ?? 0;
|
||||||
const hasActiveFilters =
|
const hasActiveFilters =
|
||||||
searchQuery.trim().length > 0 ||
|
searchQuery.trim().length > 0 ||
|
||||||
|
sourceFilter !== "all" ||
|
||||||
sort.key !== DEFAULT_SORT.key ||
|
sort.key !== DEFAULT_SORT.key ||
|
||||||
sort.direction !== DEFAULT_SORT.direction;
|
sort.direction !== DEFAULT_SORT.direction;
|
||||||
|
|
||||||
@@ -467,6 +484,33 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 self-start sm:self-auto">
|
<div className="flex flex-wrap items-center gap-2 self-start sm:self-auto">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="h-9">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
Source: {sourceFilter === "all" ? "All" : sourceLabels[sourceFilter]}
|
||||||
|
</span>
|
||||||
|
<span className="sm:hidden">Source</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuLabel>Filter by source</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
value={sourceFilter}
|
||||||
|
onValueChange={(value) => setSourceFilter(value as JobSource | "all")}
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioItem value="all">All Sources</DropdownMenuRadioItem>
|
||||||
|
{(Object.keys(sourceLabels) as JobSource[]).map((key) => (
|
||||||
|
<DropdownMenuRadioItem key={key} value={key}>
|
||||||
|
{sourceLabels[key]}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-9">
|
<Button variant="outline" size="sm" className="h-9">
|
||||||
@@ -519,6 +563,7 @@ export const JobList: React.FC<JobListProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
|
setSourceFilter("all");
|
||||||
setSort(DEFAULT_SORT);
|
setSort(DEFAULT_SORT);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user