- Render welcome_message in gallery view for all non-fullpage layouts
(grid, masonry, carousel, timeline, mosaic) as a centered banner
- Add /api prefix to thumbnail/photo URLs in adminGuests.js and
adminPhotos.js so they route correctly through Nginx proxy
- Gallery now respects the configured sort direction (asc/desc) from
default_photo_sort setting instead of using hard-coded directions
- Photos endpoint zeroes out feedback fields (like_count, favorite_count,
average_rating, comment_count, has_feedback) when show_feedback_to_guests
is disabled, while still showing data to admin/client users
Fixes two bugs reported on #292 after 3.27.0-beta.0 shipped:
1. Masonry grid showed no visual feedback after liking a photo.
MasonryGalleryLayout's Like button had no liked-state plumbing —
the Heart icon was a static <Heart> regardless of whether the user
had liked the photo.
2. PhotoLightbox (fullscreen view) silently failed to like photos in
guest identity mode. submitLike() and submitRating() never called
ensureIdentity() before firing the API request, so the first
interaction from a fresh session hit a 401 from the server instead
of opening the name prompt.
Root causes:
1. MasonryGalleryLayout was missing the 'liked' state pattern that
GridGalleryLayout already uses (likedPhotoIds Set in the parent,
passed down as a `liked` prop, updated via onLikeSuccess callback).
The bug was invisible in simple mode (no personal state) but
surfaced immediately in guest mode where each guest expects to see
confirmation of their own action.
2. PhotoLightbox's submit handlers were written before the guest
identity context existed and only checked the legacy
require_name_email flag. They were never updated when guest mode
landed.
Also fixed: z-index conflict where the GuestNamePromptModal (z-50)
was sitting at the same level as PhotoLightbox (z-50), so when the
prompt opened over the lightbox, the fullscreen image intercepted
pointer events and the modal's Continue button was unclickable.
Bumped both guest modals to z-[60].
Changes:
- MasonryGalleryLayout.tsx
- MasonryPhotoProps gains `liked?: boolean` + `onLikeSuccess?: () => void`.
- Like button: red bg + filled white Heart icon when liked; aria-label
toggles between "Like photo"/"Unlike photo"; aria-pressed mirrors state.
- onClick wires onLikeSuccess() for optimistic UI in both guest-mode
and simple-mode branches plus the FeedbackIdentityModal onSubmit path.
- Parent layout holds `likedPhotoIds: Set<number>` and passes it to
each MasonryPhoto (matches the GridGalleryLayout pattern).
- PhotoLightbox.tsx
- Consumes useGuestIdentityOptional(); new `isGuestMode` flag.
- submitLike() and submitRating() get a guest-mode branch that calls
ensureIdentity() first and submits without body guest_name/email
(server reads from the verified token).
- Optimistic UI updates happen after successful submit in guest mode.
- GuestNamePromptModal.tsx, GuestRecoveryModal.tsx
- z-50 → z-[60] so they render above PhotoLightbox.
Verified end-to-end against local Docker with Playwright MCP on event
168 (Masonry Columns Test layout):
- Fresh session, click Like in Masonry grid → name prompt opens, register,
feedback persists with guest_id, Heart button turns red with
aria-pressed and "Unlike photo" label. Subsequent likes on other
photos also show red state. DB confirms feedback rows.
- Fresh session, open photo in lightbox BEFORE registering → click Like,
the name prompt correctly opens on top of the lightbox, register,
feedback persists. Rate 4 stars → works, average 4.0 (1) displayed
in lightbox, ★ badge appears on toggle-feedback button, grid cell
shows "1 likes" + "Rating: 4.0" indicators after closing lightbox.
- Backend DB: gallery_guests row created, photo_feedback rows have
correct guest_id, server reads name from verified token (body values
ignored).
Out of scope (documented in audit, not reported by the user, no
regression from guest mode): Mosaic/Carousel/Timeline have partial
optimistic-UI issues unrelated to this report; they pre-date guest
mode and behave the same in simple mode. Leaving alone per scope
discipline.
Introduces a new "Per-guest selections" identity mode for event
feedback, letting each visitor register under their own name so their
likes/favorites/comments/ratings are tracked independently. Includes
admin insights (list, per-guest detail, aggregate view, export) and
advanced identity features (forget-me, email recovery, invite tokens,
merge).
New event-level setting
- event_feedback_settings.identity_mode = 'simple' | 'guest' (default
'simple' → zero behavior change for existing events).
- Admin UI radio under Feedback Settings to toggle per event.
Root cause of the previous "all guests share state" bug
- generateGuestIdentifier() was sha256(ip + userAgent), so every visitor
on the same WiFi + similar device collided into one identity.
- Now: when a verified guest JWT is present (x-guest-token header),
req.guest.identifier takes precedence — per-person rate limits and
per-person deduplication.
Phase 1 — identity layer
- Migration 078: new gallery_guests, guest_invites, guest_verification_
codes tables; identity_mode column + check constraint; nullable
guest_id FK on photo_feedback.
- New guest JWT type scoped to (eventId, guestId).
- New middleware guestAuth.resolveGuest (non-blocking) + requireGuest.
- POST /gallery/:slug/guest, GET /guest/me, DELETE /guest/me.
- Gallery feedback route enforces guest identity in guest mode and
reads name/email from the verified token (never from the body).
- Frontend GuestIdentityContext + GuestNamePromptModal; axios
interceptor injects x-guest-token on gallery API calls.
- Feedback-only blocking: gallery opens freely, prompt only on first
interactive feedback action.
- Admin "Guests" tab (conditional on identity_mode='guest') with the
AdminGuestsList component.
Phase 2 — admin insights
- GET /admin/events/:eventId/guests list + aggregated counts.
- GET /admin/events/:eventId/guests/:guestId detail with per-type
groupings; AdminGuestDetail modal with thumbnail grid + tabs.
- GET /admin/events/:eventId/guests/aggregate sorted by distinct guest
pick count; GuestSelectionsAggregate component.
- Per-guest export (txt/csv/json) and bulk export-all ZIP.
Phase 3 — polish
- 3.1 Self-service forget-me link in gallery footer.
- 3.2 Email-based identity recovery: POST /guest/recover sends a
6-digit code via the existing emailProcessor, POST /guest/verify
exchanges it for a token (rate-limited, enumeration-safe).
- 3.3 Admin invite tokens: pre-mint identities, share URLs with
?invite=, single-use redemption stripping the param from history.
- 3.4 Admin merge endpoint reassigns feedback + soft-deletes sources.
Shared helper
- useGalleryFeedbackAction hook wraps the identity-check logic for
inline like buttons across Masonry/Grid/Justified/Mosaic/Carousel/
Timeline/Premium layouts.
Backwards compatibility
- Existing events default to 'simple' after migration; behavior
unchanged.
- Legacy photo_feedback rows keep guest_id NULL; admin shows them in
the generic feedback moderation view as before.
- feedback_count denormalized stat now uses COALESCE(guest_id,
guest_identifier) so per-guest counts are accurate without touching
legacy rows.
Verified end-to-end against local Docker
- Migration clean on existing data.
- Simple mode unchanged (no prompt, legacy flow).
- Guest mode: Alice registers on click, tokens persist in
sessionStorage, feedback rows carry guest_id.
- Carol via invite link auto-redeems, sees Alice's "1 likes" badge.
- Admin Guests tab shows both with correct counts; detail modal
displays thumbnail grid with badges; aggregate view sorts by picker
count (photo 227 = 2, others = 1); CSV/JSON export matches DB.
- Merge Carol into Alice: feedback reassigned, Carol soft-deleted,
Alice count = 4.
The redirect loop fix only covered MandatoryPasswordChangeModal.
The regular PasswordChangeModal (profile settings) had the same
issue — onSuccess updated React state but didn't handle the new
JWT cookie, causing the same redirect loop.
Also increase redirect delay from 500ms to 2000ms in both modals
so the success toast is visible before the page reloads.
Add per-event default photo sort setting with 6 options:
- Upload Date (Newest/Oldest First)
- Date Taken (Newest/Oldest First) — uses EXIF captured_at
- Filename (A-Z / Z-A)
Backend:
- Migration 077 adds default_photo_sort column to events table
- Event create/update handlers accept and validate the setting
- Gallery info endpoint returns default_photo_sort for frontend
Frontend:
- "Date Taken" added to gallery sort dropdown (alongside Date, Name,
Size, Rating)
- Gallery initializes with event's default sort instead of hardcoded
"date"
- "Default Photo Sort" dropdown in event create and edit forms
- Photos without EXIF dates fall back to upload date
i18n: All 5 locales (EN, DE, NL, PT, RU) updated with sort labels.
Closes#283
#263: The mandatory password change modal updated React state before
the browser stored the new JWT cookie, causing a race condition where
the auth context checked the session with the old (invalidated) token.
Replace the state update with a full page redirect to /admin/dashboard
after a brief delay, ensuring the new cookie is applied cleanly.
#269: The file watcher service imported isVideoMimeType from
fileSecurityUtils where it doesn't exist. The function is exported
from videoProcessor. Fix the import path.
Closes#269
Draft Mode:
- Events are created as drafts by default — no email sent until published
- Add "Publish & Notify Client" button with confirmation dialog
- Draft banner with yellow styling on event details page
- Draft filter tab in events list
- Gallery middleware blocks public access to draft events
- Migration 076 adds is_draft column to events table
Admin Draft Preview:
- Admins can preview draft galleries via JWT preview token (?preview=)
- "View Gallery" link on drafts auto-appends preview token
Admin & Login Page Branding:
- Admin header uses configured company logo/name from branding settings
- Login page shows configured logo instead of hardcoded PicPeak
- Respects logo_display_mode (logo_only, text_only, logo_and_text)
OG Tag Branding:
- DynamicFavicon component updates OG meta tags and page title from
branding settings
Editable Client Email:
- Customer email is now editable after event creation in edit mode
Branding Inheritance:
- New events inherit hero logo settings (visibility, size, position)
from global branding configuration
Share Link Full Domain URL:
- New getFrontendBaseUrl() utility with DB fallback to general_site_url
- Used in email processor and share link service
Beta themes (Gallery Premium, Gallery Story) display thumbnails at
400-800px, but the default thumbnail size is 300x300px, causing visible
pixelation. Show an amber warning banner with a link to Thumbnail
Settings when a beta layout is active and thumbnails are below 500px.
Warning appears both in the preset selector and the layout selector
sections of the theme customizer.
Add complete Dutch translation (2054 keys) with Netherlands flag in the
language selector. Also synchronize all existing locales so every language
has the same set of keys: added 29 missing keys to EN/RU/PT and 95 missing
keys to DE (moderation, analytics, CSS templates, backup, events).
Add a thumbnailScale field (xs/sm/md/lg/xl) to gallery layout settings
that adjusts column counts for Grid, Masonry (columns mode), and Mosaic
layouts. Each scale maps to a column offset applied on top of the
layout's base columns, letting photographers control photo density.
- Add thumbnailScale to GalleryLayoutSettings type
- Apply scale offset in Grid, Masonry, and Mosaic layout components
- Add thumbnail scale dropdown to admin theme customizer
- Conditionally show dropdown only for applicable layouts
- Safelist dynamic grid-cols classes in Tailwind config
- Add i18n keys for EN, DE, PT, RU locales
Co-authored-by: Paul Nothaft <[email protected]>
- Add Russian (Русский) to admin settings language dropdown
- Fix Premium layout hero using thumbnail instead of hero_url
- Hide "Uncategorized" section header in Story layout for uncategorized photos
- Add PhotoLightbox to Story layout so photo clicks open full-screen view
- Use full-res images in StoryPhotoCard instead of thumbnails
- Defer public gallery auto-login text until settings/locale are loaded
- Add validation and debug logging for email logo URL construction
Co-authored-by: Paul Nothaft <[email protected]>
- Fix dimension repair for external media by using photoResolver instead of hardcoded paths
- Extract photo dimensions via Sharp during external media import
- Remove duplicate theme useEffect from GalleryView to prevent flash/revert race condition
- Pass event welcome_message to Story layout footer for per-event customization
- Add email_primary_color/email_secondary_color settings with admin UI color pickers
- Add i18n keys for email branding in all 4 locales (en, de, ru, pt)
- Add photo_cap column to events table (migration 074) to limit photos per event
- Enforce photo cap in upload route, returning 400 when limit exceeded
- Pass photo_cap through all event CRUD routes and frontend forms
- Add complete Portuguese (pt-BR) translation (2300+ strings)
- Register pt locale in i18n config, language selector, date formatting
- Add photoCap/photoCapHelp translation keys to all locale files (en, de, ru, pt)
Replace raw HTML textarea with TipTap-based rich text editor for email
templates. Includes formatting toolbar, variable insertion dropdown,
source/visual toggle, and dark mode support. Add Mailhog service to
docker-compose for local email testing.
- Fix admin video upload missing media_type/mime_type and video processing (#203)
- Fix Gallery-Premium Select All using atomic callbacks instead of stale closure loop (#220)
- Add photo dimension repair endpoint and admin UI (#180)
- Add E2E tests for all three fixes
Users behind Cloudflare Tunnel and other reverse proxies cannot upload
batches >100MB. The upload chunking previously used a hardcoded 500MB
limit. This adds a configurable `max_upload_batch_size_mb` setting
(default 95MB) to the admin General settings, leaving headroom below
Cloudflare's 100MB limit.
The "Allowed File Types" admin setting was stored in the database but
never actually read during upload validation. Both frontend and backend
used hardcoded MIME type lists, causing video uploads (e.g. MP4) to be
rejected even when explicitly added to the setting.
Changes:
- Add getAllowedMimeTypes() to uploadSettings service that reads the
general_allowed_file_types DB setting and converts extensions to MIME types
- Backend admin upload route now resolves allowed types from settings
before multer processes files (via resolveAllowedTypes middleware)
- Backend gallery upload route uses dynamic allowed types from settings
- Expose allowed_file_types in public settings API for gallery clients
- Frontend PhotoUpload and UserPhotoUpload components now derive allowed
MIME types from settings instead of hardcoded image-only lists
- Add shared fileTypes.ts utility for extension-to-MIME conversion
Closes#203
- #194: Send full date format object instead of just format string to prevent JSON parse errors
- #195: Remove non-functional forgot password link, fix README port 3005 -> 3000
- #196: Use ADMIN_PASSWORD env var in migration, update existing user in create-admin script instead of failing
- #197: Convert camelCase filter keys to snake_case in photo export to match backend PhotoFilterBuilder
Surface the existing original_filename from the database in the admin
photo grid hover overlay and photo viewer sidebar, so photographers can
correlate uploaded images with their Lightroom/disk originals. Only shown
when it differs from the system-generated filename. Gallery guests remain
unaffected.
- Add Update Instructions Dialog with environment-specific commands (Docker/Git/Standalone)
- Add email notification settings for new version alerts
- Add "Sort by Capture Date" option using EXIF metadata extraction
- Fix E2E tests by loading environment variables via dotenv
- Add test-images/ and backend/*.db to .gitignore
Closes#181
- Fix masonry/mosaic layout regression where tiles displayed uniform heights
instead of respecting image aspect ratios. Changed from fixed 150-500px
height constraints to dynamic constraints based on column width.
- Add hero image optimization pipeline generating 1920x1080 images for
full-width hero sections instead of using low-quality thumbnails.
- New /hero/:photoId endpoint serves optimized hero images with watermark
support and automatic generation/caching.
- Add hero_url field to photos API response for frontend consumption.
- Migration 069 adds hero_path column to photos table.
- Update hero photo help text to mention category override capability
- Add hint in category manager about default hero photo fallback
- Add placeholder text in gallery preview for hero section
- Ensure live preview updates correctly for header/divider style changes
Admin Dark Mode:
- Add AdminDarkModeContext with light/dark/system preference
- Update all admin components with Tailwind dark: classes
- Add dark mode toggle in admin header
- Persist preference in localStorage
SEO Settings:
- Add robots.txt configuration in Settings > SEO tab
- Block AI crawlers (GPTBot, ChatGPT-User, etc.) with toggle
- Custom robots.txt rules management
- Add RobotsMetaTags component for gallery pages
- Backend service for dynamic robots.txt generation
- Database migration for SEO settings storage
UI/UX Improvements:
- Consistent dark mode styling across all admin pages
- Update gallery components with themed CSS classes
- Fix input, card, and button styling for dark mode
Update ghost button variant to use proper dark mode colors:
- Add dark:hover:bg-neutral-700 for hover state
- Add dark:text-neutral-300 for better icon/text visibility
- Fixes too-dark edit and view gallery buttons in Events table
- Update .card class to use explicit Tailwind colors instead of CSS
variables, preventing gallery theme from affecting admin UI
- Add .card-themed and .input-themed classes for gallery components
that need to use theme CSS variables
- Add dark mode support to CardHeader and CardFooter components
- Update .input class to use explicit colors for proper light/dark mode
- Update dark mode selectors for consistency (.dark .class)
- Add Gallery Premium layout: elegant light theme with masonry grid,
hero section, sticky navigation, and integrated lightbox
- Add Gallery Story layout: cinematic dark theme with scene-based
sections, carousels, and gold accents
- Implement full-page layout support: bypass standard header/footer/
sidebar for immersive experience
- Add logout button to both layouts for authenticated galleries
- Mark both layouts as (Beta) in theme editor and layout selectors
- Fix hero title color visibility in Gallery Premium layout
- Add distinct rendering branches for minimal and none header styles in
GalleryLayout (grid and non-grid), skipping the colored banner/wave
divider for both
- Cap hero section height at 700px via max-h to prevent it dominating
ultra-wide viewports
- Watch selectedCategoryId in GalleryView and swap the hero photo to
the category's hero_photo_id when filtering, reverting to the event
default when cleared
- Add minimal/none preview branches in GalleryPreview so the admin
theme editor shows visually distinct previews for all four styles
- Remove unused AdminPhoto import that was blocking the build
- Add Playwright e2e tests covering all four header styles, hero max
height, and category hero switching
Wire up the hero_photo_id column on photo_categories that was added in
the migration but never connected. Backend routes now accept and persist
hero_photo_id on category create/update, a dedicated PUT /:id/hero
endpoint is added, and the gallery API returns hero_photo_id for each
category. Frontend EventCategoryManager shows a clickable thumbnail per
category that opens a photo picker modal. Includes EN/DE i18n keys.
Add missing i18n translations for hero image focal point picker in both
EN and DE locales. Fix lint errors across touched files: remove unused
imports/variables, replace raw buttons with shared Button component,
eliminate inline styles, extract duplicated backend validation, and
remove dead heroImagePosition type.
Add interactive focal point picker for hero images, allowing precise
crop positioning via click or preset buttons (top/center/bottom).
Includes backend validation, migrations, and gallery rendering support.
- Add hero header rendering to GalleryPreview component with divider styles
- Support event-specific header_style prop in GalleryLayout
- Pass header_style from event data to GalleryLayout in GalleryView
- Divider options now properly show/hide when switching header styles
This ensures the live preview accurately reflects hero header changes
and event-specific header styles are respected in the gallery view.
- Add try-catch and file existence check for photo path resolution (#161)
- Fix gallery categories to use photo_categories table instead of legacy type field (#156)
- Add byte-size-based chunking (500MB max) for uploads to prevent oversized batches (#155)
- Add separate header_style setting (hero/standard/minimal/none) that can
be combined with any layout type (grid/masonry/carousel/timeline/mosaic)
- Create HeroHeader and HeroDivider components for reusable hero section
- Add hero_divider_style setting (wave/straight/angle/curve/none)
- Add database migration for header_style and hero_divider_style columns
- Remove deprecated HeroGalleryLayout component
- Fix various TypeScript errors across the codebase:
- Add missing type properties (css_template_id, updatedAt, justified settings)
- Fix null handling for event_date and expires_at fields
- Fix translation function calls and i18n config
- Remove unused imports and variables
Thumbnails are generated as 300x300 squares, so CSS Columns alone
couldn't show varied aspect ratios. Now using the photo's width/height
metadata with CSS aspect-ratio property to force correct proportions.
Replaced CSS Grid with span rules approach with CSS Columns to eliminate
gaps and white spaces in the mosaic layout. Images now flow vertically
within columns, maintaining their natural aspect ratios without gaps.
- Add migration to backfill width/height for existing photos without dimensions
- Replace justified masonry mode with quilted layout (mixed sizes based on aspect ratio)
- Rewrite mosaic layout to use proper CSS Grid with span rules
- Fix theme not being applied after gallery login
- Improve columns mode distribution using shortest-column algorithm
- Apply gallery theme regardless of authentication status
Previously, the Pinterest-style columns mode assigned random heights to
photos, causing landscape images to be cropped into portrait slots.
Now the height is calculated based on the photo's actual aspect ratio
and the column width, preserving natural proportions.
- Add Flickr justified-layout and react-photo-album as masonry mode options
- Implement aspect-ratio-aware mosaic layout that dynamically selects
patterns based on photo orientations to minimize cropping
- Add 9 mosaic pattern types optimized for different orientation combinations
- Add theme customizer options for masonry mode selection (columns/rows/flickr/justified)
- Add i18n translations for new layout options
Add Google Photos-style justified row layout as a mode within masonry:
- Add masonryMode setting: 'columns' (Pinterest) or 'rows' (Google Photos)
- Create justifiedLayoutCalculator utility for row-based layouts
- Extract and store image dimensions on upload for layout calculations
- Include width/height in gallery API response
- Add row height and last row behavior controls to theme customizer
- Support responsive container width detection with ResizeObserver
Photos in rows mode maintain their aspect ratios while filling
horizontal rows at a consistent height. The number of photos per
row is automatically calculated based on target row height and
photo dimensions.
Closes#146