refactor: Centralized styling with CSS and global MUI overrides

- Migrated all Pages from Material-UI to HTML+CSS (GroupsOverviewPage, ManagementPortalPage, ModerationGroupImagesPage, ModerationGroupsPage, PublicGroupImagesPage, SlideshowPage, MultiUploadPage)
- Added comprehensive typography system in App.css (h1-h3, p, utility classes)
- Added global Material-UI font overrides for Open Sans
- Removed redundant fontFamily: 'roboto' from all components
- Fixed button alignment in ImageGalleryCard (margin-top: auto)
- Removed emojis from titles for cleaner UI
- Standardized button padding (12px 30px) across application
- Improved code consistency and maintainability with centralized CSS approach
This commit is contained in:
Matthias Lotz 2025-11-27 19:47:39 +01:00
parent 25dda32c4e
commit 215acaa67f
13 changed files with 444 additions and 375 deletions

View File

@ -1,5 +1,193 @@
/* Main shared styles for cards, buttons, modals used across pages */
/* ============================================
TYPOGRAPHY - Zentrale Schrift-Definitionen
============================================ */
body {
font-family: 'Open Sans', sans-serif;
color: #333333;
line-height: 1.6;
}
h1, .h1 {
font-family: 'Open Sans', sans-serif;
font-weight: 500;
font-size: 28px;
color: #333333;
margin-bottom: 10px;
}
h2, .h2 {
font-family: 'Open Sans', sans-serif;
font-weight: 500;
font-size: 24px;
color: #333333;
margin-bottom: 15px;
}
h3, .h3 {
font-family: 'Open Sans', sans-serif;
font-weight: 500;
font-size: 20px;
color: #333333;
margin-bottom: 12px;
}
p, .text-body {
font-family: 'Open Sans', sans-serif;
font-weight: 400;
font-size: 16px;
color: #666666;
margin-bottom: 16px;
}
.text-subtitle {
font-family: 'Open Sans', sans-serif;
font-weight: 300;
font-size: 16px;
color: #666666;
}
.text-small {
font-size: 14px;
color: #666666;
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
/* ============================================
LAYOUT & CONTAINERS
============================================ */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
}
.card-content {
padding: 20px;
}
/* ============================================
PAGE HEADERS
============================================ */
.page-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
font-family: 'Open Sans', sans-serif;
font-weight: 500;
font-size: 28px;
color: #333333;
text-align: center;
margin-bottom: 10px;
}
.page-subtitle {
font-family: 'Open Sans', sans-serif;
font-weight: 300;
font-size: 16px;
color: #666666;
text-align: center;
margin-bottom: 30px;
}
/* ============================================
UTILITY CLASSES
============================================ */
.flex-center {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
margin-top: 30px;
}
.text-center-block {
text-align: center;
padding: 40px 0;
}
/* Spacing utilities */
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.mt-3 { margin-top: 24px; }
.mt-4 { margin-top: 32px; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.mb-3 { margin-bottom: 24px; }
.mb-4 { margin-bottom: 32px; }
.p-2 { padding: 16px; }
.p-3 { padding: 24px; }
/* ============================================
SUCCESS BOX (Upload Success)
============================================ */
.success-box {
margin-top: 32px;
padding: 24px;
border-radius: 12px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.4);
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success-box h2 {
font-size: 28px;
font-weight: bold;
margin-bottom: 8px;
color: white;
}
.success-box p {
font-size: 18px;
margin-bottom: 16px;
color: white;
}
.info-box {
background: rgba(255,255,255,0.2);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.info-box-highlight {
background: rgba(255,255,255,0.95);
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
border: 2px solid rgba(255,255,255,0.3);
}
/* ============================================
EXISTING STYLES BELOW
============================================ */
/* 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; }
@ -52,7 +240,7 @@ p { font-family: 'Open Sans', sans-serif; color:#555; line-height:1.6; }
}
/* Buttons */
.btn { padding:8px 12px; border:none; border-radius:6px; cursor:pointer; font-size:0.85rem; transition:background-color 0.2s; flex:1; min-width:80px; }
.btn { padding:12px 30px; border:none; border-radius:6px; cursor:pointer; font-size:16px; transition:background-color 0.2s; min-width:80px; }
.btn-secondary { background:#6c757d; color:white; }
.btn-secondary:hover { background:#5a6268; }
.btn-outline-secondary { background:transparent; border:1px solid #6c757d; color:#6c757d; }
@ -63,7 +251,6 @@ p { font-family: 'Open Sans', sans-serif; color:#555; line-height:1.6; }
.btn-warning:hover { background:#e0a800; }
.btn-danger { background:#dc3545; color:white; }
.btn-danger:hover { background:#c82333; }
.btn-sm { padding:4px 8px; font-size:0.75rem; min-width:auto; }
.btn:disabled { opacity:0.65; cursor:not-allowed; }
/* Modal */
@ -104,3 +291,32 @@ p { font-family: 'Open Sans', sans-serif; color:#555; line-height:1.6; }
.admin-auth-card { width:100%; max-width:420px; box-shadow:0 8px 24px rgba(0,0,0,0.08); }
.admin-auth-form { width:100%; }
.admin-auth-error { max-width:420px; background:#fff3f3; border:1px solid #ffcdd2; padding:24px; border-radius:12px; text-align:center; color:#b71c1c; }
/* ============================================
MATERIAL-UI OVERRIDES - Globale Schriftart
============================================ */
/* TextField, Input, Textarea */
.MuiTextField-root input,
.MuiTextField-root textarea,
.MuiInputBase-root,
.MuiInputBase-input,
.MuiOutlinedInput-input {
font-family: 'Open Sans', sans-serif !important;
}
/* Labels */
.MuiFormLabel-root,
.MuiInputLabel-root,
.MuiTypography-root {
font-family: 'Open Sans', sans-serif !important;
}
/* Buttons */
.MuiButton-root {
font-family: 'Open Sans', sans-serif !important;
}
/* Checkbox Labels */
.MuiFormControlLabel-label {
font-family: 'Open Sans', sans-serif !important;
}

View File

@ -112,6 +112,8 @@
display: flex;
gap: 8px;
flex-wrap: wrap;
flex-direction: column;
margin-top: auto;
}
/* ImageGalleryCard - No preview state */

View File

@ -144,7 +144,7 @@ function GroupMetadataEditor({
>
{/* Component Header */}
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
📝 Projekt-Informationen
Projekt-Informationen
</Typography>
<DescriptionInput

View File

@ -221,71 +221,30 @@ const ImageGalleryCard = ({
mode === 'preview' ? (
// Preview mode actions (for upload preview)
<>
<button
className="btn btn-danger"
onClick={() => onDelete(itemId)}
>
🗑 Löschen
</button>
<button className="btn btn-danger" onClick={() => onDelete(itemId)}>🗑 Löschen</button>
{!isEditMode ? (
<button
className="btn btn-primary btn-sm"
onClick={() => onEditMode?.(true)}
>
Edit
</button>
<button className="btn btn-primary" onClick={() => onEditMode?.(true)}> Edit </button>
) : (
<button
className="btn btn-success btn-sm"
onClick={() => onEditMode?.(false)}
>
Fertig
</button>
<button className="btn btn-success" onClick={() => onEditMode?.(false)}> Fertig</button>
)}
</>
) : (
// Moderation mode actions (for existing groups)
<>
<button
className="btn btn-secondary"
onClick={() => onViewImages(item)}
>
Gruppe editieren
</button>
<button className="btn btn-secondary" onClick={() => onViewImages(item)}> Gruppe editieren</button>
{isPending ? (
<button
className="btn btn-success"
onClick={() => onApprove(itemId, true)}
>
Freigeben
</button>
<button className="btn btn-success" onClick={() => onApprove(itemId, true)}> Freigeben</button>
) : (
<button
className="btn btn-warning"
onClick={() => onApprove(itemId, false)}
>
Sperren
</button>
<button className="btn btn-warning" onClick={() => onApprove(itemId, false)}> Sperren</button>
)}
<button
className="btn btn-danger"
onClick={() => onDelete(itemId)}
>
🗑 Löschen
</button>
<button className="btn btn-danger" onClick={() => onDelete(itemId)}>🗑 Löschen</button>
</>
)
) : mode !== 'single-image' ? (
// Public view mode (only for group cards, not single images)
<button
className="view-button"
onClick={() => onViewImages(item)}
title="Anzeigen"
>
Anzeigen
</button>
<button className="view-button" onClick={() => onViewImages(item)} title="Anzeigen">Anzeigen</button>
) : null}
</div>
)}

View File

@ -17,7 +17,6 @@ function DescriptionInput({
const currentYear = new Date().getFullYear();
const fieldLabelSx = {
fontFamily: 'roboto',
fontSize: '14px',
color: '#555555',
marginBottom: '8px',
@ -25,7 +24,6 @@ function DescriptionInput({
};
const sectionTitleSx = {
fontFamily: 'roboto',
fontSize: '18px',
color: '#333333',
marginBottom: '15px',
@ -68,7 +66,7 @@ function DescriptionInput({
};
const requiredIndicatorSx = { color: '#E57373', fontSize: '16px' };
const optionalIndicatorSx = { color: '#9E9E9E', fontSize: '12px', fontStyle: 'italic' };
const optionalIndicatorSx = { color: '#9E9E9E', fontSize: '12px' };
return (
<Box sx={{ marginTop: '20px', marginBottom: '20px' }}>

View File

@ -77,15 +77,13 @@ function MultiImageDropzone({ onImagesSelected, selectedImages = [] }) {
const dropzoneTextSx = {
fontSize: '18px',
fontFamily: 'roboto',
color: '#666666',
margin: '10px 0'
};
const dropzoneSubtextSx = {
fontSize: '14px',
color: '#999999',
fontFamily: 'roboto'
color: '#999999'
};
const fileCountSx = {
@ -106,7 +104,7 @@ function MultiImageDropzone({ onImagesSelected, selectedImages = [] }) {
onClick={handleClick}
>
<Typography sx={dropzoneTextSx}>
📸 Mehrere Bilder hier hinziehen oder klicken zum Auswählen
Mehrere Bilder hier hinziehen oder klicken zum Auswählen
</Typography>
<Typography sx={dropzoneSubtextSx}>

View File

@ -1,13 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import {
Container,
Card,
Typography,
Box,
CircularProgress
} from '@mui/material';
@ -63,14 +56,14 @@ function GroupsOverviewPage() {
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" className="page-container">
<div className="loading-container">
<CircularProgress size={60} color="primary" />
<Typography variant="h6" style={{ marginTop: '20px', color: '#666666' }}>
Slideshows werden geladen...
</Typography>
<div className="container">
<div className="flex-center" style={{ minHeight: '400px' }}>
<div className="text-center">
<div className="loading-spinner" style={{ width: '60px', height: '60px', border: '4px solid #f3f3f3', borderTop: '4px solid #3498db', borderRadius: '50%', animation: 'spin 1s linear infinite', margin: '0 auto' }}></div>
<p className="mt-3" style={{ color: '#666666' }}>Slideshows werden geladen...</p>
</div>
</div>
</Container>
</div>
<Footer />
</div>
);
@ -86,53 +79,39 @@ function GroupsOverviewPage() {
</Helmet>
<Navbar />
<Container maxWidth="lg" className="page-container">
<div className="container page-container">
{/* Header */}
<Card className="header-card">
<Typography className="header-title">
Alle Slideshows
</Typography>
<Typography className="header-subtitle">
Übersicht aller erstellten Slideshows.
</Typography>
</Card>
<div className="card">
<h1 className="page-title">Alle Slideshows</h1>
<p className="page-subtitle">Übersicht aller erstellten Slideshows.</p>
</div>
{/* Groups Grid */}
{error ? (
<div className="empty-state">
<Typography variant="h6" style={{ color: '#f44336', marginBottom: '20px' }}>
😕 Fehler beim Laden
</Typography>
<Typography variant="body1" style={{ color: '#666666', marginBottom: '30px' }}>
{error}
</Typography>
<div className="empty-state">
<h2 style={{ color: '#f44336' }} className="mb-3">😕 Fehler beim Laden</h2>
<p style={{ color: '#666666' }} className="mb-4">{error}</p>
<button onClick={loadGroups} className="btn btn-secondary">
🔄 Erneut versuchen
</button>
</div>
) : groups.length === 0 ? (
<div className="empty-state">
<Typography variant="h4" style={{ color: '#666666', marginBottom: '20px' }}>
📸 Keine Slideshows vorhanden
</Typography>
<Typography variant="body1" style={{ color: '#999999', marginBottom: '30px' }}>
<div className="empty-state">
<h2 style={{ color: '#666666' }} className="mb-3">📸 Keine Slideshows vorhanden</h2>
<p style={{ color: '#999999' }} className="mb-4">
Erstellen Sie Ihre erste Slideshow, indem Sie mehrere Bilder hochladen.
</Typography>
<button
className="btn btn-success"
onClick={handleCreateNew}
style={{ fontSize: '16px', padding: '12px 24px' }}
>
</p>
<button className="btn btn-success" onClick={handleCreateNew}>
Erste Slideshow erstellen
</button>
</div>
) : (
<>
<Box marginBottom={2}>
<Typography variant="h6" style={{ color: '#666666' }}>
📊 {groups.length} Slideshow{groups.length !== 1 ? 's' : ''} gefunden
</Typography>
</Box>
<div className="mb-3">
<h3 style={{ color: '#666666' }}>
{groups.length} Slideshow{groups.length !== 1 ? 's' : ''} gefunden
</h3>
</div>
<ImageGallery
items={groups}
onViewImages={(group) => handleViewGroup(group.groupId)}
@ -142,7 +121,7 @@ function GroupsOverviewPage() {
/>
</>
)}
</Container>
</div>
<div className="footerContainer">
<Footer />

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Container, Card, CardContent, Typography, Box, Button } from '@mui/material';
import Swal from 'sweetalert2';
import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload';
import Footer from '../ComponentUtils/Footer';
@ -180,9 +179,9 @@ function ManagementPortalPage() {
return (
<div className="allContainer">
<NavbarUpload />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div className="container flex-center" style={{ minHeight: '80vh', paddingTop: '20px', paddingBottom: '40px' }}>
<Loading />
</Container>
</div>
<Footer />
</div>
);
@ -192,19 +191,15 @@ function ManagementPortalPage() {
return (
<div className="allContainer">
<NavbarUpload />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', textAlign: 'center' }}>
<Typography variant="h5" color="error" gutterBottom>
{error}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{error}
</Typography>
<Button variant="contained" onClick={() => navigate('/')}>
<div className="container" style={{ minHeight: '80vh', paddingTop: '20px', paddingBottom: '40px' }}>
<div className="card text-center">
<h2 style={{ color: '#f44336' }} className="mb-2">{error}</h2>
<p style={{ color: '#666666' }} className="mb-4">{error}</p>
<button className="btn btn-primary" onClick={() => navigate('/')}>
Zur Startseite
</Button>
</Card>
</Container>
</button>
</div>
</div>
<Footer />
</div>
);
@ -214,19 +209,17 @@ function ManagementPortalPage() {
<div className="allContainer">
<NavbarUpload />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', mb: '20px' }}>
<CardContent>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 400, fontSize: '28px', textAlign: 'center', mb: '10px', color: '#333333' }}>
Mein Upload verwalten
</Typography>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 300, fontSize: '16px', color: '#666666', textAlign: 'center', mb: '30px' }}>
<div className="container" style={{ minHeight: '80vh', paddingTop: '20px', paddingBottom: '40px' }}>
<div className="card mb-3">
<div className="card-content">
<h1 className="page-title text-center mb-2">Mein Upload verwalten</h1>
<p className="page-subtitle text-center mb-4">
Hier können Sie Ihre hochgeladenen Bilder verwalten, Metadaten bearbeiten und Einwilligungen ändern.
</Typography>
</p>
{/* Group Overview */}
{group && (
<Box sx={{ mb: 3 }}>
<div className="mb-4">
<ImageGalleryCard
item={group}
showActions={false}
@ -235,29 +228,25 @@ function ManagementPortalPage() {
hidePreview={true}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 600 }}>
Erteilte Einwilligungen:
</Typography>
<div className="mt-3">
<h3 className="text-small" style={{ fontWeight: 600 }}>Erteilte Einwilligungen:</h3>
<ConsentBadges group={group} />
</Box>
</Box>
</div>
</div>
)}
{/* Add Images Dropzone */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
Weitere Bilder hinzufügen
</Typography>
<div className="mb-4">
<h3 className="mb-2" style={{ fontWeight: 600 }}>Weitere Bilder hinzufügen</h3>
<MultiImageDropzone
onImagesSelected={handleImagesSelected}
selectedImages={[]}
/>
</Box>
</div>
{/* Image Descriptions Manager */}
{group && group.images && group.images.length > 0 && (
<Box sx={{ mb: 3 }}>
<div className="mb-4">
<ImageDescriptionManager
images={group.images}
token={token}
@ -265,44 +254,44 @@ function ManagementPortalPage() {
onReorder={handleReorder}
onRefresh={loadGroup}
/>
</Box>
</div>
)}
{/* Group Metadata Editor */}
{group && (
<Box sx={{ mb: 3 }}>
<div className="mb-4">
<GroupMetadataEditor
initialMetadata={group.metadata}
token={token}
onRefresh={loadGroup}
/>
</Box>
</div>
)}
{/* Consent Manager */}
{group && (
<Box sx={{ mb: 3 }}>
<div className="mb-4">
<ConsentManager
initialConsents={group.consents}
token={token}
groupId={group.groupId}
onRefresh={loadGroup}
/>
</Box>
</div>
)}
{/* Delete Group Button */}
{group && (
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
<div className="mt-4 flex-center">
<DeleteGroupButton
token={token}
groupName={group.title || group.name || 'diese Gruppe'}
/>
</Box>
</div>
)}
</CardContent>
</Card>
</Container>
</div>
</div>
</div>
<div className="footerContainer">
<Footer />

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Container, Box } from '@mui/material';
// Services
import { adminGet } from '../../services/adminApi';
@ -81,7 +80,7 @@ const ModerationGroupImagesPage = () => {
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<div className="container" style={{ minHeight: '80vh', paddingTop: '20px', paddingBottom: '40px' }}>
{/* Image Descriptions Manager */}
<ImageDescriptionManager
images={group.images}
@ -99,15 +98,15 @@ const ModerationGroupImagesPage = () => {
/>
{/* Back Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
<div className="flex-center mt-4">
<button
className="btn btn-secondary"
onClick={() => navigate('/moderation')}
>
Zurück zur Übersicht
</button>
</Box>
</Container>
</div>
</div>
<div className="footerContainer"><Footer /></div>
</div>

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { Container, Box, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox, Typography } from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';
import Swal from 'sweetalert2/dist/sweetalert2.js';
@ -268,23 +267,14 @@ const ModerationGroupsPage = () => {
<meta name="description" content="Interne Moderationsseite - Nicht öffentlich zugänglich" />
</Helmet>
<Container className="moderation-content" maxWidth="lg" style={{ paddingTop: '20px' }}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 2,
mb: 3
}}>
<Typography variant="h4" component="h1">
Moderation
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<div className="container moderation-content" style={{ paddingTop: '20px' }}>
<div className="flex-center" style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: '16px', marginBottom: '24px' }}>
<h1>Moderation</h1>
<div className="flex-center" style={{ gap: '16px' }}>
{user?.username && (
<Typography variant="body2" color="text.secondary">
<p className="text-small" style={{ color: '#666666', margin: 0 }}>
Eingeloggt als <strong>{user.username}</strong>
</Typography>
</p>
)}
<button
type="button"
@ -295,8 +285,8 @@ const ModerationGroupsPage = () => {
>
{logoutPending ? 'Wird abgemeldet…' : 'Logout'}
</button>
</Box>
</Box>
</div>
</div>
<div className="moderation-stats">
<div className="stat-item">
@ -314,79 +304,65 @@ const ModerationGroupsPage = () => {
</div>
{/* Filter und Export Controls */}
<Box sx={{
display: 'flex',
gap: 2,
mb: 3,
alignItems: 'center',
flexWrap: 'wrap'
}}>
<FormControl component="fieldset" sx={{ minWidth: 250 }}>
<FormLabel component="legend" sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<FilterListIcon sx={{ mr: 0.5, fontSize: 18 }} />
<div style={{ display: 'flex', gap: '16px', marginBottom: '24px', alignItems: 'flex-start', flexWrap: 'wrap' }}>
<fieldset style={{ minWidth: '250px', border: '1px solid #ccc', borderRadius: '8px', padding: '16px' }}>
<legend style={{ display: 'flex', alignItems: 'center', marginBottom: '8px', fontSize: '14px', fontWeight: 600 }}>
<FilterListIcon style={{ marginRight: '4px', fontSize: '18px' }} />
Consent-Filter
</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.workshop}
onChange={(e) => setConsentFilters({...consentFilters, workshop: e.target.checked})}
size="small"
/>
}
label="Werkstatt"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.facebook}
onChange={(e) => setConsentFilters({...consentFilters, facebook: e.target.checked})}
size="small"
/>
}
label="Facebook"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.instagram}
onChange={(e) => setConsentFilters({...consentFilters, instagram: e.target.checked})}
size="small"
/>
}
label="Instagram"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.tiktok}
onChange={(e) => setConsentFilters({...consentFilters, tiktok: e.target.checked})}
size="small"
/>
}
label="TikTok"
/>
</FormGroup>
</FormControl>
</legend>
<div>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consentFilters.workshop}
onChange={(e) => setConsentFilters({...consentFilters, workshop: e.target.checked})}
style={{ marginRight: '8px' }}
/>
Werkstatt
</label>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consentFilters.facebook}
onChange={(e) => setConsentFilters({...consentFilters, facebook: e.target.checked})}
style={{ marginRight: '8px' }}
/>
Facebook
</label>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consentFilters.instagram}
onChange={(e) => setConsentFilters({...consentFilters, instagram: e.target.checked})}
style={{ marginRight: '8px' }}
/>
Instagram
</label>
<label style={{ display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={consentFilters.tiktok}
onChange={(e) => setConsentFilters({...consentFilters, tiktok: e.target.checked})}
style={{ marginRight: '8px' }}
/>
TikTok
</label>
</div>
</fieldset>
<button
className="btn btn-success"
onClick={exportConsentData}
style={{
fontSize: '14px',
padding: '10px 20px'
}}
>
📥 Consent-Daten exportieren
Consent-Daten exportieren
</button>
</Box>
</div>
{/* Wartende Gruppen */}
<section className="moderation-section">
<ImageGallery
items={pendingGroups}
title={`🔍 Wartende Freigabe (${pendingGroups.length})`}
title={`Wartende Freigabe (${pendingGroups.length})`}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
@ -400,7 +376,7 @@ const ModerationGroupsPage = () => {
<section className="moderation-section">
<ImageGallery
items={approvedGroups}
title={`Freigegebene Gruppen (${approvedGroups.length})`}
title={`Freigegebene Gruppen (${approvedGroups.length})`}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
@ -426,7 +402,7 @@ const ModerationGroupsPage = () => {
onDeleteImage={deleteImage}
/>
)}
</Container>
</div>
<div className="footerContainer"><Footer /></div>
</div>
);
@ -471,7 +447,7 @@ const ImageModal = ({ group, onClose, onDeleteImage }) => {
<div className="image-actions">
<span className="image-name">{image.originalName}</span>
<button
className="btn btn-danger btn-sm"
className="btn btn-danger"
onClick={() => onDeleteImage(group.groupId, image.id)}
title="Bild löschen"
>

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, Typography, Container, Box } from '@mui/material';
// Components
import NavbarUpload from '../ComponentUtils/Headers/NavbarUpload';
@ -163,17 +162,17 @@ function MultiUploadPage() {
<div className="allContainer">
{<NavbarUpload />}
<Container maxWidth="lg" sx={{ pt: '20px', pb: '40px', minHeight: '80vh' }}>
<Card sx={{ borderRadius: '12px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', p: '20px', mb: '20px' }}>
<CardContent>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 400, fontSize: '28px', textAlign: 'center', mb: '10px', color: '#333333' }}>
<div className="container">
<div className="card">
<div className="card-content">
<h1 className="page-title">
Project Image Uploader
</Typography>
<Typography sx={{ fontFamily: 'roboto', fontWeight: 300, fontSize: '16px', color: '#666666', textAlign: 'center', mb: '30px' }}>
</h1>
<p className="page-subtitle">
Lade ein oder mehrere Bilder von deinem Projekt hoch und beschreibe dein Projekt in wenigen Worten.
<br />
Die Bilder werden nur hier im Hobbyhimmel auf dem Monitor gezeigt, es wird an keine Dritten weiter gegeben.
</Typography>
</p>
{!uploading ? (
<>
@ -215,15 +214,11 @@ function MultiUploadPage() {
/>
{/* Action Buttons */}
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center', mt: 3, flexWrap: 'wrap' }}>
<div className="flex-center">
<button
className="btn btn-success"
onClick={handleUpload}
disabled={!canUpload()}
style={{
fontSize: '16px',
padding: '12px 30px'
}}
>
🚀 {selectedImages.length} Bild{selectedImages.length !== 1 ? 'er' : ''} hochladen
</button>
@ -231,14 +226,10 @@ function MultiUploadPage() {
<button
className="btn btn-secondary"
onClick={handleClearAll}
style={{
fontSize: '16px',
padding: '12px 30px'
}}
>
🗑 Alle entfernen
</button>
</Box>
</div>
</>
)}
</>
@ -254,85 +245,58 @@ function MultiUploadPage() {
/>
</>
) : (
<Box sx={{
mt: 4,
p: 3,
borderRadius: '12px',
background: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
color: 'white',
boxShadow: '0 4px 20px rgba(76, 175, 80, 0.4)',
animation: 'slideIn 0.5s ease-out',
'@keyframes slideIn': {
from: {
opacity: 0,
transform: 'translateY(-20px)'
},
to: {
opacity: 1,
transform: 'translateY(0)'
}
}
}}>
<Typography sx={{ fontSize: '28px', fontWeight: 'bold', mb: 1 }}>
<div className="success-box">
<h2>
Upload erfolgreich!
</Typography>
<Typography sx={{ fontSize: '18px', mb: 2 }}>
</h2>
<p>
{uploadResult?.imageCount || 0} Bild{uploadResult?.imageCount === 1 ? '' : 'er'} {uploadResult?.imageCount === 1 ? 'wurde' : 'wurden'} hochgeladen.
</Typography>
</p>
<Box sx={{ bgcolor: 'rgba(255,255,255,0.2)', borderRadius: '8px', p: 2, mb: 2 }}>
<Typography sx={{ fontSize: '14px', mb: 1 }}>
<div className="info-box">
<p className="text-small">
Ihre Referenz-Nummer:
</Typography>
<Typography sx={{ fontSize: '20px', fontFamily: 'monospace', fontWeight: 'bold', mb: 1 }}>
</p>
<p style={{ fontSize: '20px', fontFamily: 'monospace', fontWeight: 'bold', marginBottom: '8px' }}>
{uploadResult?.groupId}
</Typography>
<Typography sx={{ fontSize: '12px', opacity: 0.9 }}>
</p>
<p className="text-small" style={{ opacity: 0.9 }}>
Notieren Sie sich diese Nummer für spätere Anfragen an das Team.
</Typography>
</Box>
</p>
</div>
{uploadResult?.managementToken && (
<Box sx={{
bgcolor: 'rgba(255,255,255,0.95)',
borderRadius: '8px',
p: 2.5,
mb: 2,
border: '2px solid rgba(255,255,255,0.3)'
}}>
<Typography sx={{ fontSize: '16px', fontWeight: 'bold', mb: 1.5, color: '#2e7d32' }}>
<div className="info-box-highlight">
<h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '12px', color: '#2e7d32' }}>
🔗 Verwaltungslink für Ihren Upload
</Typography>
<Typography sx={{ fontSize: '13px', mb: 1.5, color: '#333' }}>
</h3>
<p style={{ fontSize: '13px', marginBottom: '12px', color: '#333' }}>
Mit diesem Link können Sie später Ihre Bilder verwalten, Einwilligungen widerrufen oder die Gruppe löschen:
</Typography>
</p>
<Box sx={{
bgcolor: '#f5f5f5',
p: 1.5,
<div style={{
background: '#f5f5f5',
padding: '12px',
borderRadius: '6px',
mb: 1.5,
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
gap: 1,
gap: '8px',
flexWrap: 'wrap'
}}>
<Typography sx={{
<p style={{
fontSize: '13px',
fontFamily: 'monospace',
color: '#1976d2',
wordBreak: 'break-all',
flex: 1,
minWidth: '200px'
minWidth: '200px',
margin: 0
}}>
{window.location.origin}/manage/{uploadResult.managementToken}
</Typography>
</p>
<button
className="btn btn-secondary"
style={{
fontSize: '12px',
padding: '6px 16px'
}}
onClick={() => {
const link = `${window.location.origin}/manage/${uploadResult.managementToken}`;
// Fallback für HTTP (wenn navigator.clipboard nicht verfügbar)
@ -351,43 +315,39 @@ function MultiUploadPage() {
>
📋 Kopieren
</button>
</Box>
</div>
<Typography sx={{ fontSize: '11px', color: '#666', mb: 0.5 }}>
<p className="text-small" style={{ color: '#666', marginBottom: '4px' }}>
<strong>Wichtig:</strong> Bewahren Sie diesen Link sicher auf! Jeder mit diesem Link kann Ihren Upload verwalten.
</Typography>
<Typography sx={{ fontSize: '11px', color: '#666', fontStyle: 'italic' }}>
</p>
<p className="text-small" style={{ color: '#666', fontStyle: 'italic' }}>
<strong>Hinweis:</strong> Über diesen Link können Sie nur die Bilder in der Werkstatt verwalten. Bereits auf Social Media Plattformen veröffentlichte Bilder müssen separat dort gelöscht werden.
</Typography>
</Box>
</p>
</div>
)}
<Typography sx={{ fontSize: '13px', mb: 2, opacity: 0.95 }}>
<p style={{ fontSize: '13px', marginBottom: '16px', opacity: 0.95 }}>
Die Bilder werden geprüft und nach Freigabe auf dem Werkstatt-Monitor angezeigt.
{' '}Bei Social Media Einwilligung werden sie entsprechend veröffentlicht.
</Typography>
</p>
<Typography sx={{ fontSize: '12px', mb: 3, opacity: 0.9 }}>
<p style={{ fontSize: '12px', marginBottom: '24px', opacity: 0.9 }}>
<strong>Fragen oder Widerruf?</strong> Kontakt: <strong>it@hobbyhimmel.de</strong>
</Typography>
</p>
<button
className="btn btn-success"
style={{
fontSize: '16px',
padding: '12px 30px'
}}
onClick={() => window.location.reload()}
>
👍 Weitere Bilder hochladen
</button>
</Box>
</div>
)}
</div>
)}
</CardContent>
</Card>
</Container>
</div>
</div>
</div>
<div className="footerContainer">
<Footer />

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Container } from '@mui/material';
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import ImageGalleryCard from '../ComponentUtils/ImageGalleryCard';
@ -42,7 +41,7 @@ const PublicGroupImagesPage = () => {
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" className="page-container" style={{ marginTop: '40px' }}>
<div className="container page-container" style={{ marginTop: '40px' }}>
<ImageGalleryCard
item={group}
showActions={false}
@ -70,7 +69,7 @@ const PublicGroupImagesPage = () => {
return acc;
}, {}) : {}}
/>
</Container>
</div>
<div className="footerContainer"><Footer /></div>
</div>

View File

@ -1,11 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Typography,
Box,
CircularProgress,
IconButton
} from '@mui/material';
import {
Home as HomeIcon,
ExitToApp as ExitIcon
@ -172,12 +166,12 @@ function SlideshowPage() {
if (loading) {
return (
<Box sx={fullscreenSx}>
<Box sx={loadingContainerSx}>
<CircularProgress sx={{ color: 'white', mb: 2 }} />
<Typography sx={{ color: 'white' }}>Slideshow wird geladen...</Typography>
</Box>
</Box>
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: '#000', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', zIndex: 9999, overflow: 'hidden' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', color: 'white' }}>
<div className="loading-spinner" style={{ width: '60px', height: '60px', border: '4px solid rgba(255,255,255,0.3)', borderTop: '4px solid white', borderRadius: '50%', animation: 'spin 1s linear infinite', marginBottom: '16px' }}></div>
<p style={{ color: 'white', margin: 0 }}>Slideshow wird geladen...</p>
</div>
</div>
);
}
@ -192,27 +186,27 @@ function SlideshowPage() {
if (error) {
return (
<Box sx={fullscreenSx}>
<Box sx={loadingContainerSx}>
<Typography sx={{ color: 'white', fontSize: '24px' }}>{error}</Typography>
<IconButton sx={homeButtonSx} onClick={() => navigate('/')} title="Zur Startseite">
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: '#000', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', zIndex: 9999, overflow: 'hidden' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', color: 'white' }}>
<p style={{ color: 'white', fontSize: '24px', margin: 0 }}>{error}</p>
<button style={{ position: 'absolute', top: '20px', left: '20px', color: 'white', backgroundColor: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', width: '48px', height: '48px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={() => navigate('/')} title="Zur Startseite">
<HomeIcon />
</IconButton>
</Box>
</Box>
</button>
</div>
</div>
);
}
if (!currentGroup || !currentImage) {
return (
<Box sx={fullscreenSx}>
<Box sx={loadingContainerSx}>
<Typography sx={{ color: 'white', fontSize: '24px' }}>Keine Bilder verfügbar</Typography>
<IconButton sx={homeButtonSx} onClick={() => navigate('/')} title="Zur Startseite">
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: '#000', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', zIndex: 9999, overflow: 'hidden' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', color: 'white' }}>
<p style={{ color: 'white', fontSize: '24px', margin: 0 }}>Keine Bilder verfügbar</p>
<button style={{ position: 'absolute', top: '20px', left: '20px', color: 'white', backgroundColor: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', width: '48px', height: '48px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={() => navigate('/')} title="Zur Startseite">
<HomeIcon />
</IconButton>
</Box>
</Box>
</button>
</div>
</div>
);
}
@ -275,41 +269,41 @@ function SlideshowPage() {
const metaTextSx = { color: '#999', fontSize: '12px', mt: 1, fontFamily: 'roboto' };
return (
<Box sx={fullscreenSx}>
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: '#000', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', zIndex: 9999, overflow: 'hidden' }}>
{/* Navigation Buttons */}
<IconButton sx={homeButtonSx} onClick={() => navigate('/')} title="Zur Startseite">
<button style={{ position: 'absolute', top: '20px', left: '20px', color: 'white', backgroundColor: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', width: '48px', height: '48px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={() => navigate('/')} title="Zur Startseite" onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.8)'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.5)'}>
<HomeIcon />
</IconButton>
</button>
<IconButton sx={exitButtonSx} onClick={() => navigate('/')} title="Slideshow beenden">
<button style={{ position: 'absolute', top: '20px', right: '20px', color: 'white', backgroundColor: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', width: '48px', height: '48px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={() => navigate('/')} title="Slideshow beenden" onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.8)'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.5)'}>
<ExitIcon />
</IconButton>
</button>
{/* Hauptbild */}
<Box component="img" src={getImageSrc(currentImage, false)} alt={currentImage.originalName} sx={{ ...slideshowImageSx, opacity: fadeOut ? 0 : 1 }} />
<img src={getImageSrc(currentImage, false)} alt={currentImage.originalName} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', transition: `opacity ${TRANSITION_TIME}ms ease-in-out`, opacity: fadeOut ? 0 : 1 }} />
{/* Bildbeschreibung (wenn vorhanden) */}
{currentImage.imageDescription && (
<Box sx={imageDescriptionSx}>
<Typography sx={imageDescriptionTextSx}>{currentImage.imageDescription}</Typography>
</Box>
<div style={{ position: 'fixed', bottom: '140px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.7)', padding: '15px 30px', borderRadius: '8px', maxWidth: '80%', textAlign: 'center', backdropFilter: 'blur(5px)', zIndex: 10002 }}>
<p style={{ color: 'white', fontSize: '18px', margin: 0, lineHeight: 1.4, fontFamily: 'Open Sans, sans-serif' }}>{currentImage.imageDescription}</p>
</div>
)}
{/* Beschreibung */}
<Box sx={descriptionContainerSx}>
<div style={{ position: 'fixed', left: '40px', bottom: '40px', backgroundColor: 'rgba(0,0,0,0.8)', padding: '25px 35px', borderRadius: '12px', maxWidth: '35vw', minWidth: '260px', textAlign: 'left', backdropFilter: 'blur(5px)', zIndex: 10001, boxShadow: '0 4px 24px rgba(0,0,0,0.4)' }}>
{/* Titel */}
<Typography sx={titleTextSx}>{currentGroup.title || 'Unbenanntes Projekt'}</Typography>
<h2 style={{ color: 'white', fontSize: '28px', fontWeight: 500, marginBottom: '8px', marginTop: 0, fontFamily: 'Open Sans, sans-serif' }}>{currentGroup.title || 'Unbenanntes Projekt'}</h2>
{/* Jahr und Name */}
<Typography sx={yearAuthorTextSx}>{currentGroup.year}{currentGroup.name && `${currentGroup.name}`}</Typography>
<p style={{ color: '#FFD700', fontSize: '18px', fontWeight: 400, marginBottom: '8px', marginTop: 0, fontFamily: 'Open Sans, sans-serif' }}>{currentGroup.year}{currentGroup.name && `${currentGroup.name}`}</p>
{/* Beschreibung (wenn vorhanden) */}
{currentGroup.description && <Typography sx={descriptionTextSx}>{currentGroup.description}</Typography>}
{currentGroup.description && <p style={{ color: '#E0E0E0', fontSize: '16px', fontWeight: 300, marginBottom: '8px', marginTop: 0, fontFamily: 'Open Sans, sans-serif', lineHeight: 1.4 }}>{currentGroup.description}</p>}
{/* Meta-Informationen */}
<Typography sx={metaTextSx}>Bild {currentImageIndex + 1} von {currentGroup.images.length} Slideshow {currentGroupIndex + 1} von {allGroups.length}</Typography>
</Box>
</Box>
<p style={{ color: '#999', fontSize: '12px', marginTop: '8px', marginBottom: 0, fontFamily: 'Open Sans, sans-serif' }}>Bild {currentImageIndex + 1} von {currentGroup.images.length} Slideshow {currentGroupIndex + 1} von {allGroups.length}</p>
</div>
</div>
);
}