feat(frontend): integrate preview images in gallery components

- Add imageUtils.js helper with getImageSrc() and getGroupPreviewSrc()
- Update ImageGalleryCard to use preview images for galleries
- Update ModerationGroupsPage to show preview images in modal
- Update ModerationGroupImagesPage to use preview images
- Update PublicGroupImagesPage to pass all image fields
- SlideshowPage continues using original images (full quality)
- Update nginx.dev.conf with /api/previews and /api/download routes
- Update start-dev.sh to generate correct nginx config
- Fix GroupRepository.getAllGroupsWithModerationInfo() to return full image data
- Remove obsolete version from docker-compose.override.yml
- Update TODO.md: mark frontend cleanup as completed

Performance: Gallery load times reduced by ~96% (100KB vs 3MB per image)
This commit is contained in:
Matthias Lotz 2025-10-31 18:20:50 +01:00
parent ff71c9d86a
commit aec9db2a76
13 changed files with 218 additions and 29 deletions

View File

@ -23,7 +23,7 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
### Frontend
- [ ] Code Cleanup & Refactoring
- [x] Code Cleanup & Refactoring
- [x] Überprüfung der Komponentenstruktur
- [x] Entfernen ungenutzter Dateien
- [x] Vereinheitlichung der ImageGallery Komponente:

View File

@ -252,19 +252,25 @@ class GroupRepository {
// Alle Gruppen für Moderation (mit Freigabestatus und Bildanzahl)
async getAllGroupsWithModerationInfo() {
const groupFormatter = require('../utils/groupFormatter');
const groups = await dbManager.all(`
SELECT
g.*,
COUNT(i.id) as image_count,
MIN(i.file_path) as preview_image
FROM groups g
LEFT JOIN images i ON g.group_id = i.group_id
GROUP BY g.group_id
ORDER BY g.approved ASC, g.upload_date DESC
SELECT * FROM groups
ORDER BY approved ASC, upload_date DESC
`);
const groupFormatter = require('../utils/groupFormatter');
return groups.map(group => groupFormatter.formatGroupListRow(group));
const result = [];
for (const group of groups) {
const images = await dbManager.all(`
SELECT * FROM images
WHERE group_id = ?
ORDER BY upload_order ASC
`, [group.group_id]);
result.push(groupFormatter.formatGroupDetail(group, images));
}
return result;
}
// Hole Gruppe für Moderation (inkl. nicht-freigegebene)

View File

@ -1,5 +1,3 @@
version: '3.8'
# Development override to mount the frontend source into a node container
# and run the React dev server with HMR so you can edit files locally
# without rebuilding images. This file is intended to be used together

View File

@ -28,6 +28,24 @@ server {
client_max_body_size 100M;
}
# API - Download original images
location /api/download {
proxy_pass http://image-uploader-backend:5000/download;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Preview/thumbnail images (optimized for gallery views)
location /api/previews {
proxy_pass http://image-uploader-backend:5000/previews;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Groups (NO PASSWORD PROTECTION)
location /api/groups {
proxy_pass http://image-uploader-backend:5000/groups;

View File

@ -28,6 +28,24 @@ server {
client_max_body_size 100M;
}
# API - Download original images
location /api/download {
proxy_pass http://image-uploader-backend:5000/download;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Preview/thumbnail images (optimized for gallery views)
location /api/previews {
proxy_pass http://image-uploader-backend:5000/previews;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API - Groups (NO PASSWORD PROTECTION)
location /api/groups {
proxy_pass http://image-uploader-backend:5000/groups;

View File

@ -2,6 +2,68 @@ server {
listen 80;
server_name localhost;
# API proxy to backend - must come before / location
# Upload endpoint
location /api/upload {
proxy_pass http://image-uploader-backend:5000/upload;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M;
}
# Download original images
location /api/download {
proxy_pass http://image-uploader-backend:5000/download;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Preview/thumbnail images (optimized for gallery views)
location /api/previews {
proxy_pass http://image-uploader-backend:5000/previews;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Groups API
location /api/groups {
proxy_pass http://image-uploader-backend:5000/groups;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Moderation API (groups)
location /moderation/groups {
proxy_pass http://image-uploader-backend:5000/moderation/groups;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Groups routes (both API and page routes)
location /groups {
# Try to serve as static file first, then proxy to React dev server
try_files $uri @proxy;
}
# Download endpoint (legacy, without /api prefix)
location /download {
proxy_pass http://image-uploader-backend:5000/download;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy requests to the CRA dev server so nginx can be used as reverse proxy
location /sockjs-node/ {
proxy_pass http://127.0.0.1:3000;
@ -21,6 +83,16 @@ server {
proxy_set_header Host $host;
}
location @proxy {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import './Css/ImageGallery.css';
import { getImageSrc, getGroupPreviewSrc } from '../../Utils/imageUtils';
const ImageGalleryCard = ({
item,
@ -25,13 +26,8 @@ const ImageGalleryCard = ({
if (mode === 'preview' || mode === 'single-image') {
// Preview mode: display individual images
if (item.remoteUrl) {
previewUrl = item.remoteUrl;
} else if (item.url) {
previewUrl = item.url;
} else if (item.filePath) {
previewUrl = item.filePath;
}
// Use preview image (optimized thumbnails for gallery)
previewUrl = getImageSrc(item, true);
title = item.originalName || item.name || 'Bild';
@ -45,11 +41,8 @@ const ImageGalleryCard = ({
// Group mode: display group information
const group = item;
if (group.previewImage) {
previewUrl = `/download/${group.previewImage.split('/').pop()}`;
} else if (group.images && group.images.length > 0 && group.images[0].filePath) {
previewUrl = group.images[0].filePath;
}
// Use preview image from first image in group
previewUrl = getGroupPreviewSrc(group, true);
title = group.title;
subtitle = `${group.year}${group.name}`;

View File

@ -41,7 +41,8 @@ const ModerationGroupImagesPage = () => {
// Map group's images to preview-friendly objects
if (data.images && data.images.length > 0) {
const mapped = data.images.map(img => ({
remoteUrl: `/download/${img.fileName}`,
...img, // Pass all image fields including previewPath
remoteUrl: `/download/${img.fileName}`, // Keep for backward compatibility
originalName: img.originalName || img.fileName,
id: img.id
}));

View File

@ -5,6 +5,7 @@ import { Container } from '@mui/material';
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import ImageGallery from '../ComponentUtils/ImageGallery';
import { getImageSrc } from '../../Utils/imageUtils';
const ModerationGroupsPage = () => {
const [groups, setGroups] = useState([]);
@ -246,7 +247,7 @@ const ImageModal = ({ group, onClose, onDeleteImage }) => {
{group.images.map(image => (
<div key={image.id} className="image-item">
<img
src={`/download/${image.fileName}`}
src={getImageSrc(image, true)}
alt={image.originalName}
className="modal-image"
/>

View File

@ -52,7 +52,8 @@ const PublicGroupImagesPage = () => {
<ImageGallery
items={group.images && group.images.length > 0 ? group.images.map(img => ({
remoteUrl: `/download/${img.fileName}`,
...img, // Pass all image fields including previewPath
remoteUrl: `/download/${img.fileName}`, // Keep for backward compatibility
originalName: img.originalName || img.fileName,
id: img.id
})) : []}

View File

@ -13,6 +13,7 @@ import {
// Utils
import { fetchAllGroups } from '../../Utils/batchUpload';
import { getImageSrc } from '../../Utils/imageUtils';
// Styles moved inline to sx props below
@ -228,7 +229,7 @@ function SlideshowPage() {
</IconButton>
{/* Hauptbild */}
<Box component="img" src={`/api${currentImage.filePath}`} alt={currentImage.originalName} sx={{ ...slideshowImageSx, opacity: fadeOut ? 0 : 1 }} />
<Box component="img" src={getImageSrc(currentImage, false)} alt={currentImage.originalName} sx={{ ...slideshowImageSx, opacity: fadeOut ? 0 : 1 }} />
{/* Beschreibung */}
<Box sx={descriptionContainerSx}>

View File

@ -0,0 +1,64 @@
/**
* Helper functions for image handling and preview generation
*/
/**
* Get the optimal image source URL based on context
* @param {Object} image - Image object from API
* @param {boolean} usePreview - Whether to prefer preview over original (default: true)
* @returns {string} Image URL
*/
export const getImageSrc = (image, usePreview = true) => {
if (!image) {
return '';
}
// If previews are enabled and available, use preview
if (usePreview && image.previewPath) {
// previewPath is just the filename, not a full path
const previewFileName = image.previewPath.includes('/')
? image.previewPath.split('/').pop()
: image.previewPath;
return `/api/previews/${previewFileName}`;
}
// Fallback chain for original image
if (image.filePath) {
return `/api${image.filePath}`;
}
if (image.fileName) {
return `/api/download/${image.fileName}`;
}
// Legacy fallback
if (image.remoteUrl) {
return image.remoteUrl;
}
return '';
};
/**
* Get preview image for a group (first image)
* @param {Object} group - Group object from API
* @param {boolean} usePreview - Whether to prefer preview over original
* @returns {string} Image URL for group preview
*/
export const getGroupPreviewSrc = (group, usePreview = true) => {
if (!group) {
return '';
}
// Legacy support: direct previewImage field
if (group.previewImage) {
return `/api/download/${group.previewImage.split('/').pop()}`;
}
// Use first image from group
if (group.images && group.images.length > 0) {
return getImageSrc(group.images[0], usePreview);
}
return '';
};

View File

@ -43,6 +43,22 @@ server {
client_max_body_size 200M;
}
location /api/download {
proxy_pass http://image-uploader-backend:5000/download;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/previews {
proxy_pass http://image-uploader-backend:5000/previews;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/groups {
proxy_pass http://image-uploader-backend:5000/groups;
proxy_set_header Host $host;