Implement complete frontend with admin panel and theme system

- Add admin authentication and dashboard
- Create event management pages (list, create, edit, archive)
- Implement gallery enhancements (search, sorting, bulk download)
- Add email configuration and archive management pages
- Integrate Umami analytics with tracking throughout the app
- Add comprehensive error boundaries and loading states
- Implement accessibility features (WCAG 2.1 AA compliance)
- Create theme system with preset themes and customization
- Add branding settings and company information management
- Fix backend database initialization and health check
- Configure proper API URLs and environment variables

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-06 22:04:45 +02:00
parent 6c82958c79
commit 28632e8970
53 changed files with 13843 additions and 181 deletions
+5 -14
View File
@@ -7,7 +7,7 @@ import type { AdminUser } from '../types';
interface AdminAuthContextType {
isAuthenticated: boolean;
user: AdminUser | null;
login: (username: string, password: string) => Promise<void>;
login: (token: string, user: AdminUser) => void;
logout: () => void;
isLoading: boolean;
error: string | null;
@@ -43,19 +43,10 @@ export const AdminAuthProvider: React.FC<AdminAuthProviderProps> = ({ children }
setIsLoading(false);
}, []);
const login = async (username: string, password: string) => {
try {
setError(null);
setIsLoading(true);
const response = await authService.adminLogin(username, password);
setUser(response.user);
setIsAuthenticated(true);
} catch (err: any) {
setError(err.response?.data?.error || 'Invalid credentials');
throw err;
} finally {
setIsLoading(false);
}
const login = (_token: string, user: AdminUser) => {
setUser(user);
setIsAuthenticated(true);
setError(null);
};
const logout = () => {
+233
View File
@@ -0,0 +1,233 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
export interface ThemeConfig {
primaryColor?: string;
accentColor?: string;
backgroundColor?: string;
textColor?: string;
fontFamily?: string;
borderRadius?: 'none' | 'sm' | 'md' | 'lg';
logoUrl?: string;
customCss?: string;
}
export interface EventTheme {
name: string;
config: ThemeConfig;
}
// Predefined themes
export const PRESET_THEMES: Record<string, EventTheme> = {
default: {
name: 'Default',
config: {
primaryColor: '#5C8762',
accentColor: '#22c55e',
backgroundColor: '#fafafa',
textColor: '#171717',
borderRadius: 'md',
}
},
wedding: {
name: 'Wedding',
config: {
primaryColor: '#c9a961',
accentColor: '#e6ddd4',
backgroundColor: '#fdfcfb',
textColor: '#3f3f3f',
borderRadius: 'lg',
fontFamily: 'Georgia, serif',
}
},
birthday: {
name: 'Birthday',
config: {
primaryColor: '#ec4899',
accentColor: '#fbbf24',
backgroundColor: '#fef3c7',
textColor: '#451a03',
borderRadius: 'lg',
}
},
corporate: {
name: 'Corporate',
config: {
primaryColor: '#3b82f6',
accentColor: '#1e40af',
backgroundColor: '#f8fafc',
textColor: '#0f172a',
borderRadius: 'sm',
fontFamily: 'Inter, sans-serif',
}
},
minimal: {
name: 'Minimal',
config: {
primaryColor: '#000000',
accentColor: '#666666',
backgroundColor: '#ffffff',
textColor: '#000000',
borderRadius: 'none',
fontFamily: 'Helvetica, Arial, sans-serif',
}
}
};
interface ThemeContextType {
theme: ThemeConfig;
themeName: string;
setTheme: (theme: ThemeConfig) => void;
setThemeByName: (themeName: string) => void;
applyTheme: (theme: ThemeConfig) => void;
resetTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
interface ThemeProviderProps {
children: ReactNode;
initialTheme?: ThemeConfig;
initialThemeName?: string;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
children,
initialTheme = PRESET_THEMES.default.config,
initialThemeName = 'default'
}) => {
const [theme, setTheme] = useState<ThemeConfig>(initialTheme);
const [themeName, setThemeName] = useState(initialThemeName);
const applyTheme = (themeConfig: ThemeConfig) => {
const root = document.documentElement;
// Apply CSS variables
if (themeConfig.primaryColor) {
root.style.setProperty('--color-primary', themeConfig.primaryColor);
// Generate primary color shades
root.style.setProperty('--color-primary-light', lightenColor(themeConfig.primaryColor, 20));
root.style.setProperty('--color-primary-dark', darkenColor(themeConfig.primaryColor, 20));
}
if (themeConfig.accentColor) {
root.style.setProperty('--color-accent', themeConfig.accentColor);
}
if (themeConfig.backgroundColor) {
root.style.setProperty('--color-background', themeConfig.backgroundColor);
}
if (themeConfig.textColor) {
root.style.setProperty('--color-text', themeConfig.textColor);
}
if (themeConfig.fontFamily) {
root.style.setProperty('--font-family', themeConfig.fontFamily);
}
if (themeConfig.borderRadius) {
const radiusMap = {
none: '0',
sm: '0.25rem',
md: '0.5rem',
lg: '1rem',
};
root.style.setProperty('--border-radius', radiusMap[themeConfig.borderRadius]);
}
// Apply custom CSS if provided
if (themeConfig.customCss) {
let styleElement = document.getElementById('custom-theme-styles');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-styles';
document.head.appendChild(styleElement);
}
styleElement.textContent = themeConfig.customCss;
}
};
const setThemeByName = (name: string) => {
const presetTheme = PRESET_THEMES[name];
if (presetTheme) {
setThemeName(name);
setTheme(presetTheme.config);
applyTheme(presetTheme.config);
}
};
const resetTheme = () => {
setThemeByName('default');
};
useEffect(() => {
applyTheme(theme);
}, [theme]);
// Load theme from localStorage on mount
useEffect(() => {
const savedTheme = localStorage.getItem('gallery-theme');
if (savedTheme) {
try {
const parsed = JSON.parse(savedTheme);
setTheme(parsed.config);
setThemeName(parsed.name);
} catch (e) {
console.error('Failed to load saved theme:', e);
}
}
}, []);
// Save theme to localStorage when it changes
useEffect(() => {
localStorage.setItem('gallery-theme', JSON.stringify({ name: themeName, config: theme }));
}, [theme, themeName]);
return (
<ThemeContext.Provider value={{
theme,
themeName,
setTheme: (newTheme) => {
setTheme(newTheme);
applyTheme(newTheme);
},
setThemeByName,
applyTheme,
resetTheme
}}>
{children}
</ThemeContext.Provider>
);
};
// Utility functions for color manipulation
function lightenColor(color: string, percent: number): string {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
}
function darkenColor(color: string, percent: number): string {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = (num >> 8 & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return '#' + (0x1000000 + (R > 0 ? R : 0) * 0x10000 +
(G > 0 ? G : 0) * 0x100 +
(B > 0 ? B : 0)).toString(16).slice(1);
}
+3 -1
View File
@@ -1,2 +1,4 @@
export { GalleryAuthProvider, useGalleryAuth } from './GalleryAuthContext';
export { AdminAuthProvider, useAdminAuth } from './AdminAuthContext';
export { AdminAuthProvider, useAdminAuth } from './AdminAuthContext';
export { ThemeProvider, useTheme, PRESET_THEMES } from './ThemeContext';
export type { ThemeConfig, EventTheme } from './ThemeContext';