Conversation
There was a problem hiding this comment.
Pull request overview
Implements dark mode support in the React/Vite frontend by enabling Tailwind’s class-based dark mode, introducing a persisted theme preference hook, and wiring a UI toggle into the header.
Changes:
- Enabled Tailwind dark mode via
darkMode: "class"and added dark-theme CSS variables. - Added
useThemehook + early theme bootstrap inindex.htmlto avoid theme flash. - Added a header theme toggle and dark-mode styling tweaks across cards/background.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/tailwind.config.js | Enables class-based dark mode for Tailwind. |
| frontend/src/index.css | Adds dark theme CSS variables + global transition changes. |
| frontend/src/hooks/useTheme.ts | Adds persisted theme preference + DOM application logic. |
| frontend/src/components/ui/theme-toggle.tsx | Adds toggle button component with icons/labels. |
| frontend/src/components/JobsTableCard.tsx | Adjusts card/error styling for dark mode. |
| frontend/src/components/JobsHeaderCard.tsx | Adds actions slot to host the theme toggle; dark styling tweaks. |
| frontend/src/components/JobsFiltersCard.tsx | Adjusts card styling for dark mode. |
| frontend/src/App.tsx | Integrates theme hook + toggle; adds dark background gradient variant. |
| frontend/index.html | Adds pre-React theme initialization + switches to main.tsx. |
| README.md | Updates docker command in docs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <script> | ||
| (function () { | ||
| const storageKey = "jobs-theme-preference"; | ||
| const root = document.documentElement; | ||
| const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; | ||
| const storedPreference = localStorage.getItem(storageKey); | ||
| const themePreference = storedPreference === "light" || storedPreference === "dark" || storedPreference === "system" | ||
| ? storedPreference | ||
| : "system"; | ||
| const resolvedTheme = themePreference === "system" ? (prefersDark ? "dark" : "light") : themePreference; |
There was a problem hiding this comment.
This inline theme bootstrap script reads from localStorage unguarded; localStorage.getItem can throw in environments where storage is blocked, causing a blank page before React loads. Consider wrapping storage access in try/catch (and defaulting to "system") to avoid hard failures.
|
|
||
| ```bash | ||
| docker compose up --build | ||
| docker compose up --build -D |
There was a problem hiding this comment.
docker compose up uses -d/--detach (lowercase). -D is not a valid flag and will fail for readers following the README.
| docker compose up --build -D | |
| docker compose up --build -d |
| 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]); |
There was a problem hiding this comment.
resolvedTheme is memoized only on themePreference, so when themePreference === "system" and the OS theme changes, the hook applies the new theme to the DOM but does not trigger a re-render and resolvedTheme stays stale. This can leave UI (toggle icon/labels) out of sync with the actual applied theme. Consider storing the resolved theme in state (or using useSyncExternalStore/a media-query subscription) and updating it in the media query handler so consumers re-render.
| export function useTheme() { | ||
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | ||
| 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); |
There was a problem hiding this comment.
Direct localStorage.getItem access during state initialization can throw (e.g., in some privacy modes or when storage is blocked), which would break the app before it renders. Wrap storage reads/writes in a try/catch (and fall back to "system") to make theme initialization resilient.
| export function useTheme() { | |
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | |
| 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); | |
| function getStoredThemePreference(): ThemePreference { | |
| try { | |
| const storedPreference = localStorage.getItem(THEME_STORAGE_KEY); | |
| return isThemePreference(storedPreference) ? storedPreference : "system"; | |
| } catch { | |
| return "system"; | |
| } | |
| } | |
| export function useTheme() { | |
| const [themePreference, setThemePreference] = useState<ThemePreference>(() => { | |
| return getStoredThemePreference(); | |
| }); | |
| const resolvedTheme = useMemo(() => resolveTheme(themePreference), [themePreference]); | |
| useEffect(() => { | |
| applyThemeToRoot(resolvedTheme); | |
| try { | |
| localStorage.setItem(THEME_STORAGE_KEY, themePreference); | |
| } catch { | |
| // Ignore storage write errors; theme is still applied to the DOM. | |
| } |
| * { | ||
| @apply border-border; | ||
| @apply border-border transition-colors duration-300; | ||
| } |
There was a problem hiding this comment.
Applying transition-colors to the universal selector (*) makes every element animate color changes, which can cause unnecessary repaints and unintended transitions across the UI. Prefer limiting the transition to a top-level container (e.g., body/main) or only to components that actually need it.
Dark Mode theme