Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,857 changes: 0 additions & 3,857 deletions frontend/package-lock.json

This file was deleted.

36 changes: 15 additions & 21 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { JobsFiltersCard } from "@/components/JobsFiltersCard";
import { JobsHeaderCard } from "@/components/JobsHeaderCard";
import { JobsTableCard } from "@/components/JobsTableCard";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/ui/theme-toggle";
import { useJobsData } from "@/hooks/useJobsData";
import { useJobsFiltering } from "@/hooks/useJobsFiltering";
import { useJobsPagination } from "@/hooks/useJobsPagination";
import { useTheme } from "@/hooks/useTheme";
import type { JobsMeta } from "@/types/jobs";
import { RefreshCcw } from "lucide-react";
import { useCallback, type SetStateAction } from "react";
Expand All @@ -19,9 +17,7 @@ function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
}

function App() {
const { resolvedTheme, toggleTheme } = useTheme();

const { files, selectedFile, setSelectedFile, jobs, meta, loading, scraping, error, loadJobs, triggerScraper } =
const { files, selectedFile, setSelectedFile, jobs, meta, loading, scraping, error, triggerScraper } =
useJobsData();

const { search, setSearch, keywordFilter, setKeywordFilter, keywords, filteredJobs } = useJobsFiltering(jobs);
Expand Down Expand Up @@ -68,22 +64,11 @@ function App() {
}, [triggerScraper]);

return (
<main className="relative min-h-screen overflow-hidden bg-background px-4 py-8 transition-colors duration-300 md:px-8">
<main className="relative min-h-screen overflow-hidden bg-background px-4 transition-colors duration-300 md:px-8">
<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%)] dark:bg-[radial-gradient(circle_at_20%_15%,rgba(240,180,95,0.18),transparent_30%),radial-gradient(circle_at_80%_10%,rgba(92,151,191,0.18),transparent_35%),radial-gradient(circle_at_50%_95%,rgba(201,120,99,0.15),transparent_40%)]" />

<section className="mx-auto flex w-full max-w-7xl flex-col gap-6">
<JobsHeaderCard
meta={meta}
actions={
<>
<Button onClick={handleScraper} disabled={loading || scraping}>
<RefreshCcw className={`h-4 w-4 ${scraping ? "animate-spin" : ""}`} />
{scraping ? "Buscando vagas..." : "Buscar vagas"}
</Button>
<ThemeToggle theme={resolvedTheme} onToggle={toggleTheme} />
</>
}
/>
<section className="mx-auto flex w-full flex-col gap-6">
<JobsHeaderCard />

<JobsFiltersCard
Comment on lines +70 to 73

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App now renders JobsHeaderCard without the previous title text/props and moved the primary action into JobsFiltersCard. The existing frontend/tests/unit/pages/App.test.tsx assertions/mocks (e.g., expecting "Painel de Vagas" and mocking useJobsData without scraping/triggerScraper) will need updating to match this new composition, otherwise CI will fail.

Copilot uses AI. Check for mistakes.
search={search}
Expand All @@ -94,8 +79,17 @@ function App() {
selectedFile={selectedFile}
setSelectedFile={handleSelectedFileChange}
files={files}
loading={loading || scraping}
onRefresh={() => loadJobs(selectedFile)}
meta={meta}
actions={
<>
<Button onClick={handleScraper} disabled={loading || scraping}>
<RefreshCcw
className={`h-4 w-4 ${scraping ? "animate-spin" : ""}`}
/>
{scraping ? "Buscando vagas..." : "Buscar vagas"}
</Button>
</>
}
/>

<JobsTableCard
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/assets/logo-painel-vagas.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 56 additions & 40 deletions frontend/src/components/JobsFiltersCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import type { JobFile } from "@/types/jobs";
import { RefreshCcw, Search } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
import type { JobFile, JobsMeta } from "@/types/jobs";
import { Search } from "lucide-react";
import type { Dispatch, ReactNode, SetStateAction } from "react";

import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
Comment on lines +5 to +8

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two separate imports from lucide-react in this file. Consolidating them into a single import keeps imports cleaner and avoids accidental duplication as the icon list grows.

Suggested change
import { Search } from "lucide-react";
import type { Dispatch, ReactNode, SetStateAction } from "react";
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
import { Search, BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
import type { Dispatch, ReactNode, SetStateAction } from "react";

Copilot uses AI. Check for mistakes.
interface JobsFiltersCardProps {
search: string;
setSearch: Dispatch<SetStateAction<string>>;
Expand All @@ -14,8 +15,8 @@ interface JobsFiltersCardProps {
selectedFile: string;
setSelectedFile: Dispatch<SetStateAction<string>>;
files: JobFile[];
loading: boolean;
onRefresh: () => void;
meta: JobsMeta;
actions?: ReactNode;
}
Comment on lines 9 to 20

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JobsFiltersCard props were changed (removed loading/onRefresh, added required meta and optional actions). The existing unit tests for this component still pass the old props, so they will fail to compile/run. Update the tests to pass meta and assert the new UI/behavior.

Copilot uses AI. Check for mistakes.

export function JobsFiltersCard({
Expand All @@ -27,50 +28,65 @@ export function JobsFiltersCard({
selectedFile,
setSelectedFile,
files,
loading,
onRefresh,
meta,
actions,
}: JobsFiltersCardProps) {
return (
<Card className="border-border/70 bg-card/85 backdrop-blur dark:bg-card/95">
<CardContent className="grid gap-3 pt-6 md:grid-cols-4">
<div className="relative md:col-span-2">
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
className="pl-9"
placeholder="Buscar por titulo, empresa, local ou link"
/>
<CardContent className="flex flex-col gap-3 pt-6">
{/* Campo de busca */}
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
className="pl-9 w-full"
placeholder="Buscar por titulo, empresa, local ou link"
/>
</div>
<div className="flex items-center gap-2">{actions}</div>
</div>

<select
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
value={keywordFilter}
onChange={(event) => setKeywordFilter(event.target.value)}
>
<option value="all">Todas as palavras-chave</option>
{keywords.map((keyword) => (
<option key={keyword} value={keyword}>
{keyword}
</option>
))}
</select>

<div className="flex gap-2">
{/* Seção de filtros */}
<div className="flex items-center gap-2 flex">
<select
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm"
value={selectedFile}
onChange={(event) => setSelectedFile(event.target.value)}
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"
value={keywordFilter}
onChange={(event) => setKeywordFilter(event.target.value)}
>
{files.map((file) => (
<option key={file.file} value={file.file}>
{file.file}
<option value="all">Todas as palavras-chave</option>
{keywords.map((keyword) => (
<option key={keyword} value={keyword}>
{keyword}
</option>
))}
</select>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={loading}>
<RefreshCcw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
</Button>

<div>
<select
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"
value={selectedFile}
onChange={(event) => setSelectedFile(event.target.value)}
>
{files.map((file) => (
<option key={file.file} value={file.file}>
{file.file}
</option>
))}
</select>
</div>

<div className="flex items-center gap-2">
<Badge variant="secondary" className="gap-1 text-xs">
<FileSpreadsheet className="h-3.5 w-3.5" />
{meta.file || "Sem arquivo"}
</Badge>
<Badge className="gap-1 text-xs">
<BriefcaseBusiness className="h-3.5 w-3.5" />
{meta.total} vagas
</Badge>
</div>
</div>
</CardContent>
</Card>
Expand Down
51 changes: 22 additions & 29 deletions frontend/src/components/JobsHeaderCard.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { JobsMeta } from "@/types/jobs";
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
import type { ReactNode } from "react";
import {
CardDescription,
CardTitle,
} from "@/components/ui/card";
import Logo from "../assets/logo-painel-vagas.svg";
import { ThemeToggle } from "@/components/ui/theme-toggle";
import { useTheme } from "@/hooks/useTheme";

interface JobsHeaderCardProps {
meta: JobsMeta;
actions?: ReactNode;
}

export function JobsHeaderCard({ meta, actions }: JobsHeaderCardProps) {
export function JobsHeaderCard() {

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JobsHeaderCard no longer accepts meta/actions props, but the repository still has unit tests/usages that render it with those props (e.g. frontend/tests/unit/components/JobsHeaderCard.test.tsx). Update the tests/usages (or keep the props for backward compatibility) to avoid CI failures.

Suggested change
export function JobsHeaderCard() {
export function JobsHeaderCard(_props: JobsHeaderCardProps) {

Copilot uses AI. Check for mistakes.
const { resolvedTheme, toggleTheme } = useTheme();
return (
<Card className="border-border/70 bg-card/85 backdrop-blur dark:bg-card/95">
<CardHeader className="gap-4 pb-4 md:flex-row md:items-center md:justify-between">
<div>
<CardTitle className="text-3xl">Painel de Vagas</CardTitle>
<CardDescription>Leitura automatica dos arquivos XLSX gerados em output.</CardDescription>
</div>
<div className="flex flex-wrap items-center gap-2">
{actions}
<Badge variant="secondary" className="gap-1 text-xs">
<FileSpreadsheet className="h-3.5 w-3.5" />
{meta.file || "Sem arquivo"}
</Badge>
<Badge className="gap-1 text-xs">
<BriefcaseBusiness className="h-3.5 w-3.5" />
{meta.total} vagas
</Badge>
<>
<div className="w-screen bg-[#004726] dark:bg-[#003318] p-6 -mx-8 flex items-end">

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header background uses hard-coded hex colors (bg-[#004726] / dark:bg-[#003318]), bypassing the design tokens defined via CSS variables/Tailwind theme. Consider using token-based classes (e.g., bg-primary / bg-background / bg-card) or introducing a dedicated CSS variable so the dark-mode palette stays centralized and adjustable.

Suggested change
<div className="w-screen bg-[#004726] dark:bg-[#003318] p-6 -mx-8 flex items-end">
<div className="w-screen bg-primary p-6 -mx-8 flex items-end">

Copilot uses AI. Check for mistakes.
<CardTitle className="text-3xl text-white">
<img src={Logo} alt="Painel de Vagas" />
</CardTitle>
<CardDescription className="text-white">
Leitura automatica dos arquivos XLSX gerados em output.
</CardDescription>
<div className="ml-auto self-start mr-8">
<ThemeToggle theme={resolvedTheme} onToggle={toggleTheme} />
</div>
</CardHeader>
</Card>
</div>

</>
);
}
14 changes: 7 additions & 7 deletions frontend/src/components/JobsTableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function JobsTableCard({
onPageSizeChange,
}: JobsTableCardProps) {
return (
<Card className="border-border/70 bg-card/90 backdrop-blur dark:bg-card/95">
<Card className="border-border/70 bg-card/90 backdrop-blur dark:bg-card/95 mb-4">
<CardHeader>
<CardTitle className="text-lg">Vagas Encontradas</CardTitle>
<CardDescription>
Expand All @@ -44,7 +44,7 @@ export function JobsTableCard({
</CardHeader>
<CardContent>
{error ? (
<div className="rounded-md border border-red-500/40 bg-red-500/10 p-4 text-sm text-red-700 dark:text-red-300">
<div className="rounded-md border border-red-500/40 bg-red-500/10 p-4 text-sm text-red-700 dark:text-red-300 mb-4">
{error}
</div>
) : null}
Expand Down Expand Up @@ -91,36 +91,36 @@ export function JobsTableCard({
<Table>
<TableHeader>
<TableRow>
<TableHead>Palavra-chave</TableHead>
<TableHead>Titulo</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Fonte</TableHead>
<TableHead>Local</TableHead>
<TableHead>Link</TableHead>
<TableHead>Palavra-chave</TableHead>
<TableHead>Fonte</TableHead>
</TableRow>
Comment on lines 94 to 100

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This table header now defines 6 columns, but the empty-state row further down still uses a colSpan sized for the previous column count. That will break alignment when there are no results. Update the empty-state TableCell colSpan to match the new 6-column layout.

Copilot uses AI. Check for mistakes.
</TableHeader>
<TableBody>
{paginatedJobs.map((job, index) => (
<TableRow key={`${job.link || `${job.titulo || "vaga"}-${index}`}-${index}`}>
<TableCell>{job.palavra || "-"}</TableCell>
<TableCell className="font-medium">{job.titulo || "-"}</TableCell>
<TableCell>{job.empresa || "-"}</TableCell>
<TableCell>{job.source || "-"}</TableCell>
<TableCell>{job.local || "-"}</TableCell>
<TableCell>
{job.link ? (
<a
href={job.link}
target="_blank"
rel="noreferrer"
className="text-primary underline-offset-4 hover:underline"
className="text-primary underline-offset-4 hover:underline dark:text-[#14AE5C]"

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link color adds a hard-coded dark:text-[#14AE5C], which bypasses the theme tokens defined via CSS variables and makes future palette changes harder. Prefer adjusting the dark theme --primary (or introducing a token for link color) so text-primary works consistently in both themes.

Suggested change
className="text-primary underline-offset-4 hover:underline dark:text-[#14AE5C]"
className="text-primary underline-offset-4 hover:underline"

Copilot uses AI. Check for mistakes.
>
Abrir vaga
</a>
) : (
"-"
)}
</TableCell>
<TableCell>{job.palavra || "-"}</TableCell>
<TableCell>{job.source || "-"}</TableCell>
</TableRow>
))}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:opacity-90",
default: "bg-primary text-primary-foreground hover:opacity-75",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
},
size: {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ui/theme-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
onClick={onToggle}
aria-label={isDark ? "Ativar tema claro" : "Ativar tema escuro"}
title={isDark ? "Ativar tema claro" : "Ativar tema escuro"}
className="gap-2"
className="gap-2 hover:bg-accent/40 transition-all duration-300 rounded-lg"
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
<span className="hidden sm:inline">{isDark ? "Tema claro" : "Tema escuro"}</span>
Expand Down
23 changes: 12 additions & 11 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@
@tailwind components;
@tailwind utilities;


@layer base {
:root {
--background: 33 53% 96%;
--background: 33 53% 100%;
--foreground: 210 30% 15%;
--card: 0 0% 100%;
--card-foreground: 210 30% 15%;
--primary: 198 63% 36%;
--primary: 140 70% 20%;
--primary-foreground: 0 0% 100%;
--secondary: 38 52% 86%;
--secondary-foreground: 210 30% 15%;
--muted: 36 35% 91%;
--muted-foreground: 210 15% 40%;
--accent: 24 67% 81%;
--accent-foreground: 210 35% 17%;
--border: 30 28% 82%;
--accent: 140 75% 38%;
--accent-foreground: 0 0% 100%;
--border: 0 0% 82%;
--input: 30 28% 82%;
--ring: 198 63% 36%;
--ring: 140 75% 38%;
--radius: 0.8rem;
color-scheme: light;
}
Expand All @@ -30,17 +31,17 @@
--foreground: 210 30% 95%;
--card: 216 26% 14%;
--card-foreground: 210 30% 95%;
--primary: 198 78% 63%;
--primary-foreground: 218 35% 12%;
--primary: 140 70% 20%;
/* --primary-foreground: 218 35% 12%; */
--secondary: 216 20% 23%;
--secondary-foreground: 210 20% 94%;
--muted: 216 20% 19%;
--muted-foreground: 215 20% 72%;
--accent: 25 75% 58%;
--accent: 140 50% 45%;
--accent-foreground: 0 0% 100%;
--border: 216 16% 30%;
--input: 216 16% 30%;
--ring: 198 78% 63%;
--ring: 140 75% 38%;
color-scheme: dark;
}

Expand All @@ -49,6 +50,6 @@
}

body {
@apply bg-background font-sans text-foreground antialiased;
@apply m-0 p-0 bg-background font-sans text-foreground antialiased;
}
}
Loading
Loading