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:
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user