Refactor frontend: extract Editor and wire stores

This commit is contained in:
james fitzsimons 2025-11-30 20:32:22 +00:00
parent b27604130a
commit 89fecc5c08
10 changed files with 265 additions and 335 deletions

View file

@ -82,45 +82,18 @@ def update_folder(
folder_id: int, folder_update: FolderUpdate, session: Session = Depends(get_session) folder_id: int, folder_update: FolderUpdate, session: Session = Depends(get_session)
): ):
"""Update a folder""" """Update a folder"""
print(f"=== UPDATE FOLDER CALLED ===")
print(f"Folder ID: {folder_id}")
print(f"Update data received: {folder_update}")
folder = session.get(Folder, folder_id) folder = session.get(Folder, folder_id)
if not folder: if not folder:
raise HTTPException(status_code=404, detail="Folder not found") raise HTTPException(status_code=404, detail="Folder not found")
print(
f"Found folder: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}"
)
# Update folder attributes from the request body
update_data = folder_update.model_dump(exclude_unset=True) update_data = folder_update.model_dump(exclude_unset=True)
print(f"Update data dict (exclude_unset): {update_data}")
print(f"Update data keys: {list(update_data.keys())}")
for key, value in update_data.items(): for key, value in update_data.items():
print(f"Setting {key} = {value}")
setattr(folder, key, value) setattr(folder, key, value)
print(
f"After setattr: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}"
)
session.add(folder) session.add(folder)
session.commit() session.commit()
print(f"Committed changes to database")
session.refresh(folder) session.refresh(folder)
print(
f"After refresh: id={folder.id}, name={folder.name}, parent_id={folder.parent_id}"
)
# Verify the change persisted
verification = session.get(Folder, folder_id)
print(
f"Verification query: id={verification.id}, name={verification.name}, parent_id={verification.parent_id}"
)
print(f"=== UPDATE COMPLETE ===")
return folder return folder

Binary file not shown.

View file

@ -0,0 +1,119 @@
import {
BoldItalicUnderlineToggles,
codeBlockPlugin,
codeMirrorPlugin,
diffSourcePlugin,
headingsPlugin,
imagePlugin,
linkPlugin,
listsPlugin,
markdownShortcutPlugin,
MDXEditor,
quotePlugin,
SandpackConfig,
sandpackPlugin,
tablePlugin,
thematicBreakPlugin,
toolbarPlugin,
UndoRedo,
} from "@mdxeditor/editor";
import { useNoteStore } from "../../stores/notesStore";
import { useEffect } from "react";
import { useUIStore } from "../../stores/uiStore";
const simpleSandpackConfig: SandpackConfig = {
defaultPreset: "react",
presets: [
{
label: "React",
name: "react",
meta: "live react",
sandpackTemplate: "react",
sandpackTheme: "dark",
snippetFileName: "/App.js",
snippetLanguage: "jsx",
},
],
};
export const Editor = () => {
const { selectedNote, setContent, setTitle, updateNote } = useNoteStore();
const { updating, setUpdating } = useUIStore();
useEffect(() => {
if (!selectedNote) return;
const timer = setTimeout(async () => {
setUpdating(true);
handleUpdate();
}, 2000);
return () => clearTimeout(timer);
}, [selectedNote]);
const handleUpdate = async () => {
if (!selectedNote) return;
await updateNote(selectedNote.id);
console.log(selectedNote.id);
setTimeout(() => {
setUpdating(false);
}, 1000);
};
return (
<div className="flex-1 flex flex-col overflow-y-auto px-8 py-6">
{/* Title input */}
<input
type="text"
placeholder="Untitled note..."
value={selectedNote?.title || ""}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-0 py-3 mb-4 text-3xl font-semibold bg-transparent border-b border-ctp-surface2 focus:outline-none focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/>
<div className="flex-1">
<MDXEditor
markdown={selectedNote?.content || ""}
key={selectedNote?.id || "new"}
onChange={setContent}
className="prose prose-invert max-w-none text-ctp-text h-full dark-editor dark-mode"
plugins={[
headingsPlugin(),
toolbarPlugin({
toolbarClassName: "toolbar",
toolbarContents: () => (
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
</>
),
}),
tablePlugin(),
listsPlugin(),
quotePlugin(),
thematicBreakPlugin(),
linkPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
sandpackPlugin({ sandpackConfig: simpleSandpackConfig }),
codeMirrorPlugin({
codeBlockLanguages: {
js: "JavaScript",
css: "CSS",
python: "Python",
typescript: "TypeScript",
html: "HTML",
},
}),
imagePlugin(),
markdownShortcutPlugin(),
diffSourcePlugin({
viewMode: "rich-text",
diffMarkdown: "boo",
}),
]}
/>
</div>
</div>
);
};

View file

@ -2,16 +2,11 @@ import React from "react";
import { useDraggable } from "@dnd-kit/core"; import { useDraggable } from "@dnd-kit/core";
import { Note } from "../../api/notes"; import { Note } from "../../api/notes";
import { NoteRead } from "../../api/folders"; import { NoteRead } from "../../api/folders";
import { useNoteStore } from "../../stores/notesStore";
export const DraggableNote = ({ note }: { note: NoteRead }) => {
const { selectedNote, setSelectedNote } = useNoteStore();
export const DraggableNote = ({
note,
selectNote,
selectedNote,
}: {
note: NoteRead;
selectNote: (note: NoteRead) => void;
selectedNote: NoteRead | null;
}) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({ const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: note.id, id: note.id,
data: { type: "note", note }, data: { type: "note", note },
@ -26,14 +21,16 @@ export const DraggableNote = ({
<button ref={setNodeRef} style={style} {...listeners} {...attributes}> <button ref={setNodeRef} style={style} {...listeners} {...attributes}>
<div <div
key={note.id} key={note.id}
onClick={() => selectNote(note)} onClick={() => setSelectedNote(note)}
className={` rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${ className={` rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
selectedNote?.id === note.id selectedNote?.id === note.id
? "bg-ctp-mauve text-ctp-base" ? "bg-ctp-mauve text-ctp-base"
: "hover:bg-ctp-surface1" : "hover:bg-ctp-surface1"
}`} }`}
> >
<span>{note.title}</span> <span>
{selectedNote?.id == note.id ? selectedNote.title : note.title}
</span>
</div> </div>
</button> </button>
); );

View file

@ -1,22 +1,19 @@
import React from "react"; import React from "react";
import { useDroppable, useDraggable } from "@dnd-kit/core"; import { useDroppable, useDraggable } from "@dnd-kit/core";
import { Folder, NoteRead } from "../../api/folders"; import { Folder, NoteRead } from "../../api/folders";
import { useNoteStore } from "../../stores/notesStore";
export const DroppableFolder = ({ export const DroppableFolder = ({
folder, folder,
setSelectedFolder,
selectedFolder,
selectedNote,
setCollapse, setCollapse,
collapse, collapse,
}: { }: {
folder: Partial<Folder>; folder: Partial<Folder>;
setSelectedFolder: (id: number | null) => void;
selectedFolder: number | null;
selectedNote: NoteRead | null;
setCollapse: React.Dispatch<React.SetStateAction<boolean>>; setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
collapse: boolean; collapse: boolean;
}) => { }) => {
const { setSelectedFolder, selectedFolder, selectedNote } = useNoteStore();
const { isOver, setNodeRef: setDroppableRef } = useDroppable({ const { isOver, setNodeRef: setDroppableRef } = useDroppable({
id: folder.id!, id: folder.id!,
data: { type: "folder", folder }, data: { type: "folder", folder },

View file

@ -0,0 +1,42 @@
import { useState } from "react";
import { FolderTreeNode } from "../../api/folders";
import { DraggableNote } from "./DraggableNote";
import { DroppableFolder } from "./DroppableFolder";
interface RecursiveFolderProps {
folder: FolderTreeNode;
depth?: number;
}
export const RecursiveFolder = ({
folder,
depth = 0,
}: RecursiveFolderProps) => {
const [collapse, setCollapse] = useState(false);
return (
<div
key={folder.id}
className="flex flex-col"
style={{ marginLeft: depth > 0 ? "1.5rem" : "0" }}
>
<DroppableFolder
folder={folder}
setCollapse={setCollapse}
collapse={collapse}
/>
{collapse && (
<>
<div className="flex flex-col gap-0.5 ml-6">
{folder.notes.map((note) => (
<DraggableNote key={note.id} note={note} />
))}
</div>
{folder.children.map((child) => (
<RecursiveFolder key={child.id} folder={child} depth={depth + 1} />
))}
</>
)}
</div>
);
};

View file

@ -17,6 +17,7 @@ import {
useSensors, useSensors,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { notesApi } from "../../api/notes"; import { notesApi } from "../../api/notes";
import { RecursiveFolder } from "./RecursiveFolder";
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => { export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null); // const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
@ -143,15 +144,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
{/* Folder tree */} {/* Folder tree */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{folderTree?.folders.map((folder) => ( {folderTree?.folders.map((folder) => (
<RenderFolder <RecursiveFolder key={folder.id} folder={folder} depth={0} />
key={folder.id}
folder={folder}
depth={0}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={setSelectedNote}
/>
))} ))}
</div> </div>
@ -162,12 +155,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
Unsorted Unsorted
</div>*/} </div>*/}
{folderTree.orphaned_notes.map((note) => ( {folderTree.orphaned_notes.map((note) => (
<DraggableNote <DraggableNote key={note.id} note={note} />
key={note.id}
note={note}
selectNote={setSelectedNote}
selectedNote={selectedNote}
/>
))} ))}
</div> </div>
)} )}
@ -205,65 +193,3 @@ export const SidebarHeader = ({
</div> </div>
); );
}; };
interface RenderFolderProps {
folder: FolderTreeNode;
depth?: number;
setSelectedFolder: (id: number | null) => void;
selectedFolder: number | null;
selectedNote: NoteRead | null;
selectNote: (note: NoteRead) => void;
}
const RenderFolder = ({
folder,
depth = 0,
setSelectedFolder,
selectedFolder,
selectedNote,
selectNote,
}: RenderFolderProps) => {
const [collapse, setCollapse] = useState(false);
return (
<div
key={folder.id}
className="flex flex-col"
style={{ marginLeft: depth > 0 ? "1.5rem" : "0" }}
>
<DroppableFolder
folder={folder}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
setCollapse={setCollapse}
collapse={collapse}
/>
{collapse && (
<>
<div className="flex flex-col gap-0.5 ml-6">
{folder.notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
</div>
{folder.children.map((child) => (
<RenderFolder
key={child.id}
folder={child}
depth={depth + 1}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={selectNote}
/>
))}
</>
)}
</div>
);
};

View file

@ -46,21 +46,8 @@ import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react";
import { useNoteStore } from "../stores/notesStore"; import { useNoteStore } from "../stores/notesStore";
import { create } from "zustand"; import { create } from "zustand";
import { Sidebar } from "../components/sidebar/SideBar"; import { Sidebar } from "../components/sidebar/SideBar";
import { Editor } from "../components/editor/Editor";
const simpleSandpackConfig: SandpackConfig = { import { useUIStore } from "../stores/uiStore";
defaultPreset: "react",
presets: [
{
label: "React",
name: "react",
meta: "live react",
sandpackTemplate: "react",
sandpackTheme: "dark",
snippetFileName: "/App.js",
snippetLanguage: "jsx",
},
],
};
function Home() { function Home() {
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null); // const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
@ -71,7 +58,7 @@ function Home() {
const [newFolderText, setNewFolderText] = useState(""); const [newFolderText, setNewFolderText] = useState("");
// const [selectedFolder, setSelectedFolder] = useState<number | null>(null); // const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
const [encrypted, setEncrypted] = useState(false); const [encrypted, setEncrypted] = useState(false);
const [updating, setUpdating] = useState(false); // const [updating, setUpdating] = useState(false);
const { const {
setSelectedFolder, setSelectedFolder,
@ -85,7 +72,7 @@ function Home() {
selectedNote, selectedNote,
} = useNoteStore(); } = useNoteStore();
const { updating } = useUIStore();
const newFolderRef = useRef<HTMLInputElement>(null); const newFolderRef = useRef<HTMLInputElement>(null);
@ -99,31 +86,11 @@ function Home() {
} }
}, [newFolder]); }, [newFolder]);
useEffect(() => {
if (!selectedNote) return;
const timer = setTimeout(async () => {
setUpdating(true);
handleUpdate();
}, 2000);
return () => clearTimeout(timer);
}, [content, title]);
const handleCreate = async () => { const handleCreate = async () => {
if (!title.trim()) return; if (!title.trim()) return;
await createNote({ title, content, folder_id: null }); await createNote({ title, content, folder_id: null });
}; };
const handleUpdate = async () => {
if (!selectedNote) return;
await updateNote(selectedNote.id, { title, content });
setTimeout(() => {
setUpdating(false);
}, 1000);
};
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
await notesApi.delete(id); await notesApi.delete(id);
loadFolderTree(); loadFolderTree();
@ -136,10 +103,7 @@ function Home() {
setContent(""); setContent("");
}; };
return ( return (
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden"> <div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
{/* Sidebar */} {/* Sidebar */}
@ -150,73 +114,18 @@ function Home() {
{/* Top accent bar */} {/* Top accent bar */}
<div className="w-full bg-ctp-crust h-1 shrink-0"></div> <div className="w-full bg-ctp-crust h-1 shrink-0"></div>
{/* Content area with padding */} <Editor />
<div className="flex-1 flex flex-col overflow-y-auto px-8 py-6">
{/* Title input */}
<input
type="text"
placeholder="Untitled note..."
value={selectedNote?.title || ""}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-0 py-3 mb-4 text-3xl font-semibold bg-transparent border-b border-ctp-surface2 focus:outline-none focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/>
{/* Editor */}
<div className="flex-1">
<MDXEditor
markdown={selectedNote?.content || ""}
key={selectedNote?.id || "new"}
onChange={setContent}
className="prose prose-invert max-w-none text-ctp-text h-full dark-editor dark-mode"
plugins={[
headingsPlugin(),
toolbarPlugin({
toolbarClassName: "toolbar",
toolbarContents: () => (
<>
<UndoRedo />
<BoldItalicUnderlineToggles />
</>
),
}),
tablePlugin(),
listsPlugin(),
quotePlugin(),
thematicBreakPlugin(),
linkPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
sandpackPlugin({ sandpackConfig: simpleSandpackConfig }),
codeMirrorPlugin({
codeBlockLanguages: {
js: "JavaScript",
css: "CSS",
python: "Python",
typescript: "TypeScript",
html: "HTML",
},
}),
imagePlugin(),
markdownShortcutPlugin(),
diffSourcePlugin({
viewMode: "rich-text",
diffMarkdown: "boo",
}),
]}
/>
</div>
</div>
{/* Action bar */} {/* Action bar */}
<div className="flex items-center gap-3 px-8 py-4 border-t border-ctp-surface2 bg-ctp-mantle shrink-0"> <div className="flex items-center gap-3 px-8 py-4 border-t border-ctp-surface2 bg-ctp-mantle shrink-0">
{selectedNote ? ( {selectedNote ? (
<> <>
<button {/*<button
onClick={handleUpdate} onClick={handleUpdate}
className="px-2 py-0.5 bg-ctp-blue text-ctp-base rounded-lg hover:bg-ctp-sapphire transition-colors font-medium shadow-sm" className="px-2 py-0.5 bg-ctp-blue text-ctp-base rounded-lg hover:bg-ctp-sapphire transition-colors font-medium shadow-sm"
> >
Save Save
</button> </button>*/}
<button <button
onClick={() => handleDelete(selectedNote.id)} onClick={() => handleDelete(selectedNote.id)}
className="px-2 py-0.5 bg-ctp-red text-ctp-base rounded-lg hover:bg-ctp-maroon transition-colors font-medium shadow-sm" className="px-2 py-0.5 bg-ctp-red text-ctp-base rounded-lg hover:bg-ctp-maroon transition-colors font-medium shadow-sm"
@ -253,9 +162,7 @@ function Home() {
) : ( ) : (
<> <>
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" /> <CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" />
<span className="text-sm text-ctp-subtext0 font-medium"> <span className="text-sm text-ctp-subtext0 font-medium">Saved</span>
Saved
</span>
</> </>
)} )}
</div> </div>
@ -264,65 +171,3 @@ function Home() {
} }
export default Home; export default Home;
interface RenderFolderProps {
folder: FolderTreeNode;
depth?: number;
setSelectedFolder: (id: number | null) => void;
selectedFolder: number | null;
selectedNote: NoteRead | null;
selectNote: (note: NoteRead) => void;
}
const RenderFolder = ({
folder,
depth = 0,
setSelectedFolder,
selectedFolder,
selectedNote,
selectNote,
}: RenderFolderProps) => {
const [collapse, setCollapse] = useState(false);
return (
<div
key={folder.id}
className="flex flex-col"
style={{ marginLeft: depth > 0 ? "1.5rem" : "0" }}
>
<DroppableFolder
folder={folder}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
setCollapse={setCollapse}
collapse={collapse}
/>
{collapse && (
<>
<div className="flex flex-col gap-0.5 ml-6">
{folder.notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
</div>
{folder.children.map((child) => (
<RenderFolder
key={child.id}
folder={child}
depth={depth + 1}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={selectNote}
/>
))}
</>
)}
</div>
);
};

View file

@ -7,15 +7,18 @@ import {
NoteRead, NoteRead,
} from "../api/folders"; } from "../api/folders";
import { Note, NoteCreate, notesApi } from "../api/notes"; import { Note, NoteCreate, notesApi } from "../api/notes";
import { getSelectedNode } from "@mdxeditor/editor";
interface NoteState { interface NoteState {
folderTree: FolderTreeResponse | null; folderTree: FolderTreeResponse | null;
selectedFolder: number | null; selectedFolder: number | null;
selectedNote: NoteRead | null; selectedNote: NoteRead | null;
setContent: (content: string) => void;
setTitle: (title: string) => void;
loadFolderTree: () => Promise<void>; loadFolderTree: () => Promise<void>;
createNote: (note: NoteCreate) => Promise<void>; createNote: (note: NoteCreate) => Promise<void>;
updateNote: (id: number, note: Partial<Note>) => Promise<void>; updateNote: (id: number) => Promise<void>;
createFolder: (folder: FolderCreate) => Promise<void>; createFolder: (folder: FolderCreate) => Promise<void>;
setSelectedFolder: (id: number | null) => void; setSelectedFolder: (id: number | null) => void;
setSelectedNote: (id: NoteRead | null) => void; setSelectedNote: (id: NoteRead | null) => void;
@ -26,6 +29,20 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
selectedFolder: null, selectedFolder: null,
selectedNote: null, selectedNote: null,
setContent: (content) => {
const currentNote = get().selectedNote;
if (currentNote) {
set({ selectedNote: { ...currentNote, content: content } });
}
},
setTitle: (title) => {
const currentNote = get().selectedNote;
if (currentNote) {
set({ selectedNote: { ...currentNote, title: title } });
}
},
loadFolderTree: async () => { loadFolderTree: async () => {
const data = await folderApi.tree(); const data = await folderApi.tree();
set({ folderTree: data }); set({ folderTree: data });
@ -41,7 +58,8 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
await get().loadFolderTree(); await get().loadFolderTree();
}, },
updateNote: async (id: number, note: Partial<Note>) => { updateNote: async (id: number) => {
const note = get().selectedNote as Partial<Note>;
await notesApi.update(id, note); await notesApi.update(id, note);
await get().loadFolderTree(); await get().loadFolderTree();
}, },

View file

@ -0,0 +1,13 @@
import { create } from "zustand";
interface UIState {
updating: boolean;
setUpdating: (update: boolean) => void;
}
export const useUIStore = create<UIState>()((set, get) => ({
updating: false,
setUpdating: (update) => {
set({ updating: update });
},
}));