diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bcb9e24b..7fe517d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "clsx": "^2.0.0", "date-fns": "4.1.0", "dompurify": "^3.2.6", + "framer-motion": "^12.33.0", "i18next": "^25.3.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -32,6 +33,7 @@ "lodash": "^4.17.21", "lowlight": "^2.9.0", "lucide-react": "0.525.0", + "photoswipe": "^5.4.4", "react": "^18.3.1", "react-countdown": "^2.3.5", "react-dom": "^18.3.1", @@ -42,7 +44,9 @@ "react-photo-album": "^3.4.0", "react-router-dom": "^6.8.0", "react-toastify": "11.0.5", - "tailwind-merge": "^3.3.1" + "swiper": "^12.1.0", + "tailwind-merge": "^3.3.1", + "yet-another-react-lightbox": "^3.28.0" }, "devDependencies": { "@eslint/js": "^9.29.0", @@ -2345,7 +2349,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "peerDependencies": { @@ -4091,6 +4095,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.33.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.33.0.tgz", + "integrity": "sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.33.0", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4947,6 +4978,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.33.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.33.0.tgz", + "integrity": "sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5194,6 +5240,15 @@ "node": ">= 14.16" } }, + "node_modules/photoswipe": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz", + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==", + "license": "MIT", + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6256,6 +6311,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swiper": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.0.tgz", + "integrity": "sha512-BD4CpAOOyEvZ2f0CDx362ea+vmOwukVcmbsQx+0BhRIaBUz8wvcCd//E7RFmvBZCrfyqXCHUVqmgUwts6ywlxw==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -6476,6 +6550,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6919,6 +6999,32 @@ "dev": true, "license": "ISC" }, + "node_modules/yet-another-react-lightbox": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.28.0.tgz", + "integrity": "sha512-bHjG1/DS4aWPkKb/6hNp0+zcgjda27wQlXBpJvb9TcblC1U1dY3Ih0pe85O8/ZLz+OIy+ZvLxN2gmsbDW0Q/QQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/igordanchenko" + }, + "peerDependencies": { + "@types/react": "^16 || ^17 || ^18 || ^19", + "@types/react-dom": "^16 || ^17 || ^18 || ^19", + "react": "^16.8.0 || ^17 || ^18 || ^19", + "react-dom": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ec5c4069..6464fe2f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "clsx": "^2.0.0", "date-fns": "4.1.0", "dompurify": "^3.2.6", + "framer-motion": "^12.33.0", "i18next": "^25.3.1", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -36,6 +37,7 @@ "lodash": "^4.17.21", "lowlight": "^2.9.0", "lucide-react": "0.525.0", + "photoswipe": "^5.4.4", "react": "^18.3.1", "react-countdown": "^2.3.5", "react-dom": "^18.3.1", @@ -46,7 +48,9 @@ "react-photo-album": "^3.4.0", "react-router-dom": "^6.8.0", "react-toastify": "11.0.5", - "tailwind-merge": "^3.3.1" + "swiper": "^12.1.0", + "tailwind-merge": "^3.3.1", + "yet-another-react-lightbox": "^3.28.0" }, "devDependencies": { "@eslint/js": "^9.29.0", diff --git a/frontend/src/components/admin/EventCategoryManager.tsx b/frontend/src/components/admin/EventCategoryManager.tsx index ee0401ad..b024d58c 100644 --- a/frontend/src/components/admin/EventCategoryManager.tsx +++ b/frontend/src/components/admin/EventCategoryManager.tsx @@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus, X, Loader2, Image as ImageIcon, Check } from 'lucide-react'; import { toast } from 'react-toastify'; import { categoriesService, type PhotoCategory } from '../../services/categories.service'; -import { photosService, type AdminPhoto } from '../../services/photos.service'; +import { photosService } from '../../services/photos.service'; import { Button, Card, AuthenticatedImage } from '../common'; import { useTranslation } from 'react-i18next'; diff --git a/frontend/src/components/admin/GalleryPreview.tsx b/frontend/src/components/admin/GalleryPreview.tsx index b2fa2e92..ae9948b3 100644 --- a/frontend/src/components/admin/GalleryPreview.tsx +++ b/frontend/src/components/admin/GalleryPreview.tsx @@ -92,8 +92,10 @@ export const GalleryPreview: React.FC = ({ ? 'justify-end text-right flex-row-reverse' : 'justify-start text-left'; - // Check if hero header style is selected + // Check header style const isHeroHeader = theme.headerStyle === 'hero'; + const isMinimalHeader = theme.headerStyle === 'minimal'; + const isNoHeader = theme.headerStyle === 'none'; const heroDividerStyle: HeroDividerStyle = theme.heroDividerStyle || 'wave'; // Render hero divider based on style @@ -210,7 +212,7 @@ export const GalleryPreview: React.FC = ({ fontFamily: theme.fontFamily || 'Inter, sans-serif', }} > - {/* Hero Header - shown when headerStyle is 'hero' */} + {/* Hero Header */} {isHeroHeader && (
= ({ >
- {/* Logo in Hero */} {showLogo && (
{resolvedLogoUrl ? ( @@ -238,7 +239,6 @@ export const GalleryPreview: React.FC = ({ )}
)} - {/* Event Name */}

= ({ > Sample Event

- {/* Event Date */}
January 15, 2026
- {/* Divider */}
{renderHeroDivider()}
)} - {/* Standard Header - shown when headerStyle is NOT 'hero' */} - {!isHeroHeader && ( + {/* Standard Header */} + {!isHeroHeader && !isMinimalHeader && !isNoHeader && (
= ({
)} + {/* Minimal Header - thin bar with just event name */} + {isMinimalHeader && ( +
+

+ Sample Event +

+
+ )} + + {/* None Header - no header content at all */} + {/* (isNoHeader renders nothing here — goes straight to layout bar) */} + {/* Layout info bar */}
Gallery preview - {isHeroHeader ? `Hero + ${activeLayout}` : `${activeLayout} layout`} + {isHeroHeader ? `Hero + ${activeLayout}` : isMinimalHeader ? `Minimal + ${activeLayout}` : isNoHeader ? `No header + ${activeLayout}` : `${activeLayout} layout`}
{/* Preview Content */} diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index 29d49837..37d90279 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info, FileCode, ImageIcon, Minimize2, EyeOff } from 'lucide-react'; +import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info, FileCode, ImageIcon, Minimize2, EyeOff, Menu, SlidersHorizontal, Columns, Film } from 'lucide-react'; import { Button, Card, Input } from '../common'; import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType, HeaderStyleType, HeroDividerStyle } from '../../types/theme.types'; import type { EnabledTemplate } from '../../services/cssTemplates.service'; @@ -28,7 +28,9 @@ const layoutIcons: Record = { masonry: , carousel: , timeline: , - mosaic: + mosaic: , + 'gallery-premium': , + 'gallery-story': }; const headerStyleIcons: Record = { @@ -178,7 +180,7 @@ export const ThemeCustomizerEnhanced: React.FC = (
{/* Preset Themes */} -

+

{t('branding.themePresets')}

@@ -189,15 +191,15 @@ export const ThemeCustomizerEnhanced: React.FC = ( onClick={() => handlePresetSelect(key)} className={`relative p-4 rounded-lg border-2 transition-all text-left ${ selectedPreset === key - ? 'border-primary-600 bg-primary-50' - : 'border-neutral-200 hover:border-neutral-300' + ? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30' + : 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600' }`} >
- {theme.name} + {theme.name} {theme.description && ( - {theme.description} + {theme.description} )}
{selectedPreset === key && ( @@ -207,15 +209,15 @@ export const ThemeCustomizerEnhanced: React.FC = (
@@ -233,7 +235,7 @@ export const ThemeCustomizerEnhanced: React.FC = ( {/* Gallery Layout */} {showGalleryLayouts && ( -

+

{t('branding.galleryLayout')}

@@ -244,16 +246,21 @@ export const ThemeCustomizerEnhanced: React.FC = ( onClick={() => handleChange('galleryLayout', layout)} className={`relative p-4 rounded-lg border-2 transition-all ${ localTheme.galleryLayout === layout - ? 'border-primary-600 bg-primary-50' - : 'border-neutral-200 hover:border-neutral-300' + ? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30' + : 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600' }`} >
-
+
{layoutIcons[layout]}
- {layout} - + + {layout} + {(layout === 'gallery-premium' || layout === 'gallery-story') && ( + (Beta) + )} + + {t(`branding.layoutDescriptions.${layout}`)}
@@ -266,19 +273,19 @@ export const ThemeCustomizerEnhanced: React.FC = ( {/* Layout-specific settings */} {localTheme.galleryLayout && ( -
-

{t('branding.layoutSettings')}

+
+

{t('branding.layoutSettings')}

{/* Common settings */}
-