Improved filtering and layout
This commit is contained in:
+122
-104
@@ -1,6 +1,6 @@
|
||||
import { type Component, createSignal, For, Show } from "solid-js";
|
||||
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
|
||||
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory } from "@/store";
|
||||
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory, setNoteFilter, clearNoteFilter, saveNoteFilterTemplate, applyNoteFilterTemplate, removeNoteFilterTemplate, syncPreferences, type Filter } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "./ui/badge";
|
||||
@@ -9,21 +9,34 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
|
||||
export const FilterBar: Component = () => {
|
||||
export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, triggerClass?: string }> = (props) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [newTemplateName, setNewTemplateName] = createSignal("");
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
||||
const mode = () => props.mode || 'tasks';
|
||||
|
||||
const currentFilter = () => mode() === 'tasks' ? store.filter : store.noteFilter;
|
||||
const currentSetFilter = (val: Partial<Filter>) => mode() === 'tasks' ? setFilter(val) : setNoteFilter(val);
|
||||
const currentClearFilter = () => mode() === 'tasks' ? clearFilter() : clearNoteFilter();
|
||||
const currentSaveTemplate = (name: string) => mode() === 'tasks' ? saveFilterTemplate(name) : saveNoteFilterTemplate(name);
|
||||
const currentApplyTemplate = (id: string) => mode() === 'tasks' ? applyFilterTemplate(id) : applyNoteFilterTemplate(id);
|
||||
const currentRemoveTemplate = (id: string) => mode() === 'tasks' ? removeFilterTemplate(id) : removeNoteFilterTemplate(id);
|
||||
|
||||
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 || f.editedToday;
|
||||
const f = currentFilter();
|
||||
const baseActive = (f.query || "") !== "" || (f.tags || []).length > 0 || f.editedToday;
|
||||
if (mode() === 'tasks') {
|
||||
return baseActive || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10;
|
||||
}
|
||||
return baseActive;
|
||||
};
|
||||
|
||||
const allTags = () => store.tagDefinitions.map(d => d.name).sort();
|
||||
|
||||
return (
|
||||
<div class="fixed top-4 right-5 sm:right-6 z-[60]">
|
||||
<div class={cn(mode() === 'tasks' ? "fixed top-4 right-5 sm:right-6 z-[60]" : "relative", props.class)}>
|
||||
<Popover open={isOpen()} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger
|
||||
as={Button}
|
||||
@@ -32,7 +45,8 @@ export const FilterBar: Component = () => {
|
||||
class={cn(
|
||||
"bg-background/80 backdrop-blur-sm border border-border hover:bg-muted shadow-sm transition-all duration-300",
|
||||
hasActiveFilters() && "border-primary/50 ring-1 ring-primary/20",
|
||||
isOpen() && "bg-muted ring-1 ring-border"
|
||||
isOpen() && "bg-muted ring-1 ring-border",
|
||||
props.triggerClass
|
||||
)}
|
||||
title="Filters & Search"
|
||||
>
|
||||
@@ -43,7 +57,7 @@ export const FilterBar: Component = () => {
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[calc(100vw-2rem)] sm:w-[480px] p-0 overflow-hidden bg-card border-border shadow-2xl rounded-2xl animate-in fade-in zoom-in-95 duration-200">
|
||||
<PopoverContent class="w-[calc(100vw-2rem)] sm:w-[480px] p-0 overflow-hidden bg-card border-border shadow-2xl rounded-2xl animate-in fade-in zoom-in-95 duration-200 z-[100]">
|
||||
{/* Header */}
|
||||
<div class="px-4 py-3 border-b border-border/50 bg-muted/20 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -53,37 +67,39 @@ export const FilterBar: Component = () => {
|
||||
</h3>
|
||||
<div class="h-4 w-px bg-border/50" />
|
||||
<button
|
||||
onClick={() => setFilter({ editedToday: !store.filter.editedToday })}
|
||||
onClick={() => currentSetFilter({ editedToday: !currentFilter().editedToday })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
store.filter.editedToday
|
||||
currentFilter().editedToday
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", store.filter.editedToday ? "bg-primary" : "bg-muted-foreground/30")} />
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", currentFilter().editedToday ? "bg-primary" : "bg-muted-foreground/30")} />
|
||||
Edited Today
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-primary"
|
||||
onClick={() => {
|
||||
loadAllHistory();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
title="Load all completed tasks"
|
||||
>
|
||||
Load All
|
||||
</Button>
|
||||
<Show when={mode() === 'tasks'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-primary"
|
||||
onClick={() => {
|
||||
loadAllHistory();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
title="Load all completed tasks"
|
||||
>
|
||||
Load All
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={hasActiveFilters()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-destructive"
|
||||
onClick={clearFilter}
|
||||
onClick={currentClearFilter}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
@@ -110,9 +126,9 @@ export const FilterBar: Component = () => {
|
||||
if (el) setTimeout(() => el.focus(), 50);
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Search tasks, descriptions, tags..."
|
||||
value={store.filter.query}
|
||||
onInput={(e) => setFilter({ query: e.currentTarget.value })}
|
||||
placeholder={mode() === 'tasks' ? "Search tasks, descriptions, tags..." : "Search notes, tags..."}
|
||||
value={currentFilter().query}
|
||||
onInput={(e) => currentSetFilter({ query: e.currentTarget.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -124,77 +140,79 @@ export const FilterBar: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Priority Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Priority Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.priorityMin.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<ArrowUpCircle size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.priorityMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.priorityMax.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.priorityMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<Show when={mode() === 'tasks'}>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Priority Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Priority Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.priorityMin.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<ArrowUpCircle size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.priorityMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.priorityMax.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.priorityMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Urgency Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Urgency Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.urgencyMin.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<Clock size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.urgencyMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.urgencyMax.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.urgencyMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
{/* Urgency Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Urgency Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.urgencyMin.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<Clock size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.urgencyMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.urgencyMax.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.urgencyMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Tags Multi-select */}
|
||||
<div class="space-y-2.5">
|
||||
@@ -206,7 +224,7 @@ export const FilterBar: Component = () => {
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2 min-h-[44px] p-2 bg-muted/10 border border-dashed border-border/60 rounded-xl">
|
||||
<For each={store.filter.tags}>
|
||||
<For each={currentFilter().tags}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
@@ -229,10 +247,10 @@ export const FilterBar: Component = () => {
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer h-full"
|
||||
onClick={() => {
|
||||
const newTags = store.filter.tags.map(t =>
|
||||
const newTags = currentFilter().tags.map(t =>
|
||||
t.name === tag.name ? { ...t, excluded: !t.excluded } : t
|
||||
);
|
||||
setFilter({ tags: newTags });
|
||||
currentSetFilter({ tags: newTags });
|
||||
}}
|
||||
>
|
||||
<Show when={tag.excluded} fallback={<Tag size={12} class="opacity-70" />}>
|
||||
@@ -244,7 +262,7 @@ export const FilterBar: Component = () => {
|
||||
class="p-0.5 hover:bg-foreground/10 rounded transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFilter({ tags: store.filter.tags.filter(t => t.name !== tag.name) });
|
||||
currentSetFilter({ tags: currentFilter().tags.filter(t => t.name !== tag.name) });
|
||||
}}
|
||||
>
|
||||
<X size={10} />
|
||||
@@ -255,8 +273,8 @@ export const FilterBar: Component = () => {
|
||||
<Select
|
||||
value=""
|
||||
onChange={(val) => {
|
||||
if (val && !store.filter.tags.some(t => t.name === val)) {
|
||||
setFilter({ tags: [...store.filter.tags, { name: val, excluded: false }] });
|
||||
if (val && !currentFilter().tags.some(t => t.name === val)) {
|
||||
currentSetFilter({ tags: [...currentFilter().tags, { name: val, excluded: false }] });
|
||||
}
|
||||
}}
|
||||
options={allTags()}
|
||||
@@ -301,7 +319,7 @@ export const FilterBar: Component = () => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (newTemplateName().trim()) {
|
||||
saveFilterTemplate(newTemplateName().trim());
|
||||
currentSaveTemplate(newTemplateName().trim());
|
||||
setNewTemplateName("");
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -315,7 +333,7 @@ export const FilterBar: Component = () => {
|
||||
class="h-8 px-3"
|
||||
disabled={!newTemplateName().trim()}
|
||||
onClick={() => {
|
||||
saveFilterTemplate(newTemplateName().trim());
|
||||
currentSaveTemplate(newTemplateName().trim());
|
||||
setNewTemplateName("");
|
||||
setIsSaving(false);
|
||||
}}
|
||||
@@ -343,7 +361,7 @@ export const FilterBar: Component = () => {
|
||||
<div class="group flex items-center justify-between p-2 rounded-xl bg-muted/20 border border-border/30 hover:bg-muted/40 transition-all">
|
||||
<button
|
||||
class="flex-1 text-left px-1 min-w-0"
|
||||
onClick={() => applyFilterTemplate(template.id)}
|
||||
onClick={() => currentApplyTemplate(template.id)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[0.6875rem] font-bold text-foreground truncate">{template.name}</p>
|
||||
@@ -354,7 +372,7 @@ export const FilterBar: Component = () => {
|
||||
<Show when={template.filter.tags.length > 0}>
|
||||
<div class="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
</Show>
|
||||
<Show when={template.filter.priorityMin > 1 || template.filter.priorityMax < 10}>
|
||||
<Show when={mode() === 'tasks' && (template.filter.priorityMin > 1 || template.filter.priorityMax < 10)}>
|
||||
<div class="w-1 h-1 rounded-full bg-amber-500" />
|
||||
</Show>
|
||||
</div>
|
||||
@@ -364,7 +382,7 @@ export const FilterBar: Component = () => {
|
||||
class="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFilterTemplate(template.id);
|
||||
currentRemoveTemplate(template.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
|
||||
+42
-110
@@ -1,58 +1,20 @@
|
||||
import { type Component, createSignal, createMemo, For, Show, onCleanup, createEffect } from "solid-js";
|
||||
import { store, setStore, syncPreferences } from "@/store";
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { store } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Plus, Lock, Search, Filter } from "lucide-solid";
|
||||
import { Plus, Lock } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
|
||||
export const NotesSidebar: Component<{
|
||||
selectedNoteId: string | null;
|
||||
setSelectedNoteId: (id: string | null) => void;
|
||||
}> = (props) => {
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
const [searchQuery, setSearchQuery] = createSignal("");
|
||||
const [showFilters, setShowFilters] = createSignal(false);
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
|
||||
createEffect(() => {
|
||||
if (!showFilters()) return;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef && !containerRef.contains(e.target as Node)) {
|
||||
setShowFilters(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
|
||||
});
|
||||
|
||||
const availableTags = createMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
store.notes.forEach(n => {
|
||||
if (!n.deletedAt) n.tags.forEach(t => tags.add(t));
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
});
|
||||
|
||||
const toggleTagFilter = (tag: string) => {
|
||||
const current = store.noteFilter.tags;
|
||||
const next = current.includes(tag) ? current.filter(t => t !== tag) : [...current, tag];
|
||||
setStore("noteFilter", "tags", next);
|
||||
syncPreferences();
|
||||
};
|
||||
|
||||
const toggleOwnedByMe = () => {
|
||||
setStore("noteFilter", "ownedByMe", !store.noteFilter.ownedByMe);
|
||||
syncPreferences();
|
||||
};
|
||||
|
||||
const isFilterActive = createMemo(() => {
|
||||
return store.noteFilter.ownedByMe || store.noteFilter.tags.length > 0;
|
||||
});
|
||||
|
||||
const filteredNotes = createMemo(() => {
|
||||
const filter = store.noteFilter;
|
||||
let n = store.notes.filter(note => {
|
||||
if (note.deletedAt) return false;
|
||||
// Hide child notes from the main list
|
||||
@@ -60,16 +22,38 @@ export const NotesSidebar: Component<{
|
||||
return true;
|
||||
});
|
||||
|
||||
if (searchQuery()) {
|
||||
const q = searchQuery().toLowerCase();
|
||||
n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q)));
|
||||
// Query Filter
|
||||
if (filter.query) {
|
||||
const q = filter.query.toLowerCase();
|
||||
n = n.filter(note =>
|
||||
note.title.toLowerCase().includes(q) ||
|
||||
note.tags.some(t => t.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
if (store.noteFilter.ownedByMe) {
|
||||
n = n.filter(note => note.user === currentUserId);
|
||||
|
||||
// Tags Filter
|
||||
if (filter.tags.length > 0) {
|
||||
const required = filter.tags.filter(t => !t.excluded).map(t => t.name);
|
||||
const excluded = filter.tags.filter(t => t.excluded).map(t => t.name);
|
||||
|
||||
if (required.length > 0) {
|
||||
n = n.filter(note => required.some(r => note.tags.includes(r)));
|
||||
}
|
||||
if (excluded.length > 0) {
|
||||
n = n.filter(note => !excluded.some(e => note.tags.includes(e)));
|
||||
}
|
||||
}
|
||||
if (store.noteFilter.tags.length > 0) {
|
||||
n = n.filter(note => store.noteFilter.tags.some(t => note.tags.includes(t)));
|
||||
|
||||
// Edited Today Filter
|
||||
if (filter.editedToday) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
n = n.filter(note => {
|
||||
const updated = new Date(note.updated);
|
||||
return updated >= today;
|
||||
});
|
||||
}
|
||||
|
||||
return n;
|
||||
});
|
||||
|
||||
@@ -91,72 +75,20 @@ export const NotesSidebar: Component<{
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full bg-card">
|
||||
<div ref={containerRef} class="z-20">
|
||||
<div class="z-20">
|
||||
{/* Header */}
|
||||
<div class="p-4 border-b border-border bg-card/50 backdrop-blur flex justify-between items-center gap-2">
|
||||
<div class="relative flex-1">
|
||||
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search notes..."
|
||||
value={searchQuery()}
|
||||
onInput={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
class="w-full bg-muted/50 border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30"
|
||||
<div class="flex-1">
|
||||
<FilterBar
|
||||
mode="notes"
|
||||
class="relative top-0 right-0 z-0"
|
||||
triggerClass="w-full justify-start gap-2 px-3 h-9 bg-muted/40 hover:bg-muted/60 border-border/40"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => setShowFilters(!showFilters())} size="sm" variant={showFilters() ? "secondary" : "ghost"} class="h-8 w-8 p-0 shrink-0 relative">
|
||||
<Filter size={16} />
|
||||
<Show when={isFilterActive()}>
|
||||
<span class="absolute top-1 right-1 w-2 h-2 bg-primary rounded-full border-2 border-background animate-in fade-in zoom-in duration-300" />
|
||||
</Show>
|
||||
</Button>
|
||||
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
|
||||
<Plus size={16} />
|
||||
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-9 w-9 p-0 shrink-0 shadow-sm rounded-xl">
|
||||
<Plus size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={showFilters()}>
|
||||
<div class="p-4 border-b border-border bg-muted/20 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-foreground">Owned by me</label>
|
||||
<button
|
||||
onClick={toggleOwnedByMe}
|
||||
class={cn(
|
||||
"w-9 h-5 rounded-full transition-colors relative",
|
||||
store.noteFilter.ownedByMe ? "bg-primary" : "bg-muted-foreground/30"
|
||||
)}
|
||||
>
|
||||
<div class={cn(
|
||||
"absolute top-0.5 left-0.5 w-4 h-4 bg-background rounded-full transition-transform shadow-sm",
|
||||
store.noteFilter.ownedByMe ? "translate-x-4" : "translate-x-0"
|
||||
)}></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-foreground">Tags (Any)</label>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={availableTags()} fallback={<span class="text-xs text-muted-foreground">No tags found</span>}>
|
||||
{(tag) => {
|
||||
const isSelected = store.noteFilter.tags.includes(tag);
|
||||
return (
|
||||
<button
|
||||
onClick={() => toggleTagFilter(tag)}
|
||||
class={cn(
|
||||
"text-xs px-2 py-1 rounded-md border transition-colors",
|
||||
isSelected
|
||||
? "bg-primary/10 border-primary/50 text-primary font-medium"
|
||||
: "bg-card border-border hover:bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
|
||||
+99
-3
@@ -269,7 +269,7 @@ interface TaskStore {
|
||||
notes: Note[];
|
||||
isNotepadMode: boolean;
|
||||
quickloadTasks: string[];
|
||||
noteFilter: { tags: string[]; ownedByMe: boolean };
|
||||
noteFilter: Filter;
|
||||
}
|
||||
|
||||
// Initial empty state
|
||||
@@ -297,7 +297,15 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
notes: [],
|
||||
isNotepadMode: false,
|
||||
quickloadTasks: [],
|
||||
noteFilter: { tags: [], ownedByMe: false }
|
||||
noteFilter: {
|
||||
query: "",
|
||||
tags: [],
|
||||
priorityMin: 1,
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
}
|
||||
});
|
||||
|
||||
export const matchesFilter = (task: Task) => {
|
||||
@@ -1138,7 +1146,15 @@ export const initStore = async () => {
|
||||
prefId: userId,
|
||||
subscribedBuckets: user.subscribedBuckets || [],
|
||||
quickloadTasks: prefs.quickloadTasks || [],
|
||||
noteFilter: prefs.noteFilter || { tags: [], ownedByMe: false }
|
||||
noteFilter: prefs.noteFilter || {
|
||||
query: "",
|
||||
tags: [],
|
||||
priorityMin: 1,
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (prefErr) {
|
||||
@@ -2692,6 +2708,86 @@ export const clearFilter = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const setNoteFilter = (update: Partial<Filter>) => {
|
||||
setStore("noteFilter", (f) => ({ ...f, ...update }));
|
||||
};
|
||||
|
||||
const NOTE_FILTER_TEMPLATES_COLLECTION = 'TasGrid_NoteFilterTemplates';
|
||||
|
||||
export const loadNoteFilterTemplates = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
try {
|
||||
const records = await pb.collection(NOTE_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 note filter templates:", err);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveNoteFilterTemplate = async (name: string) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
try {
|
||||
const record = await pb.collection(NOTE_FILTER_TEMPLATES_COLLECTION).create({
|
||||
user: pb.authStore.model?.id,
|
||||
name,
|
||||
filter: store.noteFilter
|
||||
}, { requestKey: null });
|
||||
|
||||
const newTemplate: FilterTemplate = {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
filter: record.filter as Filter
|
||||
};
|
||||
|
||||
setStore("filterTemplates", prev => [...prev, newTemplate]);
|
||||
toast.success(`Note filter template "${name}" saved`);
|
||||
} catch (err) {
|
||||
console.error("Failed to save note filter template:", err);
|
||||
toast.error("Failed to save note filter template.");
|
||||
}
|
||||
};
|
||||
|
||||
export const applyNoteFilterTemplate = (id: string) => {
|
||||
const template = store.filterTemplates.find(t => t.id === id);
|
||||
if (template) {
|
||||
setNoteFilter(template.filter);
|
||||
toast.success(`Applied note filter: ${template.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeNoteFilterTemplate = async (id: string) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
try {
|
||||
await pb.collection(NOTE_FILTER_TEMPLATES_COLLECTION).delete(id);
|
||||
setStore("filterTemplates", prev => prev.filter(t => t.id !== id));
|
||||
toast.success("Note filter template deleted");
|
||||
} catch (err) {
|
||||
console.error("Failed to delete note filter template:", err);
|
||||
toast.error("Failed to delete note filter template.");
|
||||
}
|
||||
};
|
||||
|
||||
export const clearNoteFilter = () => {
|
||||
setStore("noteFilter", {
|
||||
query: "",
|
||||
tags: [],
|
||||
priorityMin: 1,
|
||||
priorityMax: 10,
|
||||
urgencyMin: 1,
|
||||
urgencyMax: 10,
|
||||
editedToday: false
|
||||
});
|
||||
syncPreferences();
|
||||
};
|
||||
|
||||
// Legacy cleanup - We don't need local storage persistence setup anymore
|
||||
export const setupPersistence = () => {
|
||||
// Moved logic to initStore which is called on auth.
|
||||
|
||||
@@ -19,7 +19,7 @@ export const NotepadView: Component<{
|
||||
}> = (props) => {
|
||||
const [isLinking, setIsLinking] = createSignal(false);
|
||||
const [linkSearchQuery, setLinkSearchQuery] = createSignal("");
|
||||
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true);
|
||||
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(false);
|
||||
const [isDragging, setIsDragging] = createSignal(false);
|
||||
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
@@ -307,7 +307,7 @@ export const NotepadView: Component<{
|
||||
{/* Note Editor (Main Panel) */}
|
||||
<div class={cn(
|
||||
"flex-1 min-w-0 transition-all duration-200 ease-out h-full overflow-y-auto md:overflow-visible absolute md:relative inset-0 z-20 md:z-0",
|
||||
!props.selectedNoteId ? "opacity-0 pointer-events-none md:pointer-events-auto md:opacity-100 translate-x-8 md:translate-x-0 md:flex items-center justify-center bg-card md:bg-muted/5 md:border md:border-border md:rounded-xl md:shadow-sm overflow-hidden" : "opacity-100 translate-x-0 flex flex-col md:flex-row bg-transparent"
|
||||
!props.selectedNoteId ? "opacity-0 pointer-events-none md:pointer-events-auto md:opacity-100 translate-x-8 md:translate-x-0 md:flex items-center justify-center bg-card md:bg-muted/5 md:border md:border-border md:rounded-xl md:shadow-sm overflow-hidden" : "opacity-100 translate-x-0 flex flex-col md:flex-row bg-transparent md:justify-center"
|
||||
)}>
|
||||
<Show when={activeNote()} fallback={
|
||||
<div class="text-center space-y-6 opacity-40 select-none flex flex-col items-center justify-center h-full">
|
||||
@@ -382,10 +382,10 @@ export const NotepadView: Component<{
|
||||
</div>
|
||||
|
||||
{/* Editor Area Container */}
|
||||
<div class="flex-1 flex flex-col md:flex-row min-w-0 md:h-full md:bg-card md:border md:border-border md:rounded-xl md:shadow-[0_4px_12px_rgba(0,0,0,0.02)] overflow-hidden z-20 relative shadow-[-4px_0_12px_rgba(0,0,0,0.02)]">
|
||||
<div class="flex-1 md:flex-initial md:shrink w-full md:w-auto flex flex-col md:flex-row min-w-0 md:h-full md:bg-card md:border md:border-border md:rounded-xl md:shadow-[0_4px_12px_rgba(0,0,0,0.02)] overflow-hidden z-20 relative shadow-[-4px_0_12px_rgba(0,0,0,0.02)] transition-all duration-300">
|
||||
|
||||
{/* Editor Area */}
|
||||
<div class="flex-1 flex flex-col min-w-0 md:h-full overflow-visible md:overflow-y-auto relative px-4 md:px-6 pb-4 md:pb-6 space-y-4 md:space-y-6 scroll-smooth pt-0 bg-background z-20">
|
||||
<div class="flex-1 md:flex-initial md:shrink flex flex-col min-w-0 md:w-[960px] lg:w-[1120px] md:h-full overflow-visible md:overflow-y-auto relative px-4 md:px-6 pb-4 md:pb-6 space-y-4 md:space-y-6 scroll-smooth pt-0 bg-background z-20">
|
||||
|
||||
|
||||
<div class="space-y-4 sticky top-0 z-30 bg-card/95 backdrop-blur-sm pt-4 pb-2 -mx-4 px-4 md:pt-6 md:-mx-6 md:px-6 md:pb-2">
|
||||
|
||||
Reference in New Issue
Block a user