Add UI settings API and improve context initialization

- Add /settings/ui GET and PUT endpoints for UI settings
- Improve ThemeContext with better initialization and auto accent handling
- Update SidebarContext with expanded state persistence
- Fix context initialization in ModulesContext and ViewModeContext
- Update components to use improved theme/sidebar contexts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 19:50:30 +01:00
parent 0608217702
commit 41c41adb98
10 changed files with 163 additions and 52 deletions

View File

@@ -10,6 +10,7 @@ from app.models.user import User
from app.core.settings_registry import ( from app.core.settings_registry import (
THEME_KEYS, THEME_KEYS,
MODULE_KEYS, MODULE_KEYS,
UI_KEYS,
SETTINGS_REGISTRY, SETTINGS_REGISTRY,
get_default_value, get_default_value,
) )
@@ -82,6 +83,47 @@ def get_module_settings(
return result return result
@router.get("/ui", response_model=dict[str, Any])
def get_ui_settings(
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
) -> Any:
"""
Get UI settings (accessible to all authenticated users).
Returns UI/layout settings that apply to all users.
"""
result = {}
for key in UI_KEYS:
setting = crud.settings.get_setting(db, key=key)
if setting:
result[key] = setting.get_value()
else:
result[key] = get_default_value(key)
return result
@router.put("/ui", response_model=dict[str, Any])
def update_ui_settings(
*,
db: Session = Depends(get_db),
ui_data: dict[str, Any],
current_user: User = Depends(get_current_superuser)
) -> Any:
"""
Update UI settings (admin only).
Updates multiple UI/layout settings at once.
"""
result = {}
for key, value in ui_data.items():
if key in UI_KEYS:
setting = crud.settings.update_setting(db, key=key, value=value)
result[key] = setting.get_value()
return result
@router.put("/modules", response_model=dict[str, Any]) @router.put("/modules", response_model=dict[str, Any])
def update_module_settings( def update_module_settings(
*, *,

View File

@@ -46,7 +46,11 @@ import MainLayout from './components/MainLayout';
// ... // ...
function AppRoutes() { function AppRoutes() {
const { user } = useAuth(); const { user, isLoading } = useAuth();
if (isLoading) {
return <div className="loading">Loading...</div>;
}
return ( return (
<Routes> <Routes>
@@ -72,9 +76,9 @@ function App() {
return ( return (
<SiteConfigProvider> <SiteConfigProvider>
<BrowserRouter> <BrowserRouter>
<ThemeProvider> <AuthProvider>
<LanguageProvider> <ThemeProvider>
<AuthProvider> <LanguageProvider>
<ModulesProvider> <ModulesProvider>
<ViewModeProvider> <ViewModeProvider>
<SidebarProvider> <SidebarProvider>
@@ -82,9 +86,9 @@ function App() {
</SidebarProvider> </SidebarProvider>
</ViewModeProvider> </ViewModeProvider>
</ModulesProvider> </ModulesProvider>
</AuthProvider> </LanguageProvider>
</LanguageProvider> </ThemeProvider>
</ThemeProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>
</SiteConfigProvider> </SiteConfigProvider>
); );

View File

@@ -14,7 +14,7 @@ import '../styles/Sidebar.css';
export default function Sidebar() { export default function Sidebar() {
const { t, language, setLanguage } = useTranslation(); const { t, language, setLanguage } = useTranslation();
const { config } = useSiteConfig(); const { config } = useSiteConfig();
const { sidebarStyle, theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle } = useTheme(); const { sidebarStyle, theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, hasInitializedSettings: themeInitialized } = useTheme();
const { const {
isCollapsed, isCollapsed,
isMobileOpen, isMobileOpen,
@@ -235,7 +235,7 @@ export default function Sidebar() {
</button> </button>
)} )}
{showDarkModeToggle && darkModeLocation === 'sidebar' && ( {themeInitialized && showDarkModeToggle && darkModeLocation === 'sidebar' && (
<button <button
className="view-mode-toggle" className="view-mode-toggle"
onClick={() => { toggleTheme(); updateTooltipText(theme === 'dark' ? t.theme.lightMode : t.theme.darkMode); }} onClick={() => { toggleTheme(); updateTooltipText(theme === 'dark' ? t.theme.lightMode : t.theme.darkMode); }}
@@ -254,7 +254,7 @@ export default function Sidebar() {
</button> </button>
)} )}
{showLanguageToggle && languageLocation === 'sidebar' && ( {themeInitialized && showLanguageToggle && languageLocation === 'sidebar' && (
<button <button
className="view-mode-toggle" className="view-mode-toggle"
onClick={() => { setLanguage(language === 'it' ? 'en' : 'it'); updateTooltipText(language === 'it' ? t.settings.english : t.settings.italian); }} onClick={() => { setLanguage(language === 'it' ? 'en' : 'it'); updateTooltipText(language === 'it' ? t.settings.english : t.settings.italian); }}

View File

@@ -9,7 +9,7 @@ import { useViewMode } from '../contexts/ViewModeContext';
export default function UserMenu({ onOpenChange }: { onOpenChange?: (isOpen: boolean) => void }) { export default function UserMenu({ onOpenChange }: { onOpenChange?: (isOpen: boolean) => void }) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { t, language, setLanguage } = useTranslation(); const { t, language, setLanguage } = useTranslation();
const { theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle } = useTheme(); const { theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, hasInitializedSettings: themeInitialized } = useTheme();
const { isCollapsed, isMobileOpen, closeMobileMenu, sidebarMode } = useSidebar(); const { isCollapsed, isMobileOpen, closeMobileMenu, sidebarMode } = useSidebar();
const { viewMode } = useViewMode(); const { viewMode } = useViewMode();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -112,7 +112,7 @@ export default function UserMenu({ onOpenChange }: { onOpenChange?: (isOpen: boo
<span className="material-symbols-outlined">settings</span> <span className="material-symbols-outlined">settings</span>
<span>{t.sidebar.settings}</span> <span>{t.sidebar.settings}</span>
</NavLink> </NavLink>
{showDarkModeToggle && darkModeLocation === 'user_menu' && ( {themeInitialized && showDarkModeToggle && darkModeLocation === 'user_menu' && (
<button onClick={toggleTheme} className="user-menu-item"> <button onClick={toggleTheme} className="user-menu-item">
<span className="material-symbols-outlined"> <span className="material-symbols-outlined">
{theme === 'dark' ? 'dark_mode' : 'light_mode'} {theme === 'dark' ? 'dark_mode' : 'light_mode'}
@@ -120,7 +120,7 @@ export default function UserMenu({ onOpenChange }: { onOpenChange?: (isOpen: boo
<span>{theme === 'dark' ? 'Dark Mode' : 'Light Mode'}</span> <span>{theme === 'dark' ? 'Dark Mode' : 'Light Mode'}</span>
</button> </button>
)} )}
{showLanguageToggle && languageLocation === 'user_menu' && ( {themeInitialized && showLanguageToggle && languageLocation === 'user_menu' && (
<button onClick={toggleLanguage} className="user-menu-item"> <button onClick={toggleLanguage} className="user-menu-item">
<span className="material-symbols-outlined">language</span> <span className="material-symbols-outlined">language</span>
<span>{language === 'it' ? 'Italiano' : 'English'}</span> <span>{language === 'it' ? 'Italiano' : 'English'}</span>

View File

@@ -1,6 +1,7 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { settingsAPI } from '../api/client'; import { settingsAPI } from '../api/client';
import { useAuth } from './AuthContext';
import type { UserPermissions } from '../types'; import type { UserPermissions } from '../types';
// User-facing modules that can be toggled // User-facing modules that can be toggled
@@ -39,6 +40,7 @@ const getDefaultStates = (): Record<ModuleId, ModuleState> => {
}; };
export function ModulesProvider({ children }: { children: ReactNode }) { export function ModulesProvider({ children }: { children: ReactNode }) {
const { token } = useAuth();
const [moduleStates, setModuleStates] = useState<Record<ModuleId, ModuleState>>(getDefaultStates); const [moduleStates, setModuleStates] = useState<Record<ModuleId, ModuleState>>(getDefaultStates);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [hasInitialized, setHasInitialized] = useState(false); const [hasInitialized, setHasInitialized] = useState(false);
@@ -110,16 +112,18 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
} }
}, [moduleStates]); }, [moduleStates]);
// Load modules on mount when token exists // Load modules when token becomes available
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token');
if (token) { if (token) {
setIsLoading(true);
setHasInitialized(false);
loadModulesFromBackend(); loadModulesFromBackend();
} else { } else {
setModuleStates(getDefaultStates());
setIsLoading(false); setIsLoading(false);
setHasInitialized(true); // No token, mark as initialized setHasInitialized(true); // No token, mark as initialized
} }
}, [loadModulesFromBackend]); }, [token, loadModulesFromBackend]);
const isModuleEnabled = useCallback((moduleId: string): boolean => { const isModuleEnabled = useCallback((moduleId: string): boolean => {
// Dashboard is always enabled // Dashboard is always enabled

View File

@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useAuth } from './AuthContext';
export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic'; export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic';
@@ -21,6 +22,7 @@ interface SidebarContextType {
const SidebarContext = createContext<SidebarContextType | undefined>(undefined); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export function SidebarProvider({ children }: { children: ReactNode }) { export function SidebarProvider({ children }: { children: ReactNode }) {
const { token } = useAuth();
const [userCollapsed, setUserCollapsed] = useState(true); const [userCollapsed, setUserCollapsed] = useState(true);
const [isMobileOpen, setIsMobileOpen] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false);
const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle'); const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle');
@@ -31,19 +33,29 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
const loadSidebarMode = async () => { const loadSidebarMode = async () => {
try { try {
const token = localStorage.getItem('token'); if (!token) {
if (!token) return; setSidebarModeState('toggle');
setShowLogo(false);
return;
}
const response = await fetch('/api/v1/settings', { const response = await fetch('/api/v1/settings/ui', {
headers: { 'Authorization': `Bearer ${token} ` } headers: { 'Authorization': `Bearer ${token}` }
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const parseBool = (val: any, defaultVal: boolean): boolean => {
if (val === undefined || val === null) return defaultVal;
if (val === true || val === 'true' || val === 'True' || val === 1 || val === '1') return true;
if (val === false || val === 'false' || val === 'False' || val === 0 || val === '0') return false;
return defaultVal;
};
if (data.sidebar_mode && ['collapsed', 'expanded', 'toggle', 'dynamic'].includes(data.sidebar_mode)) { if (data.sidebar_mode && ['collapsed', 'expanded', 'toggle', 'dynamic'].includes(data.sidebar_mode)) {
setSidebarModeState(data.sidebar_mode as SidebarMode); setSidebarModeState(data.sidebar_mode as SidebarMode);
} }
if (data.show_logo !== undefined) { if (data.show_logo !== undefined) {
setShowLogo(data.show_logo === true); setShowLogo(parseBool(data.show_logo, false));
} }
} }
} catch (error) { } catch (error) {
@@ -51,7 +63,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
} }
}; };
loadSidebarMode(); loadSidebarMode();
}, []); }, [token]);
// Compute isCollapsed based on mode // Compute isCollapsed based on mode
const isCollapsed = sidebarMode === 'collapsed' ? true : const isCollapsed = sidebarMode === 'collapsed' ? true :
@@ -78,11 +90,11 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
const setSidebarMode = useCallback(async (mode: SidebarMode) => { const setSidebarMode = useCallback(async (mode: SidebarMode) => {
try { try {
const token = localStorage.getItem('token'); if (!token) return;
const response = await fetch('/api/v1/settings/sidebar_mode', { const response = await fetch('/api/v1/settings/sidebar_mode', {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Authorization': `Bearer ${token} `, 'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ value: mode }), body: JSON.stringify({ value: mode }),
@@ -94,7 +106,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
} catch (error) { } catch (error) {
console.error('Failed to save sidebar mode:', error); console.error('Failed to save sidebar mode:', error);
} }
}, []); }, [token]);
return ( return (
<SidebarContext.Provider <SidebarContext.Provider

View File

@@ -1,6 +1,7 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { settingsAPI } from '../api/client'; import { settingsAPI } from '../api/client';
import { useAuth } from './AuthContext';
type Theme = 'light' | 'dark'; type Theme = 'light' | 'dark';
export type AccentColor = 'blue' | 'purple' | 'green' | 'orange' | 'pink' | 'red' | 'teal' | 'amber' | 'indigo' | 'cyan' | 'rose' | 'auto'; export type AccentColor = 'blue' | 'purple' | 'green' | 'orange' | 'pink' | 'red' | 'teal' | 'amber' | 'indigo' | 'cyan' | 'rose' | 'auto';
@@ -27,6 +28,8 @@ interface ThemeContextType {
showLanguageToggle: boolean; showLanguageToggle: boolean;
showDarkModeLogin: boolean; showDarkModeLogin: boolean;
showLanguageLogin: boolean; showLanguageLogin: boolean;
hasInitializedSettings: boolean;
isLoadingSettings: boolean;
toggleTheme: () => void; toggleTheme: () => void;
setAccentColor: (color: AccentColor) => void; setAccentColor: (color: AccentColor) => void;
setBorderRadius: (radius: BorderRadius) => void; setBorderRadius: (radius: BorderRadius) => void;
@@ -433,6 +436,7 @@ export const COLOR_PALETTES: Record<ColorPalette, PaletteColors> = {
}; };
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: { children: ReactNode }) {
const { token } = useAuth();
const [theme, setTheme] = useState<Theme>(() => { const [theme, setTheme] = useState<Theme>(() => {
return (localStorage.getItem('theme') as Theme) || 'light'; return (localStorage.getItem('theme') as Theme) || 'light';
}); });
@@ -454,6 +458,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const [showLanguageToggle, setShowLanguageToggleState] = useState<boolean>(false); const [showLanguageToggle, setShowLanguageToggleState] = useState<boolean>(false);
const [showDarkModeLogin, setShowDarkModeLoginState] = useState<boolean>(true); const [showDarkModeLogin, setShowDarkModeLoginState] = useState<boolean>(true);
const [showLanguageLogin, setShowLanguageLoginState] = useState<boolean>(false); const [showLanguageLogin, setShowLanguageLoginState] = useState<boolean>(false);
const [hasInitializedSettings, setHasInitializedSettings] = useState<boolean>(false);
const [isLoadingSettings, setIsLoadingSettings] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
@@ -565,6 +571,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
// Load theme from backend when user is authenticated // Load theme from backend when user is authenticated
const loadThemeFromBackend = useCallback(async () => { const loadThemeFromBackend = useCallback(async () => {
setIsLoadingSettings(true);
try { try {
const themeData = await settingsAPI.getTheme(); const themeData = await settingsAPI.getTheme();
if (themeData.theme_accent_color) { if (themeData.theme_accent_color) {
@@ -619,6 +626,9 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
} }
} catch (error) { } catch (error) {
console.error('Failed to load theme from backend:', error); console.error('Failed to load theme from backend:', error);
} finally {
setIsLoadingSettings(false);
setHasInitializedSettings(true);
} }
}, []); }, []);
@@ -639,21 +649,41 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
showLanguageLogin: boolean; showLanguageLogin: boolean;
}>) => { }>) => {
try { try {
await settingsAPI.updateTheme({ const payload: Record<string, string> = {};
theme_accent_color: overrides?.accentColor ?? accentColor,
theme_border_radius: overrides?.borderRadius ?? borderRadius, if (overrides) {
theme_sidebar_style: overrides?.sidebarStyle ?? sidebarStyle, if (overrides.accentColor !== undefined) payload.theme_accent_color = overrides.accentColor;
theme_density: overrides?.density ?? density, if (overrides.borderRadius !== undefined) payload.theme_border_radius = overrides.borderRadius;
theme_font_family: overrides?.fontFamily ?? fontFamily, if (overrides.sidebarStyle !== undefined) payload.theme_sidebar_style = overrides.sidebarStyle;
theme_color_palette: overrides?.colorPalette ?? colorPalette, if (overrides.density !== undefined) payload.theme_density = overrides.density;
theme_custom_colors: JSON.stringify(overrides?.customColors ?? customColors), if (overrides.fontFamily !== undefined) payload.theme_font_family = overrides.fontFamily;
theme_dark_mode_location: overrides?.darkModeLocation ?? darkModeLocation, if (overrides.colorPalette !== undefined) payload.theme_color_palette = overrides.colorPalette;
theme_language_location: overrides?.languageLocation ?? languageLocation, if (overrides.customColors !== undefined) payload.theme_custom_colors = JSON.stringify(overrides.customColors);
theme_show_dark_mode_toggle: String(overrides?.showDarkModeToggle ?? showDarkModeToggle), if (overrides.darkModeLocation !== undefined) payload.theme_dark_mode_location = overrides.darkModeLocation;
theme_show_language_toggle: String(overrides?.showLanguageToggle ?? showLanguageToggle), if (overrides.languageLocation !== undefined) payload.theme_language_location = overrides.languageLocation;
theme_show_dark_mode_login: String(overrides?.showDarkModeLogin ?? showDarkModeLogin), if (overrides.showDarkModeToggle !== undefined) payload.theme_show_dark_mode_toggle = String(overrides.showDarkModeToggle);
theme_show_language_login: String(overrides?.showLanguageLogin ?? showLanguageLogin), if (overrides.showLanguageToggle !== undefined) payload.theme_show_language_toggle = String(overrides.showLanguageToggle);
}); if (overrides.showDarkModeLogin !== undefined) payload.theme_show_dark_mode_login = String(overrides.showDarkModeLogin);
if (overrides.showLanguageLogin !== undefined) payload.theme_show_language_login = String(overrides.showLanguageLogin);
} else {
payload.theme_accent_color = accentColor;
payload.theme_border_radius = borderRadius;
payload.theme_sidebar_style = sidebarStyle;
payload.theme_density = density;
payload.theme_font_family = fontFamily;
payload.theme_color_palette = colorPalette;
payload.theme_custom_colors = JSON.stringify(customColors);
payload.theme_dark_mode_location = darkModeLocation;
payload.theme_language_location = languageLocation;
payload.theme_show_dark_mode_toggle = String(showDarkModeToggle);
payload.theme_show_language_toggle = String(showLanguageToggle);
payload.theme_show_dark_mode_login = String(showDarkModeLogin);
payload.theme_show_language_login = String(showLanguageLogin);
}
if (Object.keys(payload).length === 0) return;
await settingsAPI.updateTheme(payload);
} catch (error) { } catch (error) {
console.error('Failed to save theme to backend:', error); console.error('Failed to save theme to backend:', error);
throw error; throw error;
@@ -662,11 +692,14 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
// Auto-load theme from backend when token exists // Auto-load theme from backend when token exists
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token');
if (token) { if (token) {
setHasInitializedSettings(false);
loadThemeFromBackend(); loadThemeFromBackend();
} else {
setIsLoadingSettings(false);
setHasInitializedSettings(true);
} }
}, [loadThemeFromBackend]); }, [token, loadThemeFromBackend]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
@@ -741,6 +774,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
showLanguageToggle, showLanguageToggle,
showDarkModeLogin, showDarkModeLogin,
showLanguageLogin, showLanguageLogin,
hasInitializedSettings,
isLoadingSettings,
toggleTheme, toggleTheme,
setAccentColor, setAccentColor,
setBorderRadius, setBorderRadius,

View File

@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useEffect } from 'react'; import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useAuth } from './AuthContext';
type ViewMode = 'admin' | 'user'; type ViewMode = 'admin' | 'user';
@@ -14,6 +15,7 @@ interface ViewModeContextType {
const ViewModeContext = createContext<ViewModeContextType | undefined>(undefined); const ViewModeContext = createContext<ViewModeContextType | undefined>(undefined);
export function ViewModeProvider({ children }: { children: ReactNode }) { export function ViewModeProvider({ children }: { children: ReactNode }) {
const { token } = useAuth();
// viewMode is user preference - stored in localStorage // viewMode is user preference - stored in localStorage
const [viewMode, setViewModeState] = useState<ViewMode>(() => { const [viewMode, setViewModeState] = useState<ViewMode>(() => {
const saved = localStorage.getItem('viewMode'); const saved = localStorage.getItem('viewMode');
@@ -43,7 +45,6 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
// Save to backend (this is a global setting) // Save to backend (this is a global setting)
try { try {
const token = localStorage.getItem('token');
if (token) { if (token) {
await fetch('/api/v1/settings/user_mode_enabled', { await fetch('/api/v1/settings/user_mode_enabled', {
method: 'PUT', method: 'PUT',
@@ -63,13 +64,15 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
// Sync viewMode (user preference) with localStorage on mount // Sync viewMode (user preference) with localStorage on mount
const savedMode = localStorage.getItem('viewMode'); const savedMode = localStorage.getItem('viewMode');
if (savedMode) setViewModeState(savedMode as ViewMode); if (savedMode) setViewModeState(savedMode as ViewMode);
}, []);
// Fetch user_mode_enabled (global setting) from backend useEffect(() => {
// Fetch user_mode_enabled (global setting) from backend whenever token changes
const fetchUserModeSetting = async () => { const fetchUserModeSetting = async () => {
try { try {
const token = localStorage.getItem('token');
if (!token) { if (!token) {
// No token = use default (false) // No token = use default (false)
setUserModeEnabledState(false);
return; return;
} }
@@ -93,7 +96,7 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
}; };
fetchUserModeSetting(); fetchUserModeSetting();
}, []); }, [token]);
const value = { const value = {
viewMode, viewMode,

View File

@@ -13,7 +13,7 @@ export default function Login() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [isRegister, setIsRegister] = useState(false); const [isRegister, setIsRegister] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [registrationEnabled, setRegistrationEnabled] = useState(true); const [registrationEnabled, setRegistrationEnabled] = useState<boolean | null>(null);
const { login, register } = useAuth(); const { login, register } = useAuth();
const { t, language, setLanguage } = useTranslation(); const { t, language, setLanguage } = useTranslation();
const { theme, toggleTheme, showDarkModeLogin, showLanguageLogin, showDarkModeToggle, showLanguageToggle } = useTheme(); const { theme, toggleTheme, showDarkModeLogin, showLanguageLogin, showDarkModeToggle, showLanguageToggle } = useTheme();
@@ -22,23 +22,31 @@ export default function Login() {
// Check if registration is enabled // Check if registration is enabled
useEffect(() => { useEffect(() => {
let isMounted = true;
const checkRegistrationStatus = async () => { const checkRegistrationStatus = async () => {
try { try {
const response = await fetch('/api/v1/auth/registration-status'); const response = await fetch('/api/v1/auth/registration-status');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setRegistrationEnabled(data.registration_enabled !== false); const enabled = data.registration_enabled === true;
if (isMounted) {
setRegistrationEnabled(enabled);
if (!enabled) setIsRegister(false);
}
} else { } else {
// Default to enabled if we can't fetch the setting // Default to enabled if we can't fetch the setting
setRegistrationEnabled(true); if (isMounted) setRegistrationEnabled(true);
} }
} catch (error) { } catch (error) {
// Default to enabled if we can't fetch the setting // Default to enabled if we can't fetch the setting
setRegistrationEnabled(true); if (isMounted) setRegistrationEnabled(true);
} }
}; };
checkRegistrationStatus(); checkRegistrationStatus();
return () => {
isMounted = false;
};
}, []); }, []);
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
@@ -129,7 +137,7 @@ export default function Login() {
</form> </form>
<div className="login-footer"> <div className="login-footer">
{registrationEnabled && ( {registrationEnabled === true && (
<button <button
onClick={() => { onClick={() => {
setIsRegister(!isRegister); setIsRegister(!isRegister);

View File

@@ -13,7 +13,7 @@ export default function Features() {
const { user: currentUser } = useAuth(); const { user: currentUser } = useAuth();
const { t } = useTranslation(); const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar(); const { toggleMobileMenu } = useSidebar();
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized } = useModules(); const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized, isLoading } = useModules();
const [activeTab, setActiveTab] = useState<TabId>('feature1'); const [activeTab, setActiveTab] = useState<TabId>('feature1');
const hasUserMadeChanges = useRef(false); const hasUserMadeChanges = useRef(false);
const saveRef = useRef(saveModulesToBackend); const saveRef = useRef(saveModulesToBackend);
@@ -119,6 +119,9 @@ export default function Features() {
}; };
const renderTabContent = () => { const renderTabContent = () => {
if (!hasInitialized || isLoading) {
return <div className="loading">Loading...</div>;
}
switch (activeTab) { switch (activeTab) {
case 'feature1': case 'feature1':
return ( return (