From 60fd1c9ae7aa1d657ac7a6ddbe32f02b6c964469 Mon Sep 17 00:00:00 2001 From: Sandro Date: Tue, 27 Oct 2026 08:46:10 +0100 Subject: [PATCH] feat(workspace): add hashtag tabs matching tasks view style and interactive tile grid with search and sorting --- .../src/features/workspace/WorkspaceView.tsx | 435 +++++++++++++++++- 1 file changed, 421 insertions(+), 14 deletions(-) diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 8fab347..2f55c18 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -17,6 +17,8 @@ import { X, ExternalLink, Sparkles, + ArrowLeft, + CheckSquare, } from 'lucide-react'; import { Document, @@ -40,7 +42,7 @@ interface WorkspaceViewProps { onStatsChange: () => void; } -type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos'; +type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags'; interface FolderTreeNode { name: string; @@ -248,6 +250,8 @@ export const WorkspaceView: React.FC = ({ const [selectedTag, setSelectedTag] = useState(null); const [tagDetails, setTagDetails] = useState(null); const [tagNoteDraft, setTagNoteDraft] = useState(''); + const [tagSearchQuery, setTagSearchQuery] = useState(''); + const [tagSortBy, setTagSortBy] = useState<'count' | 'name'>('count'); const [searchQuery, setSearchQuery] = useState(''); const [expandedFolders, setExpandedFolders] = useState>({}); @@ -925,6 +929,70 @@ export const WorkspaceView: React.FC = ({ return []; }, [activeDocShares, shareModalDoc, shareModalFolder]); + // Aggregated metadata map for every tag (documents where it occurs, open todos count) + const tagsMetadataMap = useMemo(() => { + const map = new Map< + string, + { + tag: string; + count: number; + docs: { id: string; title: string; icon?: string; folderPath?: string }[]; + openTodosCount: number; + } + >(); + + for (const t of tagsSummary) { + const cleanTag = t.tag.toLowerCase(); + const matchingDocs: { id: string; title: string; icon?: string; folderPath?: string }[] = []; + let openTodos = 0; + + for (const d of docs) { + let hasInDoc = false; + const blocks = d.blocks || []; + for (const b of blocks) { + const content = b.content || ''; + const regex = new RegExp(`(?:^|[\\s(])#(${cleanTag})(?=[\\s.,!?:;)#\\]}]|$)`, 'i'); + if (regex.test(content)) { + hasInDoc = true; + if (b.type === 'todo' && !b.checked) openTodos++; + } + } + if (hasInDoc) { + matchingDocs.push({ + id: d.id, + title: d.title || 'Unbenannte Notiz', + icon: d.icon, + folderPath: d.folderPath, + }); + } + } + + map.set(cleanTag, { + tag: t.tag, + count: t.count, + docs: matchingDocs, + openTodosCount: openTodos, + }); + } + + return map; + }, [tagsSummary, docs]); + + // Filtered and sorted tags for the Kachel Grid view + const filteredTags = useMemo(() => { + const q = tagSearchQuery.trim().toLowerCase().replace(/^#/, ''); + let list = [...tagsSummary]; + if (q) { + list = list.filter((t) => t.tag.toLowerCase().includes(q)); + } + if (tagSortBy === 'count') { + list.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); + } else { + list.sort((a, b) => a.tag.localeCompare(b.tag)); + } + return list; + }, [tagsSummary, tagSearchQuery, tagSortBy]); + return (
{/* LEFT MASTER SIDEBAR */} @@ -981,18 +1049,20 @@ export const WorkspaceView: React.FC = ({ )}
- {/* Sub-view Switcher: Notizen vs. To-Dos */} -
+ {/* Sub-view Switcher: Notizen vs. To-Dos vs. Hashtags */} +
+
@@ -1189,22 +1283,292 @@ export const WorkspaceView: React.FC = ({ onWikiLinkClick={handleWikiLinkClick} onBackMobile={() => setMobileShowDetail(false)} /> + ) : subView === 'tags' ? ( + /* ========================================================================= + HASHTAG OVERVIEW & TILE DIRECTORY (KACHEL-ANSICHT MIT SUCHE) + ========================================================================= */ +
+
+ {/* Top Navigation Tabs */} +
+
+ + + +
+ + +
+ + {/* Header with Title and Search Controls */} +
+
+
+ +

Hashtags Übersicht

+ {tagsSummary.length} Tags +
+

+ Entdecke alle Themen, Schlagworte und verknüpften Blöcke aus deinen Notizen +

+
+ + {/* Search & Sort Toolbar */} +
+
+ + setTagSearchQuery(e.target.value)} + placeholder="Hashtags durchsuchen…" + className="input-field w-full pl-8 pr-7 py-1.5 text-xs" + /> + {tagSearchQuery && ( + + )} +
+ + {/* Sort Mode Switcher */} +
+ + +
+
+
+ + {/* Grid of Hashtag Tiles (Kacheln) */} + {filteredTags.length === 0 ? ( +
+ +

+ {tagSearchQuery + ? `Keine Hashtags für "${tagSearchQuery}" gefunden` + : 'Noch keine Hashtags vorhanden'} +

+

+ {tagSearchQuery + ? 'Versuche einen anderen Suchbegriff oder setze die Suche zurück.' + : 'Schreibe #tag in einen beliebigen Notiz-Bullet, um Themen automatisch als Kachel zu gruppieren.'} +

+ {tagSearchQuery && ( + + )} +
+ ) : ( +
+ {filteredTags.map((t) => { + const meta = tagsMetadataMap.get(t.tag.toLowerCase()); + return ( +
handleSelectTag(t.tag)} + className="p-4 rounded-card border border-border bg-canvas hover:border-accent hover:shadow-panel transition-all cursor-pointer group flex flex-col justify-between space-y-3" + > +
+
+
+ + # + +

+ #{t.tag} +

+
+ + {t.count} {t.count === 1 ? 'Treffer' : 'Treffer'} + +
+ + {meta && meta.docs.length > 0 && ( +
+ + In {meta.docs.length} {meta.docs.length === 1 ? 'Notiz' : 'Notizen'}: + +
+ {meta.docs.slice(0, 3).map((d) => ( + + {d.icon || '📝'} + {d.title} + + ))} + {meta.docs.length > 3 && ( + + +{meta.docs.length - 3} + + )} +
+
+ )} +
+ +
+
+ {meta && meta.openTodosCount > 0 ? ( + + + {meta.openTodosCount} offene To-Dos + + ) : ( + Blöcke & Childs anzeigen + )} +
+ + Details + → + +
+
+ ); + })} +
+ )} +
+
) : subView === 'tag_detail' && selectedTag ? (
-
+ {/* Top Navigation Tabs */} +
+
+ + + +
+ +
+ + +
+
+ + {/* Tag Context Header */} +
- + + # +

#{selectedTag}

@@ -1382,6 +1746,49 @@ export const WorkspaceView: React.FC = ({ ) : subView === 'todos' ? (
+ {/* Top Navigation Tabs */} +
+
+ + + +
+ + +
+