Fix mobile responsiveness and implement enhanced theme system

- Fixed mobile gallery login box sizing and layout
- Fixed header button layout for mobile screens
- Fixed duplicate logo issue on logout
- Fixed '0' rendering when upload button is hidden
- Fixed horizontal scrolling on small screens

- Implemented comprehensive theme system with gallery layouts
- Added 6 different gallery layouts: Grid, Masonry, Carousel, Timeline, Hero, Mosaic
- Created enhanced theme customizer with layout selection
- Added theme presets for different event types
- Updated event creation with theme preview and customization
- Fixed all TypeScript compilation errors

- Added missing translation keys for create event page
- Added translations for theme customization features

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

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
2025-07-09 11:26:27 +02:00
co-authored by Claude
parent d8fb4c9565
commit 6438374258
23 changed files with 2798 additions and 168 deletions
@@ -0,0 +1,150 @@
import React, { useState, useEffect } from 'react';
import { Download, Maximize2, Check, ChevronDown } from 'lucide-react';
import { useTheme } from '../../../contexts/ThemeContext';
import { AuthenticatedImage, Button } from '../../common';
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
import type { Photo } from '../../../types';
export const HeroGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
photos,
onPhotoClick,
onDownload,
selectedPhotos = new Set(),
isSelectionMode = false,
onPhotoSelect
}) => {
const { theme } = useTheme();
const [heroPhoto, setHeroPhoto] = useState<Photo | null>(null);
const gallerySettings = theme.gallerySettings || {};
const overlayOpacity = gallerySettings.heroOverlayOpacity || 0.3;
// Select hero photo (first photo or specified one)
useEffect(() => {
if (photos.length > 0) {
const heroId = gallerySettings.heroImageId;
const hero = heroId ? photos.find(p => p.id === heroId) : photos[0];
setHeroPhoto(hero || photos[0]);
}
}, [photos, gallerySettings.heroImageId]);
if (!heroPhoto) return null;
const remainingPhotos = photos.filter(p => p.id !== heroPhoto.id);
return (
<div className="relative">
{/* Hero Section */}
<div className="relative h-[60vh] sm:h-[70vh] lg:h-[80vh] -mx-4 sm:-mx-6 lg:-mx-8 mb-8">
<AuthenticatedImage
src={heroPhoto.url}
alt={heroPhoto.filename}
className="w-full h-full object-cover"
isGallery={true}
/>
{/* Overlay */}
<div
className="absolute inset-0 bg-black"
style={{ opacity: overlayOpacity }}
/>
{/* Hero Content */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-white px-4">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4 drop-shadow-lg">
{heroPhoto.category_name || 'Featured Photo'}
</h1>
<div className="flex items-center justify-center gap-4">
<Button
variant="primary"
size="lg"
leftIcon={<Maximize2 className="w-5 h-5" />}
onClick={() => onPhotoClick(0)}
className="bg-white/20 backdrop-blur-sm hover:bg-white/30"
>
View Gallery
</Button>
<Button
variant="outline"
size="lg"
leftIcon={<Download className="w-5 h-5" />}
onClick={(e) => onDownload(heroPhoto, e)}
className="border-white text-white hover:bg-white/20"
>
Download
</Button>
</div>
</div>
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
<ChevronDown className="w-8 h-8 text-white drop-shadow-lg" />
</div>
</div>
{/* Grid Section */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{remainingPhotos.map((photo) => {
const actualIndex = photos.findIndex(p => p.id === photo.id);
return (
<div
key={photo.id}
className="relative group cursor-pointer aspect-square"
onClick={() => {
if (isSelectionMode && onPhotoSelect) {
onPhotoSelect(photo.id);
} else {
onPhotoClick(actualIndex);
}
}}
>
<AuthenticatedImage
src={photo.thumbnail_url || photo.url}
alt={photo.filename}
className="w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-105"
loading="lazy"
isGallery={true}
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2">
{!isSelectionMode && (
<>
<button
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={(e) => {
e.stopPropagation();
onPhotoClick(actualIndex);
}}
aria-label="View full size"
>
<Maximize2 className="w-5 h-5 text-neutral-800" />
</button>
<button
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={(e) => {
e.stopPropagation();
onDownload(photo, e);
}}
aria-label="Download photo"
>
<Download className="w-5 h-5 text-neutral-800" />
</button>
</>
)}
</div>
{isSelectionMode && (
<div className={`absolute top-2 right-2 ${selectedPhotos.has(photo.id) ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}>
<div className={`w-6 h-6 rounded-full border-2 ${selectedPhotos.has(photo.id) ? 'bg-primary-600 border-primary-600' : 'bg-white/80 border-white'} flex items-center justify-center transition-colors`}>
{selectedPhotos.has(photo.id) && <Check className="w-4 h-4 text-white" />}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
);
};