diff --git a/src/App.tsx b/src/App.tsx index 0ae5832..d322e3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,63 +1,69 @@ import { ApolloProvider } from '@apollo/client/react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { ConfigProvider, theme } from 'antd'; +import { ConfigProvider } from 'antd'; import { client } from './lib/apollo-client'; import { useAuth } from './hooks/useAuth'; +import { ThemeProvider, useTheme } from './hooks/useTheme'; import { LoginPage } from './pages/LoginPage'; import { MainLayout } from './components/MainLayout'; import { ProtectedRoute } from './components/ProtectedRoute'; -function App() { +function AppContent() { const { isAuthenticated } = useAuth(); + const { themeConfig } = useTheme(); + + return ( + + + + : + } + /> + + + + } + /> + + + + } + /> + {/* Legacy dashboard route - redirect to new views structure */} + } + /> + } + /> + + + + ); +} +function App() { return ( - - - - : - } - /> - - - - } - /> - - - - } - /> - {/* Legacy dashboard route - redirect to new views structure */} - } - /> - } - /> - - - + + + ); } diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 8a1e0d9..03920f2 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -9,6 +9,7 @@ import { } from '@ant-design/icons'; import { useNavigate, useParams } from 'react-router-dom'; import { useAuth } from '../hooks/useAuth'; +import { ThemeSelector } from './ThemeSelector'; import { viewRegistry, getViewCategories, getViewsByCategory, getTopLevelViews } from '../registry/viewRegistry'; const { Header, Sider, Content } = Layout; @@ -188,6 +189,7 @@ export const MainLayout: React.FC = () => { + } /> {user?.name || user?.email || 'User'} diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx new file mode 100644 index 0000000..6428c53 --- /dev/null +++ b/src/components/ThemeSelector.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Select, Space, Tooltip } from 'antd'; +import { BgColorsOutlined } from '@ant-design/icons'; +import { useTheme, type ThemeType } from '../hooks/useTheme'; + +const { Option } = Select; + +export const ThemeSelector: React.FC = () => { + const { currentTheme, changeTheme, getAvailableThemes } = useTheme(); + const themes = getAvailableThemes(); + + const handleThemeChange = (value: ThemeType) => { + changeTheme(value); + }; + + return ( + + + + + + + ); +}; \ No newline at end of file diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 0000000..96dda2b --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { theme } from 'antd'; + +export type ThemeType = 'light' | 'dark' | 'compact'; + +export interface ThemeConfig { + type: ThemeType; + name: string; + algorithm: any; + token: { + colorPrimary: string; + borderRadius: number; + colorBgContainer: string; + colorBgElevated?: string; + colorText?: string; + colorTextSecondary?: string; + }; +} + +const themeConfigs: Record = { + light: { + type: 'light', + name: 'Light', + algorithm: theme.defaultAlgorithm, + token: { + colorPrimary: '#1890ff', + borderRadius: 8, + colorBgContainer: '#ffffff', + }, + }, + dark: { + type: 'dark', + name: 'Dark', + algorithm: theme.darkAlgorithm, + token: { + colorPrimary: '#1890ff', + borderRadius: 8, + colorBgContainer: '#141414', + colorBgElevated: '#1f1f1f', + }, + }, + compact: { + type: 'compact', + name: 'Compact', + algorithm: [theme.defaultAlgorithm, theme.compactAlgorithm], + token: { + colorPrimary: '#722ed1', + borderRadius: 4, + colorBgContainer: '#ffffff', + }, + }, +}; + +interface ThemeContextType { + currentTheme: ThemeType; + changeTheme: (newTheme: ThemeType) => void; + getThemeConfig: (themeType?: ThemeType) => ThemeConfig; + getAvailableThemes: () => ThemeConfig[]; + themeConfig: ThemeConfig; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [currentTheme, setCurrentTheme] = useState('light'); + + useEffect(() => { + // Load theme from localStorage on mount + const savedTheme = localStorage.getItem('app-theme') as ThemeType; + if (savedTheme && themeConfigs[savedTheme]) { + setCurrentTheme(savedTheme); + } + }, []); + + const changeTheme = (newTheme: ThemeType) => { + setCurrentTheme(newTheme); + localStorage.setItem('app-theme', newTheme); + }; + + const getThemeConfig = (themeType: ThemeType = currentTheme): ThemeConfig => { + return themeConfigs[themeType]; + }; + + const getAvailableThemes = (): ThemeConfig[] => { + return Object.values(themeConfigs); + }; + + const value = { + currentTheme, + changeTheme, + getThemeConfig, + getAvailableThemes, + themeConfig: getThemeConfig(), + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; \ No newline at end of file