- 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
303 lines
9.9 KiB
JavaScript
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>
|
|
);
|
|
}
|