Fix brand theme application and add comprehensive translations

- Fixed theme not being reflected on gallery and admin login pages
- Created GlobalThemeProvider to apply themes globally
- Updated gallery and admin login pages to use dynamic CSS variables
- Added complete translations for all admin sections in English and German:
  - Notifications management
  - Event view and creation
  - Photo upload functionality
  - Category management
  - Archive page view
  - Analytics dashboard
  - Branding and theme settings
  - System settings
  - CMS page management
  - Email configuration
- Fixed admin photo management display issues
- Fixed photo upload category assignment
- Added password reset functionality for galleries
- Improved error handling and user feedback

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-08 17:07:40 +02:00
parent 2012b0bab9
commit d594d00227
79 changed files with 4570 additions and 329 deletions
+244 -39
View File
@@ -14,16 +14,20 @@ import {
AlertTriangle,
Copy,
CheckCircle,
Upload
Upload,
Image,
Key
} from 'lucide-react';
import { format, parseISO, differenceInDays } from 'date-fns';
import { toast } from 'react-toastify';
import { Button, Input, Card, Loading } from '../../components/common';
import { PhotoUpload, EventCategoryManager } from '../../components/admin';
import { PhotoUpload, EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { galleryService } from '../../services/gallery.service';
import { archiveService } from '../../services/archive.service';
import { photosService, AdminPhoto } from '../../services/photos.service';
export const EventDetailsPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -45,6 +49,17 @@ export const EventDetailsPage: React.FC = () => {
});
const [copiedLink, setCopiedLink] = useState(false);
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
const [activeTab, setActiveTab] = useState<'overview' | 'photos' | 'categories'>('overview');
const [selectedPhoto, setSelectedPhoto] = useState<{ photo: AdminPhoto; index: number } | null>(null);
const [showPasswordReset, setShowPasswordReset] = useState(false);
// Photo filters state
const [photoFilters, setPhotoFilters] = useState({
category_id: undefined as number | null | undefined,
search: '',
sort: 'date' as 'date' | 'name' | 'size',
order: 'desc' as 'asc' | 'desc'
});
// Fetch event details
const { data: event, isLoading: eventLoading } = useQuery({
@@ -61,6 +76,23 @@ export const EventDetailsPage: React.FC = () => {
retry: false,
});
// Fetch photos when on photos tab
const { data: photos = [], isLoading: photosLoading, refetch: refetchPhotos } = useQuery({
queryKey: ['admin-event-photos', id, photoFilters],
queryFn: () => photosService.getEventPhotos(parseInt(id!), photoFilters),
enabled: !!id && activeTab === 'photos',
});
// Fetch categories for the event
const { data: categories = [] } = useQuery({
queryKey: ['admin-event-categories', id],
queryFn: async () => {
const response = await eventsService.getEventCategories(parseInt(id!));
return response || [];
},
enabled: !!id,
});
// Update mutation
const updateMutation = useMutation({
mutationFn: (data: any) => eventsService.updateEvent(parseInt(id!), data),
@@ -257,8 +289,51 @@ export const EventDetailsPage: React.FC = () => {
</Card>
)}
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Tabs */}
<div className="mb-6 border-b border-neutral-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('overview')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'overview'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
}`}
>
Overview
</button>
<button
onClick={() => setActiveTab('photos')}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
activeTab === 'photos'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
}`}
>
<Image className="w-4 h-4" />
Photos
{event.photo_count && event.photo_count > 0 && (
<span className="ml-1 px-2 py-0.5 text-xs font-medium bg-neutral-100 text-neutral-700 rounded-full">
{event.photo_count}
</span>
)}
</button>
<button
onClick={() => setActiveTab('categories')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'categories'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
}`}
>
Categories
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Details */}
<div className="lg:col-span-2 space-y-6">
{/* Event Information */}
@@ -361,34 +436,25 @@ export const EventDetailsPage: React.FC = () => {
<p className="text-sm text-neutral-600 mt-2">
Share this link with guests. They'll need the password to access the gallery.
</p>
</Card>
{/* Photo Management */}
<Card padding="md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">Photo Management</h2>
<Button
variant="primary"
size="sm"
leftIcon={<Upload className="w-4 h-4" />}
onClick={() => setShowPhotoUpload(!showPhotoUpload)}
>
Upload Photos
</Button>
</div>
{showPhotoUpload && (
<div className="mb-4">
<PhotoUpload
eventId={parseInt(id!)}
onUploadComplete={() => {
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
toast.success('Photos uploaded successfully');
setShowPhotoUpload(false);
}}
/>
{!event.is_archived && (
<div className="mt-4 pt-4 border-t border-neutral-200">
<Button
variant="outline"
size="sm"
leftIcon={<Key className="w-4 h-4" />}
onClick={() => setShowPasswordReset(true)}
className="w-full justify-center"
>
Reset Gallery Password
</Button>
</div>
)}
</Card>
{/* Photo Statistics */}
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">Photo Statistics</h2>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
@@ -403,18 +469,22 @@ export const EventDetailsPage: React.FC = () => {
</span>
</div>
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Storage Location:</strong> /storage/events/active/{event.slug}/
</p>
<p className="text-xs text-blue-600 mt-1">
Photos are organized by categories you define.
</p>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">Categories</span>
<span className="text-sm font-medium">{categories.length}</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-neutral-200">
<EventCategoryManager eventId={parseInt(id!)} />
<div className="mt-4">
<Button
variant="outline"
size="sm"
leftIcon={<Image className="w-4 h-4" />}
onClick={() => setActiveTab('photos')}
className="w-full justify-center"
>
Manage Photos
</Button>
</div>
</Card>
@@ -503,7 +573,15 @@ export const EventDetailsPage: React.FC = () => {
variant="outline"
size="sm"
leftIcon={<Download className="w-4 h-4" />}
onClick={() => toast.info('Archive download coming soon')}
onClick={async () => {
try {
toast.info(`Downloading ${event.event_name} archive...`);
await archiveService.downloadArchive(Number(id), `${event.slug}-archive.zip`);
toast.success('Download started');
} catch (error) {
toast.error('Failed to download archive');
}
}}
className="w-full justify-center"
>
Download Archive
@@ -514,6 +592,133 @@ export const EventDetailsPage: React.FC = () => {
)}
</div>
</div>
)}
{/* Photos Tab */}
{activeTab === 'photos' && (
<div>
{/* Photo Upload */}
{showPhotoUpload && (
<Card padding="md" className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">Upload Photos</h2>
<Button
variant="ghost"
size="sm"
onClick={() => setShowPhotoUpload(false)}
>
<X className="w-4 h-4" />
</Button>
</div>
<PhotoUpload
eventId={parseInt(id!)}
onUploadComplete={() => {
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
queryClient.invalidateQueries({ queryKey: ['admin-event-photos', id] });
toast.success('Photos uploaded successfully');
setShowPhotoUpload(false);
refetchPhotos();
}}
/>
</Card>
)}
{/* Photo Filters */}
<PhotoFilters
categories={categories}
selectedCategory={photoFilters.category_id}
searchTerm={photoFilters.search}
sortBy={photoFilters.sort}
sortOrder={photoFilters.order}
onCategoryChange={(categoryId) => setPhotoFilters(prev => ({ ...prev, category_id: categoryId }))}
onSearchChange={(search) => setPhotoFilters(prev => ({ ...prev, search }))}
onSortChange={(sort, order) => setPhotoFilters(prev => ({ ...prev, sort, order }))}
/>
{/* Actions Bar */}
{!showPhotoUpload && (
<div className="mb-4 flex justify-between items-center">
<Button
variant="primary"
size="sm"
leftIcon={<Upload className="w-4 h-4" />}
onClick={() => setShowPhotoUpload(true)}
>
Upload Photos
</Button>
</div>
)}
{/* Photo Grid */}
{photosLoading ? (
<div className="flex items-center justify-center py-12">
<Loading size="lg" text="Loading photos..." />
</div>
) : (
<AdminPhotoGrid
photos={photos}
eventId={parseInt(id!)}
onPhotoClick={(photo, index) => setSelectedPhoto({ photo, index })}
onPhotosDeleted={() => {
refetchPhotos();
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
}}
/>
)}
{/* Photo Viewer */}
{selectedPhoto && (
<AdminPhotoViewer
photos={photos}
initialIndex={selectedPhoto.index}
eventId={parseInt(id!)}
onClose={() => setSelectedPhoto(null)}
onPhotoDeleted={() => {
refetchPhotos();
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
setSelectedPhoto(null);
}}
categories={categories}
/>
)}
</div>
)}
{/* Categories Tab */}
{activeTab === 'categories' && (
<div>
<Card padding="md">
<div className="mb-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-2">Photo Categories</h2>
<p className="text-sm text-neutral-600">
Organize your photos into categories. Categories help guests navigate and find specific types of photos.
</p>
</div>
<EventCategoryManager
eventId={parseInt(id!)}
/>
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Tip:</strong> Categories are specific to each event. You can create custom categories like "Ceremony", "Reception", "Portraits", etc.
</p>
</div>
</Card>
</div>
)}
{/* Password Reset Modal */}
{showPasswordReset && (
<PasswordResetModal
eventName={event.event_name}
onConfirm={async (sendEmail) => {
const result = await eventsService.resetPassword(event.id, sendEmail);
return result;
}}
onClose={() => setShowPasswordReset(false)}
/>
)}
</div>
);
};