feat: Add CSS template system with custom gallery styling support

## Changes

### CSS Template System
- Added CSS class hooks to gallery components for custom template targeting
- Gallery sidebar, header, footer, and photo cards can now be styled via CSS templates
- CSS variables on :root allow themes to override colors, effects, and spacing

### Gallery Component CSS Classes Added
- `.gallery-page` - Main gallery container
- `.gallery-header` - Top header bar
- `.gallery-sidebar` - Filter/download sidebar
- `.gallery-sidebar-header`, `.gallery-sidebar-title`, `.gallery-sidebar-close`
- `.gallery-sidebar-content`, `.gallery-sidebar-section`
- `.gallery-sidebar-search-input`, `.gallery-sidebar-search-icon`
- `.gallery-sidebar-backdrop` - Mobile overlay
- `.gallery-btn`, `.gallery-btn-download` - Sidebar buttons
- `.gallery-footer` - Footer section
- `.photo-card`, `.photo-grid` - Photo display elements

### CSS Templates (Database)
- Elegant Dark (id=1): Dark navy theme with light text and red accents
- Liquid Glass Light (id=2): iOS 26 frosted glass effect with gradient background

### Bug Fixes
- Fixed CSS variables not inheriting (moved from .gallery-page to :root)
- Fixed sidebar position breaking layout (removed position: relative override)
- Fixed Elegant Dark sidebar text visibility (white on white issue)

### Other Changes
- Settings page refactoring and cleanup
- i18n locale updates for new gallery features
- Vite proxy port configuration fix
- Admin auth route improvements
- CSS templates service updates
This commit is contained in:
Paul Nothaft
2026-01-03 08:59:01 +01:00
parent 97455ab047
commit 0da45e699a
41 changed files with 6112 additions and 2125 deletions
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid } from 'lucide-react';
import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info } from 'lucide-react';
import { Button, Card, Input } from '../common';
import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType } from '../../types/theme.types';
// import { settingsService } from '../../services/settings.service';
@@ -44,6 +44,7 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
const [localTheme, setLocalTheme] = useState<ThemeConfig>(value);
const [selectedPreset, setSelectedPreset] = useState(presetName);
const [customCss, setCustomCss] = useState(value.customCss || '');
const [showCssInstructions, setShowCssInstructions] = useState(false);
// const logoInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -494,7 +495,8 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Row 1: Font Size & Border Radius */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
{t('branding.fontSize')}
@@ -525,7 +527,10 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
<option value="lg">{t('branding.borderRadiusOptions.large')}</option>
</select>
</div>
</div>
{/* Row 2: Shadow Style & Background Pattern */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
{t('branding.shadowStyle')}
@@ -563,7 +568,93 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
{/* Custom CSS */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 mb-4">{t('branding.customCSS')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 mb-4 flex items-center gap-2">
<Code className="w-5 h-5" />
{t('branding.customCSS')}
</h3>
{/* Collapsible Instructions Panel */}
<div className="mb-4">
<button
type="button"
onClick={() => setShowCssInstructions(!showCssInstructions)}
className="flex items-center gap-2 text-sm text-primary-600 hover:text-primary-700 font-medium"
>
<Info className="w-4 h-4" />
{t('branding.cssInstructions.title', 'How to use Custom CSS')}
<ChevronDown className={`w-4 h-4 transition-transform ${showCssInstructions ? 'rotate-180' : ''}`} />
</button>
{showCssInstructions && (
<div className="mt-3 p-4 bg-neutral-50 rounded-lg border border-neutral-200 text-sm space-y-4">
{/* Available CSS Variables */}
<div>
<h4 className="font-semibold text-neutral-900 mb-2">
{t('branding.cssInstructions.variables', 'Theme CSS Variables')}
</h4>
<p className="text-neutral-600 mb-2">
{t('branding.cssInstructions.variablesDesc', 'Use these CSS variables to match your theme presets:')}
</p>
<code className="block bg-neutral-800 text-green-400 p-3 rounded text-xs overflow-x-auto">
{`--primary-color: ${localTheme.primaryColor || '#5C8762'};
--accent-color: ${localTheme.accentColor || '#22c55e'};
--background-color: ${localTheme.backgroundColor || '#fafafa'};
--text-color: ${localTheme.textColor || '#171717'};
--font-family: ${localTheme.fontFamily || 'Inter, sans-serif'};
--heading-font: ${localTheme.headingFontFamily || localTheme.fontFamily || 'Inter, sans-serif'};`}
</code>
</div>
{/* Custom Gallery Layouts */}
<div>
<h4 className="font-semibold text-neutral-900 mb-2">
{t('branding.cssInstructions.layouts', 'Custom Gallery Layouts')}
</h4>
<p className="text-neutral-600 mb-2">
{t('branding.cssInstructions.layoutsDesc', 'Target gallery elements with these selectors:')}
</p>
<code className="block bg-neutral-800 text-green-400 p-3 rounded text-xs overflow-x-auto">
{`.gallery-container { /* Main gallery wrapper */ }
.gallery-grid { /* Photo grid container */ }
.gallery-item { /* Individual photo card */ }
.gallery-header { /* Header section */ }
.gallery-hero { /* Hero image area */ }
.photo-overlay { /* Photo hover overlay */ }
.photo-actions { /* Like/favorite buttons */ }`}
</code>
</div>
{/* Glassmorphism Example */}
<div>
<h4 className="font-semibold text-neutral-900 mb-2">
{t('branding.cssInstructions.glassEffect', 'Glassmorphism Effect')}
</h4>
<p className="text-neutral-600 mb-2">
{t('branding.cssInstructions.glassEffectDesc', 'Create modern glass effects:')}
</p>
<code className="block bg-neutral-800 text-green-400 p-3 rounded text-xs overflow-x-auto">
{`.glass-panel {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 16px;
}`}
</code>
</div>
{/* Tips */}
<div className="flex items-start gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-blue-800 text-xs">
<strong>{t('branding.cssInstructions.tip', 'Tip')}:</strong>{' '}
{t('branding.cssInstructions.tipText', 'Use CSS Templates from Settings > CSS Templates for pre-built designs like Apple Liquid Glass.')}
</div>
</div>
</div>
)}
</div>
<textarea
value={customCss}
onChange={(e) => {
@@ -575,7 +666,7 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
}
}}
placeholder="/* Add custom CSS here */"
className="w-full h-32 px-3 py-2 font-mono text-sm border border-neutral-300 rounded-lg"
className="w-full h-40 px-3 py-2 font-mono text-sm border border-neutral-300 rounded-lg bg-neutral-50"
/>
<p className="mt-2 text-sm text-neutral-600">
{t('branding.customCSSHelp')}
@@ -118,12 +118,12 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
const heroLogoSize = getLogoDimensions('hero');
return (
<div className="min-h-screen bg-neutral-50">
<div className="gallery-page min-h-screen bg-neutral-50">
{/* Dynamic Favicon */}
<DynamicFavicon />
{/* Header structure */}
<header className={`bg-white border-b border-neutral-200 sticky top-0 z-40 ${isNonGridLayout || theme.galleryLayout === 'hero' ? 'shadow-sm' : ''}`}>
<header className={`gallery-header bg-white border-b border-neutral-200 sticky top-0 z-40 ${isNonGridLayout || theme.galleryLayout === 'hero' ? 'shadow-sm' : ''}`}>
{/* For non-grid layouts (excluding hero) - keep the current structure */}
{isNonGridLayout && (
<div className="bg-neutral-50 border-b border-neutral-200">
@@ -145,12 +145,13 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="gallery-btn gallery-btn-download"
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
@@ -158,7 +159,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
className="gallery-btn gallery-btn-logout sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
@@ -184,14 +185,14 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
{/* Logo - Show custom logo or fallback to PicPeak logo */}
{shouldShowLogo('header') && (
<div className={`flex-shrink-0 flex items-center gap-2 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
<img
src={brandingSettings?.logo_url ?
buildResourceUrl(brandingSettings.logo_url) :
<div className={`gallery-logo-wrapper flex-shrink-0 flex items-center gap-2 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
<img
src={brandingSettings?.logo_url ?
buildResourceUrl(brandingSettings.logo_url) :
'/picpeak-logo-transparent.png'
}
}
alt={brandingSettings?.company_name || 'PicPeak'}
className={`${headerLogoSize.className} w-auto object-contain`}
className={`gallery-logo ${headerLogoSize.className} w-auto object-contain`}
style={headerLogoSize.style}
/>
{shouldShowCompanyName() && brandingSettings?.company_name && (
@@ -253,13 +254,13 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="hidden sm:flex"
className="gallery-btn gallery-btn-download hidden sm:flex"
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
@@ -267,7 +268,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
className="gallery-btn gallery-btn-logout sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
@@ -304,7 +305,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
{menuButton}
{headerExtra}
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-3 flex-shrink-0">
{/* Download all button */}
@@ -315,12 +316,13 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="gallery-btn gallery-btn-download"
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
@@ -328,7 +330,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
className="gallery-btn gallery-btn-logout sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
@@ -341,8 +343,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
{/* Hero Header for non-grid layouts (excluding hero layout which has its own) */}
{isNonGridLayout && (
<div
className="relative text-white overflow-hidden"
<div
className="gallery-hero relative text-white overflow-hidden"
style={{
backgroundColor: theme.accentColor || '#22c55e',
backgroundImage: theme.backgroundPattern !== 'none'
@@ -425,7 +427,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
<main className="container">{children}</main>
{/* Footer */}
<footer className="mt-8 sm:mt-12 py-6 sm:py-8 border-t border-neutral-200">
<footer className="gallery-footer mt-8 sm:mt-12 py-6 sm:py-8 border-t border-neutral-200">
<div className="container text-center px-4">
{brandingSettings?.support_email && (
<p className="text-xs sm:text-sm text-neutral-600 mb-2">
@@ -110,8 +110,8 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
<>
{/* Backdrop for mobile */}
{isMobile && isOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity"
<div
className="gallery-sidebar-backdrop fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity"
onClick={onClose}
/>
)}
@@ -120,17 +120,17 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
<div
ref={sidebarRef}
className={`
fixed top-0 left-0 h-full bg-white shadow-xl z-50 transition-transform duration-300 ease-in-out flex flex-col
gallery-sidebar fixed top-0 left-0 h-full bg-white shadow-xl z-50 transition-transform duration-300 ease-in-out flex flex-col
${isMobile ? 'w-full max-w-sm' : 'w-80'}
${isOpen ? 'translate-x-0' : '-translate-x-full'}
`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-neutral-200">
<h2 className="text-lg font-semibold text-neutral-900">{t('gallery.filters')}</h2>
<div className="gallery-sidebar-header flex items-center justify-between p-4 border-b border-neutral-200">
<h2 className="gallery-sidebar-title text-lg font-semibold text-neutral-900">{t('gallery.filters')}</h2>
<button
onClick={onClose}
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors"
className="gallery-sidebar-close p-2 hover:bg-neutral-100 rounded-lg transition-colors"
aria-label={t('common.close')}
>
<X className="w-5 h-5 text-neutral-600" />
@@ -138,7 +138,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
<div className="gallery-sidebar-content flex-1 overflow-y-auto">
{/* Upload Section - Only show on mobile when uploads are allowed */}
{isMobile && allowUploads && onUploadClick && (
<div className="p-4 border-b border-neutral-200">
@@ -159,15 +159,15 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Search Section - Hidden for carousel layout */}
{galleryLayout !== 'carousel' && (
<div className="p-4 border-b border-neutral-200">
<div className="gallery-sidebar-section gallery-sidebar-search p-4 border-b border-neutral-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-neutral-400" />
<Search className="gallery-sidebar-search-icon absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-neutral-400" />
<input
type="text"
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
placeholder={t('gallery.searchPlaceholder')}
className="w-full pl-10 pr-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
className="gallery-sidebar-search-input w-full pl-10 pr-4 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
@@ -175,12 +175,12 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Download Section - Hidden if gallery is expired or downloads disabled */}
{allowDownloads && (
<div className="p-4 border-b border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<div className="gallery-sidebar-section gallery-sidebar-downloads p-4 border-b border-neutral-200">
<h3 className="gallery-sidebar-section-title text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<Download className="w-4 h-4" />
{t('gallery.download')}
</h3>
<div className="space-y-2">
<Button
variant="primary"
@@ -188,7 +188,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
disabled={isDownloading || totalPhotos === 0}
className="w-full"
className="gallery-btn gallery-btn-download w-full"
>
{t('gallery.downloadAll')} ({totalPhotos})
</Button>
@@ -197,7 +197,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
variant={isSelectionMode ? 'secondary' : 'outline'}
size="sm"
onClick={onToggleSelectionMode}
className="w-full"
className="gallery-btn w-full"
>
{isSelectionMode ? t('gallery.cancelSelection') : t('gallery.selectPhotos')}
</Button>
@@ -209,7 +209,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadSelected}
disabled={isDownloading}
className="w-full"
className="gallery-btn gallery-btn-download w-full"
>
{t('gallery.downloadSelected', { count: selectedCount })} ({selectedCount})
</Button>
@@ -220,7 +220,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Feedback Filter Section */}
{feedbackEnabled && onFilterChange && (
<div className="p-4 border-b border-neutral-200">
<div className="gallery-sidebar-section gallery-sidebar-feedback p-4 border-b border-neutral-200">
<GalleryFilter
currentFilter={filterType}
onFilterChange={(filter) => {
@@ -239,12 +239,12 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Categories Section - Hidden for carousel layout */}
{galleryLayout !== 'carousel' && categories.length > 0 && (
<div className="p-4 border-b border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<div className="gallery-sidebar-section gallery-sidebar-categories p-4 border-b border-neutral-200">
<h3 className="gallery-sidebar-section-title text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<Filter className="w-4 h-4" />
{t('gallery.categories')}
</h3>
<div className="space-y-1">
<button
onClick={() => {
@@ -252,7 +252,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
if (isMobile) onClose();
}}
className={`
w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between
gallery-btn w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between
${selectedCategoryId === null
? 'bg-primary-50 text-primary-700'
: 'hover:bg-neutral-50 text-neutral-700'
@@ -266,7 +266,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{categories.map((category) => {
const count = photoCounts[category.id] || 0;
const isSelected = selectedCategoryId === category.id;
return (
<button
key={category.id}
@@ -275,7 +275,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
if (isMobile) onClose();
}}
className={`
w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between
gallery-btn w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between
${isSelected
? 'bg-primary-50 text-primary-700'
: 'hover:bg-neutral-50 text-neutral-700'
@@ -295,8 +295,8 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
)}
{showMediaFilter && onMediaFilterChange && (
<div className="p-4 border-b border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<div className="gallery-sidebar-section gallery-sidebar-media p-4 border-b border-neutral-200">
<h3 className="gallery-sidebar-section-title text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<Filter className="w-4 h-4" />
{t('gallery.mediaType', 'Media')}
</h3>
@@ -304,6 +304,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
<Button
variant={mediaFilter === 'all' ? 'primary' : 'outline'}
size="sm"
className="gallery-btn"
onClick={() => {
onMediaFilterChange('all');
if (isMobile) onClose();
@@ -314,6 +315,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
<Button
variant={mediaFilter === 'photo' ? 'primary' : 'outline'}
size="sm"
className="gallery-btn"
onClick={() => {
onMediaFilterChange('photo');
if (isMobile) onClose();
@@ -324,6 +326,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
<Button
variant={mediaFilter === 'video' ? 'primary' : 'outline'}
size="sm"
className="gallery-btn"
onClick={() => {
onMediaFilterChange('video');
if (isMobile) onClose();
@@ -337,17 +340,17 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Sort Section - Hidden for carousel and timeline layouts */}
{galleryLayout !== 'carousel' && galleryLayout !== 'timeline' && (
<div className="p-4">
<h3 className="text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<div className="gallery-sidebar-section gallery-sidebar-sort p-4">
<h3 className="gallery-sidebar-section-title text-sm font-semibold text-neutral-700 mb-3 flex items-center gap-2">
<SortAsc className="w-4 h-4" />
{t('gallery.sortBy')}
</h3>
<div className="space-y-1">
{sortOptions.map((option) => {
const Icon = option.icon;
const isSelected = sortBy === option.value;
return (
<button
key={option.value}
@@ -356,7 +359,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
if (isMobile) onClose();
}}
className={`
w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center gap-3
gallery-btn w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center gap-3
${isSelected
? 'bg-primary-50 text-primary-700'
: 'hover:bg-neutral-50 text-neutral-700'
@@ -21,6 +21,7 @@ import { api } from '../../config/api';
import { Upload, Menu } from 'lucide-react';
import { galleryService } from '../../services/gallery.service';
import { useWatermarkSettings } from '../../hooks/useWatermarkSettings';
import { useGalleryCustomCss } from '../../hooks/useGalleryCustomCss';
import type { Photo } from '../../types';
interface GalleryViewProps {
@@ -55,6 +56,10 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const [feedbackEnabled, setFeedbackEnabled] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const { watermarkEnabled } = useWatermarkSettings();
// Load and inject custom CSS for this gallery
useGalleryCustomCss(slug);
const [protectionLevel, setProtectionLevel] = useState<'basic' | 'standard' | 'enhanced' | 'maximum'>('standard');
const [filterType, setFilterType] = useState<FilterType>('all');
const [mediaFilter, setMediaFilter] = useState<'all' | 'photo' | 'video'>('all');
@@ -564,6 +569,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
<Button
variant="ghost"
size="sm"
className="gallery-btn"
leftIcon={<Menu className="w-4 h-4" />}
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
@@ -70,9 +70,9 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
return (
<div className="relative">
<div className="photo-grid relative">
{/* Main Carousel */}
<div className="relative h-[50vh] sm:h-[60vh] lg:h-[70vh] bg-black rounded-lg overflow-hidden">
<div className="photo-card relative h-[50vh] sm:h-[60vh] lg:h-[70vh] bg-black rounded-lg overflow-hidden">
<AuthenticatedImage
src={currentPhoto.url}
alt={currentPhoto.filename}
@@ -176,7 +176,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
return (
<div
ref={ref}
className={`relative group cursor-pointer aspect-square ${animationClass}`}
className={`photo-card relative group cursor-pointer aspect-square ${animationClass}`}
onClick={handlePhotoClick}
style={{
opacity: !inView && animationType === 'fade' ? 0 : 1
@@ -375,10 +375,10 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
const spacingClass = spacing === 'tight' ? 'gap-2' : spacing === 'relaxed' ? 'gap-6' : 'gap-4';
const gridClass = `grid ${spacingClass}
grid-cols-${columns.mobile}
sm:grid-cols-${columns.tablet}
lg:grid-cols-${columns.desktop}
const gridClass = `photo-grid grid ${spacingClass}
grid-cols-${columns.mobile}
sm:grid-cols-${columns.tablet}
lg:grid-cols-${columns.desktop}
xl:grid-cols-${columns.desktop + 1}`;
return (
@@ -187,13 +187,13 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
</div>
{/* Grid Section */}
<div id="gallery-grid-section" className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
<div id="gallery-grid-section" className="photo-grid 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 overflow-hidden rounded-lg"
className="photo-card relative group cursor-pointer overflow-hidden rounded-lg"
onClick={() => onPhotoClick(actualIndex)}
>
<AuthenticatedImage
@@ -54,7 +54,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
return (
<div
className="relative group cursor-pointer transition-all duration-300 hover:scale-[1.02]"
className="photo-card relative group cursor-pointer transition-all duration-300 hover:scale-[1.02]"
onClick={onClick}
style={{
...style,
@@ -243,9 +243,9 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
});
return (
<div
<div
ref={containerRef}
className="flex gap-4"
className="photo-grid flex gap-4"
style={{ gap: `${gutter}px` }}
>
{photoColumns.map((column, columnIndex) => (
@@ -49,7 +49,7 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
return (
<>
<div
className={`relative group cursor-pointer overflow-hidden rounded-lg bg-neutral-100 ${className}`}
className={`photo-card relative group cursor-pointer overflow-hidden rounded-lg bg-neutral-100 ${className}`}
onClick={(e) => {
e.stopPropagation();
onClick(e);
@@ -417,7 +417,7 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
};
return (
<div className="w-full max-w-7xl mx-auto">
<div className="photo-grid w-full max-w-7xl mx-auto">
{renderMosaicLayout()}
</div>
);
@@ -94,13 +94,13 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
)}
{/* Photos grid for this date */}
<div className="lg:ml-24 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="photo-grid lg:ml-24 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
{group.photos.map((photo) => {
const actualIndex = photos.findIndex(p => p.id === photo.id);
return (
<div
key={photo.id}
className="relative group cursor-pointer aspect-square"
className="photo-card relative group cursor-pointer aspect-square"
onClick={() => onPhotoClick(actualIndex)}
>
<AuthenticatedImage