diff --git a/README.md b/README.md index fc9fdebc..1fa34a39 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Agora o Docker foi separado em 3 servicos: ### Subir dashboard + API (sem scraping automatico) ```bash -docker compose up --build +docker compose up --build -D ``` ### Executar scraping manualmente diff --git a/frontend/index.html b/frontend/index.html index f94d687d..d8cbc1e1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,10 +4,25 @@ + frontend
- + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 240af186..589bf48f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,13 @@ -import { useCallback, type SetStateAction } from "react"; import { JobsFiltersCard } from "@/components/JobsFiltersCard"; import { JobsHeaderCard } from "@/components/JobsHeaderCard"; import { JobsTableCard } from "@/components/JobsTableCard"; +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 { useCallback, type SetStateAction } from "react"; function formatDate(timestamp: JobsMeta["modifiedAt"]): string { if (!timestamp) { @@ -15,6 +17,8 @@ function formatDate(timestamp: JobsMeta["modifiedAt"]): string { } function App() { + const { resolvedTheme, toggleTheme } = useTheme(); + const { files, selectedFile, setSelectedFile, jobs, meta, loading, error, loadJobs } = useJobsData(); const { search, setSearch, keywordFilter, setKeywordFilter, keywords, filteredJobs } = useJobsFiltering(jobs); @@ -57,11 +61,11 @@ function App() { ); return ( -
-
+
+
- + } /> +
diff --git a/frontend/src/components/JobsHeaderCard.tsx b/frontend/src/components/JobsHeaderCard.tsx index 6e5887e8..3400e781 100644 --- a/frontend/src/components/JobsHeaderCard.tsx +++ b/frontend/src/components/JobsHeaderCard.tsx @@ -1,21 +1,24 @@ -import { BriefcaseBusiness, FileSpreadsheet } from "lucide-react"; 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"; interface JobsHeaderCardProps { meta: JobsMeta; + actions?: ReactNode; } -export function JobsHeaderCard({ meta }: JobsHeaderCardProps) { +export function JobsHeaderCard({ meta, actions }: JobsHeaderCardProps) { return ( - +
Painel de Vagas Leitura automatica dos arquivos XLSX gerados em output.
+ {actions} {meta.file || "Sem arquivo"} diff --git a/frontend/src/components/JobsTableCard.tsx b/frontend/src/components/JobsTableCard.tsx index 07b4e68a..bbd2ed88 100644 --- a/frontend/src/components/JobsTableCard.tsx +++ b/frontend/src/components/JobsTableCard.tsx @@ -1,5 +1,5 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import type { Job, JobsMeta } from "@/types/jobs"; @@ -35,7 +35,7 @@ export function JobsTableCard({ onPageSizeChange, }: JobsTableCardProps) { return ( - + Vagas Encontradas @@ -44,7 +44,9 @@ export function JobsTableCard({ {error ? ( -
{error}
+
+ {error} +
) : null}
diff --git a/frontend/src/components/ui/theme-toggle.tsx b/frontend/src/components/ui/theme-toggle.tsx new file mode 100644 index 00000000..f18f50d4 --- /dev/null +++ b/frontend/src/components/ui/theme-toggle.tsx @@ -0,0 +1,27 @@ +import { Button } from "@/components/ui/button"; +import type { Theme } from "@/hooks/useTheme"; +import { Moon, Sun } from "lucide-react"; + +interface ThemeToggleProps { + theme: Theme; + onToggle: () => void; +} + +export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) { + const isDark = theme === "dark"; + + return ( + + ); +} diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts new file mode 100644 index 00000000..5f638d70 --- /dev/null +++ b/frontend/src/hooks/useTheme.ts @@ -0,0 +1,69 @@ +import { useEffect, useMemo, useState } from "react"; + +export type Theme = "light" | "dark"; +export type ThemePreference = Theme | "system"; + +const THEME_STORAGE_KEY = "jobs-theme-preference"; +const DARK_CLASS = "dark"; + +function isThemePreference(value: string | null): value is ThemePreference { + return value === "light" || value === "dark" || value === "system"; +} + +function getSystemTheme(): Theme { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +function resolveTheme(themePreference: ThemePreference): Theme { + return themePreference === "system" ? getSystemTheme() : themePreference; +} + +function applyThemeToRoot(theme: Theme): void { + const root = document.documentElement; + + root.classList.toggle(DARK_CLASS, theme === "dark"); + root.setAttribute("data-theme", theme); +} + +export function useTheme() { + const [themePreference, setThemePreference] = useState(() => { + const storedPreference = localStorage.getItem(THEME_STORAGE_KEY); + return isThemePreference(storedPreference) ? storedPreference : "system"; + }); + + const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]); + + useEffect(() => { + applyThemeToRoot(resolvedTheme); + localStorage.setItem(THEME_STORAGE_KEY, themePreference); + }, [resolvedTheme, themePreference]); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleSystemThemeChange = () => { + if (themePreference === "system") { + applyThemeToRoot(getSystemTheme()); + } + }; + + mediaQuery.addEventListener("change", handleSystemThemeChange); + + return () => { + mediaQuery.removeEventListener("change", handleSystemThemeChange); + }; + }, [themePreference]); + + const toggleTheme = () => { + setThemePreference((currentTheme) => { + const currentResolved = currentTheme === "system" ? getSystemTheme() : currentTheme; + return currentResolved === "dark" ? "light" : "dark"; + }); + }; + + return { + themePreference, + resolvedTheme, + setThemePreference, + toggleTheme, + }; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index f6868c74..7a3a2979 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -22,10 +22,30 @@ --input: 30 28% 82%; --ring: 198 63% 36%; --radius: 0.8rem; + color-scheme: light; + } + + .dark { + --background: 215 30% 10%; + --foreground: 210 30% 95%; + --card: 216 26% 14%; + --card-foreground: 210 30% 95%; + --primary: 198 78% 63%; + --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-foreground: 0 0% 100%; + --border: 216 16% 30%; + --input: 216 16% 30%; + --ring: 198 78% 63%; + color-scheme: dark; } * { - @apply border-border; + @apply border-border transition-colors duration-300; } body { diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index a45ff70c..360dca6f 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { + darkMode: "class", content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: {