merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes
This commit is contained in:
@@ -1,20 +1,19 @@
|
||||
/**
|
||||
* Dashboard Page – Project overview after login
|
||||
* Dashboard Page - 3-column layout with project folder tree, project cards, and info panel
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { NotificationPanel } from '../components/NotificationPanel';
|
||||
import { ShareDialog } from '../components/ShareDialog';
|
||||
import {
|
||||
getProjects, deleteProject, updateProject,
|
||||
getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder,
|
||||
moveProjectToFolder, getProjectShares,
|
||||
type Project, type ProjectFolder, type ProjectShare,
|
||||
} from '../services/api';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
const UNFILED_ID = '__unfiled__';
|
||||
|
||||
interface DashboardProps {
|
||||
onOpenProject: (projectId: string) => void;
|
||||
@@ -23,21 +22,42 @@ interface DashboardProps {
|
||||
export function Dashboard({ onOpenProject }: DashboardProps) {
|
||||
const { user, logout, token } = useAuth();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [allProjects, setAllProjects] = useState<Project[]>([]);
|
||||
const [folders, setFolders] = useState<ProjectFolder[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newDesc, setNewDesc] = useState('');
|
||||
const [shareProjectId, setShareProjectId] = useState<string | null>(null);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||
const [shares, setShares] = useState<ProjectShare[]>([]);
|
||||
const [sharesLoading, setSharesLoading] = useState(false);
|
||||
const [draggedProjectId, setDraggedProjectId] = useState<string | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; folderId: string | null } | null>(null);
|
||||
const [renamingFolderId, setRenamingFolderId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const [showCreateFolder, setShowCreateFolder] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
||||
const [editingProjectName, setEditingProjectName] = useState('');
|
||||
const [editingProjectDesc, setEditingProjectDesc] = useState('');
|
||||
const [savingProject, setSavingProject] = useState(false);
|
||||
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [mobileTreeOpen, setMobileTreeOpen] = useState(false);
|
||||
const [mobileInfoOpen, setMobileInfoOpen] = useState(false);
|
||||
|
||||
const fetchProjects = useCallback(async () => {
|
||||
const fetchAll = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/projects`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (res.ok) {
|
||||
setProjects(await res.json());
|
||||
}
|
||||
const [projs, fldrs] = await Promise.all([
|
||||
getProjects(token),
|
||||
getProjectFolders(token),
|
||||
]);
|
||||
setAllProjects(projs);
|
||||
setFolders(fldrs);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@@ -46,22 +66,86 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, [fetchProjects]);
|
||||
fetchAll();
|
||||
}, [fetchAll]);
|
||||
|
||||
// Default: expand all folders on first load or when folders change
|
||||
useEffect(() => {
|
||||
if (folders.length > 0 && expandedFolders.size === 0) {
|
||||
setExpandedFolders(new Set(folders.map(f => f.id)));
|
||||
}
|
||||
}, [folders, expandedFolders.size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFolderId === null) {
|
||||
setProjects(allProjects);
|
||||
} else if (selectedFolderId === UNFILED_ID) {
|
||||
setProjects(allProjects.filter(p => p.folder_id === null));
|
||||
} else {
|
||||
setProjects(allProjects.filter(p => p.folder_id === selectedFolderId));
|
||||
}
|
||||
}, [allProjects, selectedFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !selectedProjectId) {
|
||||
setShares([]);
|
||||
return;
|
||||
}
|
||||
setSharesLoading(true);
|
||||
getProjectShares(token, selectedProjectId)
|
||||
.then(s => setShares(s))
|
||||
.catch(() => setShares([]))
|
||||
.finally(() => setSharesLoading(false));
|
||||
}, [token, selectedProjectId]);
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() => allProjects.find(p => p.id === selectedProjectId) ?? null,
|
||||
[allProjects, selectedProjectId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject) {
|
||||
setEditingProjectName(selectedProject.name);
|
||||
setEditingProjectDesc(selectedProject.description ?? '');
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
const folderNameById = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
map.set(UNFILED_ID, 'Ohne Ordner');
|
||||
for (const f of folders) map.set(f.id, f.name);
|
||||
return map;
|
||||
}, [folders]);
|
||||
|
||||
const currentFolderName = useMemo(() => {
|
||||
if (selectedFolderId === null) return 'Alle Projekte';
|
||||
return folderNameById.get(selectedFolderId) ?? 'Projekte';
|
||||
}, [selectedFolderId, folderNameById]);
|
||||
|
||||
const toggleFolder = (id: string) => {
|
||||
setExpandedFolders(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim()) return;
|
||||
if (!newName.trim() || !token) return;
|
||||
try {
|
||||
const folderId = selectedFolderId === null || selectedFolderId === UNFILED_ID
|
||||
? null : selectedFolderId;
|
||||
const res = await fetch(`${API_BASE}/api/projects`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ name: newName, description: newDesc || null }),
|
||||
body: JSON.stringify({ name: newName, description: newDesc || null, folder_id: folderId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setNewName('');
|
||||
setNewDesc('');
|
||||
setShowCreate(false);
|
||||
fetchProjects();
|
||||
fetchAll();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -70,22 +154,241 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
||||
|
||||
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('Projekt wirklich löschen?')) return;
|
||||
if (!confirm('Projekt wirklich loeschen?')) return;
|
||||
if (!token) return;
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/projects/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
fetchProjects();
|
||||
await deleteProject(token, id);
|
||||
if (selectedProjectId === id) setSelectedProjectId(null);
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderSelect = (id: string | null) => {
|
||||
setSelectedFolderId(id);
|
||||
};
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
if (!newFolderName.trim() || !token) return;
|
||||
const parentId = newFolderParent === UNFILED_ID ? null : newFolderParent;
|
||||
try {
|
||||
await createProjectFolder(token, newFolderName, parentId);
|
||||
setNewFolderName('');
|
||||
setShowCreateFolder(false);
|
||||
setNewFolderParent(null);
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolder = async (id: string) => {
|
||||
if (!renameValue.trim() || !token) return;
|
||||
try {
|
||||
await renameProjectFolder(token, id, renameValue);
|
||||
setRenamingFolderId(null);
|
||||
setRenameValue('');
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolder = async (id: string) => {
|
||||
if (!token) return;
|
||||
if (!confirm('Ordner loeschen? Projekte darin werden nach "Ohne Ordner" verschoben.')) return;
|
||||
try {
|
||||
await deleteProjectFolder(token, id);
|
||||
if (selectedFolderId === id) setSelectedFolderId(null);
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectCardClick = (projectId: string) => {
|
||||
setSelectedProjectId(projectId);
|
||||
setMobileInfoOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveProject = async () => {
|
||||
if (!selectedProject || !token) return;
|
||||
if (!editingProjectName.trim()) return;
|
||||
setSavingProject(true);
|
||||
try {
|
||||
await updateProject(token, selectedProject.id, {
|
||||
name: editingProjectName.trim(),
|
||||
description: editingProjectDesc.trim() || null,
|
||||
});
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setSavingProject(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectDragStart = (e: React.DragEvent, projectId: string) => {
|
||||
setDraggedProjectId(projectId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', `project:${projectId}`);
|
||||
};
|
||||
|
||||
const handleFolderDragOver = (e: React.DragEvent, folderId: string | null) => {
|
||||
if (!draggedProjectId) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverFolderId(folderId);
|
||||
};
|
||||
|
||||
const handleFolderDragLeave = (folderId: string | null) => {
|
||||
if (dragOverFolderId === folderId) {
|
||||
setDragOverFolderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderDrop = async (e: React.DragEvent, folderId: string | null) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!draggedProjectId || !token) return;
|
||||
const targetFolderId = folderId === UNFILED_ID ? null : folderId;
|
||||
try {
|
||||
await moveProjectToFolder(token, draggedProjectId, targetFolderId);
|
||||
fetchAll();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setDraggedProjectId(null);
|
||||
setDragOverFolderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent, folderId: string | null) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, folderId });
|
||||
};
|
||||
|
||||
const handleContextAction = (action: string, folderId: string | null) => {
|
||||
setContextMenu(null);
|
||||
if (action === 'new') {
|
||||
setNewFolderParent(folderId);
|
||||
setShowCreateFolder(true);
|
||||
} else if (action === 'newSubfolder' && folderId) {
|
||||
setNewFolderParent(folderId);
|
||||
setShowCreateFolder(true);
|
||||
} else if (action === 'rename' && folderId) {
|
||||
const folder = folders.find(f => f.id === folderId);
|
||||
if (folder) {
|
||||
setRenamingFolderId(folderId);
|
||||
setRenameValue(folder.name);
|
||||
}
|
||||
} else if (action === 'delete' && folderId) {
|
||||
handleDeleteFolder(folderId);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
year: 'numeric', month: 'long', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const folderIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderFolderItem = (folder: ProjectFolder, depth: number): React.ReactNode => {
|
||||
const children = folders.filter(f => f.parent_id === folder.id);
|
||||
const isExpanded = expandedFolders.has(folder.id);
|
||||
const projectCount = allProjects.filter(p => p.folder_id === folder.id).length;
|
||||
const isSelected = selectedFolderId === folder.id;
|
||||
const isDragOver = dragOverFolderId === folder.id;
|
||||
const isRenaming = renamingFolderId === folder.id;
|
||||
|
||||
return (
|
||||
<div key={folder.id}>
|
||||
<div
|
||||
className={`folder-tree-item${isSelected ? ' selected' : ''}${isDragOver ? ' drag-over' : ''}`}
|
||||
draggable
|
||||
onDragOver={(e) => handleFolderDragOver(e, folder.id)}
|
||||
onDragLeave={() => handleFolderDragLeave(folder.id)}
|
||||
onDrop={(e) => handleFolderDrop(e, folder.id)}
|
||||
onClick={() => handleFolderSelect(folder.id)}
|
||||
onContextMenu={(e) => handleContextMenu(e, folder.id)}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
>
|
||||
{children.length > 0 ? (
|
||||
<button
|
||||
className="folder-chevron"
|
||||
onClick={(e) => { e.stopPropagation(); toggleFolder(folder.id); }}
|
||||
aria-label={isExpanded ? 'Einklappen' : 'Ausklappen'}
|
||||
>
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
) : (
|
||||
<span className="folder-chevron-placeholder" />
|
||||
)}
|
||||
{folderIcon}
|
||||
{isRenaming ? (
|
||||
<input
|
||||
className="folder-rename-inline"
|
||||
type="text"
|
||||
value={renameValue}
|
||||
onChange={e => setRenameValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handleRenameFolder(folder.id);
|
||||
if (e.key === 'Escape') { setRenamingFolderId(null); setRenameValue(''); }
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (renameValue.trim()) handleRenameFolder(folder.id);
|
||||
else { setRenamingFolderId(null); setRenameValue(''); }
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span className="folder-tree-name">{folder.name}</span>
|
||||
)}
|
||||
{projectCount > 0 && <span className="folder-count">({projectCount})</span>}
|
||||
{!isRenaming && (
|
||||
<span className="folder-action-buttons">
|
||||
<button
|
||||
className="folder-action-btn"
|
||||
title="Umbenennen"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setRenamingFolderId(folder.id);
|
||||
setRenameValue(folder.name);
|
||||
}}
|
||||
>✎</button>
|
||||
<button
|
||||
className="folder-action-btn delete"
|
||||
title="Loeschen"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteFolder(folder.id);
|
||||
}}
|
||||
>✕</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && children.map(child => renderFolderItem(child, depth + 1))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dashboard-page">
|
||||
<header className="dashboard-header">
|
||||
<div className="dashboard-brand">
|
||||
<button className="dashboard-hamburger" aria-label="Ordner" onClick={() => setMobileTreeOpen(true)}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
</button>
|
||||
<h1>Web CAD</h1>
|
||||
<span className="dashboard-user">{user?.name} ({user?.role})</span>
|
||||
</div>
|
||||
@@ -95,71 +398,252 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="dashboard-content">
|
||||
<div className="dashboard-section-header">
|
||||
<h2>Projekte</h2>
|
||||
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
|
||||
+ Neues Projekt
|
||||
</button>
|
||||
{(mobileTreeOpen || mobileInfoOpen) && (
|
||||
<div className="dashboard-scrim" onClick={() => { setMobileTreeOpen(false); setMobileInfoOpen(false); }} />
|
||||
)}
|
||||
<div className="dashboard-3col">
|
||||
<div className={`dashboard-treeview${mobileTreeOpen ? ' mobile-open' : ''}`} onContextMenu={(e) => handleContextMenu(e, null)}>
|
||||
<button className="mobile-panel-close" onClick={() => setMobileTreeOpen(false)}>×</button>
|
||||
<div className="treeview-header">
|
||||
<h3>Ordner</h3>
|
||||
<button
|
||||
className="treeview-add-folder-btn"
|
||||
title="Neuer Ordner"
|
||||
onClick={() => { setNewFolderParent(null); setShowCreateFolder(true); }}
|
||||
>+</button>
|
||||
</div>
|
||||
|
||||
{showCreateFolder && (
|
||||
<div className="folder-create-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={newFolderParent ? 'Unterordnername' : 'Ordnername'}
|
||||
value={newFolderName}
|
||||
onChange={e => setNewFolderName(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="folder-create-actions">
|
||||
<button onClick={handleCreateFolder}>OK</button>
|
||||
<button onClick={() => { setShowCreateFolder(false); setNewFolderName(''); setNewFolderParent(null); }}>×</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Root: Alle Projekte */}
|
||||
<div
|
||||
className={`folder-tree-item${selectedFolderId === null ? ' selected' : ''}${dragOverFolderId === null && draggedProjectId ? ' drag-over' : ''}`}
|
||||
onClick={() => handleFolderSelect(null)}
|
||||
onDragOver={(e) => handleFolderDragOver(e, null)}
|
||||
onDragLeave={() => handleFolderDragLeave(null)}
|
||||
onDrop={(e) => handleFolderDrop(e, null)}
|
||||
onContextMenu={(e) => handleContextMenu(e, null)}
|
||||
style={{ paddingLeft: '8px' }}
|
||||
>
|
||||
<span className="folder-chevron-placeholder" />
|
||||
{folderIcon}
|
||||
<span className="folder-tree-name">Alle Projekte</span>
|
||||
<span className="folder-count">({allProjects.length})</span>
|
||||
</div>
|
||||
|
||||
{/* Ohne Ordner node */}
|
||||
<div
|
||||
className={`folder-tree-item${selectedFolderId === UNFILED_ID ? ' selected' : ''}${dragOverFolderId === UNFILED_ID ? ' drag-over' : ''}`}
|
||||
onClick={() => handleFolderSelect(UNFILED_ID)}
|
||||
onDragOver={(e) => handleFolderDragOver(e, UNFILED_ID)}
|
||||
onDragLeave={() => handleFolderDragLeave(UNFILED_ID)}
|
||||
onDrop={(e) => handleFolderDrop(e, UNFILED_ID)}
|
||||
onContextMenu={(e) => handleContextMenu(e, null)}
|
||||
style={{ paddingLeft: '24px' }}
|
||||
>
|
||||
<span className="folder-chevron-placeholder" />
|
||||
{folderIcon}
|
||||
<span className="folder-tree-name">Ohne Ordner</span>
|
||||
<span className="folder-count">({allProjects.filter(p => p.folder_id === null).length})</span>
|
||||
</div>
|
||||
|
||||
{/* Top-level folders rendered recursively */}
|
||||
{folders.filter(f => !f.parent_id).map(folder => renderFolderItem(folder, 1))}
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div className="dashboard-create-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Projektname"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Beschreibung (optional)"
|
||||
value={newDesc}
|
||||
onChange={e => setNewDesc(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleCreate}>Erstellen</button>
|
||||
<button onClick={() => setShowCreate(false)}>Abbrechen</button>
|
||||
<div className="dashboard-content">
|
||||
<div className="dashboard-section-header">
|
||||
<h2>{currentFolderName}</h2>
|
||||
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
|
||||
+ Neues Projekt
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="dashboard-loading">Lade Projekte…</p>
|
||||
) : projects.length === 0 ? (
|
||||
<p className="dashboard-empty">Noch keine Projekte. Erstellen Sie ein neues Projekt.</p>
|
||||
) : (
|
||||
<div className="dashboard-project-grid">
|
||||
{projects.map(project => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="dashboard-project-card"
|
||||
onClick={() => onOpenProject(project.id)}
|
||||
>
|
||||
<h3>{project.name}</h3>
|
||||
{project.description && <p>{project.description}</p>}
|
||||
<div className="dashboard-project-meta">
|
||||
<span>Erstellt: {new Date(project.created_at).toLocaleDateString('de-DE')}</span>
|
||||
<div className="dashboard-project-actions">
|
||||
<button
|
||||
className="dashboard-share-btn"
|
||||
onClick={(e) => { e.stopPropagation(); setShareProjectId(project.id); }}
|
||||
>
|
||||
Teilen
|
||||
</button>
|
||||
<button
|
||||
className="dashboard-delete-btn"
|
||||
onClick={(e) => handleDelete(project.id, e)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
{showCreate && (
|
||||
<div className="dashboard-create-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Projektname"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Beschreibung (optional)"
|
||||
value={newDesc}
|
||||
onChange={e => setNewDesc(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleCreate(); }}
|
||||
/>
|
||||
<button onClick={handleCreate}>Erstellen</button>
|
||||
<button onClick={() => setShowCreate(false)}>Abbrechen</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="dashboard-loading">Lade Projekte...</p>
|
||||
) : projects.length === 0 ? (
|
||||
<p className="dashboard-empty">Keine Projekte in dieser Ansicht. Erstellen Sie ein neues Projekt.</p>
|
||||
) : (
|
||||
<div className="dashboard-project-grid">
|
||||
{projects.map(project => (
|
||||
<div
|
||||
key={project.id}
|
||||
className={`dashboard-project-card${selectedProjectId === project.id ? ' selected' : ''}`}
|
||||
onClick={() => handleProjectCardClick(project.id)}
|
||||
draggable
|
||||
onDragStart={(e) => handleProjectDragStart(e, project.id)}
|
||||
onDragEnd={() => { setDraggedProjectId(null); setDragOverFolderId(null); }}
|
||||
>
|
||||
<h3>{project.name}</h3>
|
||||
{project.description && <p>{project.description}</p>}
|
||||
<div className="dashboard-project-meta">
|
||||
<span>{new Date(project.created_at).toLocaleDateString('de-DE')}</span>
|
||||
<div className="dashboard-project-actions">
|
||||
<button
|
||||
className="dashboard-open-btn"
|
||||
onClick={(e) => { e.stopPropagation(); onOpenProject(project.id); }}
|
||||
>
|
||||
Oeffnen
|
||||
</button>
|
||||
<button
|
||||
className="dashboard-share-btn"
|
||||
onClick={(e) => { e.stopPropagation(); setShareProjectId(project.id); }}
|
||||
>
|
||||
Teilen
|
||||
</button>
|
||||
<button
|
||||
className="dashboard-delete-btn"
|
||||
onClick={(e) => handleDelete(project.id, e)}
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`dashboard-info-panel${mobileInfoOpen ? ' mobile-open' : ''}`}>
|
||||
<button className="mobile-panel-back" onClick={() => setMobileInfoOpen(false)}>← Zurück</button>
|
||||
{selectedProject ? (
|
||||
<>
|
||||
<input
|
||||
className="info-panel-title-input"
|
||||
type="text"
|
||||
value={editingProjectName}
|
||||
onChange={(e) => setEditingProjectName(e.target.value)}
|
||||
placeholder="Projektname"
|
||||
/>
|
||||
<textarea
|
||||
className="info-panel-desc-textarea"
|
||||
value={editingProjectDesc}
|
||||
onChange={(e) => setEditingProjectDesc(e.target.value)}
|
||||
placeholder="Beschreibung (optional)"
|
||||
/>
|
||||
<button
|
||||
className="info-panel-save-btn"
|
||||
onClick={handleSaveProject}
|
||||
disabled={savingProject || !editingProjectName.trim()}
|
||||
>
|
||||
{savingProject ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
<div className="info-panel-section">
|
||||
<h4>Metadaten</h4>
|
||||
<dl className="info-panel-meta">
|
||||
<dt>Erstellt</dt>
|
||||
<dd>{formatDate(selectedProject.created_at)}</dd>
|
||||
<dt>Aktualisiert</dt>
|
||||
<dd>{formatDate(selectedProject.updated_at)}</dd>
|
||||
<dt>Owner</dt>
|
||||
<dd>{selectedProject.owner_id}</dd>
|
||||
<dt>Ordner</dt>
|
||||
<dd>{selectedProject.folder_id ? (folderNameById.get(selectedProject.folder_id) ?? 'Unbekannt') : 'Ohne Ordner'}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="info-panel-section">
|
||||
<h4>Sichtbarkeit / Freigaben</h4>
|
||||
{sharesLoading ? (
|
||||
<p className="info-panel-loading">Lade Freigaben...</p>
|
||||
) : shares.length === 0 ? (
|
||||
<p className="info-panel-empty">Keine Freigaben</p>
|
||||
) : (
|
||||
<ul className="info-panel-shares">
|
||||
{shares.map(share => (
|
||||
<li key={share.id} className="info-panel-share-item">
|
||||
<span className="info-panel-share-email">{share.shared_with_email}</span>
|
||||
<span className="info-panel-share-perm">{share.permission}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="info-panel-open-btn"
|
||||
onClick={() => onOpenProject(selectedProject.id)}
|
||||
>
|
||||
Projekt oeffnen
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="info-panel-placeholder">
|
||||
<p>Waehlen Sie ein Projekt aus, um Details zu sehen.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{contextMenu && (
|
||||
<>
|
||||
<div className="context-menu-overlay" onClick={() => setContextMenu(null)} />
|
||||
<div
|
||||
className="context-menu"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
>
|
||||
<button
|
||||
className="context-menu-item"
|
||||
onClick={() => handleContextAction('new', contextMenu.folderId)}
|
||||
>Neuer Ordner</button>
|
||||
{contextMenu.folderId && (
|
||||
<button
|
||||
className="context-menu-item"
|
||||
onClick={() => handleContextAction('newSubfolder', contextMenu.folderId)}
|
||||
>Neuer Unterordner</button>
|
||||
)}
|
||||
{contextMenu.folderId && (
|
||||
<>
|
||||
<button
|
||||
className="context-menu-item"
|
||||
onClick={() => handleContextAction('rename', contextMenu.folderId)}
|
||||
>Umbenennen</button>
|
||||
<button
|
||||
className="context-menu-item danger"
|
||||
onClick={() => handleContextAction('delete', contextMenu.folderId)}
|
||||
>Loeschen</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{token && (
|
||||
<ShareDialog
|
||||
token={token}
|
||||
|
||||
Reference in New Issue
Block a user