From 0c0547b4f5151a34b51d4c42fee4410536200ec0 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Mon, 20 Oct 2025 19:19:21 +0200 Subject: [PATCH] css --- backend/src/repositories/GroupRepository.js | 46 ++-------- backend/src/utils/groupFormatter.js | 43 +++++++++ .../ComponentUtils/Css/GroupImagesPage.css | 4 +- .../ComponentUtils/Css/GroupsOverviewPage.css | 16 +--- .../Components/ComponentUtils/Css/main.css | 9 ++ .../Components/ComponentUtils/GroupCard.js | 91 +++++++++++++++++++ .../Components/Pages/GroupsOverviewPage.js | 41 +++------ .../src/Components/Pages/ModerationPage.js | 81 +++-------------- 8 files changed, 175 insertions(+), 156 deletions(-) create mode 100644 backend/src/utils/groupFormatter.js create mode 100644 frontend/src/Components/ComponentUtils/GroupCard.js diff --git a/backend/src/repositories/GroupRepository.js b/backend/src/repositories/GroupRepository.js index 4109176..c4f9527 100644 --- a/backend/src/repositories/GroupRepository.js +++ b/backend/src/repositories/GroupRepository.js @@ -109,6 +109,8 @@ class GroupRepository { // Hole alle Gruppen mit Bildern für Slideshow (nur freigegebene) async getAllGroupsWithImages() { + const groupFormatter = require('../utils/groupFormatter'); + const groups = await dbManager.all(` SELECT * FROM groups WHERE approved = TRUE @@ -123,21 +125,7 @@ class GroupRepository { ORDER BY upload_order ASC `, [group.group_id]); - result.push({ - groupId: group.group_id, - year: group.year, - title: group.title, - description: group.description, - name: group.name, - uploadDate: group.upload_date, - images: images.map(img => ({ - fileName: img.file_name, - originalName: img.original_name, - filePath: img.file_path, - uploadOrder: img.upload_order - })), - imageCount: images.length - }); + result.push(groupFormatter.formatGroupDetail(group, images)); } return result; @@ -273,11 +261,8 @@ class GroupRepository { ORDER BY g.approved ASC, g.upload_date DESC `); - return groups.map(group => ({ - ...group, - approved: Boolean(group.approved), - image_count: group.image_count || 0 - })); + const groupFormatter = require('../utils/groupFormatter'); + return groups.map(group => groupFormatter.formatGroupListRow(group)); } // Hole Gruppe für Moderation (inkl. nicht-freigegebene) @@ -296,25 +281,8 @@ class GroupRepository { ORDER BY upload_order ASC `, [groupId]); - return { - group_id: group.group_id, - year: group.year, - title: group.title, - description: group.description, - name: group.name, - uploadDate: group.upload_date, - approved: group.approved, - images: images.map(img => ({ - id: img.id, - fileName: img.file_name, - originalName: img.original_name, - filePath: img.file_path, - uploadOrder: img.upload_order, - fileSize: img.file_size, - mimeType: img.mime_type - })), - imageCount: images.length - }; + const groupFormatter = require('../utils/groupFormatter'); + return groupFormatter.formatGroupDetail(group, images); } // Statistiken (erweitert um Freigabe-Status) diff --git a/backend/src/utils/groupFormatter.js b/backend/src/utils/groupFormatter.js new file mode 100644 index 0000000..6b8dd60 --- /dev/null +++ b/backend/src/utils/groupFormatter.js @@ -0,0 +1,43 @@ +/** + * Utility to format DB rows / repository results into a consistent API DTO (camelCase). + */ +function formatGroupListRow(row) { + return { + groupId: row.group_id, + year: row.year, + title: row.title, + description: row.description, + name: row.name, + uploadDate: row.upload_date, + approved: Boolean(row.approved), + imageCount: row.image_count ? Number(row.image_count) : 0, + previewImage: row.preview_image || null + }; +} + +function formatGroupDetail(groupRow, images) { + return { + groupId: groupRow.group_id, + year: groupRow.year, + title: groupRow.title, + description: groupRow.description, + name: groupRow.name, + uploadDate: groupRow.upload_date, + approved: Boolean(groupRow.approved), + images: images.map(img => ({ + id: img.id, + fileName: img.file_name, + originalName: img.original_name, + filePath: img.file_path, + uploadOrder: img.upload_order, + fileSize: img.file_size || null, + mimeType: img.mime_type || null + })), + imageCount: images.length + }; +} + +module.exports = { + formatGroupListRow, + formatGroupDetail +}; diff --git a/frontend/src/Components/ComponentUtils/Css/GroupImagesPage.css b/frontend/src/Components/ComponentUtils/Css/GroupImagesPage.css index 2d780dd..2f5a7db 100644 --- a/frontend/src/Components/ComponentUtils/Css/GroupImagesPage.css +++ b/frontend/src/Components/ComponentUtils/Css/GroupImagesPage.css @@ -1,6 +1,4 @@ -/* Styles extracted from GroupImagesPage makeStyles */ +/* Page-specific styles for GroupImagesPage */ .group-images-container { padding-top: 20px; padding-bottom: 40px; min-height: 80vh; } -.group-card { border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; } .header-text { font-family: roboto; font-weight: 400; font-size: 28px; text-align: center; margin-bottom: 10px; color: #333333; } .subheader-text { font-family: roboto; font-weight: 300; font-size: 16px; color: #666666; text-align: center; margin-bottom: 30px; } -.action-buttons { display: flex; gap: 15px; justify-content: center; margin-top: 20px; flex-wrap: wrap; } diff --git a/frontend/src/Components/ComponentUtils/Css/GroupsOverviewPage.css b/frontend/src/Components/ComponentUtils/Css/GroupsOverviewPage.css index e0d67d2..378d225 100644 --- a/frontend/src/Components/ComponentUtils/Css/GroupsOverviewPage.css +++ b/frontend/src/Components/ComponentUtils/Css/GroupsOverviewPage.css @@ -1,20 +1,6 @@ -/* Extracted styles from GroupsOverviewPage makeStyles */ +/* Page-specific styles for GroupsOverviewPage */ .groups-overview-container { padding-top: 20px; padding-bottom: 40px; min-height: 80vh; } .header-card { border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 30px; text-align: center; padding: 20px; } .header-title { font-family: roboto; font-weight: 500; font-size: 28px; color: #333333; margin-bottom: 10px; } .header-subtitle { font-family: roboto; font-size: 16px; color: #666666; margin-bottom: 20px; } -.group-card { border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s ease; height: 100%; display: flex; flex-direction: column; } -.group-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.15); } -.group-image { height: 180px; object-fit: cover; } -.group-content { flex-grow: 1; display: flex; flex-direction: column; } -.group-title { font-family: roboto; font-weight: 500; font-size: 16px; color: #333333; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.group-meta { font-size: 12px; color: #999999; margin-bottom: 15px; } -.group-actions { margin-top: auto; display: flex; gap: 8px; justify-content: space-between; } -.view-button { border-radius: 20px; text-transform: none; font-size: 12px; padding: 6px 16px; background: linear-gradient(45deg, #4CAF50 30%, #45a049 90%); color: white; } -.view-button:hover { background: linear-gradient(45deg, #45a049 30%, #4CAF50 90%); } -.action-buttons { display:flex; gap:15px; justify-content:center; flex-wrap: wrap; margin-top:20px; } -.primary-button { border-radius: 25px; padding: 12px 30px; font-size:16px; font-weight:500; text-transform:none; background: linear-gradient(45deg, #2196F3 30%, #1976D2 90%); color:white; } -.home-button { border-radius:25px; padding:12px 30px; font-size:16px; font-weight:500; text-transform:none; border:2px solid #4CAF50; color:#4CAF50; background-color: transparent; } -.empty-state { text-align:center; padding:60px 20px; } -.loading-container { text-align:center; padding:60px 20px; } @media (max-width:800px) { .nav__links, .cta { display:none; } } diff --git a/frontend/src/Components/ComponentUtils/Css/main.css b/frontend/src/Components/ComponentUtils/Css/main.css index 63e797a..4ece334 100644 --- a/frontend/src/Components/ComponentUtils/Css/main.css +++ b/frontend/src/Components/ComponentUtils/Css/main.css @@ -74,3 +74,12 @@ /* Standard groups grid used by moderation and overview pages */ .groups-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; } + +/* Common CTA / page-level utilities (moved from page CSS) */ +.view-button { border-radius: 20px; text-transform: none; font-size: 12px; padding: 6px 16px; background: linear-gradient(45deg, #4CAF50 30%, #45a049 90%); color: white; border: none; cursor: pointer; } +.view-button:hover { background: linear-gradient(45deg, #45a049 30%, #4CAF50 90%); } +.action-buttons { display:flex; gap:15px; justify-content:center; flex-wrap: wrap; margin-top:20px; } +.primary-button { border-radius: 25px; padding: 12px 30px; font-size:16px; font-weight:500; text-transform:none; background: linear-gradient(45deg, #2196F3 30%, #1976D2 90%); color:white; border:none; cursor:pointer; } +.home-button { border-radius:25px; padding:12px 30px; font-size:16px; font-weight:500; text-transform:none; border:2px solid #4CAF50; color:#4CAF50; background-color: transparent; cursor:pointer; } +.empty-state { text-align:center; padding:60px 20px; } +.loading-container { text-align:center; padding:60px 20px; } diff --git a/frontend/src/Components/ComponentUtils/GroupCard.js b/frontend/src/Components/ComponentUtils/GroupCard.js new file mode 100644 index 0000000..0934f30 --- /dev/null +++ b/frontend/src/Components/ComponentUtils/GroupCard.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './Css/main.css'; + +const GroupCard = ({ group, onApprove, onViewImages, onDelete, isPending, showActions = true }) => { + let previewUrl = null; + if (group.previewImage) { + previewUrl = `/download/${group.previewImage.split('/').pop()}`; + } else if (group.images && group.images.length > 0 && group.images[0].filePath) { + // images may provide filePath already + previewUrl = group.images[0].filePath; + } + + return ( +
+
+ {previewUrl ? ( + Preview + ) : ( +
Kein Vorschaubild
+ )} +
{group.imageCount} Bilder
+
+ +
+

{group.title}

+

{group.year} • {group.name}

+ {group.description && ( +

{group.description}

+ )} +

+ Hochgeladen: {new Date(group.uploadDate).toLocaleDateString('de-DE')} +

+
+ +
+ {showActions ? ( + <> + + + {isPending ? ( + + ) : ( + + )} + + + + ) : ( + + )} +
+
+ ); +}; + +GroupCard.propTypes = { + group: PropTypes.object.isRequired, + onApprove: PropTypes.func.isRequired, + onViewImages: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + isPending: PropTypes.bool +}; + +export default GroupCard; diff --git a/frontend/src/Components/Pages/GroupsOverviewPage.js b/frontend/src/Components/Pages/GroupsOverviewPage.js index 4349ebf..fa5c219 100644 --- a/frontend/src/Components/Pages/GroupsOverviewPage.js +++ b/frontend/src/Components/Pages/GroupsOverviewPage.js @@ -24,6 +24,7 @@ import Swal from 'sweetalert2/dist/sweetalert2.js'; // Components import Navbar from '../ComponentUtils/Headers/Navbar'; import Footer from '../ComponentUtils/Footer'; +import GroupCard from '../ComponentUtils/GroupCard'; // Utils import { fetchAllGroups, deleteGroup } from '../../Utils/batchUpload'; @@ -174,36 +175,16 @@ function GroupsOverviewPage() {
{groups.map((group) => ( - - - {group.images && group.images.length > 0 && ( -
- {group.description -
- )} - - - - {group.description || 'Unbenannte Slideshow'} - - - - 📅 {formatDate(group.uploadDate)} • 📸 {group.images?.length || 0} Bilder - - -
- -
-
-
-
+
+ { /* no-op on public page */ }} + onViewImages={() => handleViewSlideshow(group.groupId)} + onDelete={() => { /* no-op on public page */ }} + isPending={false} + showActions={false} + /> +
))}
diff --git a/frontend/src/Components/Pages/ModerationPage.js b/frontend/src/Components/Pages/ModerationPage.js index 94be63d..d69d71c 100644 --- a/frontend/src/Components/Pages/ModerationPage.js +++ b/frontend/src/Components/Pages/ModerationPage.js @@ -5,6 +5,7 @@ import { Container } from '@material-ui/core'; import './Css/ModerationPage.css'; import Navbar from '../ComponentUtils/Headers/Navbar'; import Footer from '../ComponentUtils/Footer'; +import GroupCard from '../ComponentUtils/GroupCard'; const ModerationPage = () => { const [groups, setGroups] = useState([]); @@ -53,7 +54,7 @@ const ModerationPage = () => { // Update local state setGroups(groups.map(group => - group.group_id === groupId + group.groupId === groupId ? { ...group, approved: approved } : group )); @@ -84,7 +85,7 @@ const ModerationPage = () => { } // Remove image from selectedGroup - if (selectedGroup && selectedGroup.group_id === groupId) { + if (selectedGroup && selectedGroup.groupId === groupId) { const updatedImages = selectedGroup.images.filter(img => img.id !== imageId); setSelectedGroup({ ...selectedGroup, @@ -95,8 +96,8 @@ const ModerationPage = () => { // Update group image count setGroups(groups.map(group => - group.group_id === groupId - ? { ...group, image_count: group.image_count - 1 } + group.groupId === groupId + ? { ...group, imageCount: group.imageCount - 1 } : group )); @@ -121,8 +122,8 @@ const ModerationPage = () => { throw new Error(`HTTP error! status: ${response.status}`); } - setGroups(groups.filter(group => group.group_id !== groupId)); - if (selectedGroup && selectedGroup.group_id === groupId) { + setGroups(groups.filter(group => group.groupId !== groupId)); + if (selectedGroup && selectedGroup.groupId === groupId) { setSelectedGroup(null); setShowImages(false); } @@ -134,7 +135,7 @@ const ModerationPage = () => { // Navigate to the dedicated group images page const viewGroupImages = (group) => { - history.push(`/groups/${group.group_id}`); + history.push(`/groups/${group.groupId}`); }; if (loading) { @@ -199,7 +200,7 @@ const ModerationPage = () => {
{pendingGroups.map(group => ( {
{approvedGroups.map(group => ( { ); }; -const GroupCard = ({ group, onApprove, onViewImages, onDelete, isPending }) => { - const previewUrl = group.preview_image ? `/download/${group.preview_image.split('/').pop()}` : null; - - return ( -
-
- {previewUrl ? ( - Preview - ) : ( -
Kein Vorschaubild
- )} -
{group.image_count} Bilder
-
- -
-

{group.title}

-

{group.year} • {group.name}

- {group.description && ( -

{group.description}

- )} -

- Hochgeladen: {new Date(group.upload_date).toLocaleDateString('de-DE')} -

-
- -
- - - {isPending ? ( - - ) : ( - - )} - - -
-
- ); -}; +// `GroupCard` has been extracted to `../ComponentUtils/GroupCard` const ImageModal = ({ group, onClose, onDeleteImage }) => { return ( @@ -340,7 +283,7 @@ const ImageModal = ({ group, onClose, onDeleteImage }) => { {image.originalName}