From 6948aaa92afc29609f85cf7fd631095f3e32ad3f Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 15 Sep 2025 22:55:35 +0200 Subject: [PATCH] feat(gallery): always-visible feedback indicators on grid tiles; fallback image rendering in lightbox/hero; auto-auth from shared-link token; fix external photo resolver\n\n- GridGallery: bottom-left icons for like/rated/comment on every tile\n- Hero layout grid: added same indicators (non-intrusive icons)\n- Lightbox/Hero: add fallbackSrc to display thumbnail if original fails\n- GalleryAuth: auto-store token from /gallery/:slug/:token and hydrate event\n- Backend gallery photo route: use resolvePhotoFilePath for external-media\n\nfix(admin): move photo feedback badges to bottom-right on admin grid tiles\n\nfix(dashboard): add missing i18n keys for activity types + fallback to formatter\n\nfix(admin/feedback): correct thumbnail URL base + robust date parsing\n\nRefs: #19 --- backend/src/routes/gallery.js | 15 ++------- docker-compose.yml | 1 + .../src/components/admin/AdminPhotoGrid.tsx | 16 ++++----- .../admin/FeedbackModerationPanel.tsx | 2 +- .../src/components/gallery/PhotoLightbox.tsx | 1 + .../gallery/layouts/GridGalleryLayout.tsx | 29 ++++++++-------- .../gallery/layouts/HeroGalleryLayout.tsx | 26 +++++++++++++++ .../gallery/layouts/MosaicGalleryLayout.tsx | 2 ++ frontend/src/contexts/GalleryAuthContext.tsx | 33 +++++++++++++++++-- frontend/src/i18n/locales/de.json | 18 ++++++++++ frontend/src/i18n/locales/en.json | 18 ++++++++++ frontend/src/pages/admin/AdminDashboard.tsx | 14 ++++---- .../src/pages/admin/EventFeedbackPage.tsx | 18 +++++++--- 13 files changed, 143 insertions(+), 50 deletions(-) diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 9bb8824..7ed8b36 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -413,18 +413,9 @@ router.get('/:slug/photo/:photoId', }); } - // Photo path should be in storage/events/active directory - // Handle both legacy paths (just slug/filename) and new paths (events/active/slug/filename) - const storagePath = getStoragePath(); - - let filePath; - if (photo.path.startsWith('events/active/')) { - // New format: path already includes events/active/ prefix - filePath = path.join(storagePath, photo.path); - } else { - // Legacy format: path is just slug/filename - filePath = path.join(storagePath, 'events/active', photo.path); - } + // Resolve the absolute file path for this photo, supporting both managed and external reference modes + const { resolvePhotoFilePath } = require('../services/photoResolver'); + const filePath = resolvePhotoFilePath(req.event, photo); // Log access - temporarily disabled for debugging diff --git a/docker-compose.yml b/docker-compose.yml index 9d34369..b67a585 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - DB_NAME=${DB_NAME} + - EXTERNAL_MEDIA_ROOT=${EXTERNAL_MEDIA_ROOT:-/app/storage/external-media} - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_SECURE=${SMTP_SECURE:-false} diff --git a/frontend/src/components/admin/AdminPhotoGrid.tsx b/frontend/src/components/admin/AdminPhotoGrid.tsx index 3aecd44..aff1b2d 100644 --- a/frontend/src/components/admin/AdminPhotoGrid.tsx +++ b/frontend/src/components/admin/AdminPhotoGrid.tsx @@ -257,21 +257,21 @@ export const AdminPhotoGrid: React.FC = ({ )} - {/* Feedback Indicators */} + {/* Feedback Indicators (moved to bottom-right to avoid covering category) */} {(photo.comment_count > 0 || photo.average_rating > 0 || photo.like_count > 0) && ( -
- {photo.comment_count > 0 && ( -
- - {photo.comment_count} -
- )} +
{photo.average_rating > 0 && (
{Number(photo.average_rating).toFixed(1)}
)} + {photo.comment_count > 0 && ( +
+ + {photo.comment_count} +
+ )}
)}
diff --git a/frontend/src/components/admin/FeedbackModerationPanel.tsx b/frontend/src/components/admin/FeedbackModerationPanel.tsx index 673cb84..d15fe8b 100644 --- a/frontend/src/components/admin/FeedbackModerationPanel.tsx +++ b/frontend/src/components/admin/FeedbackModerationPanel.tsx @@ -132,7 +132,7 @@ export const FeedbackModerationPanel: React.FC = (
diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index 95380d1..b43bb5c 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -331,6 +331,7 @@ export const PhotoLightbox: React.FC = ({ = ({
- {/* Feedback Indicators */} + {/* Feedback Indicators (always visible, bottom-left) */} {feedbackEnabled && (photo.comment_count > 0 || photo.average_rating > 0 || photo.like_count > 0) && ( -
- {photo.comment_count > 0 && ( -
- - {photo.comment_count} -
+
+ {photo.like_count > 0 && ( + + + )} {photo.average_rating > 0 && ( -
+ - {Number(photo.average_rating).toFixed(1)} -
+ )} - {photo.like_count > 0 && ( -
- - {photo.like_count} -
+ {photo.comment_count > 0 && ( + + + )}
)} {photo.type === 'collage' && ( -
+
Collage diff --git a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx index a5a44e4..181d1c6 100644 --- a/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/HeroGalleryLayout.tsx @@ -84,11 +84,13 @@ export const HeroGalleryLayout: React.FC = ({ const remainingPhotos = photos; return ( + <>
{/* Hero Section */}
= ({ {selectedPhotos.has(photo.id) && }
+ + {/* Feedback indicators (always visible, bottom-left) */} + {(feedbackEnabled && (photo.like_count > 0 || (photo.average_rating || 0) > 0 || (photo.comment_count || 0) > 0)) && ( +
+ {photo.like_count > 0 && ( + + + + )} + {(photo.average_rating || 0) > 0 && ( + + + {/* Using star icon to indicate rating */} + + + )} + {(photo.comment_count || 0) > 0 && ( + + + + )} +
+ )}
); })} @@ -282,5 +307,6 @@ export const HeroGalleryLayout: React.FC = ({ }} feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'} /> + ); }; diff --git a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx index 33bacf5..1a414ae 100644 --- a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx @@ -42,6 +42,7 @@ const MosaicPhoto: React.FC = ({ const [pendingAction, setPendingAction] = React.useState(null); const [savedIdentity, setSavedIdentity] = React.useState<{ name: string; email: string } | null>(null); return ( + <>
{ @@ -172,6 +173,7 @@ const MosaicPhoto: React.FC = ({ }} feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'} /> + ); }; diff --git a/frontend/src/contexts/GalleryAuthContext.tsx b/frontend/src/contexts/GalleryAuthContext.tsx index d1e2c26..83d3689 100644 --- a/frontend/src/contexts/GalleryAuthContext.tsx +++ b/frontend/src/contexts/GalleryAuthContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import type { ReactNode } from 'react'; -import { authService } from '../services'; +import { authService, galleryService } from '../services'; import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth'; interface GalleryEvent { @@ -79,6 +79,35 @@ export const GalleryAuthProvider: React.FC = ({ childr localStorage.removeItem(`gallery_event_${currentSlug}`); localStorage.removeItem(`gallery_token_${currentSlug}`); } + } else { + // No stored auth; check for token in URL and auto-authenticate + const parts = window.location.pathname.split('/'); + const urlToken = parts.length >= 5 ? parts[4] : (parts.length >= 4 ? parts[3] : undefined); + if (urlToken) { + (async () => { + try { + setIsLoading(true); + // Verify token against backend + const verify = await galleryService.verifyToken(currentSlug, urlToken); + if (verify?.valid) { + // Store token and fetch event via photos endpoint to get full event object + localStorage.setItem(`gallery_token_${currentSlug}`, urlToken); + const data = await galleryService.getGalleryPhotos(currentSlug); + if (data?.event) { + setEvent(data.event); + setIsAuthenticated(true); + localStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(data.event)); + } + } + } catch (e) { + // Invalid token; ensure any residual storage is cleared + localStorage.removeItem(`gallery_token_${currentSlug}`); + localStorage.removeItem(`gallery_event_${currentSlug}`); + } finally { + setIsLoading(false); + } + })(); + } } } setIsLoading(false); @@ -128,4 +157,4 @@ export const GalleryAuthProvider: React.FC = ({ childr {children} ); -}; \ No newline at end of file +}; diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 2daa04a..be4ccbc 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1079,6 +1079,24 @@ "bulk_download": "{{count}} Fotos heruntergeladen von {{eventName}}", "gallery_password_entry": "Passwort eingegeben für {{eventName}}", "expiration_warning_viewed": "Ablaufwarnung angesehen für {{eventName}}", + "feedback_settings_updated": "Feedback-Einstellungen aktualisiert", + "feedback_moderated": "Feedback moderiert", + "feedback_deleted": "Feedback gelöscht", + "photo_like": "Foto mit Gefällt mir markiert in {{eventName}}", + "photo_favorite": "Foto favorisiert in {{eventName}}", + "photo_rating": "Foto bewertet in {{eventName}}", + "photo_comment": "Foto kommentiert in {{eventName}}", + "guest_feedback_like": "Gast hat ein Foto mit Gefällt mir markiert in {{eventName}}", + "guest_feedback_favorite": "Gast hat ein Foto favorisiert in {{eventName}}", + "guest_feedback_rating": "Gast hat ein Foto bewertet in {{eventName}}", + "guest_feedback_comment": "Gast hat ein Foto kommentiert in {{eventName}}", + "word_filter_added": "Wortfilter hinzugefügt", + "external_import_completed": "Externer Medienimport abgeschlossen ({{imported}} importiert, {{skipped}} übersprungen)", + "bulk_archive_completed": "Sammelarchivierung abgeschlossen", + "event_activated": "Veranstaltung aktiviert: {{eventName}}", + "event_deactivated": "Veranstaltung deaktiviert: {{eventName}}", + "photo_deleted": "Foto gelöscht aus {{eventName}}", + "photos_bulk_deleted": "{{count}} Fotos gelöscht aus {{eventName}}", "settings_updated": "Einstellungen aktualisiert", "event_updated": "Veranstaltung aktualisiert: {{eventName}}", "event_deleted": "Veranstaltung gelöscht: {{eventName}}", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 0dfd3f3..f024836 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -821,6 +821,24 @@ "bulk_download": "{{count}} photos downloaded from {{eventName}}", "gallery_password_entry": "Password entered for {{eventName}}", "expiration_warning_viewed": "Expiration warning viewed for {{eventName}}", + "feedback_settings_updated": "Feedback settings updated", + "feedback_moderated": "Feedback moderated", + "feedback_deleted": "Feedback deleted", + "photo_like": "Photo liked in {{eventName}}", + "photo_favorite": "Photo favorited in {{eventName}}", + "photo_rating": "Photo rated in {{eventName}}", + "photo_comment": "Photo commented in {{eventName}}", + "guest_feedback_like": "Guest liked a photo in {{eventName}}", + "guest_feedback_favorite": "Guest favorited a photo in {{eventName}}", + "guest_feedback_rating": "Guest rated a photo in {{eventName}}", + "guest_feedback_comment": "Guest commented on a photo in {{eventName}}", + "word_filter_added": "Word filter added", + "external_import_completed": "External media import completed ({{imported}} imported, {{skipped}} skipped)", + "bulk_archive_completed": "Bulk archive completed", + "event_activated": "Event activated: {{eventName}}", + "event_deactivated": "Event deactivated: {{eventName}}", + "photo_deleted": "Photo deleted from {{eventName}}", + "photos_bulk_deleted": "{{count}} photos deleted from {{eventName}}", "settings_updated": "Settings updated", "event_updated": "Event updated: {{eventName}}", "event_deleted": "Event deleted: {{eventName}}", diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx index 209423a..c73d3ba 100644 --- a/frontend/src/pages/admin/AdminDashboard.tsx +++ b/frontend/src/pages/admin/AdminDashboard.tsx @@ -268,13 +268,13 @@ export const AdminDashboard: React.FC = () => { categoryName: activity.metadata?.category_name || '' }; - // Check if translation exists - const translated = t(translationKey, params); - if (typeof translated === 'string') { - return translated; + // Translate; if key missing i18n returns the key string itself + const translated = t(translationKey, params) as string; + if (!translated || translated === translationKey) { + // Fallback: format a readable English message + return adminService.formatActivityMessage(activity); } - // Fallback to unknown activity if translation not found - return t('admin.activities.unknown') as string; + return translated; }; return ( @@ -302,4 +302,4 @@ export const AdminDashboard: React.FC = () => { ); }; -AdminDashboard.displayName = 'AdminDashboard'; \ No newline at end of file +AdminDashboard.displayName = 'AdminDashboard'; diff --git a/frontend/src/pages/admin/EventFeedbackPage.tsx b/frontend/src/pages/admin/EventFeedbackPage.tsx index f7be1f2..21b1b89 100644 --- a/frontend/src/pages/admin/EventFeedbackPage.tsx +++ b/frontend/src/pages/admin/EventFeedbackPage.tsx @@ -17,7 +17,7 @@ import { Trash2 } from 'lucide-react'; import { toast } from 'react-toastify'; -import { format } from 'date-fns'; +import { format, parseISO } from 'date-fns'; import { Button, Card, Loading } from '../../components/common'; import { AdminAuthenticatedImage } from '../../components/admin/AdminAuthenticatedImage'; @@ -254,7 +254,7 @@ export const EventFeedbackPage: React.FC = () => { {item.photo_id && (
@@ -290,7 +290,12 @@ export const EventFeedbackPage: React.FC = () => {

{item.comment_text}

)}

- {format(new Date(item.created_at), 'PPpp')} + {(() => { + const d = typeof item.created_at === 'string' + ? parseISO(item.created_at) + : new Date(item.created_at); + return isNaN(d.getTime()) ? t('common.unknownDate', 'Unknown date') : format(d, 'PPpp'); + })()}

@@ -492,7 +497,12 @@ export const EventFeedbackPage: React.FC = () => {

{comment.comment_text}

{comment.guest_name} • {comment.filename} • - {format(new Date(comment.created_at), 'PP')} + {(() => { + const d = typeof comment.created_at === 'string' + ? parseISO(comment.created_at) + : new Date(comment.created_at); + return isNaN(d.getTime()) ? t('common.unknownDate', 'Unknown date') : format(d, 'PP'); + })()}

))}