diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..68a2726 --- /dev/null +++ b/TODO.md @@ -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 \ No newline at end of file diff --git a/frontend/src/components/admin/PhotoUploadModal.tsx b/frontend/src/components/admin/PhotoUploadModal.tsx new file mode 100644 index 0000000..fe92ace --- /dev/null +++ b/frontend/src/components/admin/PhotoUploadModal.tsx @@ -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 = ({ + isOpen, + onClose, + eventId, + onUploadComplete +}) => { + const { t } = useTranslation(); + + if (!isOpen) return null; + + const handleUploadComplete = () => { + if (onUploadComplete) { + onUploadComplete(); + } + onClose(); + }; + + return ( +
+
+ {/* Fixed Header */} +
+

{t('events.uploadPhotos')}

+ +
+ + {/* Scrollable Content */} +
+ +
+
+
+ ); +}; + +PhotoUploadModal.displayName = 'PhotoUploadModal'; \ No newline at end of file diff --git a/frontend/src/components/admin/index.ts b/frontend/src/components/admin/index.ts index 762b132..a51f9cb 100644 --- a/frontend/src/components/admin/index.ts +++ b/frontend/src/components/admin/index.ts @@ -19,4 +19,5 @@ export { AdminAuthenticatedImage } from './AdminAuthenticatedImage'; export { ThemeCustomizerEnhanced } from './ThemeCustomizerEnhanced'; export { ThemeDisplay } from './ThemeDisplay'; export { ThemeEditorModal } from './ThemeEditorModal'; -export { HeroPhotoSelector } from './HeroPhotoSelector'; \ No newline at end of file +export { HeroPhotoSelector } from './HeroPhotoSelector'; +export { PhotoUploadModal } from './PhotoUploadModal'; \ No newline at end of file diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx index 192ad76..b38d1fb 100644 --- a/frontend/src/components/gallery/GalleryLayout.tsx +++ b/frontend/src/components/gallery/GalleryLayout.tsx @@ -91,8 +91,9 @@ export const GalleryLayout: React.FC = ({ size="sm" leftIcon={} onClick={onLogout} + className="sm:min-w-0" > - {t('common.logout')} + {t('common.logout')} )} @@ -101,17 +102,12 @@ export const GalleryLayout: React.FC = ({ )} - {/* For grid and hero layouts - everything in one bar */} - {(!isNonGridLayout || theme.galleryLayout === 'hero') && ( + {/* For grid layout - everything in one bar */} + {!isNonGridLayout && theme.galleryLayout !== 'hero' && (
-
- {/* Left side - Logo, Title, and Dates */} -
- {/* Menu button */} -
- {headerExtra} -
- +
+ {/* Left side - Logo and menu button on mobile */} +
{/* Logo - Show custom logo or fallback to PicPeak logo */}
= ({ '/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" />
- {/* Event info */} -
-

- {event.event_name} -

- {(event.event_date || event.expires_at) && ( -
- {event.event_date && ( - - - {format(parseISO(event.event_date), 'PP')} - - )} - {event.expires_at && ( - - - {t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')} - - )} -
- )} + {/* Menu button on mobile */} +
+ {headerExtra}
+ {/* Center - Event info */} +
+

+ {event.event_name} +

+ {(event.event_date || event.expires_at) && ( +
+ {event.event_date && ( + + + {format(parseISO(event.event_date), 'PP')} + + )} + {event.expires_at && ( + + + {t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')} + + )} +
+ )} +
+ + {/* Right side - Action buttons */} +
+ {/* Menu button on desktop */} +
+ {headerExtra} +
+ + {/* Download all button - hidden on mobile when sidebar is shown */} + {showDownloadAll && onDownloadAll && ( + + )} + + {/* Logout button */} + {showLogout && onLogout && ( + + )} +
+
+ + {/* Mobile dates row */} + {(event.event_date || event.expires_at) && ( +
+ {event.event_date && ( + + + {format(parseISO(event.event_date), 'PP')} + + )} + {event.expires_at && ( + + + {t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')} + + )} +
+ )} +
+ )} + + {/* For hero layout - minimal header with just menu and logout */} + {theme.galleryLayout === 'hero' && ( +
+
+ {/* Left side - Menu button */} +
+ {headerExtra} +
+ {/* Right side - Action buttons */}
{/* Download all button */} @@ -174,8 +241,9 @@ export const GalleryLayout: React.FC = ({ size="sm" leftIcon={} onClick={onLogout} + className="sm:min-w-0" > - {t('common.logout')} + {t('common.logout')} )}
diff --git a/frontend/src/components/gallery/GallerySidebar.tsx b/frontend/src/components/gallery/GallerySidebar.tsx index be55e17..c406a6f 100644 --- a/frontend/src/components/gallery/GallerySidebar.tsx +++ b/frontend/src/components/gallery/GallerySidebar.tsx @@ -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 = ({ @@ -45,7 +47,9 @@ export const GallerySidebar: React.FC = ({ photoCounts = {}, totalPhotos, isMobile, - galleryLayout + galleryLayout, + allowUploads, + onUploadClick }) => { const { t } = useTranslation(); const sidebarRef = useRef(null); @@ -113,6 +117,24 @@ export const GallerySidebar: React.FC = ({ {/* Content */}
+ {/* Upload Section - Only show on mobile when uploads are allowed */} + {isMobile && allowUploads && onUploadClick && ( +
+ +
+ )} + {/* Search Section - Hidden for carousel layout */} {galleryLayout !== 'carousel' && (
diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 4935f31..9d28efc 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -307,6 +307,8 @@ export const GalleryView: React.FC = ({ 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 = ({ slug, event }) => { onClick={() => setSidebarOpen(!sidebarOpen)} aria-label={t('gallery.toggleMenu')} > - {t('common.menu')} + {t('common.menu')} ); } @@ -342,7 +344,23 @@ export const GalleryView: React.FC = ({ 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( + + ); + } + + // Upload button for non-sidebar layouts + if (event.allow_user_uploads && !showSidebar) { items.push(
diff --git a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx index 3adc911..4fd7b4a 100644 --- a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx +++ b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx @@ -32,6 +32,8 @@ interface PhotoGridWithLayoutsProps { showSelectionControls?: boolean; eventName?: string; eventLogo?: string | null; + eventDate?: string; + expiresAt?: string; } export const PhotoGridWithLayouts: React.FC = ({ @@ -44,7 +46,9 @@ export const PhotoGridWithLayouts: React.FC = ({ onToggleSelectionMode: parentToggleSelectionMode, showSelectionControls = true, eventName, - eventLogo + eventLogo, + eventDate, + expiresAt }) => { const { t } = useTranslation(); const { theme } = useTheme(); @@ -160,6 +164,8 @@ export const PhotoGridWithLayouts: React.FC = ({ onPhotoSelect: handlePhotoSelect, eventName, eventLogo, + eventDate, + expiresAt, }; let LayoutComponent; diff --git a/frontend/src/components/gallery/UserPhotoUpload.tsx b/frontend/src/components/gallery/UserPhotoUpload.tsx index 1729360..4ad704d 100644 --- a/frontend/src/components/gallery/UserPhotoUpload.tsx +++ b/frontend/src/components/gallery/UserPhotoUpload.tsx @@ -113,22 +113,21 @@ export const UserPhotoUpload: React.FC = ({ }; return ( -
- - - {/* Header */} -
-

{t('upload.uploadPhotos')}

- -
+
+
+ {/* Fixed Header */} +
+

{t('upload.uploadPhotos')}

+ +
- {/* Content */} -
+ {/* Scrollable Content */} +
{/* Upload Area */}
)} -
+
- {/* Footer */} -
- - -
- - + {/* Fixed Footer */} +
+ + +
+
); }; diff --git a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx index 936c503..2ccaa65 100644 --- a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx @@ -11,6 +11,8 @@ export interface BaseGalleryLayoutProps { onPhotoSelect?: (photoId: number) => void; eventName?: string; eventLogo?: string | null; + eventDate?: string; + expiresAt?: string; } export abstract class BaseGalleryLayout extends React.Component { diff --git a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx index 98ea554..f236407 100644 --- a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx @@ -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 = ({ @@ -18,8 +23,12 @@ export const HeroGalleryLayout: React.FC = ({ isSelectionMode = false, onPhotoSelect, eventName, - eventLogo + eventLogo, + eventDate, + expiresAt }) => { + const { t } = useTranslation(); + const { format } = useLocalizedDate(); const { theme } = useTheme(); const [heroPhoto, setHeroPhoto] = useState(null); const gallerySettings = theme.gallerySettings || {}; @@ -72,10 +81,28 @@ export const HeroGalleryLayout: React.FC = ({ {/* Event Title */} {eventName && ( -

+

{eventName}

)} + + {/* Event Dates */} + {(eventDate || expiresAt) && ( +
+ {eventDate && ( + + + {format(parseISO(eventDate), 'PP')} + + )} + {expiresAt && ( + + + {t('gallery.expires')} {format(parseISO(expiresAt), 'PP')} + + )} +
+ )}
diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 276636c..8eabbd6 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -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' && (
- {/* Photo Upload */} - {showPhotoUpload && ( - -
-

{t('events.uploadPhotos')}

- -
- { - queryClient.invalidateQueries({ queryKey: ['admin-event', id] }); - queryClient.invalidateQueries({ queryKey: ['admin-event-photos', id] }); - toast.success(t('toast.uploadSuccess')); - setShowPhotoUpload(false); - refetchPhotos(); - }} - /> -
- )} + {/* Photo Upload Modal */} + 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 */} { /> {/* Actions Bar */} - {!showPhotoUpload && ( -
- -
- )} +
+ +
{/* Photo Grid */} {photosLoading ? (