diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx new file mode 100644 index 0000000..870e7a6 --- /dev/null +++ b/src/components/FilterBar.tsx @@ -0,0 +1,243 @@ +import { type Component, createSignal, For, Show } from "solid-js"; +import { Search, X, Hash, ChevronDown, ArrowUpCircle, Clock } from "lucide-solid"; +import { store, setFilter, clearFilter } from "@/store"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { Badge } from "./ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +export const FilterBar: Component = () => { + const [isExpanded, setIsExpanded] = createSignal(false); + + const hasActiveFilters = () => { + const f = store.filter; + return f.query !== "" || f.tags.length > 0 || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10; + }; + + const allTags = () => Object.keys(store.tagDefinitions || {}).sort(); + + const FilterControls: Component<{ class?: string }> = (props) => ( +
+ {/* Main Search */} +
+ + setFilter({ query: e.currentTarget.value })} + class="bg-transparent border-none focus:ring-0 text-sm flex-1 md:w-40 h-8 outline-none placeholder:text-muted-foreground/50 min-w-0" + autofocus + /> +
+ + {/* Priority Range */} +
+ Priority +
+ + - + +
+
+ + {/* Urgency Range */} +
+ Urgency +
+ + - + +
+
+ + {/* Tags */} +
+ +
+ + + + + {(tag) => ( + { + e.stopPropagation(); + setFilter({ tags: store.filter.tags.filter(t => t !== tag) }); + }} + > + {tag} + + + )} + + 0}> + + +
+
+ + {/* Actions for Desktop */} + + + {/* Clear All for Mobile */} +
+ +
+
+ ); + + return ( +
+ {/* Desktop View: Inline expansion */} + + + {/* Mobile View: Popover */} +
+ + + + Filter + {hasActiveFilters() && ( +
+ )} + + + + + +
+
+ ); +}; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 23c12a0..17d4a1f 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -7,6 +7,7 @@ const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.Tas const QuickEntry = lazy(() => import("./QuickEntry").then(m => ({ default: m.QuickEntry }))); import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; +import { FilterBar } from "./FilterBar"; interface LayoutProps { children: JSX.Element; @@ -32,7 +33,7 @@ export const Layout: Component = (props) => { const activeTask = () => store.tasks.find(t => t.id === activeTaskId()); return ( -
+
= (props) => { )} {/* Main Content Area */} -
-
+
+
+ {/* Global Top Header for Filter/Search */} +
+ +
+ {props.children}
diff --git a/src/components/TagPicker.tsx b/src/components/TagPicker.tsx index 36a0634..3d5e2e4 100644 --- a/src/components/TagPicker.tsx +++ b/src/components/TagPicker.tsx @@ -53,7 +53,7 @@ export const TagPicker: Component = (props) => { Add Tag - +
diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index e22680a..7ad798f 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -24,43 +24,43 @@ export const TaskCard: Component<{ task: Task }> = (props) => { return (
setActiveTaskId(props.task.id)} > -
+

{props.task.title}

-
+
{/* Priority */} -
+
- P{props.task.priority} + P{props.task.priority}
{/* Tags */} {props.task.tags && props.task.tags.length > 0 && ( -
+
{(tag) => ( - -
5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} /> + +
5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} /> {tag} )} @@ -69,11 +69,11 @@ export const TaskCard: Component<{ task: Task }> = (props) => { )} {/* Due Date + Urgency Slide-out */} -
+
{/* Due Date - Static Layer on Top */} -
- - {formattedDate()} +
+ + {formattedDate()}
{/* Urgency Number - Slides out from behind with fade */} @@ -81,7 +81,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => { "absolute left-4 top-0 bottom-0 flex items-center transition-all duration-300 ease-out z-10 opacity-0 group-hover/date:opacity-100 group-hover/date:left-full whitespace-nowrap", urgencyColor() )}> - + Urgency {urgencyLevel()}
@@ -89,9 +89,9 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
-
-
diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index 26d099a..a531762 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -107,7 +107,7 @@ export const TaskDetail: Component = (props) => { /> {/* Properties Bar */} -
+
{/* Priority */}
@@ -224,19 +224,19 @@ export const TaskDetail: Component = (props) => {
{/* Tags Row */} -
+
Tags -
+
-
+
{(tag) => ( updateTags((props.task.tags || []).filter(t => t !== tag))} >
5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} /> diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index cc0702c..8570c5f 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -39,6 +39,7 @@ export interface ButtonProps extends VariantProps { variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" type?: "button" | "submit" | "reset" disabled?: boolean + title?: string } const Button = (props: ButtonProps) => { diff --git a/src/store/index.ts b/src/store/index.ts index 0596997..ac03b1b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -25,6 +25,15 @@ export interface Task { deletedAt?: number; // Timestamp of soft delete } +export interface Filter { + query: string; + tags: string[]; + priorityMin: number; + priorityMax: number; + urgencyMin: number; + urgencyMax: number; +} + interface TaskStore { tasks: Task[]; pWeight: number; @@ -32,6 +41,7 @@ interface TaskStore { matrixScaleDays: number; prefId?: string; // ID of the user_preferences record tagDefinitions?: Record; + filter: Filter; } // Initial empty state @@ -41,8 +51,43 @@ export const [store, setStore] = createStore({ uWeight: 1.0, matrixScaleDays: 30, tagDefinitions: {}, // Map + filter: { + query: "", + tags: [], + priorityMin: 1, + priorityMax: 10, + urgencyMin: 1, + urgencyMax: 10 + } }); +export const matchesFilter = (task: Task) => { + const f = store.filter; + + // Query search (title or content) + if (f.query) { + const q = f.query.toLowerCase(); + const inTitle = task.title.toLowerCase().includes(q); + const inContent = task.content?.toLowerCase().includes(q); + if (!inTitle && !inContent) return false; + } + + // Tags (OR logic if multiple selected?) + if (f.tags.length > 0) { + const hasTag = f.tags.some(tag => task.tags?.includes(tag)); + if (!hasTag) return false; + } + + // Priority + if (task.priority < f.priorityMin || task.priority > f.priorityMax) return false; + + // Urgency (calculated) + const urgency = calculateUrgencyFromDate(task.dueDate); + if (urgency < f.urgencyMin || urgency > f.urgencyMax) return false; + + return true; +}; + // Auto-persist changes to localStorage (wrapped in root to avoid console warning) createRoot(() => { createEffect(() => { @@ -527,6 +572,21 @@ export const renameTagDefinition = async (oldName: string, newName: string) => { }; +export const setFilter = (update: Partial) => { + setStore("filter", (f) => ({ ...f, ...update })); +}; + +export const clearFilter = () => { + setStore("filter", { + query: "", + tags: [], + priorityMin: 1, + priorityMax: 10, + urgencyMin: 1, + urgencyMax: 10 + }); +}; + // Legacy cleanup - We don't need local storage persistence setup anymore export const setupPersistence = () => { // Moved logic to initStore which is called on auth. diff --git a/src/views/CriticalView.tsx b/src/views/CriticalView.tsx index 54a20a8..8fae723 100644 --- a/src/views/CriticalView.tsx +++ b/src/views/CriticalView.tsx @@ -1,13 +1,15 @@ import { type Component, For, createMemo } from "solid-js"; -import { store, getCombinedScore } from "@/store"; +import { store, getCombinedScore, matchesFilter } from "@/store"; import { TaskCard } from "@/components/TaskCard"; export const CriticalView: Component = () => { const sortedTasks = createMemo(() => { - return [...store.tasks].filter(t => !t.deletedAt).sort((a, b) => { - if (a.completed !== b.completed) return a.completed ? 1 : -1; - return getCombinedScore(b) - getCombinedScore(a); - }); + return [...store.tasks] + .filter(t => !t.deletedAt && matchesFilter(t)) + .sort((a, b) => { + if (a.completed !== b.completed) return a.completed ? 1 : -1; + return getCombinedScore(b) - getCombinedScore(a); + }); }); return ( diff --git a/src/views/MatrixView.tsx b/src/views/MatrixView.tsx index 5236f04..296ef7f 100644 --- a/src/views/MatrixView.tsx +++ b/src/views/MatrixView.tsx @@ -1,10 +1,10 @@ import { type Component, For, createMemo } from "solid-js"; -import { store, calculateUrgencyScore, setActiveTaskId, setStore, getCombinedScore } from "@/store"; +import { store, calculateUrgencyScore, setActiveTaskId, setStore, getCombinedScore, matchesFilter } from "@/store"; import { cn } from "@/lib/utils"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; export const MatrixView: Component = () => { - const activeTasks = createMemo(() => store.tasks.filter(t => !t.completed && !t.deletedAt)); + const activeTasks = createMemo(() => store.tasks.filter(t => !t.completed && !t.deletedAt && matchesFilter(t))); const scaleOptions = ["1", "7", "30", "60", "90"]; @@ -15,12 +15,12 @@ export const MatrixView: Component = () => {

Urgency vs Priority mapping.

-
+
{/* Axis Labels */} -
- ← Soon -
- Time: +
+ ← Soon +
+ Time:
- Later → + Later →
-
+
← Low (Priority) High →
diff --git a/src/views/PriorityView.tsx b/src/views/PriorityView.tsx index 16c5ceb..4247a3f 100644 --- a/src/views/PriorityView.tsx +++ b/src/views/PriorityView.tsx @@ -1,14 +1,16 @@ import { type Component, For, createMemo } from "solid-js"; -import { store, getCombinedScore } from "@/store"; +import { store, getCombinedScore, matchesFilter } from "@/store"; import { TaskCard } from "@/components/TaskCard"; export const PriorityView: Component = () => { const sortedTasks = createMemo(() => { - return [...store.tasks].filter(t => !t.deletedAt).sort((a, b) => { - if (a.completed !== b.completed) return a.completed ? 1 : -1; - // Use combined score to account for priority, urgency, and tags - return getCombinedScore(b) - getCombinedScore(a); - }); + return [...store.tasks] + .filter(t => !t.deletedAt && matchesFilter(t)) + .sort((a, b) => { + if (a.completed !== b.completed) return a.completed ? 1 : -1; + // Use combined score to account for priority, urgency, and tags + return getCombinedScore(b) - getCombinedScore(a); + }); }); return ( diff --git a/src/views/SettingsView.tsx b/src/views/SettingsView.tsx index 4fc4a28..40ea289 100644 --- a/src/views/SettingsView.tsx +++ b/src/views/SettingsView.tsx @@ -12,7 +12,7 @@ import { createSignal } from "solid-js"; export const SettingsView: Component = () => { return ( -
+

Settings

diff --git a/src/views/UrgencyView.tsx b/src/views/UrgencyView.tsx index a20a466..33c33bd 100644 --- a/src/views/UrgencyView.tsx +++ b/src/views/UrgencyView.tsx @@ -1,17 +1,19 @@ import { type Component, For, createMemo } from "solid-js"; -import { store, calculateUrgencyScore, getCombinedScore } from "@/store"; +import { store, calculateUrgencyScore, getCombinedScore, matchesFilter } from "@/store"; import { TaskCard } from "@/components/TaskCard"; export const UrgencyView: Component = () => { const sortedTasks = createMemo(() => { - return [...store.tasks].filter(t => !t.deletedAt).sort((a, b) => { - if (a.completed !== b.completed) return a.completed ? 1 : -1; - const uA = calculateUrgencyScore(a.dueDate); - const uB = calculateUrgencyScore(b.dueDate); - if (uA !== uB) return uB - uA; - // Tie-break with combined score (Priority + Tags) - return getCombinedScore(b) - getCombinedScore(a); - }); + return [...store.tasks] + .filter(t => !t.deletedAt && matchesFilter(t)) + .sort((a, b) => { + if (a.completed !== b.completed) return a.completed ? 1 : -1; + const uA = calculateUrgencyScore(a.dueDate); + const uB = calculateUrgencyScore(b.dueDate); + if (uA !== uB) return uB - uA; + // Tie-break with combined score (Priority + Tags) + return getCombinedScore(b) - getCombinedScore(a); + }); }); return (