diff --git a/PROGRESS.md b/PROGRESS.md index 029c331..27d6ef6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -61,6 +61,7 @@ 17. **API Authentication Enforcement:** Applied JWT Bearer token validation to all sensitive routes. 18. **Persistent Workspace Settings:** Added a `Workspace` database model to persist global dashboard settings like Name and Timezone. 19. **Dynamic UI Integration:** Completely refactored the navigation and boards to build columns and tabs dynamically from the live database user list. +20. **Functional Search:** Implemented a real-time task search feature. Clicking the search icon now reveals an inline search bar that filters tasks by title, description, or tags across all views. ### Phase 3: Advanced Features - **Real-time Notifications:** Explore WebSockets for task assignments. diff --git a/app.jsx b/app.jsx index e9832c2..b011fba 100644 --- a/app.jsx +++ b/app.jsx @@ -59,6 +59,36 @@ function App() { const { tasks, users: dbUsers, audit, workspace, deletedTasks, loading } = useApiData(authed); const [tab, setTab] = React.useState('overview'); + const [searchQuery, setSearchQuery] = React.useState(''); + const [showSearch, setShowSearch] = React.useState(false); + + // Map API fields to frontend component expectations + const frontendTasks = React.useMemo(() => tasks.map(t => ({ + ...t, + assignee: t.assignee_id, + addedBy: t.added_by, + addedAt: t.added_at, + tags: t.tags.map(tagObj => tagObj.tag) + })), [tasks]); + + const filteredTasks = React.useMemo(() => { + if (!searchQuery.trim()) return frontendTasks; + const q = searchQuery.toLowerCase(); + return frontendTasks.filter(t => + t.title.toLowerCase().includes(q) || + (t.description && t.description.toLowerCase().includes(q)) || + t.tags.some(tag => tag.toLowerCase().includes(q)) + ); + }, [frontendTasks, searchQuery]); + + const frontendAudit = React.useMemo(() => audit.map(a => ({ + ...a, + actor: a.actor, + action: a.action, + summary: a.summary, + target: a.target, + at: a.at + })), [audit]); React.useEffect(() => { window.dbUsers = dbUsers; @@ -193,24 +223,6 @@ function App() { const dismissHU = (id) => setHeadsUp(h => h.filter(x => x.id !== id)); const openTaskFromAnywhere = (id) => { setOpenTaskId(id); setShowLogs(false); }; - // Map API fields to frontend component expectations - const frontendTasks = tasks.map(t => ({ - ...t, - assignee: t.assignee_id, - addedBy: t.added_by, - addedAt: t.added_at, - tags: t.tags.map(tagObj => tagObj.tag) - })); - - const frontendAudit = audit.map(a => ({ - ...a, - actor: a.actor, - action: a.action, - summary: a.summary, - target: a.target, - at: a.at - })); - const mappedOpenTask = frontendTasks.find(x => x.id === openTaskId); return ( @@ -225,6 +237,10 @@ function App() { onLogs={() => setShowLogs(true)} onProfile={() => setShowSettings(true)} workspace={workspace} + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} + showSearch={showSearch} + onToggleSearch={() => { setShowSearch(!showSearch); if (showSearch) setSearchQuery(''); }} /> @@ -232,7 +248,7 @@ function App() {
{tab === 'overview' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} @@ -253,7 +269,7 @@ function App() { )} {tab !== 'overview' && tab !== 'deleted' && ( setOpenTaskId(task.id)} onAddFor={(uid) => setAdding(uid)} /> diff --git a/screens.jsx b/screens.jsx index 132071c..883b974 100644 --- a/screens.jsx +++ b/screens.jsx @@ -106,30 +106,54 @@ function BrandMark({ size = 22 }) { ); } -function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogout, onProfile, workspace }) { +function TopBar({ me, dbUsers = [], isAdmin, tab, setTab, onAdd, onLogs, onLogout, onProfile, workspace, searchQuery, setSearchQuery, showSearch, onToggleSearch }) { return (
- Dashy - - {workspace ? workspace.name : 'loading…'} + {showSearch ? ( +
+ + setSearchQuery(e.target.value)} + /> + + + +
+ ) : ( + <> + Dashy + + {workspace ? workspace.name : 'loading…'} + + )}
- + {!showSearch && ( + + )}
- - - - + {!showSearch && ( + <> + + + + + + )} {isAdmin && ( diff --git a/styles.css b/styles.css index 5b20094..d011f08 100644 --- a/styles.css +++ b/styles.css @@ -264,6 +264,36 @@ input, textarea { font: inherit; color: inherit; } .topbar__me-name { font-size: 12.5px; font-weight: 600; } .topbar__me-role { font-size: 10.5px; color: var(--fg-soft); } +/* === SEARCH === */ +.topbar__search { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-sunken); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 0 4px 0 10px; + height: 32px; + flex: 1; + max-width: 400px; + animation: search-slide 180ms ease; +} +.topbar__search-input { + border: none; + background: transparent; + flex: 1; + font-size: 13px; + outline: none; + color: var(--fg); + padding: 0; +} +.topbar__search-input::placeholder { color: var(--fg-faint); } + +@keyframes search-slide { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } +} + /* === MAIN === */ .main { flex: 1; overflow: auto; padding: 20px; min-height: 0; }