Partial Note Tabs Setup - NOT FOR PRODUCTION
This commit is contained in:
+120
-18
@@ -1,7 +1,7 @@
|
||||
import { type Component, createSignal, createMemo, For, Show } from "solid-js";
|
||||
import { store } from "@/store";
|
||||
import { type Component, createSignal, createMemo, For, Show, onCleanup, createEffect } from "solid-js";
|
||||
import { store, setStore, syncPreferences } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Plus, Lock, Search } from "lucide-solid";
|
||||
import { Plus, Lock, Search, Filter } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||
@@ -10,15 +10,66 @@ 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(() => {
|
||||
let n = store.notes.filter(note => !note.deletedAt);
|
||||
let n = store.notes.filter(note => {
|
||||
if (note.deletedAt) return false;
|
||||
// Hide child notes from the main list
|
||||
if (note.tags && note.tags.some(t => t.startsWith("child-of-"))) return false;
|
||||
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)));
|
||||
}
|
||||
if (store.noteFilter.ownedByMe) {
|
||||
n = n.filter(note => note.user === currentUserId);
|
||||
}
|
||||
if (store.noteFilter.tags.length > 0) {
|
||||
n = n.filter(note => store.noteFilter.tags.some(t => note.tags.includes(t)));
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
@@ -40,21 +91,72 @@ export const NotesSidebar: Component<{
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full bg-card">
|
||||
{/* 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 ref={containerRef} 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>
|
||||
<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>
|
||||
</div>
|
||||
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
|
||||
<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 */}
|
||||
|
||||
Reference in New Issue
Block a user