css
This commit is contained in:
parent
bf4ff75ce5
commit
0c0547b4f5
|
|
@ -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)
|
||||
|
|
|
|||
43
backend/src/utils/groupFormatter.js
Normal file
43
backend/src/utils/groupFormatter.js
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
91
frontend/src/Components/ComponentUtils/GroupCard.js
Normal file
91
frontend/src/Components/ComponentUtils/GroupCard.js
Normal file
|
|
@ -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 (
|
||||
<div className={`group-card ${isPending ? 'pending' : 'approved'}`}>
|
||||
<div className="group-preview">
|
||||
{previewUrl ? (
|
||||
<img src={previewUrl} alt="Preview" className="preview-image" />
|
||||
) : (
|
||||
<div className="no-preview">Kein Vorschaubild</div>
|
||||
)}
|
||||
<div className="image-count">{group.imageCount} Bilder</div>
|
||||
</div>
|
||||
|
||||
<div className="group-info">
|
||||
<h3>{group.title}</h3>
|
||||
<p className="group-meta">{group.year} • {group.name}</p>
|
||||
{group.description && (
|
||||
<p className="group-description">{group.description}</p>
|
||||
)}
|
||||
<p className="upload-date">
|
||||
Hochgeladen: {new Date(group.uploadDate).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="group-actions">
|
||||
{showActions ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => onViewImages(group)}
|
||||
>
|
||||
✏️ Gruppe editieren
|
||||
</button>
|
||||
|
||||
{isPending ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => onApprove(group.groupId, true)}
|
||||
>
|
||||
✅ Freigeben
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={() => onApprove(group.groupId, false)}
|
||||
>
|
||||
⏸️ Sperren
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => onDelete(group.groupId)}
|
||||
>
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="view-button"
|
||||
onClick={() => onViewImages(group)}
|
||||
title="Anzeigen"
|
||||
>
|
||||
Anzeigen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GroupCard.propTypes = {
|
||||
group: PropTypes.object.isRequired,
|
||||
onApprove: PropTypes.func.isRequired,
|
||||
onViewImages: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
isPending: PropTypes.bool
|
||||
};
|
||||
|
||||
export default GroupCard;
|
||||
|
|
@ -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() {
|
|||
|
||||
<div className="groups-grid">
|
||||
{groups.map((group) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={group.groupId} className="grid-item-stretch">
|
||||
<Card className="group-card card-stretch">
|
||||
{group.images && group.images.length > 0 && (
|
||||
<div className="group-preview">
|
||||
<img className="preview-image" src={group.images[0].filePath} alt={group.description || 'Slideshow Vorschau'} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="group-content">
|
||||
<Typography className="group-title">
|
||||
{group.description || 'Unbenannte Slideshow'}
|
||||
</Typography>
|
||||
|
||||
<Typography className="group-meta">
|
||||
📅 {formatDate(group.uploadDate)} • 📸 {group.images?.length || 0} Bilder
|
||||
</Typography>
|
||||
|
||||
<div className="group-actions">
|
||||
<Button
|
||||
className="view-button"
|
||||
onClick={() => handleViewSlideshow(group.groupId)}
|
||||
startIcon={<SlideshowIcon />}
|
||||
fullWidth
|
||||
>
|
||||
Anzeigen
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<div key={group.groupId} className="grid-item-stretch">
|
||||
<GroupCard
|
||||
group={group}
|
||||
onApprove={() => { /* no-op on public page */ }}
|
||||
onViewImages={() => handleViewSlideshow(group.groupId)}
|
||||
onDelete={() => { /* no-op on public page */ }}
|
||||
isPending={false}
|
||||
showActions={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<div className="groups-grid">
|
||||
{pendingGroups.map(group => (
|
||||
<GroupCard
|
||||
key={group.group_id}
|
||||
key={group.groupId}
|
||||
group={group}
|
||||
onApprove={approveGroup}
|
||||
onViewImages={viewGroupImages}
|
||||
|
|
@ -220,7 +221,7 @@ const ModerationPage = () => {
|
|||
<div className="groups-grid">
|
||||
{approvedGroups.map(group => (
|
||||
<GroupCard
|
||||
key={group.group_id}
|
||||
key={group.groupId}
|
||||
group={group}
|
||||
onApprove={approveGroup}
|
||||
onViewImages={viewGroupImages}
|
||||
|
|
@ -249,65 +250,7 @@ const ModerationPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const GroupCard = ({ group, onApprove, onViewImages, onDelete, isPending }) => {
|
||||
const previewUrl = group.preview_image ? `/download/${group.preview_image.split('/').pop()}` : null;
|
||||
|
||||
return (
|
||||
<div className={`group-card ${isPending ? 'pending' : 'approved'}`}>
|
||||
<div className="group-preview">
|
||||
{previewUrl ? (
|
||||
<img src={previewUrl} alt="Preview" className="preview-image" />
|
||||
) : (
|
||||
<div className="no-preview">Kein Vorschaubild</div>
|
||||
)}
|
||||
<div className="image-count">{group.image_count} Bilder</div>
|
||||
</div>
|
||||
|
||||
<div className="group-info">
|
||||
<h3>{group.title}</h3>
|
||||
<p className="group-meta">{group.year} • {group.name}</p>
|
||||
{group.description && (
|
||||
<p className="group-description">{group.description}</p>
|
||||
)}
|
||||
<p className="upload-date">
|
||||
Hochgeladen: {new Date(group.upload_date).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="group-actions">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => onViewImages(group)}
|
||||
>
|
||||
✏️ Gruppe editieren
|
||||
</button>
|
||||
|
||||
{isPending ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => onApprove(group.group_id, true)}
|
||||
>
|
||||
✅ Freigeben
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={() => onApprove(group.group_id, false)}
|
||||
>
|
||||
⏸️ Sperren
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => onDelete(group.group_id)}
|
||||
>
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// `GroupCard` has been extracted to `../ComponentUtils/GroupCard`
|
||||
|
||||
const ImageModal = ({ group, onClose, onDeleteImage }) => {
|
||||
return (
|
||||
|
|
@ -340,7 +283,7 @@ const ImageModal = ({ group, onClose, onDeleteImage }) => {
|
|||
<span className="image-name">{image.originalName}</span>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => onDeleteImage(group.group_id, image.id)}
|
||||
onClick={() => onDeleteImage(group.groupId, image.id)}
|
||||
title="Bild löschen"
|
||||
>
|
||||
🗑️
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user