Compare commits

..

6 Commits

Author SHA1 Message Date
3003066a5a Fix sidebar note count bug 2026-01-23 12:33:07 +00:00
4a485255b0 Implement note delete 2026-01-23 12:31:00 +00:00
211dbb72e0 Added sidebar search functionality 2026-01-23 12:05:30 +00:00
b5ac1df4e3 Temp(?) note refactors 2026-01-23 11:53:57 +00:00
b5a84c076d Note editor/Sidebar styling tweaks 2026-01-23 11:17:46 +00:00
77d3e09379 Remove AppHeader 2026-01-23 10:56:27 +00:00
8 changed files with 88 additions and 48 deletions

View File

@@ -1,11 +0,0 @@
export default function AppHeader() {
return (
<header className="absolute flex items-center justify-between px-4 py-2 border-b bg-background">
<div className="flex items-center gap-2 text-muted-foreground">
<Book className="h-5 w-5" />
</div>
</header>
)
}

View File

@@ -1,6 +1,6 @@
import { Check, Plus, Sun, Moon } from "lucide-react" import { Check, Plus, Sun, Moon } from "lucide-react"
import { Sidebar, SidebarContent, SidebarHeader, SidebarFooter, SidebarTrigger } from "@/components/ui/sidebar" import { Sidebar, SidebarContent, SidebarHeader, SidebarFooter } from "@/components/ui/sidebar"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -10,11 +10,29 @@ import ListItem from "./ListItem"
import { useNote } from "../contexts/ActiveNoteContext" import { useNote } from "../contexts/ActiveNoteContext"
import { useNotesStore } from "../contexts/NotesStore" import { useNotesStore } from "../contexts/NotesStore"
import { useTheme } from "../contexts/ThemeContext" import { useTheme } from "../contexts/ThemeContext"
import { ChangeEvent, ReactNode, useState } from "react"
function AppSidebar() { function AppSidebar() {
const { notes, createNote } = useNotesStore() const { notes, createNote } = useNotesStore()
const { setCurrentNote } = useNote() const { setCurrentNoteId } = useNote()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const [ filter, setFilter ] = useState("")
const buildNoteListItems = () => {
const out: ReactNode[] = []
notes.forEach((note, key, _) => {
if (filter && !note.title.toLowerCase().includes(filter.toLowerCase())) return;
out.push(
<ListItem
key={key}
id={note.id}
label={note.title}
onSelect={() => setCurrentNoteId(note.id)}
/>
)
})
return out;
}
return ( return (
<Sidebar> <Sidebar>
@@ -26,20 +44,17 @@ function AppSidebar() {
{theme === "dark" ? <Sun /> : <Moon />} {theme === "dark" ? <Sun /> : <Moon />}
</Button> </Button>
</div> </div>
<Input placeholder="Search Notes" className="shadow-none bg-background" /> <Input
placeholder="Search Notes"
className="shadow-none bg-background"
onChange={(e) => setFilter(e.target.value)}
/>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<ScrollArea className="h-full px-2 py-2"> <ScrollArea className="h-full px-2 py-2">
<div className="space-y-1 py-2"> <div className="space-y-1 py-2">
{notes.map((note) => ( {buildNoteListItems()}
<ListItem
key={note.id}
id={note.id}
label={note.title}
onSelect={() => setCurrentNote(note)}
/>
))}
</div> </div>
<Button <Button
@@ -57,7 +72,7 @@ function AppSidebar() {
<div className="flex items-center gap-3 rounded-md px-2 py-1.5"> <div className="flex items-center gap-3 rounded-md px-2 py-1.5">
<div className="flex flex-col leading-tight gap-2"> <div className="flex flex-col leading-tight gap-2">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{notes.length} note{notes.length !== 1 && "s"} {notes.size || 0} note{notes.size !== 1 && "s"}
</span> </span>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-foreground"> <div className="flex h-4 w-4 items-center justify-center rounded-full bg-foreground">

View File

@@ -5,10 +5,12 @@ import {
useRef, useRef,
useEffect, useEffect,
} from "react" } from "react"
import { FileText } from "lucide-react"
import { FileText, X } from "lucide-react"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useNotesStore } from "../contexts/NotesStore" import { useNotesStore } from "../contexts/NotesStore"
import { useNote } from "@/contexts/ActiveNoteContext"
type ListItemProps = { type ListItemProps = {
id: string id: string
@@ -17,7 +19,8 @@ type ListItemProps = {
} }
function ListItem({ id, label, onSelect }: ListItemProps) { function ListItem({ id, label, onSelect }: ListItemProps) {
const { updateNote } = useNotesStore() const { updateNote, deleteNote } = useNotesStore();
const { currentNoteId, setCurrentNoteId } = useNote();
const [tempLabel, setTempLabel] = useState(label) const [tempLabel, setTempLabel] = useState(label)
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const containerRef = useRef<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
@@ -45,6 +48,14 @@ function ListItem({ id, label, onSelect }: ListItemProps) {
setIsEditing(false) setIsEditing(false)
} }
const handleDelete = () => {
if (currentNoteId === id) {
setCurrentNoteId(null)
}
deleteNote(id)
}
useEffect(() => { useEffect(() => {
if (!isEditing) return if (!isEditing) return
@@ -70,10 +81,10 @@ function ListItem({ id, label, onSelect }: ListItemProps) {
className={cn( className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer", "flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer",
"text-muted-foreground hover:bg-muted hover:text-foreground", "text-muted-foreground hover:bg-muted hover:text-foreground",
"transition-colors" "transition-colors listItem"
)} )}
> >
<FileText className="h-4 w-4 shrink-0" /> <FileText className="h-3.5 w-3.5" />
{isEditing ? ( {isEditing ? (
<Input <Input
@@ -84,7 +95,15 @@ function ListItem({ id, label, onSelect }: ListItemProps) {
className="h-7 px-2 text-sm" className="h-7 px-2 text-sm"
/> />
) : ( ) : (
<span className="truncate text-sm">{label}</span> <>
<span className="truncate text-sm">{label}</span>
<div
className="opacity-0 listItem-delete h-4 w-4 items-center justify-center rounded-full bg-muted-foreground p-0.5 ml-auto"
onClick={handleDelete}
>
<X className="h-3 w-3 text-background" />
</div>
</>
)} )}
</div> </div>
) )

View File

@@ -4,24 +4,29 @@ import { BlockNoteView } from "@blocknote/shadcn";
import { useCreateBlockNote } from "@blocknote/react" import { useCreateBlockNote } from "@blocknote/react"
import { useNote } from "../contexts/ActiveNoteContext" import { useNote } from "../contexts/ActiveNoteContext"
import { useTheme } from "../contexts/ThemeContext"; import { useTheme } from "../contexts/ThemeContext";
import { useNotesStore } from "@/contexts/NotesStore";
import { Note } from "@/models/Note";
function NoteEditor() { function NoteEditor() {
const { currentNote } = useNote(); const { notes } = useNotesStore()
const { currentNoteId } = useNote();
const { theme } = useTheme(); const { theme } = useTheme();
const currentNote: Note = notes.get(currentNoteId)
const editor = useCreateBlockNote({ const editor = useCreateBlockNote({
initialContent: currentNote?.body initialContent: currentNote?.body
}); });
if (!currentNote) return ( if (!currentNoteId) return (
<div className="p-6"> <div className="p-6">
<p>Select a note</p>
</div> </div>
) )
return ( return (
<div className="p-6"> <div className="p-6 w-full max-w-7/10 mx-auto">
<h1>{ currentNote.title }</h1> <div className="h-16" role="spacer"></div>
<h1 className="text-4xl font-medium mb-4">{ currentNote?.title }</h1>
<BlockNoteView editor={editor as any} theme={theme} /> <BlockNoteView editor={editor as any} theme={theme} />
</div> </div>
) )

View File

@@ -267,7 +267,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn("size-7", className)} className={cn("size-10 top-2 left-2", className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event)
toggleSidebar() toggleSidebar()

View File

@@ -1,18 +1,17 @@
import { createContext, Dispatch, SetStateAction, ReactNode, useContext, useState } from "react"; import { createContext, Dispatch, SetStateAction, ReactNode, useContext, useState } from "react";
import { Note } from "../models/Note";
type ActiveNoteType = { type ActiveNoteType = {
currentNote: Note | null; currentNoteId: string | null;
setCurrentNote: Dispatch<SetStateAction<Note | null>>; setCurrentNoteId: Dispatch<SetStateAction<string | null>>;
} }
const ActiveNote = createContext<ActiveNoteType | null>(null); const ActiveNote = createContext<ActiveNoteType | null>(null);
export function NoteProvider({ children }: { children: ReactNode }) { export function NoteProvider({ children }: { children: ReactNode }) {
const [currentNote, setCurrentNote] = useState<Note | null>(null); const [currentNoteId, setCurrentNoteId] = useState<string | null>(null);
return ( return (
<ActiveNote.Provider value={{ currentNote, setCurrentNote }}> <ActiveNote.Provider value={{ currentNoteId, setCurrentNoteId }}>
{children} {children}
</ActiveNote.Provider> </ActiveNote.Provider>
); );

View File

@@ -1,8 +1,8 @@
import { createContext, useContext, useState, ReactNode, useEffect } from "react"; import { createContext, useContext, useState, ReactNode } from "react";
import { Note } from "../models/Note" import { Note } from "../models/Note"
type NotesStoreType = { type NotesStoreType = {
notes: Note[]; notes: Map<string, Note>;
createNote: () => void; createNote: () => void;
updateNote: (noteId: string, changeset: Partial<Note>) => void; updateNote: (noteId: string, changeset: Partial<Note>) => void;
deleteNote: (noteId: string) => void; deleteNote: (noteId: string) => void;
@@ -12,26 +12,28 @@ type NotesStoreType = {
const NotesStore = createContext<NotesStoreType | null>(null); const NotesStore = createContext<NotesStoreType | null>(null);
export function NotesStoreProvider({ children }: { children: ReactNode }) { export function NotesStoreProvider({ children }: { children: ReactNode }) {
const [notes, setNotes] = useState<Note[]>([]); const [notes, setNotes] = useState<Map<string, Note>>(new Map<string, Note>());
const createNote = () => { const createNote = () => {
const newNote = new Note(); const newNote = new Note();
setNotes([...notes, newNote]); setNotes(prev => new Map(prev.set(newNote.id, newNote)));
return newNote; return newNote;
} }
const updateNote = (noteId: string, changeset: Partial<Note>) => { const updateNote = (noteId: string, changeset: Partial<Note>) => {
setNotes(notes.map(note => const noteToUpdate = notes.get(noteId)
note.id === noteId ? new Note({ ...note, ...changeset }) : note const updatedNote = new Note({ ...noteToUpdate, ...changeset })
))
setNotes(prev => new Map(prev.set(noteId, updatedNote)));
} }
const deleteNote = (noteId: string) => { const deleteNote = (noteId: string) => {
setNotes(notes.filter(note => note.id !== noteId)) notes.delete(noteId);
setNotes(prev => new Map(prev))
} }
const getNoteById = (noteId: string) => { const getNoteById = (noteId: string) => {
return notes.find(note => note.id === noteId); return notes.get(noteId);
} }
return ( return (

View File

@@ -126,6 +126,17 @@ body {
} }
} }
.bn-editor {
padding-inline: 0 !important;
}
.listItem:hover {
.listItem-delete {
transition: opacity .25s ease-out;
opacity: 100%;
}
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;