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:
parent
ff71c9d86a
commit
aec9db2a76
2
TODO.md
2
TODO.md
|
|
@ -23,7 +23,7 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
- [ ] Code Cleanup & Refactoring
|
- [x] Code Cleanup & Refactoring
|
||||||
- [x] Überprüfung der Komponentenstruktur
|
- [x] Überprüfung der Komponentenstruktur
|
||||||
- [x] Entfernen ungenutzter Dateien
|
- [x] Entfernen ungenutzter Dateien
|
||||||
- [x] Vereinheitlichung der ImageGallery Komponente:
|
- [x] Vereinheitlichung der ImageGallery Komponente:
|
||||||
|
|
|
||||||
|
|
@ -252,19 +252,25 @@ class GroupRepository {
|
||||||
|
|
||||||
// Alle Gruppen für Moderation (mit Freigabestatus und Bildanzahl)
|
// Alle Gruppen für Moderation (mit Freigabestatus und Bildanzahl)
|
||||||
async getAllGroupsWithModerationInfo() {
|
async getAllGroupsWithModerationInfo() {
|
||||||
|
const groupFormatter = require('../utils/groupFormatter');
|
||||||
|
|
||||||
const groups = await dbManager.all(`
|
const groups = await dbManager.all(`
|
||||||
SELECT
|
SELECT * FROM groups
|
||||||
g.*,
|
ORDER BY approved ASC, upload_date DESC
|
||||||
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
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const groupFormatter = require('../utils/groupFormatter');
|
const result = [];
|
||||||
return groups.map(group => groupFormatter.formatGroupListRow(group));
|
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)
|
// Hole Gruppe für Moderation (inkl. nicht-freigegebene)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
# Development override to mount the frontend source into a node container
|
# 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
|
# 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
|
# without rebuilding images. This file is intended to be used together
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,24 @@ server {
|
||||||
client_max_body_size 100M;
|
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)
|
# API - Groups (NO PASSWORD PROTECTION)
|
||||||
location /api/groups {
|
location /api/groups {
|
||||||
proxy_pass http://image-uploader-backend:5000/groups;
|
proxy_pass http://image-uploader-backend:5000/groups;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,24 @@ server {
|
||||||
client_max_body_size 100M;
|
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)
|
# API - Groups (NO PASSWORD PROTECTION)
|
||||||
location /api/groups {
|
location /api/groups {
|
||||||
proxy_pass http://image-uploader-backend:5000/groups;
|
proxy_pass http://image-uploader-backend:5000/groups;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,68 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
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
|
# Proxy requests to the CRA dev server so nginx can be used as reverse proxy
|
||||||
location /sockjs-node/ {
|
location /sockjs-node/ {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
@ -21,6 +83,16 @@ server {
|
||||||
proxy_set_header Host $host;
|
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 / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './Css/ImageGallery.css';
|
import './Css/ImageGallery.css';
|
||||||
|
import { getImageSrc, getGroupPreviewSrc } from '../../Utils/imageUtils';
|
||||||
|
|
||||||
const ImageGalleryCard = ({
|
const ImageGalleryCard = ({
|
||||||
item,
|
item,
|
||||||
|
|
@ -25,13 +26,8 @@ const ImageGalleryCard = ({
|
||||||
|
|
||||||
if (mode === 'preview' || mode === 'single-image') {
|
if (mode === 'preview' || mode === 'single-image') {
|
||||||
// Preview mode: display individual images
|
// Preview mode: display individual images
|
||||||
if (item.remoteUrl) {
|
// Use preview image (optimized thumbnails for gallery)
|
||||||
previewUrl = item.remoteUrl;
|
previewUrl = getImageSrc(item, true);
|
||||||
} else if (item.url) {
|
|
||||||
previewUrl = item.url;
|
|
||||||
} else if (item.filePath) {
|
|
||||||
previewUrl = item.filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
title = item.originalName || item.name || 'Bild';
|
title = item.originalName || item.name || 'Bild';
|
||||||
|
|
||||||
|
|
@ -45,11 +41,8 @@ const ImageGalleryCard = ({
|
||||||
// Group mode: display group information
|
// Group mode: display group information
|
||||||
const group = item;
|
const group = item;
|
||||||
|
|
||||||
if (group.previewImage) {
|
// Use preview image from first image in group
|
||||||
previewUrl = `/download/${group.previewImage.split('/').pop()}`;
|
previewUrl = getGroupPreviewSrc(group, true);
|
||||||
} else if (group.images && group.images.length > 0 && group.images[0].filePath) {
|
|
||||||
previewUrl = group.images[0].filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
title = group.title;
|
title = group.title;
|
||||||
subtitle = `${group.year} • ${group.name}`;
|
subtitle = `${group.year} • ${group.name}`;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ const ModerationGroupImagesPage = () => {
|
||||||
// Map group's images to preview-friendly objects
|
// Map group's images to preview-friendly objects
|
||||||
if (data.images && data.images.length > 0) {
|
if (data.images && data.images.length > 0) {
|
||||||
const mapped = data.images.map(img => ({
|
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,
|
originalName: img.originalName || img.fileName,
|
||||||
id: img.id
|
id: img.id
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Container } from '@mui/material';
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||||||
import Footer from '../ComponentUtils/Footer';
|
import Footer from '../ComponentUtils/Footer';
|
||||||
import ImageGallery from '../ComponentUtils/ImageGallery';
|
import ImageGallery from '../ComponentUtils/ImageGallery';
|
||||||
|
import { getImageSrc } from '../../Utils/imageUtils';
|
||||||
|
|
||||||
const ModerationGroupsPage = () => {
|
const ModerationGroupsPage = () => {
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
|
|
@ -246,7 +247,7 @@ const ImageModal = ({ group, onClose, onDeleteImage }) => {
|
||||||
{group.images.map(image => (
|
{group.images.map(image => (
|
||||||
<div key={image.id} className="image-item">
|
<div key={image.id} className="image-item">
|
||||||
<img
|
<img
|
||||||
src={`/download/${image.fileName}`}
|
src={getImageSrc(image, true)}
|
||||||
alt={image.originalName}
|
alt={image.originalName}
|
||||||
className="modal-image"
|
className="modal-image"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ const PublicGroupImagesPage = () => {
|
||||||
|
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
items={group.images && group.images.length > 0 ? group.images.map(img => ({
|
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,
|
originalName: img.originalName || img.fileName,
|
||||||
id: img.id
|
id: img.id
|
||||||
})) : []}
|
})) : []}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { fetchAllGroups } from '../../Utils/batchUpload';
|
import { fetchAllGroups } from '../../Utils/batchUpload';
|
||||||
|
import { getImageSrc } from '../../Utils/imageUtils';
|
||||||
|
|
||||||
// Styles moved inline to sx props below
|
// Styles moved inline to sx props below
|
||||||
|
|
||||||
|
|
@ -228,7 +229,7 @@ function SlideshowPage() {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{/* Hauptbild */}
|
{/* 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 */}
|
{/* Beschreibung */}
|
||||||
<Box sx={descriptionContainerSx}>
|
<Box sx={descriptionContainerSx}>
|
||||||
|
|
|
||||||
64
frontend/src/Utils/imageUtils.js
Normal file
64
frontend/src/Utils/imageUtils.js
Normal 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 '';
|
||||||
|
};
|
||||||
|
|
@ -43,6 +43,22 @@ server {
|
||||||
client_max_body_size 200M;
|
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 {
|
location /api/groups {
|
||||||
proxy_pass http://image-uploader-backend:5000/groups;
|
proxy_pass http://image-uploader-backend:5000/groups;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user