From 633d4a0f301e355ee9f057347f2f8dee8c5b4163 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 9 Apr 2026 15:10:51 +0200 Subject: [PATCH] feat: sort photos by capture date with configurable default sort (#283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../core/077_add_default_photo_sort.js | 17 +++++++++++ backend/src/routes/adminEvents.js | 19 ++++++++++-- backend/src/routes/gallery.js | 7 +++-- .../src/components/gallery/GallerySidebar.tsx | 2 +- .../src/components/gallery/GalleryView.tsx | 29 +++++++++++++++++++ .../src/components/gallery/PhotoFilterBar.tsx | 20 ++++++++++--- frontend/src/i18n/locales/de.json | 10 +++++++ frontend/src/i18n/locales/en.json | 10 +++++++ frontend/src/i18n/locales/nl.json | 10 +++++++ frontend/src/i18n/locales/pt.json | 10 +++++++ frontend/src/i18n/locales/ru.json | 10 +++++++ frontend/src/pages/admin/CreateEventPage.tsx | 24 +++++++++++++++ frontend/src/pages/admin/EventDetailsPage.tsx | 27 +++++++++++++++++ frontend/src/services/events.service.ts | 2 ++ frontend/src/types/index.ts | 5 ++++ 15 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/core/077_add_default_photo_sort.js diff --git a/backend/migrations/core/077_add_default_photo_sort.js b/backend/migrations/core/077_add_default_photo_sort.js new file mode 100644 index 00000000..c6468903 --- /dev/null +++ b/backend/migrations/core/077_add_default_photo_sort.js @@ -0,0 +1,17 @@ +exports.up = async function(knex) { + const hasColumn = await knex.schema.hasColumn('events', 'default_photo_sort'); + if (!hasColumn) { + await knex.schema.alterTable('events', (table) => { + table.string('default_photo_sort', 50).defaultTo('upload_date_desc'); + }); + } +}; + +exports.down = async function(knex) { + const hasColumn = await knex.schema.hasColumn('events', 'default_photo_sort'); + if (hasColumn) { + await knex.schema.alterTable('events', (table) => { + table.dropColumn('default_photo_sort'); + }); + } +}; diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index c76f538e..b04bbc7e 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -259,7 +259,12 @@ router.post('/', adminAuth, requirePermission('events.create'), [ body('hero_image_anchor').optional().custom(validateHeroImageAnchor), // Client access settings (#172) body('client_access_enabled').optional().isBoolean(), - body('client_password').optional().isString() + body('client_password').optional().isString(), + body('default_photo_sort').optional().isIn([ + 'upload_date_desc', 'upload_date_asc', + 'capture_date_desc', 'capture_date_asc', + 'filename_asc', 'filename_desc' + ]) ], async (req, res) => { try { logger.debug('Create event request body', { body: req.body }); @@ -314,7 +319,9 @@ router.post('/', adminAuth, requirePermission('events.create'), [ client_access_enabled = false, client_password = null, // Draft mode - is_draft = true + is_draft = true, + // Default photo sort + default_photo_sort = 'upload_date_desc' } = req.body; const customerName = getCustomerNameFromPayload(req.body); @@ -483,6 +490,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [ hero_image_anchor: hero_image_anchor || 'center', photo_cap: photo_cap || null, is_draft: formatBoolean(parseBooleanInput(is_draft, true)), + default_photo_sort: default_photo_sort || 'upload_date_desc', // Client access (#172) client_access_enabled: formatBoolean(client_access_enabled), ...(client_access_enabled && client_password ? { @@ -856,7 +864,12 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne // Client access settings (#172) body('client_access_enabled').optional().isBoolean(), body('client_password').optional().isString(), - body('regenerate_client_token').optional().isBoolean() + body('regenerate_client_token').optional().isBoolean(), + body('default_photo_sort').optional().isIn([ + 'upload_date_desc', 'upload_date_asc', + 'capture_date_desc', 'capture_date_asc', + 'filename_asc', 'filename_desc' + ]) ], async (req, res) => { try { const errors = validationResult(req); diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 3072d370..ebaa35c2 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -123,7 +123,8 @@ router.get('/:slug/info', async (req, res) => { 'header_style', 'hero_divider_style', 'hero_image_anchor', - 'is_draft' + 'is_draft', + 'default_photo_sort' ) .first(); @@ -182,7 +183,8 @@ router.get('/:slug/info', async (req, res) => { hero_logo_url: event.hero_logo_url || null, header_style: event.header_style || 'standard', hero_divider_style: event.hero_divider_style || 'wave', - hero_image_anchor: event.hero_image_anchor || 'center' + hero_image_anchor: event.hero_image_anchor || 'center', + default_photo_sort: event.default_photo_sort || 'upload_date_desc' }); } catch (error) { console.error('Error fetching gallery info:', error); @@ -397,6 +399,7 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { header_style: req.event.header_style || 'standard', hero_divider_style: req.event.hero_divider_style || 'wave', hero_image_anchor: req.event.hero_image_anchor || 'center', + default_photo_sort: req.event.default_photo_sort || 'upload_date_desc', ...protectionSettings }, categories: categories, diff --git a/frontend/src/components/gallery/GallerySidebar.tsx b/frontend/src/components/gallery/GallerySidebar.tsx index 9936ca7f..43e9bd13 100644 --- a/frontend/src/components/gallery/GallerySidebar.tsx +++ b/frontend/src/components/gallery/GallerySidebar.tsx @@ -356,7 +356,7 @@ export const GallerySidebar: React.FC = ({ @@ -101,6 +102,17 @@ export const PhotoFilterBar: React.FC = ({ > {t('gallery.sortByDate')} +