mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-06 23:13:17 +00:00
252 lines
6.8 KiB
JavaScript
252 lines
6.8 KiB
JavaScript
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { userPreferencesAPI } from "../utils/api";
|
|
import { useAuth } from "./AuthContext";
|
|
|
|
const ColorThemeContext = createContext();
|
|
|
|
// Theme configurations matching the login backgrounds
|
|
export const THEME_PRESETS = {
|
|
default: {
|
|
name: "Normal Dark",
|
|
login: {
|
|
cellSize: 90,
|
|
variance: 0.85,
|
|
xColors: ["#0f172a", "#1e293b", "#334155", "#475569", "#64748b"],
|
|
yColors: ["#0f172a", "#1e293b", "#334155", "#475569", "#64748b"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#1e293b",
|
|
bgSecondary: "#1e293b",
|
|
bgTertiary: "#334155",
|
|
borderColor: "#475569",
|
|
cardBg: "#1e293b",
|
|
cardBorder: "#334155",
|
|
buttonBg: "#334155",
|
|
buttonHover: "#475569",
|
|
},
|
|
},
|
|
cyber_blue: {
|
|
name: "Cyber Blue",
|
|
login: {
|
|
cellSize: 90,
|
|
variance: 0.85,
|
|
xColors: ["#0a0820", "#1a1f3a", "#2d3561", "#4a5584", "#667eaf"],
|
|
yColors: ["#0a0820", "#1a1f3a", "#2d3561", "#4a5584", "#667eaf"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#0a0820",
|
|
bgSecondary: "#1a1f3a",
|
|
bgTertiary: "#2d3561",
|
|
borderColor: "#4a5584",
|
|
cardBg: "#1a1f3a",
|
|
cardBorder: "#2d3561",
|
|
buttonBg: "#2d3561",
|
|
buttonHover: "#4a5584",
|
|
},
|
|
},
|
|
neon_purple: {
|
|
name: "Neon Purple",
|
|
login: {
|
|
cellSize: 80,
|
|
variance: 0.9,
|
|
xColors: ["#0f0a1e", "#1e0f3e", "#4a0082", "#7209b7", "#b5179e"],
|
|
yColors: ["#0f0a1e", "#1e0f3e", "#4a0082", "#7209b7", "#b5179e"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#0f0a1e",
|
|
bgSecondary: "#1e0f3e",
|
|
bgTertiary: "#4a0082",
|
|
borderColor: "#7209b7",
|
|
cardBg: "#1e0f3e",
|
|
cardBorder: "#4a0082",
|
|
buttonBg: "#4a0082",
|
|
buttonHover: "#7209b7",
|
|
},
|
|
},
|
|
matrix_green: {
|
|
name: "Matrix Green",
|
|
login: {
|
|
cellSize: 70,
|
|
variance: 0.7,
|
|
xColors: ["#001a00", "#003300", "#004d00", "#006600", "#00b300"],
|
|
yColors: ["#001a00", "#003300", "#004d00", "#006600", "#00b300"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#001a00",
|
|
bgSecondary: "#003300",
|
|
bgTertiary: "#004d00",
|
|
borderColor: "#006600",
|
|
cardBg: "#003300",
|
|
cardBorder: "#004d00",
|
|
buttonBg: "#004d00",
|
|
buttonHover: "#006600",
|
|
},
|
|
},
|
|
ocean_blue: {
|
|
name: "Ocean Blue",
|
|
login: {
|
|
cellSize: 85,
|
|
variance: 0.8,
|
|
xColors: ["#001845", "#023e7d", "#0077b6", "#0096c7", "#00b4d8"],
|
|
yColors: ["#001845", "#023e7d", "#0077b6", "#0096c7", "#00b4d8"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#001845",
|
|
bgSecondary: "#023e7d",
|
|
bgTertiary: "#0077b6",
|
|
borderColor: "#0096c7",
|
|
cardBg: "#023e7d",
|
|
cardBorder: "#0077b6",
|
|
buttonBg: "#0077b6",
|
|
buttonHover: "#0096c7",
|
|
},
|
|
},
|
|
sunset_gradient: {
|
|
name: "Sunset Gradient",
|
|
login: {
|
|
cellSize: 95,
|
|
variance: 0.75,
|
|
xColors: ["#1a0033", "#330066", "#4d0099", "#6600cc", "#9933ff"],
|
|
yColors: ["#1a0033", "#660033", "#990033", "#cc0066", "#ff0099"],
|
|
},
|
|
app: {
|
|
bgPrimary: "#1a0033",
|
|
bgSecondary: "#330066",
|
|
bgTertiary: "#4d0099",
|
|
borderColor: "#6600cc",
|
|
cardBg: "#330066",
|
|
cardBorder: "#4d0099",
|
|
buttonBg: "#4d0099",
|
|
buttonHover: "#6600cc",
|
|
},
|
|
},
|
|
};
|
|
|
|
export const ColorThemeProvider = ({ children }) => {
|
|
const queryClient = useQueryClient();
|
|
const lastThemeRef = useRef(null);
|
|
|
|
// Use reactive authentication state from AuthContext
|
|
// This ensures the query re-enables when user logs in
|
|
const { user } = useAuth();
|
|
const isAuthenticated = !!user;
|
|
|
|
// Source of truth: Database (via userPreferences query)
|
|
// localStorage is only used as a temporary cache until DB loads
|
|
// Only fetch if user is authenticated to avoid 401 errors on login page
|
|
const { data: userPreferences, isLoading: preferencesLoading } = useQuery({
|
|
queryKey: ["userPreferences"],
|
|
queryFn: () => userPreferencesAPI.get().then((res) => res.data),
|
|
enabled: isAuthenticated, // Only run query if user is authenticated
|
|
retry: 2,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
refetchOnWindowFocus: true, // Refetch when user returns to tab
|
|
});
|
|
|
|
// Get theme from database (source of truth), fallback to user object from login, then localStorage cache, then default
|
|
// Memoize to prevent recalculation on every render
|
|
const colorThemeValue = useMemo(() => {
|
|
return (
|
|
userPreferences?.color_theme ||
|
|
user?.color_theme ||
|
|
localStorage.getItem("colorTheme") ||
|
|
"cyber_blue"
|
|
);
|
|
}, [userPreferences?.color_theme, user?.color_theme]);
|
|
|
|
// Only update state if the theme value actually changed (prevent loops)
|
|
const [colorTheme, setColorTheme] = useState(() => colorThemeValue);
|
|
|
|
useEffect(() => {
|
|
// Only update if the value actually changed from what we last saw (prevent loops)
|
|
if (colorThemeValue !== lastThemeRef.current) {
|
|
setColorTheme(colorThemeValue);
|
|
lastThemeRef.current = colorThemeValue;
|
|
}
|
|
}, [colorThemeValue]);
|
|
|
|
const isLoading = preferencesLoading;
|
|
|
|
// Sync localStorage cache when DB data is available (for offline/performance)
|
|
useEffect(() => {
|
|
if (userPreferences?.color_theme) {
|
|
localStorage.setItem("colorTheme", userPreferences.color_theme);
|
|
}
|
|
}, [userPreferences?.color_theme]);
|
|
|
|
const updateColorTheme = useCallback(
|
|
async (theme) => {
|
|
// Store previous theme for potential revert
|
|
const previousTheme = colorTheme;
|
|
|
|
// Immediately update state for instant UI feedback
|
|
setColorTheme(theme);
|
|
lastThemeRef.current = theme;
|
|
|
|
// Also update localStorage cache
|
|
localStorage.setItem("colorTheme", theme);
|
|
|
|
// Save to backend (source of truth)
|
|
try {
|
|
await userPreferencesAPI.update({ color_theme: theme });
|
|
|
|
// Invalidate and refetch user preferences to ensure sync across tabs/browsers
|
|
await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
|
|
} catch (error) {
|
|
console.error("Failed to save color theme preference:", error);
|
|
// Revert to previous theme if save failed
|
|
setColorTheme(previousTheme);
|
|
lastThemeRef.current = previousTheme;
|
|
localStorage.setItem("colorTheme", previousTheme);
|
|
|
|
// Invalidate to refresh from DB
|
|
await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
|
|
|
|
// Show error to user if possible (could add toast notification here)
|
|
throw error; // Re-throw so calling code can handle it
|
|
}
|
|
},
|
|
[colorTheme, queryClient],
|
|
);
|
|
|
|
// Memoize themeConfig to prevent unnecessary re-renders
|
|
const themeConfig = useMemo(
|
|
() => THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
|
|
[colorTheme],
|
|
);
|
|
|
|
// Memoize the context value to prevent unnecessary re-renders
|
|
const value = useMemo(
|
|
() => ({
|
|
colorTheme,
|
|
setColorTheme: updateColorTheme,
|
|
themeConfig,
|
|
isLoading,
|
|
}),
|
|
[colorTheme, themeConfig, isLoading, updateColorTheme],
|
|
);
|
|
|
|
return (
|
|
<ColorThemeContext.Provider value={value}>
|
|
{children}
|
|
</ColorThemeContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useColorTheme = () => {
|
|
const context = useContext(ColorThemeContext);
|
|
if (!context) {
|
|
throw new Error("useColorTheme must be used within ColorThemeProvider");
|
|
}
|
|
return context;
|
|
};
|