Skip to content

Commit 6b75aeb

Browse files
committed
Convert theme toggle to dropdown with Auto/Light/Dark options - Replace checkbox with dropdown select for better UX - Add theme initializer to apply theme on app load - Auto mode switches between light/dark based on time (6 PM - 6 AM) - Update CSS styling for dropdown to match theme
1 parent 516e850 commit 6b75aeb

4 files changed

Lines changed: 128 additions & 63 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Initialize theme on app load - runs before any components mount
2+
export function initializeTheme() {
3+
// Check for old theme format and migrate if needed
4+
const oldTheme = localStorage.getItem("theme");
5+
if (oldTheme && !localStorage.getItem("themeMode")) {
6+
// Migrate old "light"/"dark" to new format
7+
if (oldTheme === "light" || oldTheme === "dark") {
8+
localStorage.setItem("themeMode", oldTheme);
9+
localStorage.removeItem("theme");
10+
}
11+
}
12+
13+
const savedMode = localStorage.getItem("themeMode") || "auto";
14+
15+
let theme;
16+
if (savedMode === "auto") {
17+
// Auto mode: dark between 6 PM (18:00) and 6 AM (06:00)
18+
const hour = new Date().getHours();
19+
theme = hour >= 18 || hour < 6 ? "dark" : "light";
20+
} else {
21+
// Manual mode: use saved preference
22+
theme = savedMode; // "light" or "dark"
23+
}
24+
25+
// Apply theme to DOM immediately
26+
const root = document.documentElement;
27+
if (theme === "dark") {
28+
root.setAttribute("data-theme", "dark");
29+
} else {
30+
root.removeAttribute("data-theme");
31+
}
32+
33+
return { mode: savedMode, theme };
34+
}
35+
36+
// Run initialization immediately when this module loads
37+
initializeTheme();
38+
Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,92 @@
11
import React, { useEffect, useState } from "react";
22
import "../styles/themeToggle.css";
33

4+
// Helper function to get theme based on mode
5+
const getThemeFromMode = (currentMode) => {
6+
if (currentMode === "auto") {
7+
// Auto mode: dark between 6 PM and 6 AM
8+
const hour = new Date().getHours();
9+
return hour >= 18 || hour < 6 ? "dark" : "light";
10+
} else {
11+
// Manual mode: use the selected mode directly
12+
return currentMode; // "light" or "dark"
13+
}
14+
};
15+
416
export default function ThemeToggle() {
5-
const [theme, setTheme] = useState(() => {
6-
const saved = localStorage.getItem("theme");
7-
return saved ? saved : new Date().getHours() >= 18 ? "dark" : "light";
17+
// Get theme mode: "auto", "light", or "dark"
18+
const [mode, setMode] = useState(() => {
19+
const savedMode = localStorage.getItem("themeMode");
20+
return savedMode || "auto"; // Default to auto mode
821
});
922

10-
// 1. Apply theme to DOM and save to localStorage
23+
const [theme, setTheme] = useState(() => getThemeFromMode(mode));
24+
25+
// 1. Apply theme to DOM
1126
useEffect(() => {
1227
const root = document.documentElement;
1328
if (theme === "dark") {
1429
root.setAttribute("data-theme", "dark");
1530
} else {
1631
root.removeAttribute("data-theme");
1732
}
18-
localStorage.setItem("theme", theme);
1933
}, [theme]);
2034

21-
// 2. Live time updates - ONLY if no saved user preference
35+
// 2. Update theme when mode changes
2236
useEffect(() => {
23-
const interval = setInterval(() => {
24-
const saved = localStorage.getItem("theme");
25-
// Skip if user has manually set a preference
26-
if (saved && !saved.startsWith("auto-")) return;
27-
37+
const newTheme = getThemeFromMode(mode);
38+
setTheme(newTheme);
39+
40+
// Save mode preference
41+
localStorage.setItem("themeMode", mode);
42+
}, [mode]);
43+
44+
// 3. Auto-update theme based on time (only in auto mode)
45+
useEffect(() => {
46+
if (mode !== "auto") return; // Only run in auto mode
47+
48+
// Update immediately on mount
49+
const updateTheme = () => {
2850
const hour = new Date().getHours();
2951
const newTheme = hour >= 18 || hour < 6 ? "dark" : "light";
30-
31-
// Only update if different from current
3252
if (newTheme !== theme) {
3353
setTheme(newTheme);
3454
}
35-
}, 60000); // Check every minute
55+
};
56+
57+
updateTheme(); // Initial update
58+
59+
// Check every minute for time-based changes
60+
const interval = setInterval(updateTheme, 60000);
3661

3762
return () => clearInterval(interval);
38-
}, [theme]); // Re-run if theme changes manually
63+
}, [mode, theme]);
3964

40-
const toggleTheme = () => {
41-
// Clear auto mode when user manually toggles
42-
localStorage.removeItem("theme");
43-
setTheme(theme === "dark" ? "light" : "dark");
65+
const handleModeChange = (e) => {
66+
const newMode = e.target.value;
67+
setMode(newMode);
68+
};
69+
70+
// Get display label for auto option
71+
const getAutoLabel = () => {
72+
if (mode === "auto") {
73+
return `Auto (${theme === "dark" ? "Dark" : "Light"})`;
74+
}
75+
return "Auto";
4476
};
4577

4678
return (
47-
<label className="theme-toggle">
48-
<input
49-
type="checkbox"
50-
checked={theme === "dark"}
51-
onChange={toggleTheme}
52-
/>
53-
<span className="slider" />
54-
<span className="label">{theme === "dark" ? "Dark" : "Light"}</span>
55-
</label>
79+
<div className="theme-toggle">
80+
<select
81+
className="theme-select"
82+
value={mode}
83+
onChange={handleModeChange}
84+
aria-label="Select theme mode"
85+
>
86+
<option value="auto">{getAutoLabel()}</option>
87+
<option value="light">Light</option>
88+
<option value="dark">Dark</option>
89+
</select>
90+
</div>
5691
);
5792
}

‎src/client/styles/themeToggle.css‎

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,34 @@
1-
/* src/components/ThemeToggle.css */
1+
/* Theme Toggle Dropdown Styles */
22
.theme-toggle {
33
display: inline-flex;
44
align-items: center;
5-
cursor: pointer;
6-
}
7-
.theme-toggle input {
8-
position: absolute;
9-
opacity: 0;
10-
width: 0;
11-
height: 0;
12-
}
13-
.theme-toggle .slider {
14-
width: 2rem;
15-
height: 1rem;
16-
background: #ccc;
17-
border-radius: 1rem;
18-
position: relative;
19-
margin-right: 0.5rem;
20-
transition: background 0.3s;
215
}
22-
.theme-toggle .slider::before {
23-
content: "";
24-
position: absolute;
25-
top: 2px;
26-
left: 2px;
27-
width: 0.8rem;
28-
height: 0.8rem;
29-
background: white;
30-
border-radius: 50%;
31-
transition: transform 0.3s;
6+
7+
.theme-select {
8+
padding: 0.5rem 1rem;
9+
font-size: 0.9rem;
10+
border: 1px solid var(--fg);
11+
border-radius: 0.5rem;
12+
background: var(--card);
13+
color: var(--fg);
14+
cursor: pointer;
15+
transition: all 0.3s ease;
16+
outline: none;
17+
min-width: 120px;
3218
}
33-
/* when checked, slide the circle and darken track */
34-
.theme-toggle input:checked + .slider {
35-
background: #4ca1af;
19+
20+
.theme-select:hover {
21+
border-color: var(--accent);
22+
background: var(--card-hover);
3623
}
37-
.theme-toggle input:checked + .slider::before {
38-
transform: translateX(1rem);
24+
25+
.theme-select:focus {
26+
border-color: var(--accent);
27+
box-shadow: 0 0 0 2px rgba(76, 161, 175, 0.2);
3928
}
40-
.theme-toggle .label {
41-
font-size: 0.9rem;
42-
user-select: none;
29+
30+
.theme-select option {
31+
background: var(--card);
32+
color: var(--fg);
33+
padding: 0.5rem;
4334
}

‎src/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import { createRoot } from "react-dom/client";
33
import App from "./client/app";
4+
import "./client/components/themeInitializer"; // Initialize theme on app load
45

56
const root = createRoot(document.getElementById("root"));
67
root.render(

0 commit comments

Comments
 (0)