Skip to content

Commit 1b8145a

Browse files
committed
Fix Theme Switching Issue & Two Factor QR Code for dark mode
1 parent d116cca commit 1b8145a

File tree

3 files changed

+76
-43
lines changed

3 files changed

+76
-43
lines changed

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default [
3838
},
3939
},
4040
{
41-
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
41+
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr'],
4242
},
4343
prettier, // Turn off all rules that might conflict with Prettier
4444
];

resources/js/components/two-factor-setup-modal.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
InputOTPGroup,
1313
InputOTPSlot,
1414
} from '@/components/ui/input-otp';
15+
import { useAppearance } from '@/hooks/use-appearance';
1516
import { useClipboard } from '@/hooks/use-clipboard';
1617
import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth';
1718
import { confirm } from '@/routes/two-factor';
@@ -60,6 +61,7 @@ function TwoFactorSetupStep({
6061
onNextStep: () => void;
6162
errors: string[];
6263
}) {
64+
const { resolvedAppearance } = useAppearance();
6365
const [copiedText, copy] = useClipboard();
6466
const IconComponent = copiedText === manualSetupKey ? Check : Copy;
6567

@@ -77,6 +79,12 @@ function TwoFactorSetupStep({
7779
dangerouslySetInnerHTML={{
7880
__html: qrCodeSvg,
7981
}}
82+
style={{
83+
filter:
84+
resolvedAppearance === 'dark'
85+
? 'invert(1) brightness(1.5)'
86+
: undefined,
87+
}}
8088
/>
8189
) : (
8290
<Loader2 className="flex size-4 animate-spin" />
Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,96 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
22

3-
export type Appearance = 'light' | 'dark' | 'system';
3+
export type ResolvedAppearance = 'light' | 'dark';
4+
export type Appearance = ResolvedAppearance | 'system';
45

5-
const prefersDark = () => {
6-
if (typeof window === 'undefined') {
7-
return false;
8-
}
6+
// Global state management
7+
const listeners = new Set<() => void>();
8+
let currentAppearance: Appearance = 'system';
99

10+
// Utility functions
11+
const prefersDark = (): boolean => {
12+
if (typeof window === 'undefined') return false;
1013
return window.matchMedia('(prefers-color-scheme: dark)').matches;
1114
};
1215

13-
const setCookie = (name: string, value: string, days = 365) => {
14-
if (typeof document === 'undefined') {
15-
return;
16-
}
17-
16+
const setCookie = (name: string, value: string, days = 365): void => {
17+
if (typeof document === 'undefined') return;
1818
const maxAge = days * 24 * 60 * 60;
1919
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
2020
};
2121

22-
const applyTheme = (appearance: Appearance) => {
23-
const isDark =
24-
appearance === 'dark' || (appearance === 'system' && prefersDark());
22+
const getStoredAppearance = (): Appearance => {
23+
if (typeof window === 'undefined') return 'system';
24+
return (localStorage.getItem('appearance') as Appearance) || 'system';
25+
};
26+
27+
const isDarkMode = (appearance: Appearance): boolean => {
28+
return appearance === 'dark' || (appearance === 'system' && prefersDark());
29+
};
2530

31+
const applyTheme = (appearance: Appearance): void => {
32+
if (typeof document === 'undefined') return;
33+
const isDark = isDarkMode(appearance);
2634
document.documentElement.classList.toggle('dark', isDark);
2735
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
2836
};
2937

30-
const mediaQuery = () => {
31-
if (typeof window === 'undefined') {
32-
return null;
33-
}
38+
const notify = (): void => listeners.forEach((listener) => listener());
3439

40+
const mediaQuery = (): MediaQueryList | null => {
41+
if (typeof window === 'undefined') return null;
3542
return window.matchMedia('(prefers-color-scheme: dark)');
3643
};
3744

38-
const handleSystemThemeChange = () => {
39-
const currentAppearance = localStorage.getItem('appearance') as Appearance;
40-
applyTheme(currentAppearance || 'system');
45+
const handleSystemThemeChange = (): void => {
46+
applyTheme(currentAppearance);
47+
notify();
4148
};
4249

43-
export function initializeTheme() {
44-
const savedAppearance =
45-
(localStorage.getItem('appearance') as Appearance) || 'system';
50+
export function initializeTheme(): void {
51+
if (typeof window === 'undefined') return;
4652

47-
applyTheme(savedAppearance);
53+
const storedAppearance = getStoredAppearance();
4854

49-
// Add the event listener for system theme changes...
55+
// Initialize default appearance if none exists
56+
if (!localStorage.getItem('appearance')) {
57+
localStorage.setItem('appearance', 'system');
58+
setCookie('appearance', 'system');
59+
}
60+
61+
currentAppearance = storedAppearance;
62+
applyTheme(currentAppearance);
63+
64+
// Set up system theme change listener
5065
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
5166
}
5267

5368
export function useAppearance() {
54-
const [appearance, setAppearance] = useState<Appearance>('system');
69+
const [appearance, setAppearance] =
70+
useState<Appearance>(getStoredAppearance);
5571

56-
const updateAppearance = useCallback((mode: Appearance) => {
72+
useEffect(() => {
73+
const handleChange = (): void => {
74+
const newAppearance = getStoredAppearance();
75+
setAppearance(newAppearance);
76+
};
77+
78+
listeners.add(handleChange);
79+
mediaQuery()?.addEventListener('change', handleChange);
80+
81+
return () => {
82+
listeners.delete(handleChange);
83+
mediaQuery()?.removeEventListener('change', handleChange);
84+
};
85+
}, []);
86+
87+
const resolvedAppearance: ResolvedAppearance = useMemo(
88+
() => (isDarkMode(appearance) ? 'dark' : 'light'),
89+
[appearance],
90+
);
91+
92+
const updateAppearance = useCallback((mode: Appearance): void => {
93+
currentAppearance = mode;
5794
setAppearance(mode);
5895

5996
// Store in localStorage for client-side persistence...
@@ -63,20 +100,8 @@ export function useAppearance() {
63100
setCookie('appearance', mode);
64101

65102
applyTheme(mode);
103+
notify();
66104
}, []);
67105

68-
useEffect(() => {
69-
const savedAppearance = localStorage.getItem(
70-
'appearance',
71-
) as Appearance | null;
72-
updateAppearance(savedAppearance || 'system');
73-
74-
return () =>
75-
mediaQuery()?.removeEventListener(
76-
'change',
77-
handleSystemThemeChange,
78-
);
79-
}, [updateAppearance]);
80-
81-
return { appearance, updateAppearance } as const;
106+
return { appearance, resolvedAppearance, updateAppearance } as const;
82107
}

0 commit comments

Comments
 (0)