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
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 reduces noise and avoids accidental duplicate specifiers during future edits.

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;
}

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}
Comment on lines +52 to +55

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 className string contains redundant/typo spacing ("h-10 rounded-md") and an extra standalone flex token on the parent ("... gap-2 flex"). Cleaning these up makes the styling easier to read and avoids accidental class collisions later.

Copilot uses AI. Check for mistakes.
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() {
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">
<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>

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 is trailing whitespace / an effectively blank line here inside the component. Please remove it to keep the file clean and avoid noisy diffs in future formatting changes.

Suggested change

Copilot uses AI. Check for mistakes.
</>
);
}
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>
</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]"
>
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