Skip to content

Commit 89c1cd5

Browse files
authored
Develop (#56)
2 parents 54719d4 + 4841dc2 commit 89c1cd5

12 files changed

Lines changed: 379 additions & 94 deletions

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"lucide-react": "^0.577.0",
1919
"react": "^19.2.4",
2020
"react-dom": "^19.2.4",
21+
"react-icons": "^5.6.0",
2122
"rolldown": "^1.0.0-rc.10",
2223
"tailwind-merge": "^3.5.0"
2324
},

frontend/src/App.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { useJobsData } from "@/hooks/useJobsData";
66
import { useJobsFiltering } from "@/hooks/useJobsFiltering";
77
import { useJobsPagination } from "@/hooks/useJobsPagination";
88
import type { JobsMeta } from "@/types/jobs";
9-
import { RefreshCcw } from "lucide-react";
109
import { useCallback, type SetStateAction } from "react";
10+
import { FiRefreshCw } from "react-icons/fi";
1111

1212
function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
1313
if (!timestamp) {
@@ -36,7 +36,7 @@ function App() {
3636
);
3737

3838
const handleKeywordFilterChange = useCallback(
39-
(value: SetStateAction<string>) => {
39+
(value: SetStateAction<string[]>) => {
4040
setKeywordFilter((previous) => (typeof value === "function" ? value(previous) : value));
4141
resetPagination();
4242
},
@@ -82,10 +82,12 @@ function App() {
8282
meta={meta}
8383
actions={
8484
<>
85-
<Button onClick={handleScraper} disabled={scraping}>
86-
<RefreshCcw
87-
className={`h-4 w-4 ${scraping ? "animate-spin" : ""}`}
88-
/>
85+
<Button
86+
onClick={handleScraper}
87+
disabled={scraping}
88+
className="h-12 rounded-2xl bg-[#0c6b35] px-5 text-white shadow-sm hover:bg-[#0a5b2d]"
89+
>
90+
<FiRefreshCw className={`h-4 w-4 ${scraping ? "animate-spin" : ""}`} />
8991
{scraping ? "Buscando vagas..." : "Buscar vagas"}
9092
</Button>
9193
</>

frontend/src/components/JobsFiltersCard.tsx

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ import { Badge } from "@/components/ui/badge";
22
import { Card, CardContent } from "@/components/ui/card";
33
import { Input } from "@/components/ui/input";
44
import type { JobFile, JobsMeta } from "@/types/jobs";
5+
import { useMemo, useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
6+
import { FiBriefcase, FiCheck, FiFileText, FiFilter, FiSearch, FiSlash, FiTag } from "react-icons/fi";
57
import { KeywordsModal } from "./KeywordsModal";
6-
import { Search } from "lucide-react";
7-
import { useState, Dispatch, ReactNode, SetStateAction } from "react";
8-
9-
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
108
import { Button } from "./ui/button";
119
interface JobsFiltersCardProps {
1210
search: string;
1311
setSearch: Dispatch<SetStateAction<string>>;
14-
keywordFilter: string;
15-
setKeywordFilter: Dispatch<SetStateAction<string>>;
12+
keywordFilter: string[];
13+
setKeywordFilter: Dispatch<SetStateAction<string[]>>;
1614
keywords: string[];
1715
selectedFile: string;
1816
setSelectedFile: Dispatch<SetStateAction<string>>;
@@ -21,6 +19,16 @@ interface JobsFiltersCardProps {
2119
actions?: ReactNode;
2220
}
2321

22+
function getSelectedFilters(search: string, keywordFilter: string[]) {
23+
const searchTerms = search
24+
.split(/[,;/]+/)
25+
.map((item) => item.trim())
26+
.filter(Boolean)
27+
.slice(0, 5);
28+
29+
return Array.from(new Set([...keywordFilter.filter(Boolean), ...searchTerms]));
30+
}
31+
2432
export function JobsFiltersCard({
2533
search,
2634
setSearch,
@@ -35,42 +43,77 @@ export function JobsFiltersCard({
3543
}: JobsFiltersCardProps) {
3644
const [seeKeywordsModal, setSeeKeywordsModal] = useState(false);
3745

46+
const selectedFilters = useMemo(() => getSelectedFilters(search, keywordFilter), [search, keywordFilter]);
47+
48+
const handleManageFilters = () => {
49+
setSeeKeywordsModal(true);
50+
};
51+
52+
const handleKeywordSelect = (value: string) => {
53+
if (!value) {
54+
return;
55+
}
56+
57+
if (value === "all") {
58+
setKeywordFilter([]);
59+
return;
60+
}
61+
62+
setKeywordFilter((current) => (current.includes(value) ? current : [...current, value]));
63+
};
64+
65+
const handleRemoveFilter = (filterToRemove: string) => {
66+
setKeywordFilter((current) => current.filter((item) => item !== filterToRemove));
67+
setSearch((current) =>
68+
current
69+
.split(/[,;/]+/)
70+
.map((item) => item.trim())
71+
.filter((item) => item && item !== filterToRemove)
72+
.join(", "),
73+
);
74+
};
75+
76+
const handleClearFilters = () => {
77+
setSearch("");
78+
setKeywordFilter([]);
79+
};
80+
3881
return (
3982
<>
40-
<Card className="border-border/70 bg-card/85 backdrop-blur dark:bg-card/95">
41-
<CardContent className="flex flex-col gap-3 pt-6">
42-
{/* Campo de busca */}
43-
<div className="flex items-center gap-3">
83+
<Card className="rounded-[24px] border border-slate-200 bg-white/95 shadow-[0_18px_45px_rgba(15,23,42,0.08)] backdrop-blur dark:border-[#2b3f58] dark:bg-[#131d31]/95 dark:shadow-[0_18px_45px_rgba(3,8,20,0.25)]">
84+
<CardContent className="flex flex-col gap-4 p-5 md:p-6">
85+
<div className="flex flex-col gap-3 xl:flex-row xl:items-center">
4486
<div className="relative flex-1">
45-
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
87+
<FiSearch className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500 dark:text-slate-400" />
4688
<Input
4789
value={search}
4890
onChange={(event) => setSearch(event.target.value)}
49-
className="pl-9 w-full"
50-
placeholder="Buscar por titulo, empresa, local ou link"
91+
className="h-14 w-full rounded-2xl border border-slate-300 bg-white pl-11 text-base text-slate-900 placeholder:text-slate-500 focus-visible:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#091224] dark:text-slate-100 dark:placeholder:text-slate-400"
92+
placeholder="Buscar por título, empresa, local ou link"
5193
/>
5294
</div>
5395
<div className="flex items-center gap-2">{actions}</div>
5496
</div>
5597

56-
{/* Seção de filtros */}
57-
<div className="flex items-center gap-2">
58-
<select
59-
className="h-10 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
60-
value={keywordFilter}
61-
onChange={(event) => setKeywordFilter(event.target.value)}
62-
>
63-
<option value="all">Todas as palavras-chave</option>
64-
{keywords.map((keyword) => (
65-
<option key={keyword} value={keyword}>
66-
{keyword}
67-
</option>
68-
))}
69-
</select>
70-
71-
<div>
98+
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
99+
<div className="flex flex-1 flex-wrap items-center gap-2">
100+
<select
101+
aria-label="Filtrar por palavra-chave"
102+
className="h-12 min-w-[220px] rounded-2xl border border-slate-300 bg-white px-3 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#0b1527] dark:text-slate-100"
103+
value={keywordFilter[keywordFilter.length - 1] ?? "all"}
104+
onChange={(event) => handleKeywordSelect(event.target.value)}
105+
>
106+
<option value="all">Todas as palavras chaves</option>
107+
{keywords.map((keyword) => (
108+
<option key={keyword} value={keyword}>
109+
{keyword}
110+
</option>
111+
))}
112+
</select>
113+
72114
<select
73-
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
115+
aria-label="Selecionar arquivo"
116+
className="h-12 min-w-[180px] rounded-2xl border border-slate-300 bg-white px-3 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#0b1527] dark:text-slate-100"
74117
value={selectedFile}
75118
onChange={(event) => setSelectedFile(event.target.value)}
76119
>
@@ -80,21 +123,65 @@ export function JobsFiltersCard({
80123
</option>
81124
))}
82125
</select>
126+
127+
<Badge variant="secondary" className="gap-1.5 rounded-full border border-slate-300 bg-slate-100 px-3 py-1 text-xs text-slate-700 dark:border-[#35506f] dark:bg-[#24324c] dark:text-slate-100">
128+
<FiFileText className="h-3.5 w-3.5" />
129+
{meta.file || "Sem arquivo"}
130+
</Badge>
131+
132+
<Badge className="gap-1.5 rounded-full bg-[#0c6b35] px-3 py-1 text-xs text-white">
133+
<FiBriefcase className="h-3.5 w-3.5" />
134+
{meta.total} vagas
135+
</Badge>
136+
</div>
137+
138+
<div className="flex items-center gap-2 self-start xl:self-auto">
139+
<Button
140+
type="button"
141+
className="h-10 rounded-xl bg-[#0c6b35] px-4 text-white hover:bg-[#0a5b2d]"
142+
onClick={handleManageFilters}
143+
>
144+
<FiFilter className="h-4 w-4" />
145+
Filtrar
146+
</Button>
147+
<Button
148+
type="button"
149+
variant="outline"
150+
aria-label="Limpar filtros"
151+
className="h-10 w-10 rounded-md border-[#0c6b35] bg-[#0c6b35] p-0 text-white shadow-sm hover:bg-[#0a5b2d]"
152+
onClick={handleClearFilters}
153+
>
154+
<FiSlash className="h-4 w-4" />
155+
</Button>
83156
</div>
157+
</div>
84158

85-
<div className="flex justify-between w-full">
86-
<div className="flex items-center gap-2">
87-
<Badge variant="secondary" className="gap-1 text-xs">
88-
<FileSpreadsheet className="h-3.5 w-3.5" />
89-
{meta.file || "Sem arquivo"}
90-
</Badge>
91-
<Badge className="gap-1 text-xs">
92-
<BriefcaseBusiness className="h-3.5 w-3.5" />
93-
{meta.total} vagas
94-
</Badge>
95-
</div>
159+
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-3 dark:border-[#2b3f58] dark:bg-[#12203a]/45">
160+
<div className="flex flex-wrap items-center gap-2">
161+
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">Filtros selecionados</span>
162+
{selectedFilters.length > 0 ? (
163+
selectedFilters.map((filter) => (
164+
<Badge
165+
key={filter}
166+
className="gap-1.5 rounded-full border border-[#14AE5C]/30 bg-[#14AE5C]/12 px-2 py-1 text-xs font-semibold text-[#0c6b35] dark:text-[#8df0af]"
167+
>
168+
<FiCheck className="h-3 w-3" />
169+
<FiTag className="h-3 w-3" />
170+
<span>{filter}</span>
171+
<button
172+
type="button"
173+
aria-label={`Remover filtro ${filter}`}
174+
className="ml-1 rounded-full p-0.5 text-[#0c6b35] hover:bg-[#14AE5C]/20 dark:text-[#8df0af]"
175+
onClick={() => handleRemoveFilter(filter)}
176+
>
177+
<FiSlash className="h-3 w-3" />
178+
</button>
179+
</Badge>
180+
))
181+
) : (
182+
<span className="text-sm text-slate-500 dark:text-slate-400">Use o botão Filtrar para adicionar ou remover palavras-chave.</span>
183+
)}
96184
</div>
97-
<Button variant={"outline"} onClick={() => setSeeKeywordsModal(true)}>Keywords</Button>
98185
</div>
99186
</CardContent>
100187
</Card>

0 commit comments

Comments
 (0)