11import { useEffect , useMemo , useState } from "react" ;
2- import { BriefcaseBusiness , FileSpreadsheet , RefreshCcw , Search } from "lucide-react" ;
3- import { Badge } from "@/components/ui/badge" ;
4- import { Button } from "@/components/ui/button" ;
5- import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/card" ;
6- import { Input } from "@/components/ui/input" ;
7- import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table" ;
2+ import { JobsFiltersCard } from "@/components/JobsFiltersCard" ;
3+ import { JobsHeaderCard } from "@/components/JobsHeaderCard" ;
4+ import { JobsTableCard } from "@/components/JobsTableCard" ;
85
96function formatDate ( timestamp ) {
107 if ( ! timestamp ) {
@@ -49,16 +46,6 @@ function App() {
4946 } ) ;
5047 } , [ jobs , search , keywordFilter ] ) ;
5148
52- async function loadFiles ( ) {
53- const response = await fetch ( "/api/jobs/files" ) ;
54- const payload = await response . json ( ) ;
55- const foundFiles = Array . isArray ( payload . files ) ? payload . files : [ ] ;
56- setFiles ( foundFiles ) ;
57- if ( ! selectedFile && foundFiles [ 0 ] ?. file ) {
58- setSelectedFile ( foundFiles [ 0 ] . file ) ;
59- }
60- }
61-
6249 async function loadJobs ( fileName ) {
6350 setLoading ( true ) ;
6451 setError ( "" ) ;
@@ -88,7 +75,17 @@ function App() {
8875 }
8976
9077 useEffect ( ( ) => {
91- loadFiles ( ) . catch ( ( ) => {
78+ async function initializeFiles ( ) {
79+ const response = await fetch ( "/api/jobs/files" ) ;
80+ const payload = await response . json ( ) ;
81+ const foundFiles = Array . isArray ( payload . files ) ? payload . files : [ ] ;
82+ setFiles ( foundFiles ) ;
83+ if ( foundFiles [ 0 ] ?. file ) {
84+ setSelectedFile ( foundFiles [ 0 ] . file ) ;
85+ }
86+ }
87+
88+ initializeFiles ( ) . catch ( ( ) => {
9289 setError ( "Nao foi possivel listar arquivos .xlsx da pasta output." ) ;
9390 } ) ;
9491 } , [ ] ) ;
@@ -104,125 +101,29 @@ function App() {
104101 < div className = "pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_20%_15%,rgba(236,195,117,0.35),transparent_30%),radial-gradient(circle_at_80%_10%,rgba(92,151,191,0.28),transparent_35%),radial-gradient(circle_at_50%_95%,rgba(201,120,99,0.22),transparent_40%)]" />
105102
106103 < section className = "mx-auto flex w-full max-w-7xl flex-col gap-6" >
107- < Card className = "border-white/30 bg-card/85 backdrop-blur" >
108- < CardHeader className = "gap-4 pb-4 md:flex-row md:items-center md:justify-between" >
109- < div >
110- < CardTitle className = "text-3xl" > Painel de Vagas</ CardTitle >
111- < CardDescription >
112- Leitura automatica dos arquivos XLSX gerados em output.
113- </ CardDescription >
114- </ div >
115- < div className = "flex flex-wrap items-center gap-2" >
116- < Badge variant = "secondary" className = "gap-1 text-xs" >
117- < FileSpreadsheet className = "h-3.5 w-3.5" />
118- { meta . file || "Sem arquivo" }
119- </ Badge >
120- < Badge className = "gap-1 text-xs" >
121- < BriefcaseBusiness className = "h-3.5 w-3.5" />
122- { meta . total } vagas
123- </ Badge >
124- </ div >
125- </ CardHeader >
126- < CardContent className = "grid gap-3 md:grid-cols-4" >
127- < div className = "relative md:col-span-2" >
128- < Search className = "pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
129- < Input
130- value = { search }
131- onChange = { ( event ) => setSearch ( event . target . value ) }
132- className = "pl-9"
133- placeholder = "Buscar por titulo, empresa, local ou link"
134- />
135- </ div >
136-
137- < select
138- className = "h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
139- value = { keywordFilter }
140- onChange = { ( event ) => setKeywordFilter ( event . target . value ) }
141- >
142- < option value = "all" > Todas as palavras-chave</ option >
143- { keywords . map ( ( keyword ) => (
144- < option key = { keyword } value = { keyword } >
145- { keyword }
146- </ option >
147- ) ) }
148- </ select >
149-
150- < div className = "flex gap-2" >
151- < select
152- className = "h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
153- value = { selectedFile }
154- onChange = { ( event ) => setSelectedFile ( event . target . value ) }
155- >
156- { files . map ( ( file ) => (
157- < option key = { file . file } value = { file . file } >
158- { file . file }
159- </ option >
160- ) ) }
161- </ select >
162- < Button variant = "outline" size = "sm" onClick = { ( ) => loadJobs ( selectedFile ) } disabled = { loading } >
163- < RefreshCcw className = { `h-4 w-4 ${ loading ? "animate-spin" : "" } ` } />
164- </ Button >
165- </ div >
166- </ CardContent >
167- </ Card >
168-
169- < Card className = "border-white/30 bg-card/90 backdrop-blur" >
170- < CardHeader >
171- < CardTitle className = "text-lg" > Vagas Encontradas</ CardTitle >
172- < CardDescription >
173- Atualizado em { formatDate ( meta . modifiedAt ) } . Mostrando { filteredJobs . length } de { jobs . length } vagas.
174- </ CardDescription >
175- </ CardHeader >
176- < CardContent >
177- { error ? (
178- < div className = "rounded-md border border-red-400/40 bg-red-500/10 p-4 text-sm text-red-900" > { error } </ div >
179- ) : null }
180-
181- < Table >
182- < TableHeader >
183- < TableRow >
184- < TableHead > Palavra-chave</ TableHead >
185- < TableHead > Titulo</ TableHead >
186- < TableHead > Empresa</ TableHead >
187- < TableHead > Local</ TableHead >
188- < TableHead > Link</ TableHead >
189- </ TableRow >
190- </ TableHeader >
191- < TableBody >
192- { filteredJobs . map ( ( job , index ) => (
193- < TableRow key = { `${ job . link || index } -${ index } ` } >
194- < TableCell > { job . palavra || "-" } </ TableCell >
195- < TableCell className = "font-medium" > { job . titulo || "-" } </ TableCell >
196- < TableCell > { job . empresa || "-" } </ TableCell >
197- < TableCell > { job . local || "-" } </ TableCell >
198- < TableCell >
199- { job . link ? (
200- < a
201- href = { job . link }
202- target = "_blank"
203- rel = "noreferrer"
204- className = "text-primary underline-offset-4 hover:underline"
205- >
206- Abrir vaga
207- </ a >
208- ) : (
209- "-"
210- ) }
211- </ TableCell >
212- </ TableRow >
213- ) ) }
214-
215- { ! loading && filteredJobs . length === 0 ? (
216- < TableRow >
217- < TableCell colSpan = { 5 } className = "py-8 text-center text-muted-foreground" >
218- Nenhuma vaga encontrada com os filtros atuais.
219- </ TableCell >
220- </ TableRow >
221- ) : null }
222- </ TableBody >
223- </ Table >
224- </ CardContent >
225- </ Card >
104+ < JobsHeaderCard meta = { meta } />
105+
106+ < JobsFiltersCard
107+ search = { search }
108+ setSearch = { setSearch }
109+ keywordFilter = { keywordFilter }
110+ setKeywordFilter = { setKeywordFilter }
111+ keywords = { keywords }
112+ selectedFile = { selectedFile }
113+ setSelectedFile = { setSelectedFile }
114+ files = { files }
115+ loading = { loading }
116+ onRefresh = { ( ) => loadJobs ( selectedFile ) }
117+ />
118+
119+ < JobsTableCard
120+ meta = { meta }
121+ filteredJobs = { filteredJobs }
122+ jobs = { jobs }
123+ loading = { loading }
124+ error = { error }
125+ formatDate = { formatDate }
126+ />
226127 </ section >
227128 </ main >
228129 ) ;
0 commit comments