Skip to content

Commit a335f1d

Browse files
authored
Merge pull request #8 from Benevanio/develop
feat: implement dark mode support with theme toggle and CSS adjustments
2 parents 964f57c + 743454f commit a335f1d

10 files changed

Lines changed: 157 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ Agora o Docker foi separado em 3 servicos:
145145
### Subir dashboard + API (sem scraping automatico)
146146

147147
```bash
148-
docker compose up --build
148+
docker compose up --build -D
149149
```
150150

151151
### Executar scraping manualmente

frontend/index.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<script>
8+
(function () {
9+
const storageKey = "jobs-theme-preference";
10+
const root = document.documentElement;
11+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
12+
const storedPreference = localStorage.getItem(storageKey);
13+
const themePreference = storedPreference === "light" || storedPreference === "dark" || storedPreference === "system"
14+
? storedPreference
15+
: "system";
16+
const resolvedTheme = themePreference === "system" ? (prefersDark ? "dark" : "light") : themePreference;
17+
18+
root.classList.toggle("dark", resolvedTheme === "dark");
19+
root.setAttribute("data-theme", resolvedTheme);
20+
})();
21+
</script>
722
<title>frontend</title>
823
</head>
924
<body>
1025
<div id="root"></div>
11-
<script type="module" src="/src/main.jsx"></script>
26+
<script type="module" src="/src/main.tsx"></script>
1227
</body>
1328
</html>

frontend/src/App.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { useCallback, type SetStateAction } from "react";
21
import { JobsFiltersCard } from "@/components/JobsFiltersCard";
32
import { JobsHeaderCard } from "@/components/JobsHeaderCard";
43
import { JobsTableCard } from "@/components/JobsTableCard";
4+
import { ThemeToggle } from "@/components/ui/theme-toggle";
55
import { useJobsData } from "@/hooks/useJobsData";
66
import { useJobsFiltering } from "@/hooks/useJobsFiltering";
77
import { useJobsPagination } from "@/hooks/useJobsPagination";
8+
import { useTheme } from "@/hooks/useTheme";
89
import type { JobsMeta } from "@/types/jobs";
10+
import { useCallback, type SetStateAction } from "react";
911

1012
function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
1113
if (!timestamp) {
@@ -15,6 +17,8 @@ function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
1517
}
1618

1719
function App() {
20+
const { resolvedTheme, toggleTheme } = useTheme();
21+
1822
const { files, selectedFile, setSelectedFile, jobs, meta, loading, error, loadJobs } = useJobsData();
1923

2024
const { search, setSearch, keywordFilter, setKeywordFilter, keywords, filteredJobs } = useJobsFiltering(jobs);
@@ -57,11 +61,11 @@ function App() {
5761
);
5862

5963
return (
60-
<main className="relative min-h-screen overflow-hidden bg-background px-4 py-8 md:px-8">
61-
<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%)]" />
64+
<main className="relative min-h-screen overflow-hidden bg-background px-4 py-8 transition-colors duration-300 md:px-8">
65+
<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%)]" />
6266

6367
<section className="mx-auto flex w-full max-w-7xl flex-col gap-6">
64-
<JobsHeaderCard meta={meta} />
68+
<JobsHeaderCard meta={meta} actions={<ThemeToggle theme={resolvedTheme} onToggle={toggleTheme} />} />
6569

6670
<JobsFiltersCard
6771
search={search}

frontend/src/components/JobsFiltersCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { RefreshCcw, Search } from "lucide-react";
2-
import type { Dispatch, SetStateAction } from "react";
31
import { Button } from "@/components/ui/button";
42
import { Card, CardContent } from "@/components/ui/card";
53
import { Input } from "@/components/ui/input";
64
import type { JobFile } from "@/types/jobs";
5+
import { RefreshCcw, Search } from "lucide-react";
6+
import type { Dispatch, SetStateAction } from "react";
77

88
interface JobsFiltersCardProps {
99
search: string;
@@ -31,7 +31,7 @@ export function JobsFiltersCard({
3131
onRefresh,
3232
}: JobsFiltersCardProps) {
3333
return (
34-
<Card className="border-white/30 bg-card/85 backdrop-blur">
34+
<Card className="border-border/70 bg-card/85 backdrop-blur dark:bg-card/95">
3535
<CardContent className="grid gap-3 pt-6 md:grid-cols-4">
3636
<div className="relative md:col-span-2">
3737
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-muted-foreground" />

frontend/src/components/JobsHeaderCard.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
21
import { Badge } from "@/components/ui/badge";
32
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
43
import type { JobsMeta } from "@/types/jobs";
4+
import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react";
5+
import type { ReactNode } from "react";
56

67
interface JobsHeaderCardProps {
78
meta: JobsMeta;
9+
actions?: ReactNode;
810
}
911

10-
export function JobsHeaderCard({ meta }: JobsHeaderCardProps) {
12+
export function JobsHeaderCard({ meta, actions }: JobsHeaderCardProps) {
1113
return (
12-
<Card className="border-white/30 bg-card/85 backdrop-blur">
14+
<Card className="border-border/70 bg-card/85 backdrop-blur dark:bg-card/95">
1315
<CardHeader className="gap-4 pb-4 md:flex-row md:items-center md:justify-between">
1416
<div>
1517
<CardTitle className="text-3xl">Painel de Vagas</CardTitle>
1618
<CardDescription>Leitura automatica dos arquivos XLSX gerados em output.</CardDescription>
1719
</div>
1820
<div className="flex flex-wrap items-center gap-2">
21+
{actions}
1922
<Badge variant="secondary" className="gap-1 text-xs">
2023
<FileSpreadsheet className="h-3.5 w-3.5" />
2124
{meta.file || "Sem arquivo"}

frontend/src/components/JobsTableCard.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
21
import { Button } from "@/components/ui/button";
2+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
33
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
44
import type { Job, JobsMeta } from "@/types/jobs";
55

@@ -35,7 +35,7 @@ export function JobsTableCard({
3535
onPageSizeChange,
3636
}: JobsTableCardProps) {
3737
return (
38-
<Card className="border-white/30 bg-card/90 backdrop-blur">
38+
<Card className="border-border/70 bg-card/90 backdrop-blur dark:bg-card/95">
3939
<CardHeader>
4040
<CardTitle className="text-lg">Vagas Encontradas</CardTitle>
4141
<CardDescription>
@@ -44,7 +44,9 @@ export function JobsTableCard({
4444
</CardHeader>
4545
<CardContent>
4646
{error ? (
47-
<div className="rounded-md border border-red-400/40 bg-red-500/10 p-4 text-sm text-red-900">{error}</div>
47+
<div className="rounded-md border border-red-500/40 bg-red-500/10 p-4 text-sm text-red-700 dark:text-red-300">
48+
{error}
49+
</div>
4850
) : null}
4951

5052
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Button } from "@/components/ui/button";
2+
import type { Theme } from "@/hooks/useTheme";
3+
import { Moon, Sun } from "lucide-react";
4+
5+
interface ThemeToggleProps {
6+
theme: Theme;
7+
onToggle: () => void;
8+
}
9+
10+
export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
11+
const isDark = theme === "dark";
12+
13+
return (
14+
<Button
15+
type="button"
16+
variant="outline"
17+
size="sm"
18+
onClick={onToggle}
19+
aria-label={isDark ? "Ativar tema claro" : "Ativar tema escuro"}
20+
title={isDark ? "Ativar tema claro" : "Ativar tema escuro"}
21+
className="gap-2"
22+
>
23+
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
24+
<span className="hidden sm:inline">{isDark ? "Tema claro" : "Tema escuro"}</span>
25+
</Button>
26+
);
27+
}

frontend/src/hooks/useTheme.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
3+
export type Theme = "light" | "dark";
4+
export type ThemePreference = Theme | "system";
5+
6+
const THEME_STORAGE_KEY = "jobs-theme-preference";
7+
const DARK_CLASS = "dark";
8+
9+
function isThemePreference(value: string | null): value is ThemePreference {
10+
return value === "light" || value === "dark" || value === "system";
11+
}
12+
13+
function getSystemTheme(): Theme {
14+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
15+
}
16+
17+
function resolveTheme(themePreference: ThemePreference): Theme {
18+
return themePreference === "system" ? getSystemTheme() : themePreference;
19+
}
20+
21+
function applyThemeToRoot(theme: Theme): void {
22+
const root = document.documentElement;
23+
24+
root.classList.toggle(DARK_CLASS, theme === "dark");
25+
root.setAttribute("data-theme", theme);
26+
}
27+
28+
export function useTheme() {
29+
const [themePreference, setThemePreference] = useState<ThemePreference>(() => {
30+
const storedPreference = localStorage.getItem(THEME_STORAGE_KEY);
31+
return isThemePreference(storedPreference) ? storedPreference : "system";
32+
});
33+
34+
const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]);
35+
36+
useEffect(() => {
37+
applyThemeToRoot(resolvedTheme);
38+
localStorage.setItem(THEME_STORAGE_KEY, themePreference);
39+
}, [resolvedTheme, themePreference]);
40+
41+
useEffect(() => {
42+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
43+
const handleSystemThemeChange = () => {
44+
if (themePreference === "system") {
45+
applyThemeToRoot(getSystemTheme());
46+
}
47+
};
48+
49+
mediaQuery.addEventListener("change", handleSystemThemeChange);
50+
51+
return () => {
52+
mediaQuery.removeEventListener("change", handleSystemThemeChange);
53+
};
54+
}, [themePreference]);
55+
56+
const toggleTheme = () => {
57+
setThemePreference((currentTheme) => {
58+
const currentResolved = currentTheme === "system" ? getSystemTheme() : currentTheme;
59+
return currentResolved === "dark" ? "light" : "dark";
60+
});
61+
};
62+
63+
return {
64+
themePreference,
65+
resolvedTheme,
66+
setThemePreference,
67+
toggleTheme,
68+
};
69+
}

frontend/src/index.css

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,30 @@
2222
--input: 30 28% 82%;
2323
--ring: 198 63% 36%;
2424
--radius: 0.8rem;
25+
color-scheme: light;
26+
}
27+
28+
.dark {
29+
--background: 215 30% 10%;
30+
--foreground: 210 30% 95%;
31+
--card: 216 26% 14%;
32+
--card-foreground: 210 30% 95%;
33+
--primary: 198 78% 63%;
34+
--primary-foreground: 218 35% 12%;
35+
--secondary: 216 20% 23%;
36+
--secondary-foreground: 210 20% 94%;
37+
--muted: 216 20% 19%;
38+
--muted-foreground: 215 20% 72%;
39+
--accent: 25 75% 58%;
40+
--accent-foreground: 0 0% 100%;
41+
--border: 216 16% 30%;
42+
--input: 216 16% 30%;
43+
--ring: 198 78% 63%;
44+
color-scheme: dark;
2545
}
2646

2747
* {
28-
@apply border-border;
48+
@apply border-border transition-colors duration-300;
2949
}
3050

3151
body {

frontend/tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @type {import('tailwindcss').Config} */
22
export default {
3+
darkMode: "class",
34
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
45
theme: {
56
extend: {

0 commit comments

Comments
 (0)