102 lines
4.8 KiB
TypeScript
102 lines
4.8 KiB
TypeScript
import { type Component, createSignal, createMemo, For, Show } from "solid-js";
|
|
import { store } from "@/store";
|
|
import { pb } from "@/lib/pocketbase";
|
|
import { Plus, Lock, Search } from "lucide-solid";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { NOTES_COLLECTION } from "@/lib/constants";
|
|
|
|
export const NotesSidebar: Component<{
|
|
selectedNoteId: string | null;
|
|
setSelectedNoteId: (id: string | null) => void;
|
|
}> = (props) => {
|
|
const [searchQuery, setSearchQuery] = createSignal("");
|
|
const currentUserId = pb.authStore.model?.id;
|
|
|
|
const filteredNotes = createMemo(() => {
|
|
let n = store.notes.filter(note => !note.deletedAt);
|
|
if (searchQuery()) {
|
|
const q = searchQuery().toLowerCase();
|
|
n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q)));
|
|
}
|
|
return n;
|
|
});
|
|
|
|
const handleCreateNote = async () => {
|
|
if (!currentUserId) return;
|
|
try {
|
|
const result = await pb.collection(NOTES_COLLECTION).create({
|
|
title: "New Note",
|
|
content: "",
|
|
tags: [],
|
|
isPrivate: false,
|
|
user: currentUserId,
|
|
});
|
|
props.setSelectedNoteId(result.id);
|
|
} catch (e) {
|
|
console.error("Failed to create note", e);
|
|
}
|
|
};
|
|
|
|
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>
|
|
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
|
|
<Plus size={16} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* List */}
|
|
<div class="flex-1 overflow-y-auto p-2 space-y-1 pb-20 md:pb-2">
|
|
<For each={filteredNotes()} fallback={<div class="p-4 text-center text-xs text-muted-foreground">No notes found.</div>}>
|
|
{(note) => (
|
|
<button
|
|
onClick={() => props.setSelectedNoteId(note.id)}
|
|
class={cn(
|
|
"w-full text-left p-3 rounded-xl transition-all border",
|
|
props.selectedNoteId === note.id
|
|
? "bg-primary/5 border-primary/20 shadow-sm"
|
|
: "bg-card border-transparent hover:border-border hover:bg-muted/50"
|
|
)}
|
|
>
|
|
<div class="font-semibold text-sm truncate flex items-center justify-between">
|
|
<span>{note.title || "Untitled"}</span>
|
|
<Show when={note.isPrivate}>
|
|
<Lock size={12} class="text-orange-500 shrink-0 ml-2" />
|
|
</Show>
|
|
</div>
|
|
<Show when={note.tags && note.tags.length > 0}>
|
|
<div class="flex gap-1 mt-2 flex-wrap">
|
|
<For each={note.tags.slice(0, 3)}>
|
|
{(t) => (
|
|
<span class="text-[0.6rem] px-1.5 py-0.5 bg-muted rounded-md text-muted-foreground whitespace-nowrap border border-border/50">
|
|
{t}
|
|
</span>
|
|
)}
|
|
</For>
|
|
<Show when={note.tags.length > 3}>
|
|
<span class="text-[0.6rem] px-1.5 py-0.5 text-muted-foreground whitespace-nowrap shrink-0">
|
|
+{note.tags.length - 3}
|
|
</span>
|
|
</Show>
|
|
</div>
|
|
</Show>
|
|
</button>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|