diff --git a/backend/app/core/user_initializer/initializers/settings_initializer.py b/backend/app/core/user_initializer/initializers/settings_initializer.py index 6f550d3..fd24209 100644 --- a/backend/app/core/user_initializer/initializers/settings_initializer.py +++ b/backend/app/core/user_initializer/initializers/settings_initializer.py @@ -54,6 +54,30 @@ class SettingsInitializer(UserInitializerBase): "is_multiple": False, "tags": '["auto_generated", "ui"]' }, + { + "id": "branding_logo_settings", + "name": "Branding Logo Settings", + "description": "Light/dark logo URLs and alt text", + "category": "ui", + "type": "object", + "default_value": '{"light": "/braindrive/braindrive-light.svg", "dark": "/braindrive/braindrive-dark.svg", "alt": "BrainDrive"}', + "allowed_scopes": '["system", "user"]', + "validation": None, + "is_multiple": False, + "tags": '["auto_generated", "ui"]' + }, + { + "id": "copyright_settings", + "name": "Copyright", + "description": "Footer copyright line content", + "category": "ui", + "type": "object", + "default_value": '{"text": "© 2025 BrainDrive"}', + "allowed_scopes": '["system", "user"]', + "validation": None, + "is_multiple": False, + "tags": '["auto_generated", "ui"]' + }, { "id": "ollama_servers_settings", "name": "Ollama Servers Settings", @@ -96,6 +120,20 @@ class SettingsInitializer(UserInitializerBase): "scope": "user", "page_id": None }, + { + "definition_id": "branding_logo_settings", + "name": "Branding Logo Settings", + "value": '{"light": "/braindrive/braindrive-light.svg", "dark": "/braindrive/braindrive-dark.svg", "alt": "BrainDrive"}', + "scope": "user", + "page_id": None + }, + { + "definition_id": "copyright_settings", + "name": "Copyright", + "value": '{"text": "© 2025 BrainDrive"}', + "scope": "user", + "page_id": None + }, { "definition_id": "ollama_servers_settings", "name": "Ollama Servers Settings", diff --git a/frontend/src/components/dashboard/DashboardLayout.tsx b/frontend/src/components/dashboard/DashboardLayout.tsx index 11d5214..35ae63b 100644 --- a/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/frontend/src/components/dashboard/DashboardLayout.tsx @@ -4,6 +4,7 @@ import { Outlet } from 'react-router-dom'; import Header from './Header'; import Sidebar from './Sidebar'; import { ThemeSelector } from '../ThemeSelector'; +import { useSettings } from '../../contexts/ServiceContext'; const DRAWER_WIDTH = 240; @@ -11,12 +12,49 @@ const DashboardLayout = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + const settingsService = useSettings(); + const defaultCopyright = { text: '© 2025 BrainDrive' }; + const [copyright, setCopyright] = useState(defaultCopyright); // Update sidebar state when screen size changes useEffect(() => { setSidebarOpen(!isMobile); }, [isMobile]); + // Load copyright setting + useEffect(() => { + let active = true; + (async () => { + try { + const value = await settingsService.getSetting('copyright_settings'); + if (!active) return; + if (value) { + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (parsed && parsed.text) { + // Only update if we have text; else keep default + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + setCopyright({ text: parsed.text }); + } + } catch { + // Ignore parse errors, keep default + } + } else if (typeof value === 'object' && value.text) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + setCopyright({ text: value.text }); + } + } + } catch { + // Keep default on error + } + })(); + return () => { + active = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleToggleSidebar = () => { setSidebarOpen(!sidebarOpen); }; @@ -39,6 +77,8 @@ const DashboardLayout = () => { flexGrow: 1, p: { xs: 1, sm: 2 }, width: '100%', + display: 'flex', + flexDirection: 'column', marginLeft: { xs: 0, sm: sidebarOpen ? 0 : `-${DRAWER_WIDTH}px` @@ -49,15 +89,29 @@ const DashboardLayout = () => { }), }} > - {/* This creates space for the header */} + {/* Spacer for header */} + + {copyright.text} + ); diff --git a/frontend/src/components/dashboard/Header.tsx b/frontend/src/components/dashboard/Header.tsx index c5a3655..0f2fc06 100644 --- a/frontend/src/components/dashboard/Header.tsx +++ b/frontend/src/components/dashboard/Header.tsx @@ -15,6 +15,7 @@ import MenuIcon from '@mui/icons-material/Menu'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import { useAuth } from '../../contexts/AuthContext'; import { useLocation } from 'react-router-dom'; +import { useSettings } from '../../contexts/ServiceContext'; // Declare a global interface for the Window object declare global { @@ -35,6 +36,15 @@ const Header = ({ onToggleSidebar, rightContent, sidebarOpen }: HeaderProps) => const theme = useTheme(); const { user, logout } = useAuth(); const location = useLocation(); + const settingsService = useSettings(); + + type BrandingLogo = { light: string; dark: string; alt?: string }; + const defaultBranding: BrandingLogo = { + light: '/braindrive/braindrive-light.svg', + dark: '/braindrive/braindrive-dark.svg', + alt: 'BrainDrive', + }; + const [branding, setBranding] = useState(defaultBranding); // State to track the global variables const [pageTitle, setPageTitle] = useState(''); @@ -94,6 +104,45 @@ const Header = ({ onToggleSidebar, rightContent, sidebarOpen }: HeaderProps) => return () => clearInterval(intervalId); }, [pageTitle, isStudioPage, location.pathname]); + // Load branding logo settings + useEffect(() => { + let active = true; + (async () => { + try { + const value = await settingsService.getSetting('branding_logo_settings'); + if (!active) return; + if (value) { + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + setBranding({ + light: parsed?.light || defaultBranding.light, + dark: parsed?.dark || defaultBranding.dark, + alt: parsed?.alt || defaultBranding.alt, + }); + } catch { + setBranding(defaultBranding); + } + } else if (typeof value === 'object') { + setBranding({ + light: value?.light || defaultBranding.light, + dark: value?.dark || defaultBranding.dark, + alt: value?.alt || defaultBranding.alt, + }); + } + } else { + setBranding(defaultBranding); + } + } catch { + setBranding(defaultBranding); + } + })(); + return () => { + active = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleMenu = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -131,11 +180,8 @@ const Header = ({ onToggleSidebar, rightContent, sidebarOpen }: HeaderProps) => }} >