diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index bd768c7..7cbe94a 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,44 +1,71 @@ +import { useState } from 'react'; import { Link } from '@tanstack/react-router'; import { useAuth } from '../../contexts/AuthContext'; import { Button } from '../ui/button'; +import { ThemeToggle } from './ThemeToggle'; +import { Menu, X } from 'lucide-react'; export function Header() { const { user, logout, isAuthenticated, hasRole } = useAuth(); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + + const closeMobileMenu = () => { + setIsMobileMenuOpen(false); + }; + + const navLinks = [ + { to: '/', label: 'Home' }, + { to: '/articles', label: 'Articles' }, + { to: '/live-blogs', label: 'Live' }, + ]; + + const adminLinks = [ + { to: '/admin', label: 'Admin' }, + { to: '/admin/live-blogs/create', label: '+ New Live Blog' }, + ]; return ( -
+
-

+

Placebo.mk

-
+ + {/* Mobile Navigation */} + {isMobileMenuOpen && ( +
+
+ {navLinks.map((link) => ( + + {link.label} + + ))} + + {isAuthenticated ? ( + <> + {(hasRole('admin') || hasRole('contributor')) && ( +
+

Admin

+ {adminLinks.map((link) => ( + + {link.label} + + ))} +
+ )} + +
+
+ + Logged in as: {user?.username} + + +
+
+ + ) : ( + + Login / Register + + )} +
+
+ )}
); diff --git a/frontend/src/components/layout/ThemeToggle.tsx b/frontend/src/components/layout/ThemeToggle.tsx new file mode 100644 index 0000000..1aabce0 --- /dev/null +++ b/frontend/src/components/layout/ThemeToggle.tsx @@ -0,0 +1,38 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Sun, Moon } from 'lucide-react'; +import { getInitialTheme, toggleTheme, watchSystemTheme } from '@/lib/theme'; + +export function ThemeToggle() { + const [theme, setTheme] = useState<'light' | 'dark'>(getInitialTheme); + + useEffect(() => { + // Watch for system theme changes + const cleanup = watchSystemTheme((newTheme) => { + setTheme(newTheme); + }); + + return cleanup; + }, []); + + const handleToggle = () => { + const newTheme = toggleTheme(); + setTheme(newTheme); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts new file mode 100644 index 0000000..da0b897 --- /dev/null +++ b/frontend/src/lib/theme.ts @@ -0,0 +1,79 @@ +/** + * Theme utilities for dark/light mode + */ + +export type Theme = 'light' | 'dark'; + +/** + * Get the current theme from localStorage or system preference + */ +export function getInitialTheme(): Theme { + // Check localStorage first + const savedTheme = localStorage.getItem('theme') as Theme; + if (savedTheme) { + return savedTheme; + } + + // Check system preference + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + + // Default to light + return 'light'; +} + +/** + * Apply theme to document + */ +export function applyTheme(theme: Theme): void { + const root = document.documentElement; + + if (theme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + + // Save to localStorage + localStorage.setItem('theme', theme); +} + +/** + * Initialize theme on page load + */ +export function initializeTheme(): void { + const theme = getInitialTheme(); + applyTheme(theme); +} + +/** + * Toggle between light and dark themes + */ +export function toggleTheme(): Theme { + const currentTheme = getInitialTheme(); + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + applyTheme(newTheme); + return newTheme; +} + +/** + * Listen for system theme changes + */ +export function watchSystemTheme(callback: (theme: Theme) => void): () => void { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = (e: MediaQueryListEvent) => { + // Only update if user hasn't set a preference in localStorage + if (!localStorage.getItem('theme')) { + const theme = e.matches ? 'dark' : 'light'; + callback(theme); + applyTheme(theme); + } + }; + + mediaQuery.addEventListener('change', handleChange); + + // Return cleanup function + return () => mediaQuery.removeEventListener('change', handleChange); +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 81e5307..fdc386f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,10 @@ import { RouterProvider } from '@tanstack/react-router' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { router } from './routes' import { AuthProvider } from './contexts/AuthContext' +import { initializeTheme } from './lib/theme' + +// Initialize theme before rendering +initializeTheme(); const queryClient = new QueryClient()