security: clean repository without media files and sensitive data
- Removed area/ directory with 816MB of media files - Removed sensitive FTP credentials from Git history - Implemented .env.upload system for secure deployments - Added comprehensive .gitignore for future protection This commit represents a clean slate with all sensitive data removed.
This commit is contained in:
303
frontend/src/pages/ClientProjects.js
Normal file
303
frontend/src/pages/ClientProjects.js
Normal file
@@ -0,0 +1,303 @@
|
||||
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 style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: darkMode ? '#001f1e' : '#001f1e',
|
||||
color: darkMode ? '#fff' : '#fff',
|
||||
padding: '0 32px'
|
||||
}}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user