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:
@@ -10,6 +10,7 @@ from app.models.user import User
|
||||
from app.core.settings_registry import (
|
||||
THEME_KEYS,
|
||||
MODULE_KEYS,
|
||||
UI_KEYS,
|
||||
SETTINGS_REGISTRY,
|
||||
get_default_value,
|
||||
)
|
||||
@@ -82,6 +83,47 @@ def get_module_settings(
|
||||
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])
|
||||
def update_module_settings(
|
||||
*,
|
||||
|
||||
@@ -46,7 +46,11 @@ import MainLayout from './components/MainLayout';
|
||||
// ...
|
||||
|
||||
function AppRoutes() {
|
||||
const { user } = useAuth();
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
@@ -72,9 +76,9 @@ function App() {
|
||||
return (
|
||||
<SiteConfigProvider>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<ModulesProvider>
|
||||
<ViewModeProvider>
|
||||
<SidebarProvider>
|
||||
@@ -82,9 +86,9 @@ function App() {
|
||||
</SidebarProvider>
|
||||
</ViewModeProvider>
|
||||
</ModulesProvider>
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</SiteConfigProvider>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import '../styles/Sidebar.css';
|
||||
export default function Sidebar() {
|
||||
const { t, language, setLanguage } = useTranslation();
|
||||
const { config } = useSiteConfig();
|
||||
const { sidebarStyle, theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle } = useTheme();
|
||||
const { sidebarStyle, theme, toggleTheme, darkModeLocation, languageLocation, showDarkModeToggle, showLanguageToggle, hasInitializedSettings: themeInitialized } = useTheme();
|
||||
const {
|
||||
isCollapsed,
|
||||
isMobileOpen,
|
||||
@@ -235,7 +235,7 @@ export default function Sidebar() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showDarkModeToggle && darkModeLocation === 'sidebar' && (
|
||||
{themeInitialized && showDarkModeToggle && darkModeLocation === 'sidebar' && (
|
||||
<button
|
||||
className="view-mode-toggle"
|
||||
onClick={() => { toggleTheme(); updateTooltipText(theme === 'dark' ? t.theme.lightMode : t.theme.darkMode); }}
|
||||
@@ -254,7 +254,7 @@ export default function Sidebar() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showLanguageToggle && languageLocation === 'sidebar' && (
|
||||
{themeInitialized && showLanguageToggle && languageLocation === 'sidebar' && (
|
||||
<button
|
||||
className="view-mode-toggle"
|
||||
onClick={() => { setLanguage(language === 'it' ? 'en' : 'it'); updateTooltipText(language === 'it' ? t.settings.english : t.settings.italian); }}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useViewMode } from '../contexts/ViewModeContext';
|
||||
export default function UserMenu({ onOpenChange }: { onOpenChange?: (isOpen: boolean) => void }) {
|
||||
const { user, logout } = useAuth();
|
||||
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 { viewMode } = useViewMode();
|
||||
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>{t.sidebar.settings}</span>
|
||||
</NavLink>
|
||||
{showDarkModeToggle && darkModeLocation === 'user_menu' && (
|
||||
{themeInitialized && showDarkModeToggle && darkModeLocation === 'user_menu' && (
|
||||
<button onClick={toggleTheme} className="user-menu-item">
|
||||
<span className="material-symbols-outlined">
|
||||
{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>
|
||||
</button>
|
||||
)}
|
||||
{showLanguageToggle && languageLocation === 'user_menu' && (
|
||||
{themeInitialized && showLanguageToggle && languageLocation === 'user_menu' && (
|
||||
<button onClick={toggleLanguage} className="user-menu-item">
|
||||
<span className="material-symbols-outlined">language</span>
|
||||
<span>{language === 'it' ? 'Italiano' : 'English'}</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { settingsAPI } from '../api/client';
|
||||
import { useAuth } from './AuthContext';
|
||||
import type { UserPermissions } from '../types';
|
||||
|
||||
// User-facing modules that can be toggled
|
||||
@@ -39,6 +40,7 @@ const getDefaultStates = (): Record<ModuleId, ModuleState> => {
|
||||
};
|
||||
|
||||
export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
const [moduleStates, setModuleStates] = useState<Record<ModuleId, ModuleState>>(getDefaultStates);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
@@ -110,16 +112,18 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [moduleStates]);
|
||||
|
||||
// Load modules on mount when token exists
|
||||
// Load modules when token becomes available
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setIsLoading(true);
|
||||
setHasInitialized(false);
|
||||
loadModulesFromBackend();
|
||||
} else {
|
||||
setModuleStates(getDefaultStates());
|
||||
setIsLoading(false);
|
||||
setHasInitialized(true); // No token, mark as initialized
|
||||
}
|
||||
}, [loadModulesFromBackend]);
|
||||
}, [token, loadModulesFromBackend]);
|
||||
|
||||
const isModuleEnabled = useCallback((moduleId: string): boolean => {
|
||||
// Dashboard is always enabled
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic';
|
||||
|
||||
@@ -21,6 +22,7 @@ interface SidebarContextType {
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
|
||||
export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
const [userCollapsed, setUserCollapsed] = useState(true);
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle');
|
||||
@@ -31,19 +33,29 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
const loadSidebarMode = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
if (!token) {
|
||||
setSidebarModeState('toggle');
|
||||
setShowLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/settings', {
|
||||
headers: { 'Authorization': `Bearer ${token} ` }
|
||||
const response = await fetch('/api/v1/settings/ui', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
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)) {
|
||||
setSidebarModeState(data.sidebar_mode as SidebarMode);
|
||||
}
|
||||
if (data.show_logo !== undefined) {
|
||||
setShowLogo(data.show_logo === true);
|
||||
setShowLogo(parseBool(data.show_logo, false));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -51,7 +63,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
loadSidebarMode();
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
// Compute isCollapsed based on mode
|
||||
const isCollapsed = sidebarMode === 'collapsed' ? true :
|
||||
@@ -78,11 +90,11 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const setSidebarMode = useCallback(async (mode: SidebarMode) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const response = await fetch('/api/v1/settings/sidebar_mode', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token} `,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ value: mode }),
|
||||
@@ -94,7 +106,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
} catch (error) {
|
||||
console.error('Failed to save sidebar mode:', error);
|
||||
}
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { settingsAPI } from '../api/client';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
export type AccentColor = 'blue' | 'purple' | 'green' | 'orange' | 'pink' | 'red' | 'teal' | 'amber' | 'indigo' | 'cyan' | 'rose' | 'auto';
|
||||
@@ -27,6 +28,8 @@ interface ThemeContextType {
|
||||
showLanguageToggle: boolean;
|
||||
showDarkModeLogin: boolean;
|
||||
showLanguageLogin: boolean;
|
||||
hasInitializedSettings: boolean;
|
||||
isLoadingSettings: boolean;
|
||||
toggleTheme: () => void;
|
||||
setAccentColor: (color: AccentColor) => void;
|
||||
setBorderRadius: (radius: BorderRadius) => void;
|
||||
@@ -433,6 +436,7 @@ export const COLOR_PALETTES: Record<ColorPalette, PaletteColors> = {
|
||||
};
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
return (localStorage.getItem('theme') as Theme) || 'light';
|
||||
});
|
||||
@@ -454,6 +458,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [showLanguageToggle, setShowLanguageToggleState] = useState<boolean>(false);
|
||||
const [showDarkModeLogin, setShowDarkModeLoginState] = useState<boolean>(true);
|
||||
const [showLanguageLogin, setShowLanguageLoginState] = useState<boolean>(false);
|
||||
const [hasInitializedSettings, setHasInitializedSettings] = useState<boolean>(false);
|
||||
const [isLoadingSettings, setIsLoadingSettings] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
@@ -565,6 +571,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Load theme from backend when user is authenticated
|
||||
const loadThemeFromBackend = useCallback(async () => {
|
||||
setIsLoadingSettings(true);
|
||||
try {
|
||||
const themeData = await settingsAPI.getTheme();
|
||||
if (themeData.theme_accent_color) {
|
||||
@@ -619,6 +626,9 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
} catch (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;
|
||||
}>) => {
|
||||
try {
|
||||
await settingsAPI.updateTheme({
|
||||
theme_accent_color: overrides?.accentColor ?? accentColor,
|
||||
theme_border_radius: overrides?.borderRadius ?? borderRadius,
|
||||
theme_sidebar_style: overrides?.sidebarStyle ?? sidebarStyle,
|
||||
theme_density: overrides?.density ?? density,
|
||||
theme_font_family: overrides?.fontFamily ?? fontFamily,
|
||||
theme_color_palette: overrides?.colorPalette ?? colorPalette,
|
||||
theme_custom_colors: JSON.stringify(overrides?.customColors ?? customColors),
|
||||
theme_dark_mode_location: overrides?.darkModeLocation ?? darkModeLocation,
|
||||
theme_language_location: overrides?.languageLocation ?? languageLocation,
|
||||
theme_show_dark_mode_toggle: String(overrides?.showDarkModeToggle ?? showDarkModeToggle),
|
||||
theme_show_language_toggle: String(overrides?.showLanguageToggle ?? showLanguageToggle),
|
||||
theme_show_dark_mode_login: String(overrides?.showDarkModeLogin ?? showDarkModeLogin),
|
||||
theme_show_language_login: String(overrides?.showLanguageLogin ?? showLanguageLogin),
|
||||
});
|
||||
const payload: Record<string, string> = {};
|
||||
|
||||
if (overrides) {
|
||||
if (overrides.accentColor !== undefined) payload.theme_accent_color = overrides.accentColor;
|
||||
if (overrides.borderRadius !== undefined) payload.theme_border_radius = overrides.borderRadius;
|
||||
if (overrides.sidebarStyle !== undefined) payload.theme_sidebar_style = overrides.sidebarStyle;
|
||||
if (overrides.density !== undefined) payload.theme_density = overrides.density;
|
||||
if (overrides.fontFamily !== undefined) payload.theme_font_family = overrides.fontFamily;
|
||||
if (overrides.colorPalette !== undefined) payload.theme_color_palette = overrides.colorPalette;
|
||||
if (overrides.customColors !== undefined) payload.theme_custom_colors = JSON.stringify(overrides.customColors);
|
||||
if (overrides.darkModeLocation !== undefined) payload.theme_dark_mode_location = overrides.darkModeLocation;
|
||||
if (overrides.languageLocation !== undefined) payload.theme_language_location = overrides.languageLocation;
|
||||
if (overrides.showDarkModeToggle !== undefined) payload.theme_show_dark_mode_toggle = String(overrides.showDarkModeToggle);
|
||||
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) {
|
||||
console.error('Failed to save theme to backend:', error);
|
||||
throw error;
|
||||
@@ -662,11 +692,14 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Auto-load theme from backend when token exists
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setHasInitializedSettings(false);
|
||||
loadThemeFromBackend();
|
||||
} else {
|
||||
setIsLoadingSettings(false);
|
||||
setHasInitializedSettings(true);
|
||||
}
|
||||
}, [loadThemeFromBackend]);
|
||||
}, [token, loadThemeFromBackend]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
@@ -741,6 +774,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
showLanguageToggle,
|
||||
showDarkModeLogin,
|
||||
showLanguageLogin,
|
||||
hasInitializedSettings,
|
||||
isLoadingSettings,
|
||||
toggleTheme,
|
||||
setAccentColor,
|
||||
setBorderRadius,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
type ViewMode = 'admin' | 'user';
|
||||
|
||||
@@ -14,6 +15,7 @@ interface ViewModeContextType {
|
||||
const ViewModeContext = createContext<ViewModeContextType | undefined>(undefined);
|
||||
|
||||
export function ViewModeProvider({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
// viewMode is user preference - stored in localStorage
|
||||
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
|
||||
const saved = localStorage.getItem('viewMode');
|
||||
@@ -43,7 +45,6 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Save to backend (this is a global setting)
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
await fetch('/api/v1/settings/user_mode_enabled', {
|
||||
method: 'PUT',
|
||||
@@ -63,13 +64,15 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
|
||||
// Sync viewMode (user preference) with localStorage on mount
|
||||
const savedMode = localStorage.getItem('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 () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
// No token = use default (false)
|
||||
setUserModeEnabledState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
fetchUserModeSetting();
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
const value = {
|
||||
viewMode,
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState(true);
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState<boolean | null>(null);
|
||||
const { login, register } = useAuth();
|
||||
const { t, language, setLanguage } = useTranslation();
|
||||
const { theme, toggleTheme, showDarkModeLogin, showLanguageLogin, showDarkModeToggle, showLanguageToggle } = useTheme();
|
||||
@@ -22,23 +22,31 @@ export default function Login() {
|
||||
|
||||
// Check if registration is enabled
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const checkRegistrationStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/registration-status');
|
||||
if (response.ok) {
|
||||
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 {
|
||||
// Default to enabled if we can't fetch the setting
|
||||
setRegistrationEnabled(true);
|
||||
if (isMounted) setRegistrationEnabled(true);
|
||||
}
|
||||
} catch (error) {
|
||||
// Default to enabled if we can't fetch the setting
|
||||
setRegistrationEnabled(true);
|
||||
if (isMounted) setRegistrationEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkRegistrationStatus();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
@@ -129,7 +137,7 @@ export default function Login() {
|
||||
</form>
|
||||
|
||||
<div className="login-footer">
|
||||
{registrationEnabled && (
|
||||
{registrationEnabled === true && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRegister(!isRegister);
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized } = useModules();
|
||||
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('feature1');
|
||||
const hasUserMadeChanges = useRef(false);
|
||||
const saveRef = useRef(saveModulesToBackend);
|
||||
@@ -119,6 +119,9 @@ export default function Features() {
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
if (!hasInitialized || isLoading) {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
switch (activeTab) {
|
||||
case 'feature1':
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user