Skip to content

Commit c460dfb

Browse files
feat: refactor job listing components; add JobsHeaderCard, JobsFiltersCard, and JobsTableCard for improved UI structure
1 parent 0548c50 commit c460dfb

4 files changed

Lines changed: 190 additions & 136 deletions

File tree

frontend/src/App.jsx

Lines changed: 37 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { 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

96
function 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
);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { RefreshCcw, Search } from "lucide-react";
2+
import { Button } from "@/components/ui/button";
3+
import { Card, CardContent } from "@/components/ui/card";
4+
import { Input } from "@/components/ui/input";
5+
6+
export function JobsFiltersCard({
7+
search,
8+
setSearch,
9+
keywordFilter,
10+
setKeywordFilter,
11+
keywords,
12+
selectedFile,
13+
setSelectedFile,
14+
files,
15+
loading,
16+
onRefresh,
17+
}) {
18+
return (
19+
<Card className="border-white/30 bg-card/85 backdrop-blur">
20+
<CardContent className="grid gap-3 pt-6 md:grid-cols-4">
21+
<div className="relative md:col-span-2">
22+
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
23+
<Input
24+
value={search}
25+
onChange={(event) => setSearch(event.target.value)}
26+
className="pl-9"
27+
placeholder="Buscar por titulo, empresa, local ou link"
28+
/>
29+
</div>
30+
31+
<select
32+
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
33+
value={keywordFilter}
34+
onChange={(event) => setKeywordFilter(event.target.value)}
35+
>
36+
<option value="all">Todas as palavras-chave</option>
37+
{keywords.map((keyword) => (
38+
<option key={keyword} value={keyword}>
39+
{keyword}
40+
</option>
41+
))}
42+
</select>
43+
44+
<div className="flex gap-2">
45+
<select
46+
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
47+
value={selectedFile}
48+
onChange={(event) => setSelectedFile(event.target.value)}
49+
>
50+
{files.map((file) => (
51+
<option key={file.file} value={file.file}>
52+
{file.file}
53+
</option>
54+
))}
55+
</select>
56+
<Button variant="outline" size="sm" onClick={onRefresh} disabled={loading}>
57+
<RefreshCcw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
58+
</Button>
59+
</div>
60+
</CardContent>
61+
</Card>
62+
);
63+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
2+
import { Badge } from "@/components/ui/badge";
3+
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4+
5+
export function JobsHeaderCard({ meta }) {
6+
return (
7+
<Card className="border-white/30 bg-card/85 backdrop-blur">
8+
<CardHeader className="gap-4 pb-4 md:flex-row md:items-center md:justify-between">
9+
<div>
10+
<CardTitle className="text-3xl">Painel de Vagas</CardTitle>
11+
<CardDescription>Leitura automatica dos arquivos XLSX gerados em output.</CardDescription>
12+
</div>
13+
<div className="flex flex-wrap items-center gap-2">
14+
<Badge variant="secondary" className="gap-1 text-xs">
15+
<FileSpreadsheet className="h-3.5 w-3.5" />
16+
{meta.file || "Sem arquivo"}
17+
</Badge>
18+
<Badge className="gap-1 text-xs">
19+
<BriefcaseBusiness className="h-3.5 w-3.5" />
20+
{meta.total} vagas
21+
</Badge>
22+
</div>
23+
</CardHeader>
24+
</Card>
25+
);
26+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
3+
4+
export function JobsTableCard({ meta, filteredJobs, jobs, loading, error, formatDate }) {
5+
return (
6+
<Card className="border-white/30 bg-card/90 backdrop-blur">
7+
<CardHeader>
8+
<CardTitle className="text-lg">Vagas Encontradas</CardTitle>
9+
<CardDescription>
10+
Atualizado em {formatDate(meta.modifiedAt)}. Mostrando {filteredJobs.length} de {jobs.length} vagas.
11+
</CardDescription>
12+
</CardHeader>
13+
<CardContent>
14+
{error ? (
15+
<div className="rounded-md border border-red-400/40 bg-red-500/10 p-4 text-sm text-red-900">{error}</div>
16+
) : null}
17+
18+
<Table>
19+
<TableHeader>
20+
<TableRow>
21+
<TableHead>Palavra-chave</TableHead>
22+
<TableHead>Titulo</TableHead>
23+
<TableHead>Empresa</TableHead>
24+
<TableHead>Local</TableHead>
25+
<TableHead>Link</TableHead>
26+
</TableRow>
27+
</TableHeader>
28+
<TableBody>
29+
{filteredJobs.map((job, index) => (
30+
<TableRow key={`${job.link || index}-${index}`}>
31+
<TableCell>{job.palavra || "-"}</TableCell>
32+
<TableCell className="font-medium">{job.titulo || "-"}</TableCell>
33+
<TableCell>{job.empresa || "-"}</TableCell>
34+
<TableCell>{job.local || "-"}</TableCell>
35+
<TableCell>
36+
{job.link ? (
37+
<a
38+
href={job.link}
39+
target="_blank"
40+
rel="noreferrer"
41+
className="text-primary underline-offset-4 hover:underline"
42+
>
43+
Abrir vaga
44+
</a>
45+
) : (
46+
"-"
47+
)}
48+
</TableCell>
49+
</TableRow>
50+
))}
51+
52+
{!loading && filteredJobs.length === 0 ? (
53+
<TableRow>
54+
<TableCell colSpan={5} className="py-8 text-center text-muted-foreground">
55+
Nenhuma vaga encontrada com os filtros atuais.
56+
</TableCell>
57+
</TableRow>
58+
) : null}
59+
</TableBody>
60+
</Table>
61+
</CardContent>
62+
</Card>
63+
);
64+
}

0 commit comments

Comments
 (0)