Fix gallery mobile view issues

- Make logout button show only icon on mobile (no text)
- Move upload button from top bar to sidebar menu on mobile
- Fix top bar layout with proper structure:
  - Logo on left
  - Gallery title centered
  - Event date and expiration date shown below title on mobile
- Improve responsive design for header elements
- Ensure upload button only appears in menu when uploads are enabled

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-11 08:30:07 +02:00
parent 5328b4f73a
commit 9006b754a8
11 changed files with 357 additions and 122 deletions
+47
View File
@@ -0,0 +1,47 @@
# TODO - Open Items Before Release
## Priority Items
- [ ] **Gallery Mobile View**
- Logout button should only show logo icon (no text)
- If photo upload is enabled, move upload button inside menu (not on top bar)
- Top bar should show: logo (left), gallery title (center), event date + expiration date
- [ ] **Gallery Preview**
- Preview should correctly reflect the selected grid layout style
- Add grid style selector above current top bar
- Selector should match the style of event template settings grid selector
- [ ] **Hero Grid Layout**
- Top bar: only menu and logout buttons
- Title + logo displayed centered on hero photo
- Event date and expiration date also on hero photo
- No logo/title in top bar
- [ ] **Logo Testing** - Test new PicPeak logos across all grid styles
- [ ] **Welcome Message**
- Add welcome message to email template when creating new event
- Use as personal message in the email
- [ ] **Gallery Upload Function**
- Fix scrolling in upload popup when multiple images selected
- Save/Cancel buttons unreachable due to incorrect scroll formatting
- [ ] **Watermarks** - Test watermark functionality, styling, and image application
- [ ] **Dashboard Activities** - Remove "show all" link from latest activities widget
- [ ] **Security Audit** - Perform security review and code audit
- [ ] **Drone CI/CD** - Update drone.yaml configuration
- [ ] **Version Management** - Implement automatic version updates on commits/builds
## Completed Items
_(Move completed items here with date)_
---
Last updated: 2025-07-10
@@ -0,0 +1,59 @@
import React from 'react';
import { X } from 'lucide-react';
import { Button } from '../common';
import { PhotoUpload } from './PhotoUpload';
import { useTranslation } from 'react-i18next';
interface PhotoUploadModalProps {
isOpen: boolean;
onClose: () => void;
eventId: number;
onUploadComplete?: () => void;
}
export const PhotoUploadModal: React.FC<PhotoUploadModalProps> = ({
isOpen,
onClose,
eventId,
onUploadComplete
}) => {
const { t } = useTranslation();
if (!isOpen) return null;
const handleUploadComplete = () => {
if (onUploadComplete) {
onUploadComplete();
}
onClose();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[90vh]">
{/* Fixed Header */}
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
<h2 className="text-xl font-semibold text-neutral-900">{t('events.uploadPhotos')}</h2>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="!p-1"
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-6">
<PhotoUpload
eventId={eventId}
onUploadComplete={handleUploadComplete}
/>
</div>
</div>
</div>
);
};
PhotoUploadModal.displayName = 'PhotoUploadModal';
+2 -1
View File
@@ -19,4 +19,5 @@ export { AdminAuthenticatedImage } from './AdminAuthenticatedImage';
export { ThemeCustomizerEnhanced } from './ThemeCustomizerEnhanced';
export { ThemeDisplay } from './ThemeDisplay';
export { ThemeEditorModal } from './ThemeEditorModal';
export { HeroPhotoSelector } from './HeroPhotoSelector';
export { HeroPhotoSelector } from './HeroPhotoSelector';
export { PhotoUploadModal } from './PhotoUploadModal';
+105 -37
View File
@@ -91,8 +91,9 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
{t('common.logout')}
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
@@ -101,17 +102,12 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
</div>
)}
{/* For grid and hero layouts - everything in one bar */}
{(!isNonGridLayout || theme.galleryLayout === 'hero') && (
{/* For grid layout - everything in one bar */}
{!isNonGridLayout && theme.galleryLayout !== 'hero' && (
<div className="container py-3">
<div className="flex items-center justify-between gap-4">
{/* Left side - Logo, Title, and Dates */}
<div className="flex items-center gap-4 flex-1 min-w-0">
{/* Menu button */}
<div className="flex-shrink-0">
{headerExtra}
</div>
<div className="flex items-center justify-between gap-2 sm:gap-4">
{/* Left side - Logo and menu button on mobile */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
{/* Logo - Show custom logo or fallback to PicPeak logo */}
<div className="flex-shrink-0">
<img
@@ -120,37 +116,108 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
'/picpeak-logo-transparent.png'
}
alt={brandingSettings?.company_name || 'PicPeak'}
className="h-10 sm:h-12 w-auto object-contain"
className="h-8 sm:h-10 lg:h-12 w-auto object-contain"
/>
</div>
{/* Event info */}
<div className="flex-1 min-w-0">
<h1
className="text-lg sm:text-xl lg:text-2xl font-bold text-neutral-900 leading-tight truncate"
style={{ fontFamily: headingFontFamily }}
>
{event.event_name}
</h1>
{(event.event_date || event.expires_at) && (
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-1 text-xs sm:text-sm text-neutral-600">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{format(parseISO(event.event_date), 'PP')}</span>
</span>
)}
{event.expires_at && (
<span className="flex items-center">
<Clock className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
{/* Menu button on mobile */}
<div className="flex-shrink-0 sm:hidden">
{headerExtra}
</div>
</div>
{/* Center - Event info */}
<div className="flex-1 min-w-0 text-center sm:text-left">
<h1
className="text-base sm:text-lg lg:text-xl font-bold text-neutral-900 leading-tight truncate"
style={{ fontFamily: headingFontFamily }}
>
{event.event_name}
</h1>
{(event.event_date || event.expires_at) && (
<div className="hidden sm:flex flex-wrap gap-x-3 gap-y-1 mt-1 text-xs sm:text-sm text-neutral-600">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{format(parseISO(event.event_date), 'PP')}</span>
</span>
)}
{event.expires_at && (
<span className="flex items-center">
<Clock className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
{/* Menu button on desktop */}
<div className="hidden sm:block">
{headerExtra}
</div>
{/* Download all button - hidden on mobile when sidebar is shown */}
{showDownloadAll && onDownloadAll && (
<Button
variant="primary"
size="sm"
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="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
variant="outline"
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
</div>
{/* Mobile dates row */}
{(event.event_date || event.expires_at) && (
<div className="flex sm:hidden justify-center gap-x-3 mt-2 text-xs text-neutral-600">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{format(parseISO(event.event_date), 'PP')}</span>
</span>
)}
{event.expires_at && (
<span className="flex items-center">
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
</div>
)}
{/* For hero layout - minimal header with just menu and logout */}
{theme.galleryLayout === 'hero' && (
<div className="container py-3">
<div className="flex items-center justify-between">
{/* Left side - Menu button */}
<div className="flex-shrink-0">
{headerExtra}
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-3 flex-shrink-0">
{/* Download all button */}
@@ -174,8 +241,9 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
{t('common.logout')}
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { X, Download, Filter, SortAsc, Search, Calendar, Type, HardDrive, Check } from 'lucide-react';
import { X, Download, Filter, SortAsc, Search, Calendar, Type, HardDrive, Check, Upload } from 'lucide-react';
import { Button } from '../common';
import { PhotoCategory } from '../../types';
import { useTranslation } from 'react-i18next';
@@ -24,6 +24,8 @@ interface GallerySidebarProps {
totalPhotos: number;
isMobile: boolean;
galleryLayout?: string;
allowUploads?: boolean;
onUploadClick?: () => void;
}
export const GallerySidebar: React.FC<GallerySidebarProps> = ({
@@ -45,7 +47,9 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
photoCounts = {},
totalPhotos,
isMobile,
galleryLayout
galleryLayout,
allowUploads,
onUploadClick
}) => {
const { t } = useTranslation();
const sidebarRef = useRef<HTMLDivElement>(null);
@@ -113,6 +117,24 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{/* Content */}
<div className="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">
<Button
variant="outline"
size="sm"
leftIcon={<Upload className="w-4 h-4" />}
onClick={() => {
onUploadClick();
onClose();
}}
className="w-full"
>
{t('upload.uploadPhotos')}
</Button>
</div>
)}
{/* Search Section - Hidden for carousel layout */}
{galleryLayout !== 'carousel' && (
<div className="p-4 border-b border-neutral-200">
@@ -307,6 +307,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
totalPhotos={data?.photos.length || 0}
isMobile={isMobile}
galleryLayout={theme.galleryLayout}
allowUploads={event.allow_user_uploads}
onUploadClick={() => setShowUploadModal(true)}
/>
) : null}
@@ -331,7 +333,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
>
{t('common.menu')}
<span className="hidden sm:inline">{t('common.menu')}</span>
</Button>
);
}
@@ -342,7 +344,23 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
);
}
if (event.allow_user_uploads) {
// Upload button only on desktop when sidebar is shown
if (event.allow_user_uploads && showSidebar && !isMobile) {
items.push(
<Button
key="upload-button"
variant="outline"
size="sm"
leftIcon={<Upload className="w-4 h-4" />}
onClick={() => setShowUploadModal(true)}
>
{t('upload.uploadPhotos')}
</Button>
);
}
// Upload button for non-sidebar layouts
if (event.allow_user_uploads && !showSidebar) {
items.push(
<Button
key="upload-button"
@@ -396,6 +414,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
showSelectionControls={!showSidebar}
eventName={event.event_name}
eventLogo={brandingSettings?.logo_url}
eventDate={event.event_date}
expiresAt={event.expires_at}
/>
</div>
@@ -32,6 +32,8 @@ interface PhotoGridWithLayoutsProps {
showSelectionControls?: boolean;
eventName?: string;
eventLogo?: string | null;
eventDate?: string;
expiresAt?: string;
}
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
@@ -44,7 +46,9 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
onToggleSelectionMode: parentToggleSelectionMode,
showSelectionControls = true,
eventName,
eventLogo
eventLogo,
eventDate,
expiresAt
}) => {
const { t } = useTranslation();
const { theme } = useTheme();
@@ -160,6 +164,8 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
onPhotoSelect: handlePhotoSelect,
eventName,
eventLogo,
eventDate,
expiresAt,
};
let LayoutComponent;
@@ -113,22 +113,21 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-end sm:items-center justify-center z-50">
<Card className="w-full sm:max-w-2xl max-h-[100vh] sm:max-h-[90vh] overflow-hidden sm:mx-4 rounded-t-2xl sm:rounded-2xl">
<CardContent className="p-0">
{/* Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-neutral-200">
<h2 className="text-lg sm:text-xl font-semibold text-neutral-900">{t('upload.uploadPhotos')}</h2>
<button
onClick={onClose}
className="p-1.5 sm:p-2 hover:bg-neutral-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-neutral-500" />
</button>
</div>
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-end sm:items-center justify-center z-50 p-0 sm:p-4">
<div className="w-full sm:max-w-2xl bg-white flex flex-col max-h-[100vh] sm:max-h-[90vh] rounded-t-2xl sm:rounded-2xl shadow-xl">
{/* Fixed Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-neutral-200 flex-shrink-0">
<h2 className="text-lg sm:text-xl font-semibold text-neutral-900">{t('upload.uploadPhotos')}</h2>
<button
onClick={onClose}
className="p-1.5 sm:p-2 hover:bg-neutral-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-neutral-500" />
</button>
</div>
{/* Content */}
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 160px)', minHeight: '300px' }}>
{/* Scrollable Content */}
<div className="flex-1 p-4 sm:p-6 overflow-y-auto min-h-0">
{/* Upload Area */}
<div className="mb-4 sm:mb-6">
<label className="block">
@@ -199,30 +198,29 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
))}
</div>
)}
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 sm:gap-3 p-4 sm:p-6 border-t border-neutral-200 bg-white">
<Button
variant="outline"
onClick={onClose}
disabled={uploading}
className="text-sm sm:text-base"
>
{t('common.cancel')}
</Button>
<Button
variant="primary"
onClick={handleUpload}
disabled={files.length === 0 || uploading}
isLoading={uploading}
className="text-sm sm:text-base"
>
{uploading ? t('upload.uploading') : t('common.upload')} ({files.length})
</Button>
</div>
</CardContent>
</Card>
{/* Fixed Footer */}
<div className="flex items-center justify-end gap-2 sm:gap-3 p-4 sm:p-6 border-t border-neutral-200 bg-white flex-shrink-0">
<Button
variant="outline"
onClick={onClose}
disabled={uploading}
className="text-sm sm:text-base"
>
{t('common.cancel')}
</Button>
<Button
variant="primary"
onClick={handleUpload}
disabled={files.length === 0 || uploading}
isLoading={uploading}
className="text-sm sm:text-base"
>
{uploading ? t('upload.uploading') : t('common.upload')} ({files.length})
</Button>
</div>
</div>
</div>
);
};
@@ -11,6 +11,8 @@ export interface BaseGalleryLayoutProps {
onPhotoSelect?: (photoId: number) => void;
eventName?: string;
eventLogo?: string | null;
eventDate?: string;
expiresAt?: string;
}
export abstract class BaseGalleryLayout<T extends BaseGalleryLayoutProps = BaseGalleryLayoutProps> extends React.Component<T> {
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Download, Maximize2, Check, ChevronDown } from 'lucide-react';
import { Download, Maximize2, Check, ChevronDown, Calendar, Clock } from 'lucide-react';
import { parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
import { useTheme } from '../../../contexts/ThemeContext';
import { AuthenticatedImage } from '../../common';
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
@@ -8,6 +11,8 @@ import type { Photo } from '../../../types';
interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
eventName?: string;
eventLogo?: string | null;
eventDate?: string;
expiresAt?: string;
}
export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
@@ -18,8 +23,12 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
isSelectionMode = false,
onPhotoSelect,
eventName,
eventLogo
eventLogo,
eventDate,
expiresAt
}) => {
const { t } = useTranslation();
const { format } = useLocalizedDate();
const { theme } = useTheme();
const [heroPhoto, setHeroPhoto] = useState<Photo | null>(null);
const gallerySettings = theme.gallerySettings || {};
@@ -72,10 +81,28 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
{/* Event Title */}
{eventName && (
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-white drop-shadow-lg">
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-white drop-shadow-lg mb-4">
{eventName}
</h1>
)}
{/* Event Dates */}
{(eventDate || expiresAt) && (
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 text-white/90">
{eventDate && (
<span className="flex items-center text-lg sm:text-xl">
<Calendar className="w-5 h-5 sm:w-6 sm:h-6 mr-2" />
{format(parseISO(eventDate), 'PP')}
</span>
)}
{expiresAt && (
<span className="flex items-center text-lg sm:text-xl">
<Clock className="w-5 h-5 sm:w-6 sm:h-6 mr-2" />
{t('gallery.expires')} {format(parseISO(expiresAt), 'PP')}
</span>
)}
</div>
)}
</div>
</div>
+23 -38
View File
@@ -24,7 +24,7 @@ import { toast } from 'react-toastify';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { Button, Input, Card, Loading } from '../../components/common';
import { PhotoUpload, EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeDisplay, ThemeCustomizerEnhanced, ThemeEditorModal, HeroPhotoSelector } from '../../components/admin';
import { PhotoUpload, EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeDisplay, ThemeCustomizerEnhanced, ThemeEditorModal, HeroPhotoSelector, PhotoUploadModal } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { archiveService } from '../../services/archive.service';
@@ -887,31 +887,18 @@ export const EventDetailsPage: React.FC = () => {
{/* 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">{t('events.uploadPhotos')}</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(t('toast.uploadSuccess'));
setShowPhotoUpload(false);
refetchPhotos();
}}
/>
</Card>
)}
{/* Photo Upload Modal */}
<PhotoUploadModal
isOpen={showPhotoUpload}
onClose={() => setShowPhotoUpload(false)}
eventId={parseInt(id!)}
onUploadComplete={() => {
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
queryClient.invalidateQueries({ queryKey: ['admin-event-photos', id] });
toast.success(t('toast.uploadSuccess'));
refetchPhotos();
}}
/>
{/* Photo Filters */}
<PhotoFilters
@@ -926,18 +913,16 @@ export const EventDetailsPage: React.FC = () => {
/>
{/* 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)}
>
{t('events.uploadPhotos')}
</Button>
</div>
)}
<div className="mb-4 flex justify-between items-center">
<Button
variant="primary"
size="sm"
leftIcon={<Upload className="w-4 h-4" />}
onClick={() => setShowPhotoUpload(true)}
>
{t('events.uploadPhotos')}
</Button>
</div>
{/* Photo Grid */}
{photosLoading ? (