diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index 2736568..85e88f5 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -1,6 +1,6 @@ import { type Component, createSignal, For, Show } from "solid-js"; -import { Search, X, Hash, ArrowUpCircle, Clock, Minus } from "lucide-solid"; -import { store, setFilter, clearFilter } from "@/store"; +import { Search, X, Hash, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid"; +import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate } from "@/store"; import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; import { Badge } from "./ui/badge"; @@ -12,6 +12,8 @@ import { getThemeAdjustedColor } from "@/lib/colors"; export const FilterBar: Component = () => { const { resolvedTheme } = useTheme(); const [isOpen, setIsOpen] = createSignal(false); + const [newTemplateName, setNewTemplateName] = createSignal(""); + const [isSaving, setIsSaving] = createSignal(false); const hasActiveFilters = () => { const f = store.filter; @@ -251,6 +253,109 @@ export const FilterBar: Component = () => { + + {/* Filter Templates Section */} +
+
+ + + + +
+ + +
+ setNewTemplateName(e.currentTarget.value)} + class="flex-1 bg-muted/30 border border-primary/20 rounded-lg px-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30" + onKeyDown={(e) => { + if (e.key === 'Enter') { + if (newTemplateName().trim()) { + saveFilterTemplate(newTemplateName().trim()); + setNewTemplateName(""); + setIsSaving(false); + } + } else if (e.key === 'Escape') { + setIsSaving(false); + } + }} + /> + + +
+
+ +
+ +

No saved filters yet

+
+ }> + {(template) => ( +
+ + +
+ )} + +
+ diff --git a/src/store/index.ts b/src/store/index.ts index 69dc6d5..e6ab007 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -144,6 +144,13 @@ export interface Filter { editedToday: boolean; } +export interface FilterTemplate { + id: string; + name: string; + filter: Filter; +} + + export interface ShareRule { id: string; ownerId: string; @@ -162,6 +169,7 @@ interface TaskStore { templates: TaskTemplate[]; filter: Filter; shareRules: ShareRule[]; + filterTemplates: FilterTemplate[]; } // Initial empty state @@ -181,7 +189,8 @@ export const [store, setStore] = createStore({ urgencyMax: 10, editedToday: false }, - shareRules: [] + shareRules: [], + filterTemplates: [] }); export const matchesFilter = (task: Task) => { @@ -737,6 +746,9 @@ export const initStore = async () => { // 2.7. Fetch Share Rules (owned by current user) await loadShareRules(); + // 2.8 Fetch Filter Templates + await loadFilterTemplates(); + // 3. Subscribe to Realtime Updates await subscribeToRealtime(); @@ -1412,6 +1424,70 @@ export const removeShareRule = async (ruleId: string) => { } }; +// -- Filter Templates -- +const FILTER_TEMPLATES_COLLECTION = 'TasGrid_FilterTemplates'; + +export const loadFilterTemplates = async () => { + if (!pb.authStore.isValid) return; + try { + const records = await pb.collection(FILTER_TEMPLATES_COLLECTION).getFullList({ + filter: `user = "${pb.authStore.model?.id}"`, + requestKey: null + }); + const templates: FilterTemplate[] = records.map(r => ({ + id: r.id, + name: r.name, + filter: r.filter as Filter + })); + setStore("filterTemplates", templates); + } catch (err) { + console.error("Failed to load filter templates:", err); + } +}; + +export const saveFilterTemplate = async (name: string) => { + if (!pb.authStore.isValid) return; + try { + const record = await pb.collection(FILTER_TEMPLATES_COLLECTION).create({ + user: pb.authStore.model?.id, + name, + filter: store.filter + }, { requestKey: null }); + + const newTemplate: FilterTemplate = { + id: record.id, + name: record.name, + filter: record.filter as Filter + }; + + setStore("filterTemplates", prev => [...prev, newTemplate]); + toast.success(`Filter template "${name}" saved`); + } catch (err) { + console.error("Failed to save filter template:", err); + toast.error("Failed to save filter template."); + } +}; + +export const applyFilterTemplate = (id: string) => { + const template = store.filterTemplates.find(t => t.id === id); + if (template) { + setFilter(template.filter); + toast.success(`Applied filter: ${template.name}`); + } +}; + +export const removeFilterTemplate = async (id: string) => { + if (!pb.authStore.isValid) return; + try { + await pb.collection(FILTER_TEMPLATES_COLLECTION).delete(id); + setStore("filterTemplates", prev => prev.filter(t => t.id !== id)); + toast.success("Filter template deleted"); + } catch (err) { + console.error("Failed to delete filter template:", err); + toast.error("Failed to delete filter template."); + } +}; + export const clearFilter = () => { setStore("filter", { query: "",