Refactor frontend to use store for note selection

- Migrate note and folder selection state to a Zustand store
- and wire the Sidebar to consume it
- Extract Sidebar-related UI into a new Sidebar/RenderFolder component
- and remove inline sidebar from Home
- Update Home to render Sidebar and drive editor from selectedNote
- Extend notesStore with selectedNote and setters
- add setSelectedFolder and setSelectedNote
- Update prop types for DroppableFolder/RenderFolder
- to use store-based callbacks
This commit is contained in:
James Fitzsimons 2025-11-30 18:01:57 +00:00
parent 16313b961b
commit fb461df550
6 changed files with 137 additions and 135 deletions

Binary file not shown.

View file

@ -62,7 +62,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -639,7 +638,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -729,7 +727,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@ -739,7 +736,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@ -1276,7 +1272,6 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0" "@fortawesome/fontawesome-common-types": "7.1.0"
}, },
@ -1641,7 +1636,6 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.3.0" "@lezer/common": "^1.3.0"
} }
@ -3163,7 +3157,6 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0", "@svgr/babel-preset": "8.1.0",
@ -3588,7 +3581,6 @@
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -3599,7 +3591,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -3636,7 +3627,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3744,7 +3734,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@ -4742,6 +4731,7 @@
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "GitHub Sponsors ❤", "type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
@ -4839,6 +4829,7 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"isomorphic.js": "^0.2.4" "isomorphic.js": "^0.2.4"
}, },
@ -6410,7 +6401,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@ -6432,7 +6422,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@ -6795,8 +6784,7 @@
"version": "4.1.17", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@ -7017,7 +7005,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",

View file

@ -11,7 +11,7 @@ export const DroppableFolder = ({
collapse, collapse,
}: { }: {
folder: Partial<Folder>; folder: Partial<Folder>;
setSelectedFolder: React.Dispatch<React.SetStateAction<number | null>>; setSelectedFolder: (id: number | null) => void;
selectedFolder: number | null; selectedFolder: number | null;
selectedNote: NoteRead | null; selectedNote: NoteRead | null;
setCollapse: React.Dispatch<React.SetStateAction<boolean>>; setCollapse: React.Dispatch<React.SetStateAction<boolean>>;

View file

@ -1,13 +1,30 @@
import { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect, SetStateAction } from "react";
import { FolderCreate, FolderTreeResponse, folderApi } from "../../api/folders"; import {
FolderCreate,
FolderTreeNode,
FolderTreeResponse,
NoteRead,
folderApi,
} from "../../api/folders";
import { DraggableNote } from "./DraggableNote"; import { DraggableNote } from "./DraggableNote";
import { DroppableFolder } from "./DroppableFolder";
import { useNoteStore } from "../../stores/notesStore";
export const Sidebar = () => { export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null); // const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
const [newFolder, setNewFolder] = useState(false); const [newFolder, setNewFolder] = useState(false);
const [newFolderText, setNewFolderText] = useState(""); const [newFolderText, setNewFolderText] = useState("");
const newFolderRef = useRef<HTMLInputElement>(null); const newFolderRef = useRef<HTMLInputElement>(null);
const {
setSelectedFolder,
selectedFolder,
folderTree,
loadFolderTree,
selectedNote,
setSelectedNote,
} = useNoteStore();
useEffect(() => { useEffect(() => {
if (newFolder && newFolderRef.current) { if (newFolder && newFolderRef.current) {
newFolderRef.current.focus(); newFolderRef.current.focus();
@ -30,18 +47,16 @@ export const Sidebar = () => {
setNewFolder(false); setNewFolder(false);
}; };
const loadFolderTree = async () => {
const data = await folderApi.tree();
setFolderTree(data);
};
return ( return (
<div <div
className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-col gap-3" className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-col gap-3"
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
onTouchMove={(e) => e.preventDefault()} onTouchMove={(e) => e.preventDefault()}
> >
<SidebarHeader /> <SidebarHeader
clearSelection={clearSelection}
setNewFolder={setNewFolder}
/>
{/* New folder input */} {/* New folder input */}
{newFolder && ( {newFolder && (
<div className="mb-2"> <div className="mb-2">
@ -75,7 +90,7 @@ export const Sidebar = () => {
setSelectedFolder={setSelectedFolder} setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder} selectedFolder={selectedFolder}
selectedNote={selectedNote} selectedNote={selectedNote}
selectNote={selectNote} selectNote={setSelectedNote}
/> />
))} ))}
</div> </div>
@ -90,7 +105,7 @@ export const Sidebar = () => {
<DraggableNote <DraggableNote
key={note.id} key={note.id}
note={note} note={note}
selectNote={selectNote} selectNote={setSelectedNote}
selectedNote={selectedNote} selectedNote={selectedNote}
/> />
))} ))}
@ -100,7 +115,13 @@ export const Sidebar = () => {
); );
}; };
export const SidebarHeader = () => { export const SidebarHeader = ({
clearSelection,
setNewFolder,
}: {
clearSelection: () => void;
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
}) => {
return ( return (
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold text-ctp-text">FastNotes</h2> <h2 className="text-lg font-semibold text-ctp-text">FastNotes</h2>
@ -123,3 +144,65 @@ 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

@ -43,6 +43,7 @@ import CheckIcon from "../assets/fontawesome/svg/circle-check.svg?react";
import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react"; 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";
const simpleSandpackConfig: SandpackConfig = { const simpleSandpackConfig: SandpackConfig = {
defaultPreset: "react", defaultPreset: "react",
@ -61,17 +62,26 @@ const simpleSandpackConfig: SandpackConfig = {
function Home() { function Home() {
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null); // const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
const [selectedNote, setSelectedNote] = useState<NoteRead | null>(null); // const [selectedNote, setSelectedNote] = useState<NoteRead | null>(null);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [newFolder, setNewFolder] = useState(false); const [newFolder, setNewFolder] = useState(false);
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 { folderTree, loadFolderTree, createNote, createFolder, updateNote } = const {
useNoteStore(); setSelectedFolder,
selectedFolder,
folderTree,
loadFolderTree,
createNote,
createFolder,
updateNote,
setSelectedNote,
selectedNote,
} = useNoteStore();
const pointer = useSensor(PointerSensor, { const pointer = useSensor(PointerSensor, {
activationConstraint: { activationConstraint: {
@ -103,17 +113,11 @@ function Home() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [content, title]); }, [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 handleCreateFolder = async () => {
if (!newFolderText.trim()) return;
await createFolder({ name: newFolderText, parent_id: null });
};
const handleUpdate = async () => { const handleUpdate = async () => {
if (!selectedNote) return; if (!selectedNote) return;
await updateNote(selectedNote.id, { title, content }); await updateNote(selectedNote.id, { title, content });
@ -129,15 +133,6 @@ function Home() {
clearSelection(); clearSelection();
}; };
const selectNote = (note: NoteRead) => {
setSelectedNote(note);
setTitle(note.title);
let cleanContent = note.content.replace(/\\([_\-\[\]\(\)])/g, "$1");
cleanContent = cleanContent.replace(/^```\s*$/gm, "");
setContent(cleanContent);
};
const clearSelection = () => { const clearSelection = () => {
setSelectedNote(null); setSelectedNote(null);
setTitle(""); setTitle("");
@ -159,88 +154,8 @@ function Home() {
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}> <DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
<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 */}
<div
className="bg-ctp-mantle border-r border-ctp-surface2 w-[300px] p-4 overflow-y-auto sm:block hidden flex-shrink-0 flex flex-col gap-3"
onDragOver={(e) => e.preventDefault()}
onTouchMove={(e) => e.preventDefault()}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold text-ctp-text">FastNotes</h2>
<div className="flex gap-2">
<button
onClick={() => setNewFolder(true)}
className="hover:bg-ctp-mauve group transition-colors rounded-md p-1.5"
title="New folder"
>
<i className="fadr fa-folder-plus text-base text-ctp-mauve group-hover:text-ctp-base transition-colors"></i>
</button>
<button
onClick={clearSelection}
className="hover:bg-ctp-mauve group transition-colors rounded-md p-1.5"
title="New note"
>
<i className="fadr fa-file-circle-plus text-base text-ctp-mauve group-hover:text-ctp-base transition-colors"></i>
</button>
</div>
</div>
{/* New folder input */} <Sidebar clearSelection={clearSelection} />
{newFolder && (
<div className="mb-2">
<input
onBlur={() => setNewFolder(false)}
onChange={(e) => setNewFolderText(e.target.value)}
value={newFolderText}
type="text"
placeholder="Folder name..."
className="border border-ctp-mauve rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-ctp-mauve bg-ctp-base text-ctp-text placeholder:text-ctp-overlay0"
ref={newFolderRef}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleCreateFolder();
}
if (e.key === "Escape") {
setNewFolder(false);
}
}}
/>
</div>
)}
{/* Folder tree */}
<div className="flex flex-col gap-1">
{folderTree?.folders.map((folder) => (
<RenderFolder
key={folder.id}
folder={folder}
depth={0}
setSelectedFolder={setSelectedFolder}
selectedFolder={selectedFolder}
selectedNote={selectedNote}
selectNote={selectNote}
/>
))}
</div>
{/* Orphaned notes */}
{folderTree?.orphaned_notes &&
folderTree.orphaned_notes.length > 0 && (
<div className="mt-4 flex flex-col gap-1">
{/*<div className="text-ctp-subtext0 text-sm font-medium mb-1 px-2">
Unsorted
</div>*/}
{folderTree.orphaned_notes.map((note) => (
<DraggableNote
key={note.id}
note={note}
selectNote={selectNote}
selectedNote={selectedNote}
/>
))}
</div>
)}
</div>
{/* Main editor area */} {/* Main editor area */}
<div className="flex flex-col w-full h-screen overflow-hidden"> <div className="flex flex-col w-full h-screen overflow-hidden">
@ -253,7 +168,7 @@ function Home() {
<input <input
type="text" type="text"
placeholder="Untitled note..." placeholder="Untitled note..."
value={title} value={selectedNote?.title || ""}
onChange={(e) => setTitle(e.target.value)} 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" 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"
/> />
@ -261,7 +176,7 @@ function Home() {
{/* Editor */} {/* Editor */}
<div className="flex-1"> <div className="flex-1">
<MDXEditor <MDXEditor
markdown={content} markdown={selectedNote?.content || ""}
key={selectedNote?.id || "new"} key={selectedNote?.id || "new"}
onChange={setContent} onChange={setContent}
className="prose prose-invert max-w-none text-ctp-text h-full dark-editor dark-mode" className="prose prose-invert max-w-none text-ctp-text h-full dark-editor dark-mode"
@ -380,7 +295,7 @@ export default Home;
interface RenderFolderProps { interface RenderFolderProps {
folder: FolderTreeNode; folder: FolderTreeNode;
depth?: number; depth?: number;
setSelectedFolder: React.Dispatch<SetStateAction<number | null>>; setSelectedFolder: (id: number | null) => void;
selectedFolder: number | null; selectedFolder: number | null;
selectedNote: NoteRead | null; selectedNote: NoteRead | null;
selectNote: (note: NoteRead) => void; selectNote: (note: NoteRead) => void;

View file

@ -1,21 +1,30 @@
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import { folderApi, FolderCreate, FolderTreeResponse } from "../api/folders"; import {
folderApi,
FolderCreate,
FolderTreeResponse,
NoteRead,
} from "../api/folders";
import { Note, NoteCreate, notesApi } from "../api/notes"; import { Note, NoteCreate, notesApi } from "../api/notes";
interface NoteState { interface NoteState {
folderTree: FolderTreeResponse | null; folderTree: FolderTreeResponse | null;
selectedFolder: number | null; selectedFolder: number | null;
selectedNote: NoteRead | null;
loadFolderTree: () => Promise<void>; loadFolderTree: () => Promise<void>;
createNote: (note: NoteCreate) => Promise<void>; createNote: (note: NoteCreate) => Promise<void>;
createFolder: (folder: FolderCreate) => Promise<void>;
updateNote: (id: number, note: Partial<Note>) => Promise<void>; updateNote: (id: number, note: Partial<Note>) => Promise<void>;
createFolder: (folder: FolderCreate) => Promise<void>;
setSelectedFolder: (id: number | null) => void;
setSelectedNote: (id: NoteRead | null) => void;
} }
export const useNoteStore = create<NoteState>()((set, get) => ({ export const useNoteStore = create<NoteState>()((set, get) => ({
folderTree: null, folderTree: null,
selectedFolder: null, selectedFolder: null,
selectedNote: null,
loadFolderTree: async () => { loadFolderTree: async () => {
const data = await folderApi.tree(); const data = await folderApi.tree();
@ -36,4 +45,12 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
await notesApi.update(id, note); await notesApi.update(id, note);
await get().loadFolderTree(); await get().loadFolderTree();
}, },
setSelectedFolder: (id: number | null) => {
set({ selectedFolder: id });
},
setSelectedNote: (id: NoteRead | null) => {
set({ selectedNote: id });
},
})); }));