fix: dashboard readability + editable info panel + drag&drop visual feedback

This commit is contained in:
A0 Orchestrator
2026-07-03 18:05:56 +02:00
parent aae93fe36b
commit 061f862960
3 changed files with 249 additions and 127 deletions
+60 -13
View File
@@ -8,7 +8,7 @@ import { ShareDialog } from '../components/ShareDialog';
import TreeView from '../components/TreeView';
import type { TreeNode } from '../types/ui.types';
import {
getProjects, deleteProject,
getProjects, deleteProject, updateProject,
getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder,
moveProjectToFolder, getProjectShares,
type Project, type ProjectFolder, type ProjectShare,
@@ -42,6 +42,10 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
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 fetchAll = useCallback(async () => {
if (!token) return;
@@ -91,6 +95,13 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
[allProjects, selectedProjectId],
);
useEffect(() => {
if (selectedProject) {
setEditingProjectName(selectedProject.name);
setEditingProjectDesc(selectedProject.description ?? '');
}
}, [selectedProject]);
const treeNodes = useMemo((): TreeNode[] => {
const rootProjectsCount = allProjects.filter(p => p.folder_id === null).length;
const root: TreeNode = {
@@ -213,21 +224,40 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
setSelectedProjectId(projectId);
};
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) => {
const handleFolderDragOver = (e: React.DragEvent, folderId: string) => {
if (!draggedProjectId) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
(e.currentTarget as HTMLElement).classList.add('tree-drop-target');
setDragOverFolderId(folderId);
};
const handleFolderDragLeave = (e: React.DragEvent) => {
(e.currentTarget as HTMLElement).classList.remove('tree-drop-target');
const handleFolderDragLeave = (_e: React.DragEvent, folderId: string) => {
if (dragOverFolderId === folderId) {
setDragOverFolderId(null);
}
};
const handleFolderDrop = async (e: React.DragEvent, folderId: string) => {
@@ -243,6 +273,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
// ignore
} finally {
setDraggedProjectId(null);
setDragOverFolderId(null);
}
};
@@ -366,9 +397,9 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
{folders.map(folder => (
<div
key={folder.id}
className="treeview-drop-zone"
onDragOver={handleFolderDragOver}
onDragLeave={handleFolderDragLeave}
className={`treeview-drop-zone${dragOverFolderId === folder.id ? ' drag-over' : ''}`}
onDragOver={(e) => handleFolderDragOver(e, folder.id)}
onDragLeave={(e) => handleFolderDragLeave(e, folder.id)}
onDrop={(e) => handleFolderDrop(e, folder.id)}
/>
))}
@@ -418,7 +449,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
onClick={() => handleProjectCardClick(project.id)}
draggable
onDragStart={(e) => handleProjectDragStart(e, project.id)}
onDragEnd={() => setDraggedProjectId(null)}
onDragEnd={() => { setDraggedProjectId(null); setDragOverFolderId(null); }}
>
<h3>{project.name}</h3>
{project.description && <p>{project.description}</p>}
@@ -454,10 +485,26 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
<div className="dashboard-info-panel">
{selectedProject ? (
<>
<h3 className="info-panel-title">{selectedProject.name}</h3>
{selectedProject.description && (
<p className="info-panel-description">{selectedProject.description}</p>
)}
<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">
+10
View File
@@ -108,6 +108,16 @@ export async function deleteProject(token: string, id: string): Promise<void> {
});
}
export async function updateProject(token: string, id: string, updates: { name?: string; description?: string | null }): Promise<Project> {
const res = await fetch(`${API_BASE}/api/projects/${id}`, {
method: 'PATCH',
headers: authHeaders(token),
body: JSON.stringify(updates),
});
if (!res.ok) throw new Error('Failed to update project');
return res.json();
}
// ─── Project Folders ─────────────────────────────────────
export interface ProjectFolder {
id: string;
+179 -114
View File
@@ -6,14 +6,14 @@
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-primary, #1a1a2e);
color: var(--text-primary, #e0e0e0);
background: var(--color-surface-2);
color: var(--color-text);
font-family: 'Inter', system-ui, sans-serif;
}
.auth-card {
background: var(--bg-secondary, #16213e);
border: 1px solid var(--border-color, #2a2a4a);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 2.5rem;
width: 100%;
@@ -26,12 +26,12 @@
font-weight: 700;
margin: 0 0 0.25rem 0;
text-align: center;
color: var(--accent-primary, #4a9eff);
color: var(--color-primary);
}
.auth-subtitle {
font-size: 1rem;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
text-align: center;
margin: 0 0 1.5rem 0;
}
@@ -52,22 +52,22 @@
.auth-field label {
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
}
.auth-field input {
padding: 0.625rem 0.875rem;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--bg-input, #0f0f23);
color: var(--text-primary, #e0e0e0);
background: var(--color-surface-3);
color: var(--color-text);
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s;
}
.auth-field input:focus {
border-color: var(--accent-primary, #4a9eff);
border-color: var(--color-primary);
}
.auth-button {
@@ -75,7 +75,7 @@
padding: 0.625rem 1rem;
border: none;
border-radius: 6px;
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: #fff;
font-size: 0.875rem;
font-weight: 600;
@@ -110,13 +110,13 @@
text-align: center;
margin-top: 1.5rem;
font-size: 0.8125rem;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
}
.auth-link {
background: none;
border: none;
color: var(--accent-primary, #4a9eff);
color: var(--color-primary);
cursor: pointer;
font-size: 0.8125rem;
font-weight: 500;
@@ -131,8 +131,8 @@
/* ─── Dashboard ────────────────────────────────────── */
.dashboard-page {
min-height: 100vh;
background: var(--bg-primary, #1a1a2e);
color: var(--text-primary, #e0e0e0);
background: var(--color-surface-2);
color: var(--color-text);
font-family: 'Inter', system-ui, sans-serif;
display: flex;
flex-direction: column;
@@ -143,20 +143,20 @@
grid-template-columns: 240px 1fr 300px;
flex: 1;
gap: 1px;
background: var(--border-color, #2a2a4a);
background: var(--color-border);
overflow: hidden;
min-height: 0;
}
.dashboard-treeview {
background: var(--bg-secondary, #16213e);
background: var(--color-surface);
overflow-y: auto;
padding: 12px;
position: relative;
}
.dashboard-info-panel {
background: var(--bg-secondary, #16213e);
background: var(--color-surface);
overflow-y: auto;
padding: 16px;
}
@@ -169,14 +169,14 @@
}
.treeview-header h3 {
font-size: 0.875rem;
font-size: 14px;
font-weight: 600;
margin: 0;
color: var(--text-primary, #e0e0e0);
color: var(--color-text);
}
.treeview-add-folder-btn {
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
@@ -195,24 +195,24 @@
.folder-create-form {
margin-bottom: 8px;
padding: 8px;
background: var(--bg-input, #0f0f23);
background: var(--color-surface-3);
border-radius: 6px;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
}
.folder-create-form input {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--bg-primary, #1a1a2e);
color: var(--text-primary, #e0e0e0);
font-size: 0.8125rem;
background: var(--color-surface-2);
color: var(--color-text);
font-size: 13px;
outline: none;
box-sizing: border-box;
}
.folder-create-form input:focus { border-color: var(--accent-primary, #4a9eff); }
.folder-create-form input:focus { border-color: var(--color-primary); }
.folder-create-actions {
display: flex;
@@ -225,42 +225,42 @@
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
font-size: 12px;
font-weight: 600;
}
.folder-create-actions button:first-child {
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: white;
}
.folder-create-actions button:last-child {
background: transparent;
color: var(--text-secondary, #888);
border: 1px solid var(--border-color, #2a2a4a);
color: var(--color-text-muted);
border: 1px solid var(--color-border);
}
.folder-rename-form {
margin-bottom: 8px;
padding: 8px;
background: var(--bg-input, #0f0f23);
background: var(--color-surface-3);
border-radius: 6px;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
}
.folder-rename-form input {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--bg-primary, #1a1a2e);
color: var(--text-primary, #e0e0e0);
font-size: 0.8125rem;
background: var(--color-surface-2);
color: var(--color-text);
font-size: 13px;
outline: none;
box-sizing: border-box;
}
.folder-rename-form input:focus { border-color: var(--accent-primary, #4a9eff); }
.folder-rename-form input:focus { border-color: var(--color-primary); }
.folder-action-buttons {
display: inline-flex;
@@ -274,27 +274,26 @@
cursor: pointer;
padding: 2px 4px;
font-size: 12px;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
border-radius: 3px;
transition: color 0.15s, background 0.15s;
}
.folder-action-btn:hover { color: var(--accent-primary, #4a9eff); background: rgba(74, 158, 255, 0.1); }
.folder-action-btn:hover { color: var(--color-primary); background: rgba(37, 99, 235, 0.1); }
.folder-action-btn.delete:hover { color: #e74c3c; background: rgba(231, 76, 60, 0.1); }
.tree-drop-target {
background: rgba(74, 158, 255, 0.15) !important;
outline: 2px dashed var(--accent-primary, #4a9eff);
/* Drag-over visual feedback on tree nodes */
.tree-node.drag-over {
background: var(--color-primary) !important;
opacity: 0.7;
border: 2px dashed var(--color-primary);
border-radius: 4px;
}
.treeview-drop-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
}
.treeview-drop-zone {
pointer-events: auto;
.treeview-drop-zone.drag-over {
background: rgba(37, 99, 235, 0.15);
outline: 2px dashed var(--color-primary);
border-radius: 4px;
}
.context-menu-overlay {
@@ -306,8 +305,8 @@
.context-menu {
position: fixed;
z-index: 5001;
background: var(--bg-secondary, #16213e);
border: 1px solid var(--border-color, #2a2a4a);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
min-width: 160px;
@@ -322,24 +321,24 @@
background: none;
border: none;
cursor: pointer;
color: var(--text-primary, #e0e0e0);
font-size: 0.8125rem;
color: var(--color-text);
font-size: 13px;
transition: background 0.15s;
}
.context-menu-item:hover { background: rgba(74, 158, 255, 0.1); }
.context-menu-item:hover { background: rgba(37, 99, 235, 0.1); }
.context-menu-item.danger:hover { background: rgba(231, 76, 60, 0.1); color: #e74c3c; }
.dashboard-project-card.selected {
border-color: var(--accent-primary, #4a9eff);
box-shadow: 0 0 0 1px var(--accent-primary, #4a9eff);
border-color: var(--color-primary);
box-shadow: 0 0 0 1px var(--color-primary);
}
.dashboard-open-btn {
padding: 4px 10px;
border: 1px solid var(--accent-primary, #4a9eff);
border: 1px solid var(--color-primary);
border-radius: 4px;
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: white;
cursor: pointer;
font-size: 12px;
@@ -348,33 +347,75 @@
.dashboard-open-btn:hover { opacity: 0.85; }
/* ─── Info Panel ────────────────────────────────────── */
.info-panel-title {
font-size: 1.125rem;
font-size: 18px;
font-weight: 700;
margin: 0 0 8px 0;
color: var(--text-primary, #e0e0e0);
color: var(--color-text);
}
.info-panel-title-input {
width: 100%;
font-size: 18px;
font-weight: 700;
margin: 0 0 8px 0;
color: var(--color-text);
background: var(--color-surface-3);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 6px 8px;
outline: none;
box-sizing: border-box;
font-family: 'Inter', system-ui, sans-serif;
}
.info-panel-title-input:focus {
border-color: var(--color-primary);
}
.info-panel-description {
font-size: 0.8125rem;
color: var(--text-secondary, #888);
font-size: 14px;
color: var(--color-text-muted);
margin: 0 0 16px 0;
line-height: 1.5;
}
.info-panel-desc-textarea {
width: 100%;
font-size: 14px;
color: var(--color-text-muted);
margin: 0 0 16px 0;
line-height: 1.5;
background: var(--color-surface-3);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 8px;
outline: none;
box-sizing: border-box;
font-family: 'Inter', system-ui, sans-serif;
resize: vertical;
min-height: 60px;
}
.info-panel-desc-textarea:focus {
border-color: var(--color-primary);
}
.info-panel-section {
margin-bottom: 20px;
}
.info-panel-section h4 {
font-size: 0.75rem;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
margin: 0 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid var(--border-color, #2a2a4a);
border-bottom: 1px solid var(--color-border);
}
.info-panel-meta {
@@ -385,20 +426,20 @@
}
.info-panel-meta dt {
font-size: 0.75rem;
color: var(--text-secondary, #888);
font-size: 13px;
color: var(--color-text-muted);
font-weight: 500;
}
.info-panel-meta dd {
font-size: 0.75rem;
color: var(--text-primary, #e0e0e0);
font-size: 13px;
color: var(--color-text);
margin: 0;
}
.info-panel-loading, .info-panel-empty {
font-size: 0.75rem;
color: var(--text-secondary, #888);
font-size: 13px;
color: var(--color-text-muted);
padding: 8px 0;
}
@@ -416,23 +457,23 @@
justify-content: space-between;
align-items: center;
padding: 6px 8px;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 0.75rem;
font-size: 13px;
}
.info-panel-share-email {
color: var(--text-primary, #e0e0e0);
color: var(--color-text);
}
.info-panel-share-perm {
font-size: 0.6875rem;
font-size: 11px;
text-transform: uppercase;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
background: var(--border-color, #2a2a4a);
color: var(--text-secondary, #888);
background: var(--color-border);
color: var(--color-text-muted);
}
.info-panel-open-btn {
@@ -440,9 +481,9 @@
padding: 10px;
border: none;
border-radius: 6px;
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: white;
font-size: 0.875rem;
font-size: 14px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
@@ -450,6 +491,30 @@
.info-panel-open-btn:hover { opacity: 0.85; }
.info-panel-save-btn {
width: 100%;
padding: 8px;
border: 1px solid var(--color-primary);
border-radius: 6px;
background: var(--color-surface);
color: var(--color-primary);
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-bottom: 8px;
transition: background 0.15s, color 0.15s;
}
.info-panel-save-btn:hover {
background: var(--color-primary);
color: white;
}
.info-panel-save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-panel-placeholder {
display: flex;
align-items: center;
@@ -459,8 +524,8 @@
}
.info-panel-placeholder p {
color: var(--text-secondary, #888);
font-size: 0.875rem;
color: var(--color-text-muted);
font-size: 14px;
text-align: center;
}
@@ -478,8 +543,8 @@
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid var(--border-color, #2a2a4a);
background: var(--bg-secondary, #16213e);
border-bottom: 1px solid var(--color-border);
background: var(--color-surface);
}
.dashboard-brand {
@@ -492,21 +557,21 @@
font-size: 1.25rem;
font-weight: 700;
margin: 0;
color: var(--accent-primary, #4a9eff);
color: var(--color-primary);
}
.dashboard-user {
font-size: 0.8125rem;
color: var(--text-secondary, #888);
font-size: 13px;
color: var(--color-text-muted);
}
.dashboard-logout {
padding: 0.375rem 0.875rem;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 6px;
background: transparent;
color: var(--text-primary, #e0e0e0);
font-size: 0.8125rem;
color: var(--color-text);
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
}
@@ -518,7 +583,7 @@
.dashboard-content {
padding: 2rem;
overflow-y: auto;
background: var(--bg-secondary, #16213e);
background: var(--color-surface);
}
.dashboard-section-header {
@@ -538,9 +603,9 @@
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: #fff;
font-size: 0.8125rem;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
@@ -556,16 +621,16 @@
flex: 1;
min-width: 150px;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color, #2a2a4a);
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--bg-input, #0f0f23);
color: var(--text-primary, #e0e0e0);
font-size: 0.8125rem;
background: var(--color-surface-3);
color: var(--color-text);
font-size: 13px;
outline: none;
}
.dashboard-create-form input:focus {
border-color: var(--accent-primary, #4a9eff);
border-color: var(--color-primary);
}
.dashboard-create-form button {
@@ -573,25 +638,25 @@
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.8125rem;
font-size: 13px;
font-weight: 500;
}
.dashboard-create-form button:first-of-type {
background: var(--accent-primary, #4a9eff);
background: var(--color-primary);
color: #fff;
}
.dashboard-create-form button:last-of-type {
background: transparent;
color: var(--text-secondary, #888);
border: 1px solid var(--border-color, #2a2a4a);
color: var(--color-text-muted);
border: 1px solid var(--color-border);
}
.dashboard-loading,
.dashboard-empty {
text-align: center;
color: var(--text-secondary, #888);
color: var(--color-text-muted);
padding: 2rem;
}
@@ -602,8 +667,8 @@
}
.dashboard-project-card {
background: var(--bg-secondary, #16213e);
border: 1px solid var(--border-color, #2a2a4a);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1.25rem;
cursor: pointer;
@@ -611,7 +676,7 @@
}
.dashboard-project-card:hover {
border-color: var(--accent-primary, #4a9eff);
border-color: var(--color-primary);
transform: translateY(-2px);
}
@@ -622,8 +687,8 @@
}
.dashboard-project-card p {
font-size: 0.8125rem;
color: var(--text-secondary, #888);
font-size: 13px;
color: var(--color-text-muted);
margin: 0 0 0.75rem 0;
}
@@ -631,8 +696,8 @@
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-secondary, #888);
font-size: 12px;
color: var(--color-text-muted);
}
.dashboard-delete-btn {
@@ -641,7 +706,7 @@
border-radius: 4px;
background: transparent;
color: #ef4444;
font-size: 0.75rem;
font-size: 12px;
cursor: pointer;
}