diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a661889..37d044b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,6 @@ import { Brain, LogOut, User as UserIcon, - Plus, Bell, Shield, ChevronRight, @@ -305,7 +304,7 @@ export const App: React.FC = () => { {/* Main Container */} -
+
{/* Instant Capture Section */}
{
- {/* Floating Bottom Navigation Dock matching Template Screen 3 */} - - {/* Profile & Settings Modal styled identically to Template Screen 2 */} {showProfileModal && (
diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 6dfb1a0..5a1ab36 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -1,6 +1,13 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { - Plus, + FilePlus, + FolderPlus, + Folder, + FolderOpen, + ChevronRight, + ChevronDown, + FileText, + Search, Pin, Trash2, Download, @@ -12,7 +19,7 @@ import { Sparkles, AlignLeft, Check, - Search, + X, ArrowUp, ArrowDown, } from 'lucide-react'; @@ -26,6 +33,14 @@ interface WorkspaceViewProps { const ICONS = ['🚀', '💡', '📄', '🏡', '⚡', '🎯', '🍕', '🛠️', '🧠', '🔥', '🏔️', '📦']; +interface FolderTreeItem { + path: string; + name: string; + depth: number; + subfolders: FolderTreeItem[]; + docs: Document[]; +} + export const WorkspaceView: React.FC = ({ initialDocId, onStatsChange, @@ -36,6 +51,20 @@ export const WorkspaceView: React.FC = ({ const [saveState, setSaveState] = useState<'saved' | 'saving'>('saved'); const [showIconPicker, setShowIconPicker] = useState(false); + // Folder management + const [customFolders, setCustomFolders] = useState(() => { + try { + const stored = localStorage.getItem('mindsift_workspace_folders'); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } + }); + const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [newFolderParent, setNewFolderParent] = useState(null); + const [newFolderName, setNewFolderName] = useState(''); + const loadDocs = async (selectId?: string | null) => { const list = await api.getDocuments(); setDocs(list); @@ -75,8 +104,43 @@ export const WorkspaceView: React.FC = ({ } }; - const handleCreateDoc = async () => { + const getDocFolder = (doc: Document): string => { + const tag = doc.tags?.find((t) => t.startsWith('folder:')); + return tag ? tag.replace('folder:', '') : ''; + }; + + // Compute all available folders (persisted custom folders + tags on documents) + const allFolderPaths = useMemo(() => { + const set = new Set(customFolders); + docs.forEach((doc) => { + const f = getDocFolder(doc); + if (f) { + const parts = f.split('/'); + let cur = ''; + for (const p of parts) { + cur = cur ? `${cur}/${p}` : p; + set.add(cur); + } + } + }); + return Array.from(set).sort((a, b) => a.localeCompare(b)); + }, [customFolders, docs]); + + // Ensure newly discovered folders start expanded so users see their files + useEffect(() => { + setExpandedFolders((prev) => { + const next = new Set(prev); + allFolderPaths.forEach((p) => next.add(p)); + return next; + }); + }, [allFolderPaths.join(',')]); + + const handleCreateDoc = async (folderPath?: string | null) => { triggerHaptic(15); + const tags = ['projekt']; + if (folderPath) { + tags.push(`folder:${folderPath}`); + } const created = await api.createDocument({ title: 'Neues Projekt / Notiz', icon: '💡', @@ -98,8 +162,17 @@ export const WorkspaceView: React.FC = ({ checked: false, }, ], - tags: ['projekt'], + tags, }); + + if (folderPath) { + setExpandedFolders((prev) => { + const next = new Set(prev); + next.add(folderPath); + return next; + }); + } + await loadDocs(created.id); onStatsChange(); }; @@ -113,6 +186,84 @@ export const WorkspaceView: React.FC = ({ onStatsChange(); }; + const handleChangeDocFolder = (targetFolder: string) => { + if (!selectedDoc) return; + const cleanTags = (selectedDoc.tags || []).filter((t) => !t.startsWith('folder:')); + if (targetFolder) { + cleanTags.push(`folder:${targetFolder}`); + } + persistDocument({ ...selectedDoc, tags: cleanTags }); + }; + + const handleStartCreateFolder = (parentPath: string | null = null) => { + triggerHaptic(10); + setNewFolderParent(parentPath); + setNewFolderName(''); + setIsCreatingFolder(true); + }; + + const handleConfirmCreateFolder = () => { + const trimmed = newFolderName.trim().replace(/^\/+|\/+$/g, ''); + if (!trimmed) { + setIsCreatingFolder(false); + return; + } + const fullPath = newFolderParent ? `${newFolderParent}/${trimmed}` : trimmed; + if (!customFolders.includes(fullPath)) { + const updated = [...customFolders, fullPath]; + setCustomFolders(updated); + try { + localStorage.setItem('mindsift_workspace_folders', JSON.stringify(updated)); + } catch { + // local storage fallback + } + } + setExpandedFolders((prev) => { + const next = new Set(prev); + next.add(fullPath); + if (newFolderParent) next.add(newFolderParent); + return next; + }); + setIsCreatingFolder(false); + setNewFolderName(''); + setNewFolderParent(null); + }; + + const handleDeleteFolder = (folderPath: string, e: React.MouseEvent) => { + e.stopPropagation(); + triggerHaptic(15); + const updated = customFolders.filter( + (p) => p !== folderPath && !p.startsWith(folderPath + '/') + ); + setCustomFolders(updated); + try { + localStorage.setItem('mindsift_workspace_folders', JSON.stringify(updated)); + } catch { + // local storage fallback + } + + // Keep docs safe: remove folder prefix so they revert to root + docs.forEach((d) => { + const f = getDocFolder(d); + if (f === folderPath || f.startsWith(folderPath + '/')) { + const nextTags = (d.tags || []).filter((t) => !t.startsWith('folder:')); + persistDocument({ ...d, tags: nextTags }); + } + }); + }; + + const toggleFolder = (folderPath: string) => { + setExpandedFolders((prev) => { + const next = new Set(prev); + if (next.has(folderPath)) { + next.delete(folderPath); + } else { + next.add(folderPath); + } + return next; + }); + }; + const addBlock = (type: BlockType, afterIndex?: number) => { if (!selectedDoc) return; const newBlock: DocumentBlock = { @@ -200,61 +351,304 @@ export const WorkspaceView: React.FC = ({ const filteredDocs = docs.filter( (d) => d.title.toLowerCase().includes(searchQuery.toLowerCase()) || - d.content.toLowerCase().includes(searchQuery.toLowerCase()) + (d.content && d.content.toLowerCase().includes(searchQuery.toLowerCase())) ); + // Build recursive folder tree + const buildTree = (parentPath: string = '', depth: number = 0): FolderTreeItem[] => { + const directChildren = allFolderPaths.filter((p) => { + if (parentPath === '') { + return !p.includes('/'); + } + return p.startsWith(parentPath + '/') && !p.slice(parentPath.length + 1).includes('/'); + }); + + return directChildren.map((p) => { + const name = p.split('/').pop() || p; + const matchingDocs = filteredDocs.filter((d) => getDocFolder(d) === p); + return { + path: p, + name, + depth, + subfolders: buildTree(p, depth + 1), + docs: matchingDocs, + }; + }); + }; + + const rootTree = useMemo(() => buildTree('', 0), [allFolderPaths, filteredDocs]); + const rootDocs = useMemo( + () => filteredDocs.filter((d) => !getDocFolder(d)), + [filteredDocs] + ); + + const renderDocItem = (doc: Document, depth: number = 0) => { + const active = selectedDoc?.id === doc.id; + return ( + + ); + }; + + const renderFolder = (node: FolderTreeItem) => { + const isExpanded = expandedFolders.has(node.path); + const isCreatingSubfolder = isCreatingFolder && newFolderParent === node.path; + const totalItems = + node.docs.length + + node.subfolders.reduce((acc, s) => acc + s.docs.length, 0); + + return ( +
+ {/* Folder Header Row */} +
toggleFolder(node.path)} + style={{ paddingLeft: `${node.depth * 14 + 8}px` }} + className="group flex items-center justify-between py-1.5 pr-2 hover:bg-[#282a2f] cursor-pointer transition-colors text-xs text-zinc-300" + > +
+ + {isExpanded ? ( + + ) : ( + + )} + + {isExpanded ? ( + + ) : ( + + )} + {node.name} + ({totalItems}) +
+ + {/* Folder Actions on Hover */} +
+ + + +
+
+ + {/* Subfolder inline creation input */} + {isCreatingSubfolder && ( +
+ + setNewFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirmCreateFolder(); + if (e.key === 'Escape') setIsCreatingFolder(false); + }} + placeholder="Unterordnername…" + className="flex-1 h-6 px-1.5 bg-[#222428] text-xs text-white border border-white/20 focus:outline-none focus:border-[#1d8cf8]" + /> + + +
+ )} + + {/* Expanded Folder Content */} + {isExpanded && ( +
+ {node.subfolders.map(renderFolder)} + {node.docs.map((doc) => renderDocItem(doc, node.depth + 1))} + {node.subfolders.length === 0 && node.docs.length === 0 && !isCreatingSubfolder && ( +
+ (leer) +
+ )} +
+ )} +
+ ); + }; + + const isEmpty = docs.length === 0 && allFolderPaths.length === 0; + return (
- {/* Left Document List Sidebar */} + {/* Left Document & Folder Explorer Tree */}
-
-
- + {/* Top Explorer Action Bar */} +
+ {/* Quick Search */} +
+ setSearchQuery(e.target.value)} - placeholder="Notizen durchsuchen…" - className="w-full h-9 pl-9 pr-3 bg-[#16171a] text-xs sm:text-sm text-white placeholder-zinc-500 border border-white/[0.06] focus:outline-none focus:border-[#1d8cf8] transition-colors" + placeholder="Dateien durchsuchen…" + className="w-full h-7 pl-7 pr-2 bg-[#16171a] text-xs text-white placeholder-zinc-500 border border-white/[0.08] focus:outline-none focus:border-[#1d8cf8] transition-colors" />
- + {/* Monochromatic Square Action Buttons */} +
+ + + +
-
- {filteredDocs.map((doc) => { - const active = selectedDoc?.id === doc.id; - return ( + {/* Root Folder Creation Input */} + {isCreatingFolder && newFolderParent === null && ( +
+ + setNewFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirmCreateFolder(); + if (e.key === 'Escape') setIsCreatingFolder(false); + }} + placeholder="Neuer Ordnername…" + className="flex-1 h-6 px-1.5 bg-[#222428] text-xs text-white border border-white/20 focus:outline-none focus:border-[#1d8cf8]" + /> + + +
+ )} + + {/* Empty State */} + {isEmpty ? ( +
+ +
Keine Dateien vorhanden
+

+ Erstelle eine neue Datei oder lege Ordner an, um deine Dokumente ĂĽbersichtlich zu strukturieren. +

+
- ); - })} -
+ +
+
+ ) : ( + /* File Explorer Tree View */ +
+ {rootTree.map(renderFolder)} + {rootDocs.map((doc) => renderDocItem(doc, 0))} + {filteredDocs.length === 0 && searchQuery && ( +
+ Keine Dateien gefunden fĂĽr "{searchQuery}" +
+ )} +
+ )}
{/* Right Block Editor */} @@ -263,23 +657,42 @@ export const WorkspaceView: React.FC = ({
{/* Document Top Action Bar */}
-
+
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} + + {/* Folder Breadcrumb Selector */} +
+ + +
+ • {selectedDoc.blocks.length} Blöcke
@@ -308,14 +724,14 @@ export const WorkspaceView: React.FC = ({ {showIconPicker && ( -
+
{ICONS.map((ic) => ( @@ -337,7 +753,7 @@ export const WorkspaceView: React.FC = ({ value={selectedDoc.title} onChange={(e) => persistDocument({ ...selectedDoc, title: e.target.value })} placeholder="Dokument-Titel…" - className="w-full bg-transparent text-2xl sm:text-3xl font-bold text-white focus:outline-none" + className="w-full bg-transparent text-xl sm:text-2xl font-bold text-white focus:outline-none" />
@@ -353,7 +769,7 @@ export const WorkspaceView: React.FC = ({ value={block.content} onChange={(e) => updateBlock(block.id, { content: e.target.value })} placeholder="Überschrift 1…" - className="w-full bg-transparent text-2xl font-bold text-white focus:outline-none py-1" + className="w-full bg-transparent text-xl font-bold text-white focus:outline-none py-1" /> )} @@ -363,7 +779,7 @@ export const WorkspaceView: React.FC = ({ value={block.content} onChange={(e) => updateBlock(block.id, { content: e.target.value })} placeholder="Abschnitts-Überschrift…" - className="w-full bg-transparent text-lg font-semibold text-[#1d8cf8] focus:outline-none pt-2 pb-1" + className="w-full bg-transparent text-base font-semibold text-[#1d8cf8] focus:outline-none pt-2 pb-1" /> )} @@ -373,24 +789,24 @@ export const WorkspaceView: React.FC = ({ value={block.content} onChange={(e) => updateBlock(block.id, { content: e.target.value })} placeholder="Text schreiben…" - className="w-full bg-transparent text-sm sm:text-base text-zinc-200 leading-relaxed focus:outline-none resize-none font-normal" + className="w-full bg-transparent text-xs sm:text-sm text-zinc-200 leading-relaxed focus:outline-none resize-none font-normal" /> )} {block.type === 'todo' && ( -
+
updateBlock(block.id, { checked: e.target.checked })} - className="w-4 h-4 accent-[#1d8cf8] cursor-pointer" + className="w-3.5 h-3.5 accent-[#1d8cf8] cursor-pointer" /> updateBlock(block.id, { content: e.target.value })} placeholder="To-Do Punkt…" - className={`w-full bg-transparent text-sm sm:text-base focus:outline-none ${ + className={`w-full bg-transparent text-xs sm:text-sm focus:outline-none ${ block.checked ? 'line-through text-zinc-500' : 'text-white' }`} /> @@ -398,33 +814,33 @@ export const WorkspaceView: React.FC = ({ )} {block.type === 'bullet' && ( -
+
updateBlock(block.id, { content: e.target.value })} placeholder="Listenpunkt…" - className="w-full bg-transparent text-sm sm:text-base text-zinc-200 focus:outline-none" + className="w-full bg-transparent text-xs sm:text-sm text-zinc-200 focus:outline-none" />
)} {block.type === 'callout' && ( -
- +
+