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 -3
View File
@@ -52,14 +52,16 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)}
disabled={disabled || isLoading}
{...props}
aria-busy={isLoading}
aria-disabled={disabled || isLoading}
>
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-label="Loading" />
) : (
leftIcon && <span className="mr-2">{leftIcon}</span>
leftIcon && <span className="mr-2" aria-hidden="true">{leftIcon}</span>
)}
{children}
{!isLoading && rightIcon && <span className="ml-2">{rightIcon}</span>}
{!isLoading && rightIcon && <span className="ml-2" aria-hidden="true">{rightIcon}</span>}
</button>
);
}
@@ -0,0 +1,132 @@
import React, { Component } from 'react';
import type { ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { Button } from './Button';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
window.location.reload();
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return <>{this.props.fallback}</>;
}
return (
<div className="min-h-[400px] flex items-center justify-center p-4">
<div className="text-center max-w-md">
<AlertTriangle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h2 className="text-lg font-semibold text-neutral-900 mb-2">
Something went wrong
</h2>
<p className="text-sm text-neutral-600 mb-6">
{this.state.error?.message || 'An unexpected error occurred. Please try refreshing the page.'}
</p>
<Button
onClick={this.handleReset}
leftIcon={<RefreshCw className="w-4 h-4" />}
>
Refresh Page
</Button>
</div>
</div>
);
}
return this.props.children;
}
}
// Page-level error boundary with more prominent UI
export class PageErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Page error:', error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-6" />
<h1 className="text-2xl font-bold text-neutral-900 mb-4">
Oops! Something went wrong
</h1>
<p className="text-neutral-600 mb-8">
We encountered an unexpected error. Don't worry, your data is safe.
</p>
<div className="space-y-3">
<Button
variant="primary"
onClick={this.handleReset}
leftIcon={<RefreshCw className="w-4 h-4" />}
className="w-full"
>
Go to Homepage
</Button>
<Button
variant="outline"
onClick={() => window.location.reload()}
className="w-full"
>
Try Again
</Button>
</div>
{import.meta.env.DEV && this.state.error && (
<details className="mt-8 text-left">
<summary className="text-sm text-neutral-500 cursor-pointer hover:text-neutral-700">
Error Details
</summary>
<pre className="mt-2 text-xs bg-neutral-100 p-3 rounded overflow-auto">
{this.state.error.stack}
</pre>
</details>
)}
</div>
</div>
);
}
return this.props.children;
}
}
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { WifiOff, Wifi } from 'lucide-react';
import { cn } from '../../lib/utils';
export const OfflineIndicator: React.FC = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [showIndicator, setShowIndicator] = useState(false);
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
// Show "back online" message briefly
setShowIndicator(true);
setTimeout(() => setShowIndicator(false), 3000);
};
const handleOffline = () => {
setIsOnline(false);
setShowIndicator(true);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Check initial state
if (!navigator.onLine) {
setShowIndicator(true);
}
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (!showIndicator) return null;
return (
<div
className={cn(
'fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-auto z-50',
'transition-all duration-300 ease-in-out',
isOnline ? 'translate-y-0' : 'translate-y-0'
)}
role="status"
aria-live="polite"
>
<div
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg',
isOnline
? 'bg-green-50 border border-green-200 text-green-900'
: 'bg-red-50 border border-red-200 text-red-900'
)}
>
{isOnline ? (
<>
<Wifi className="w-5 h-5" />
<span className="text-sm font-medium">Back online</span>
</>
) : (
<>
<WifiOff className="w-5 h-5" />
<span className="text-sm font-medium">No internet connection</span>
</>
)}
</div>
</div>
);
};
// Hook to monitor online status
export const useOnlineStatus = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleStatusChange = () => {
setIsOnline(navigator.onLine);
};
window.addEventListener('online', handleStatusChange);
window.addEventListener('offline', handleStatusChange);
return () => {
window.removeEventListener('online', handleStatusChange);
window.removeEventListener('offline', handleStatusChange);
};
}, []);
return isOnline;
};
+147
View File
@@ -0,0 +1,147 @@
import React from 'react';
import { cn } from '../../lib/utils';
interface SkeletonProps {
className?: string;
variant?: 'text' | 'circular' | 'rectangular';
width?: string | number;
height?: string | number;
animation?: 'pulse' | 'wave' | 'none';
}
export const Skeleton: React.FC<SkeletonProps> = ({
className,
variant = 'rectangular',
width,
height,
animation = 'pulse'
}) => {
const baseClasses = 'bg-neutral-200';
const animationClasses = {
pulse: 'animate-pulse',
wave: 'animate-shimmer',
none: ''
};
const variantClasses = {
text: 'rounded',
circular: 'rounded-full',
rectangular: 'rounded-lg'
};
const style: React.CSSProperties = {};
if (width) style.width = typeof width === 'number' ? `${width}px` : width;
if (height) style.height = typeof height === 'number' ? `${height}px` : height;
return (
<div
className={cn(
baseClasses,
animationClasses[animation],
variantClasses[variant],
className
)}
style={style}
aria-busy="true"
aria-live="polite"
/>
);
};
// Skeleton group for consistent loading states
interface SkeletonGroupProps {
count?: number;
className?: string;
children?: React.ReactNode;
}
export const SkeletonGroup: React.FC<SkeletonGroupProps> = ({
count = 1,
className,
children
}) => {
if (children) {
return <div className={cn('space-y-3', className)}>{children}</div>;
}
return (
<div className={cn('space-y-3', className)}>
{Array.from({ length: count }).map((_, index) => (
<Skeleton key={index} height={20} />
))}
</div>
);
};
// Common skeleton patterns
export const SkeletonCard: React.FC<{ className?: string }> = ({ className }) => (
<div className={cn('bg-white rounded-lg shadow-sm p-6', className)}>
<Skeleton height={24} width="60%" className="mb-4" />
<SkeletonGroup count={3} />
<div className="flex gap-3 mt-6">
<Skeleton width={100} height={36} />
<Skeleton width={100} height={36} />
</div>
</div>
);
export const SkeletonTable: React.FC<{ rows?: number; className?: string }> = ({
rows = 5,
className
}) => (
<div className={cn('bg-white rounded-lg shadow-sm overflow-hidden', className)}>
<div className="border-b border-neutral-200 p-4">
<div className="flex gap-4">
<Skeleton width="30%" height={20} />
<Skeleton width="25%" height={20} />
<Skeleton width="20%" height={20} />
<Skeleton width="25%" height={20} />
</div>
</div>
<div className="divide-y divide-neutral-100">
{Array.from({ length: rows }).map((_, index) => (
<div key={index} className="p-4">
<div className="flex gap-4">
<Skeleton width="30%" height={16} />
<Skeleton width="25%" height={16} />
<Skeleton width="20%" height={16} />
<Skeleton width="25%" height={16} />
</div>
</div>
))}
</div>
</div>
);
export const SkeletonGalleryGrid: React.FC<{ count?: number; className?: string }> = ({
count = 12,
className
}) => (
<div className={cn('gallery-grid', className)}>
{Array.from({ length: count }).map((_, index) => (
<Skeleton
key={index}
variant="rectangular"
className="aspect-square w-full"
/>
))}
</div>
);
export const SkeletonList: React.FC<{ count?: number; className?: string }> = ({
count = 5,
className
}) => (
<div className={cn('space-y-4', className)}>
{Array.from({ length: count }).map((_, index) => (
<div key={index} className="flex items-center gap-4">
<Skeleton variant="circular" width={48} height={48} />
<div className="flex-1">
<Skeleton height={20} width="70%" className="mb-2" />
<Skeleton height={16} width="40%" />
</div>
</div>
))}
</div>
);
@@ -0,0 +1,12 @@
import React from 'react';
export const SkipLink: React.FC = () => {
return (
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-primary-600 text-white px-4 py-2 rounded-lg z-50 focus:outline-none focus:ring-2 focus:ring-primary-700"
>
Skip to main content
</a>
);
};
+5 -1
View File
@@ -1,4 +1,8 @@
export { Button } from './Button';
export { Input } from './Input';
export { Card, CardHeader, CardContent, CardFooter } from './Card';
export { Loading, LoadingSkeleton } from './Loading';
export { Loading, LoadingSkeleton } from './Loading';
export * from './ErrorBoundary';
export * from './Skeleton';
export * from './OfflineIndicator';
export * from './SkipLink';