Files
AdsPreview/frontend/src/pages/ClientProjects.js
Johannes 501dc92119 feat: UI improvements and responsive design enhancements
- Fix dark mode overscroll areas with proper background colors
- Add mobile responsiveness for project logos in header
- Improve viewport handling with interactive-widget support
- Update app title to proper case 'AdsPreview'
- Add mobile-friendly padding adjustments for tabs and headers
- Update .gitignore to exclude .code-workspace files
- Enhance CSS with anticon color overrides and mobile breakpoints
2025-09-09 18:45:39 +02:00

303 lines
9.9 KiB
JavaScript

import React, { useEffect, useState, useRef } from 'react';
import { Card, Row, Col, Spin, Alert, Image, Layout, Switch, App } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { getClientProjects } from '../services/projectService';
import UserMenu from '../components/UserMenu';
import Sortable from 'sortablejs';
const backendUrl = process.env.REACT_APP_BACKEND || '';
export default function ClientProjects({ user, darkMode, onLogout, onToggleDarkMode }) {
const { message } = App.useApp();
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [sortMode, setSortMode] = useState(false);
const [saving, setSaving] = useState(false);
const normalProjectsRef = useRef(null);
const heProjectsRef = useRef(null);
const normalSortableRef = useRef(null);
const heSortableRef = useRef(null);
useEffect(() => {
const token = localStorage.getItem('jwt');
getClientProjects(token).then(res => {
if (res.success) {
setProjects(res.projects);
} else {
setError(res.error?.message || 'Fehler beim Laden der Projekte');
}
}).catch(() => setError('Serverfehler')).finally(() => setLoading(false));
}, []);
// SortableJS initialisieren/zerstören
useEffect(() => {
if (sortMode && normalProjectsRef.current && heProjectsRef.current) {
// Zerstöre bestehende Instanzen falls vorhanden
if (normalSortableRef.current) {
try {
normalSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
normalSortableRef.current = null;
}
if (heSortableRef.current) {
try {
heSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
heSortableRef.current = null;
}
// Initialisiere SortableJS für beide Bereiche
normalSortableRef.current = Sortable.create(normalProjectsRef.current, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: handleSortEnd
});
heSortableRef.current = Sortable.create(heProjectsRef.current, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: handleSortEnd
});
} else {
// Zerstöre SortableJS Instanzen sicher
if (normalSortableRef.current) {
try {
normalSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
normalSortableRef.current = null;
}
if (heSortableRef.current) {
try {
heSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
heSortableRef.current = null;
}
}
return () => {
// Cleanup beim Unmount
if (normalSortableRef.current) {
try {
normalSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
normalSortableRef.current = null;
}
if (heSortableRef.current) {
try {
heSortableRef.current.destroy();
} catch (e) {
// Ignore cleanup errors
}
heSortableRef.current = null;
}
};
}, [sortMode]);
const handleSortEnd = async (evt) => {
// Neue Reihenfolge aus DOM extrahieren
const newOrder = [];
// Normale Projekte
if (normalProjectsRef.current) {
const normalElements = normalProjectsRef.current.children;
for (let el of normalElements) {
const projectName = el.getAttribute('data-project-name');
if (projectName) newOrder.push(projectName);
}
}
// HE Projekte
if (heProjectsRef.current) {
const heElements = heProjectsRef.current.children;
for (let el of heElements) {
const projectName = el.getAttribute('data-project-name');
if (projectName) newOrder.push(projectName);
}
}
// Speichere neue Reihenfolge
await saveProjectOrder(newOrder);
};
const saveProjectOrder = async (order) => {
setSaving(true);
try {
const token = localStorage.getItem('jwt');
const response = await fetch(`${backendUrl}/api/projects/${encodeURIComponent(user?.dir)}/project-order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ order })
});
const result = await response.json();
if (result.success) {
message.success('Reihenfolge gespeichert');
// Projekte neu laden um die neue Reihenfolge zu übernehmen
const token = localStorage.getItem('jwt');
const res = await getClientProjects(token);
if (res.success) {
setProjects(res.projects);
}
} else {
message.error('Fehler beim Speichern: ' + (result.error?.message || 'Unbekannter Fehler'));
}
} catch (err) {
message.error('Netzwerkfehler beim Speichern');
} finally {
setSaving(false);
}
};
if (loading) {
return <Spin indicator={<LoadingOutlined spin />} size="large" tip="Lade Projekte..." fullscreen />;
}
if (error) {
return <Alert type="error" message={error} showIcon />;
}
// Projekte trennen
const normalProjects = projects.filter(p => !p.isHE);
const heProjects = projects.filter(p => p.isHE);
return (
<Layout>
<style>{`
.sortable-ghost {
opacity: 0.4;
}
.sortable-chosen {
transform: scale(1.05);
}
.sortable-drag {
transform: rotate(5deg);
}
.sortable-container .ant-col {
cursor: ${sortMode ? 'grab' : 'default'};
}
.sortable-container .ant-col:active {
cursor: ${sortMode ? 'grabbing' : 'default'};
}
`}</style>
<Layout.Header className='overview-header' style={{
position: 'sticky',
top: 0,
zIndex: 100,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: darkMode ? '#001f1e' : '#001f1e',
color: darkMode ? '#fff' : '#fff'
}}>
<div style={{ fontWeight: 700, fontSize: 20, color: darkMode ? '#fff' : undefined }}>Übersicht</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<UserMenu
user={user}
onLogout={onLogout}
darkMode={darkMode}
onToggleDarkMode={onToggleDarkMode}
sortMode={sortMode}
setSortMode={setSortMode}
saving={saving}
/>
</div>
</Layout.Header>
<Layout.Content style={{ padding: 32 }}>
<Row gutter={32}>
<Col xs={24} md={12} style={{ marginBottom: 32 }}>
<h3>Cinema</h3>
<Row
ref={normalProjectsRef}
gutter={[16, 4]}
style={{ columnGap: 8, marginLeft: 0 }}
className={sortMode ? 'sortable-container' : ''}
>
{normalProjects.length === 0 && <Col><Alert type="info" message="Keine Cinema Projekte gefunden." showIcon /></Col>}
{normalProjects.map((p, i) => {
return (
<Col
key={p.name}
data-project-name={p.name}
style={{ padding: 0, margin: 0, width: 120, maxWidth: 120 }}
>
{p.poster && (
<Link
to={`/${encodeURIComponent(p.name)}`}
style={{ display: 'block', pointerEvents: sortMode ? 'none' : 'auto' }}
>
<Image
src={backendUrl + p.poster}
alt="Poster"
width={120}
height={178}
style={{ objectFit: 'cover', display: 'block', width: 120, height: 178, borderRadius: 4, cursor: sortMode ? 'grab' : 'pointer' }}
preview={false}
/>
</Link>
)}
</Col>
);
})}
</Row>
</Col>
<Col xs={24} md={12} style={{ marginBottom: 32 }}>
<h3>Home Entertainment</h3>
<Row
ref={heProjectsRef}
gutter={[16, 4]}
style={{ columnGap: 8, marginLeft: 0 }}
className={sortMode ? 'sortable-container' : ''}
>
{heProjects.length === 0 && <Col><Alert type="info" message="Keine Home Entertainment Projekte gefunden." showIcon /></Col>}
{heProjects.map((p, i) => {
return (
<Col
key={p.name}
data-project-name={p.name}
style={{ padding: 0, margin: 0, width: 120, maxWidth: 120 }}
>
{p.poster && (
<Link
to={`/${encodeURIComponent(p.name)}`}
style={{ display: 'block', pointerEvents: sortMode ? 'none' : 'auto' }}
>
<Image
src={backendUrl + p.poster}
alt="Poster"
width={120}
height={178}
style={{ objectFit: 'cover', display: 'block', width: 120, height: 178, borderRadius: 4, cursor: sortMode ? 'grab' : 'pointer' }}
preview={false}
/>
</Link>
)}
</Col>
);
})}
</Row>
</Col>
</Row>
</Layout.Content>
</Layout>
);
}