@@ -2,17 +2,15 @@ import { Badge } from "@/components/ui/badge";
22import { Card , CardContent } from "@/components/ui/card" ;
33import { Input } from "@/components/ui/input" ;
44import 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" ;
57import { 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" ;
108import { Button } from "./ui/button" ;
119interface 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+
2432export 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