diff --git a/backend/notes.db b/backend/notes.db index 168ec63..d72d915 100644 Binary files a/backend/notes.db and b/backend/notes.db differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9dbd408..f186880 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,7 +19,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.9.6", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zustand": "^5.0.8" }, "devDependencies": { "@catppuccin/tailwindcss": "^1.0.0", @@ -61,6 +62,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -637,6 +639,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -726,6 +729,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -735,6 +739,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1271,6 +1276,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1635,6 +1641,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -3156,6 +3163,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3580,6 +3588,7 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3590,6 +3599,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3626,6 +3636,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3733,6 +3744,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4730,7 +4742,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -4828,7 +4839,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -6400,6 +6410,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6421,6 +6432,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6783,7 +6795,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -7004,6 +7017,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -7104,6 +7118,35 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index d5ba118..94cc649 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.9.6", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zustand": "^5.0.8" }, "devDependencies": { "@catppuccin/tailwindcss": "^1.0.0", diff --git a/frontend/src/api/notes.tsx b/frontend/src/api/notes.tsx index 733864e..b273992 100644 --- a/frontend/src/api/notes.tsx +++ b/frontend/src/api/notes.tsx @@ -17,7 +17,6 @@ export interface NoteCreate { title: string; content: string; folder_id: number | null; - encrypted: boolean; } const createNote = async (note: NoteCreate) => { diff --git a/frontend/src/components/sidebar/SideBar.tsx b/frontend/src/components/sidebar/SideBar.tsx new file mode 100644 index 0000000..0fa6118 --- /dev/null +++ b/frontend/src/components/sidebar/SideBar.tsx @@ -0,0 +1,125 @@ +import { useState, useRef, useEffect } from "react"; +import { FolderCreate, FolderTreeResponse, folderApi } from "../../api/folders"; +import { DraggableNote } from "./DraggableNote"; + +export const Sidebar = () => { + const [folderTree, setFolderTree] = useState(null); + const [newFolder, setNewFolder] = useState(false); + const [newFolderText, setNewFolderText] = useState(""); + const newFolderRef = useRef(null); + + useEffect(() => { + if (newFolder && newFolderRef.current) { + newFolderRef.current.focus(); + } + }, [newFolder]); + + useEffect(() => { + loadFolderTree(); + }, []); + + const handleCreateFolder = async () => { + if (!newFolderText.trim()) return; + const newFolderData: FolderCreate = { + name: newFolderText, + parent_id: null, + }; + await folderApi.create(newFolderData); + setNewFolderText(""); + loadFolderTree(); + setNewFolder(false); + }; + + const loadFolderTree = async () => { + const data = await folderApi.tree(); + setFolderTree(data); + }; + + return ( +
e.preventDefault()} + onTouchMove={(e) => e.preventDefault()} + > + + {/* New folder input */} + {newFolder && ( +
+ 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); + } + }} + /> +
+ )} + + {/* Folder tree */} +
+ {folderTree?.folders.map((folder) => ( + + ))} +
+ + {/* Orphaned notes */} + {folderTree?.orphaned_notes && folderTree.orphaned_notes.length > 0 && ( +
+ {/*
+ Unsorted +
*/} + {folderTree.orphaned_notes.map((note) => ( + + ))} +
+ )} +
+ ); +}; + +export const SidebarHeader = () => { + return ( +
+

FastNotes

+
+ + +
+
+ ); +}; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 41bb9b0..d28ee98 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -41,6 +41,8 @@ import { DroppableFolder } from "../components/sidebar/DroppableFolder"; import { DraggableNote } from "../components/sidebar/DraggableNote"; import CheckIcon from "../assets/fontawesome/svg/circle-check.svg?react"; import SpinnerIcon from "../assets/fontawesome/svg/rotate.svg?react"; +import { useNoteStore } from "../stores/notesStore"; +import { create } from "zustand"; const simpleSandpackConfig: SandpackConfig = { defaultPreset: "react", @@ -58,7 +60,7 @@ const simpleSandpackConfig: SandpackConfig = { }; function Home() { - const [folderTree, setFolderTree] = useState(null); + // const [folderTree, setFolderTree] = useState(null); const [selectedNote, setSelectedNote] = useState(null); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); @@ -68,6 +70,9 @@ function Home() { const [encrypted, setEncrypted] = useState(false); const [updating, setUpdating] = useState(false); + const { folderTree, loadFolderTree, createNote, createFolder, updateNote } = + useNoteStore(); + const pointer = useSensor(PointerSensor, { activationConstraint: { distance: 30, @@ -98,41 +103,21 @@ function Home() { return () => clearTimeout(timer); }, [content, title]); - const loadFolderTree = async () => { - const data = await folderApi.tree(); - setFolderTree(data); - }; const handleCreate = async () => { if (!title.trim()) return; - const newNote: NoteCreate = { - title, - content, - folder_id: selectedFolder, - encrypted, - }; - await notesApi.create(newNote); - setTitle(""); - setContent(""); - loadFolderTree(); + await createNote({ title, content, folder_id: null }); }; const handleCreateFolder = async () => { if (!newFolderText.trim()) return; - const newFolderData: FolderCreate = { - name: newFolderText, - parent_id: null, - }; - await folderApi.create(newFolderData); - setNewFolderText(""); - loadFolderTree(); - setNewFolder(false); + await createFolder({ name: newFolderText, parent_id: null }); }; const handleUpdate = async () => { if (!selectedNote) return; - await notesApi.update(selectedNote.id, { title, content }); - loadFolderTree(); + await updateNote(selectedNote.id, { title, content }); + setTimeout(() => { setUpdating(false); }, 1000); diff --git a/frontend/src/stores/notesStore.ts b/frontend/src/stores/notesStore.ts new file mode 100644 index 0000000..cc8a0e5 --- /dev/null +++ b/frontend/src/stores/notesStore.ts @@ -0,0 +1,39 @@ +import { create } from "zustand"; +import { devtools, persist } from "zustand/middleware"; +import { folderApi, FolderCreate, FolderTreeResponse } from "../api/folders"; +import { Note, NoteCreate, notesApi } from "../api/notes"; + +interface NoteState { + folderTree: FolderTreeResponse | null; + selectedFolder: number | null; + + loadFolderTree: () => Promise; + createNote: (note: NoteCreate) => Promise; + createFolder: (folder: FolderCreate) => Promise; + updateNote: (id: number, note: Partial) => Promise; +} + +export const useNoteStore = create()((set, get) => ({ + folderTree: null, + selectedFolder: null, + + loadFolderTree: async () => { + const data = await folderApi.tree(); + set({ folderTree: data }); + }, + + createNote: async (note: NoteCreate) => { + await notesApi.create(note); + await get().loadFolderTree(); + }, + + createFolder: async (folder: FolderCreate) => { + await folderApi.create(folder); + await get().loadFolderTree(); + }, + + updateNote: async (id: number, note: Partial) => { + await notesApi.update(id, note); + await get().loadFolderTree(); + }, +}));