Skip to content

feat: implement dark mode support with theme toggle and CSS adjustments#8

Merged
Benevanio merged 1 commit into
masterfrom
develop
Mar 20, 2026
Merged

feat: implement dark mode support with theme toggle and CSS adjustments#8
Benevanio merged 1 commit into
masterfrom
develop

Conversation

@Benevanio

Copy link
Copy Markdown
Collaborator

Dark Mode theme

@Benevanio
Benevanio requested a review from Copilot March 20, 2026 21:56
@Benevanio
Benevanio merged commit a335f1d into master Mar 20, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 useTheme hook + early theme bootstrap in index.html to 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.

Comment thread frontend/index.html
Comment on lines +7 to +16
<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;

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread README.md

```bash
docker compose up --build
docker compose up --build -D

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

docker compose up uses -d/--detach (lowercase). -D is not a valid flag and will fail for readers following the README.

Suggested change
docker compose up --build -D
docker compose up --build -d

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +54
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]);

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +38
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);

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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.
}

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/index.css
Comment on lines 47 to 49
* {
@apply border-border;
@apply border-border transition-colors duration-300;
}

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants