diff --git a/backend/notes.db b/backend/notes.db index d72d915..d087600 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 f186880..d0c83b3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -62,7 +62,6 @@ "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", @@ -639,7 +638,6 @@ "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", @@ -729,7 +727,6 @@ "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" } @@ -739,7 +736,6 @@ "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", @@ -1276,7 +1272,6 @@ "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" }, @@ -1641,7 +1636,6 @@ "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" } @@ -3163,7 +3157,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3588,7 +3581,6 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3599,7 +3591,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3636,7 +3627,6 @@ "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" }, @@ -3744,7 +3734,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4742,6 +4731,7 @@ "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" @@ -4839,6 +4829,7 @@ "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" }, @@ -6410,7 +6401,6 @@ "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" }, @@ -6432,7 +6422,6 @@ "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" @@ -6795,8 +6784,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -7017,7 +7005,6 @@ "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", diff --git a/frontend/src/components/sidebar/DroppableFolder.tsx b/frontend/src/components/sidebar/DroppableFolder.tsx index 4d98c72..8618800 100644 --- a/frontend/src/components/sidebar/DroppableFolder.tsx +++ b/frontend/src/components/sidebar/DroppableFolder.tsx @@ -11,7 +11,7 @@ export const DroppableFolder = ({ collapse, }: { folder: Partial; - setSelectedFolder: React.Dispatch>; + setSelectedFolder: (id: number | null) => void; selectedFolder: number | null; selectedNote: NoteRead | null; setCollapse: React.Dispatch>; diff --git a/frontend/src/components/sidebar/SideBar.tsx b/frontend/src/components/sidebar/SideBar.tsx index 0fa6118..f3985cb 100644 --- a/frontend/src/components/sidebar/SideBar.tsx +++ b/frontend/src/components/sidebar/SideBar.tsx @@ -1,13 +1,30 @@ -import { useState, useRef, useEffect } from "react"; -import { FolderCreate, FolderTreeResponse, folderApi } from "../../api/folders"; +import React, { useState, useRef, useEffect, SetStateAction } from "react"; +import { + FolderCreate, + FolderTreeNode, + FolderTreeResponse, + NoteRead, + folderApi, +} from "../../api/folders"; import { DraggableNote } from "./DraggableNote"; +import { DroppableFolder } from "./DroppableFolder"; +import { useNoteStore } from "../../stores/notesStore"; -export const Sidebar = () => { - const [folderTree, setFolderTree] = useState(null); +export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => { + // const [folderTree, setFolderTree] = useState(null); const [newFolder, setNewFolder] = useState(false); const [newFolderText, setNewFolderText] = useState(""); const newFolderRef = useRef(null); + const { + setSelectedFolder, + selectedFolder, + folderTree, + loadFolderTree, + selectedNote, + setSelectedNote, + } = useNoteStore(); + useEffect(() => { if (newFolder && newFolderRef.current) { newFolderRef.current.focus(); @@ -30,18 +47,16 @@ export const Sidebar = () => { setNewFolder(false); }; - const loadFolderTree = async () => { - const data = await folderApi.tree(); - setFolderTree(data); - }; - return (
e.preventDefault()} onTouchMove={(e) => e.preventDefault()} > - + {/* New folder input */} {newFolder && (
@@ -75,7 +90,7 @@ export const Sidebar = () => { setSelectedFolder={setSelectedFolder} selectedFolder={selectedFolder} selectedNote={selectedNote} - selectNote={selectNote} + selectNote={setSelectedNote} /> ))}
@@ -90,7 +105,7 @@ export const Sidebar = () => { ))} @@ -100,7 +115,13 @@ export const Sidebar = () => { ); }; -export const SidebarHeader = () => { +export const SidebarHeader = ({ + clearSelection, + setNewFolder, +}: { + clearSelection: () => void; + setNewFolder: React.Dispatch>; +}) => { return (

FastNotes

@@ -123,3 +144,65 @@ export const SidebarHeader = () => {
); }; + +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 ( +
0 ? "1.5rem" : "0" }} + > + + {collapse && ( + <> +
+ {folder.notes.map((note) => ( + + ))} +
+ {folder.children.map((child) => ( + + ))} + + )} +
+ ); +}; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index d28ee98..4b7fb3d 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -43,6 +43,7 @@ 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"; +import { Sidebar } from "../components/sidebar/SideBar"; const simpleSandpackConfig: SandpackConfig = { defaultPreset: "react", @@ -61,17 +62,26 @@ const simpleSandpackConfig: SandpackConfig = { function Home() { // const [folderTree, setFolderTree] = useState(null); - const [selectedNote, setSelectedNote] = useState(null); + // const [selectedNote, setSelectedNote] = useState(null); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [newFolder, setNewFolder] = useState(false); const [newFolderText, setNewFolderText] = useState(""); - const [selectedFolder, setSelectedFolder] = useState(null); + // const [selectedFolder, setSelectedFolder] = useState(null); const [encrypted, setEncrypted] = useState(false); const [updating, setUpdating] = useState(false); - const { folderTree, loadFolderTree, createNote, createFolder, updateNote } = - useNoteStore(); + const { + setSelectedFolder, + selectedFolder, + folderTree, + loadFolderTree, + createNote, + createFolder, + updateNote, + setSelectedNote, + selectedNote, + } = useNoteStore(); const pointer = useSensor(PointerSensor, { activationConstraint: { @@ -103,17 +113,11 @@ function Home() { return () => clearTimeout(timer); }, [content, title]); - const handleCreate = async () => { if (!title.trim()) return; await createNote({ title, content, folder_id: null }); }; - const handleCreateFolder = async () => { - if (!newFolderText.trim()) return; - await createFolder({ name: newFolderText, parent_id: null }); - }; - const handleUpdate = async () => { if (!selectedNote) return; await updateNote(selectedNote.id, { title, content }); @@ -129,15 +133,6 @@ function Home() { 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 = () => { setSelectedNote(null); setTitle(""); @@ -159,88 +154,8 @@ function Home() {
{/* Sidebar */} -
e.preventDefault()} - onTouchMove={(e) => e.preventDefault()} - > - {/* Header */} -
-

FastNotes

-
- - -
-
- {/* 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) => ( - - ))} -
- )} -
+ {/* Main editor area */}
@@ -253,7 +168,7 @@ function Home() { 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" /> @@ -261,7 +176,7 @@ function Home() { {/* Editor */}
>; + setSelectedFolder: (id: number | null) => void; selectedFolder: number | null; selectedNote: NoteRead | null; selectNote: (note: NoteRead) => void; diff --git a/frontend/src/stores/notesStore.ts b/frontend/src/stores/notesStore.ts index cc8a0e5..7f407fc 100644 --- a/frontend/src/stores/notesStore.ts +++ b/frontend/src/stores/notesStore.ts @@ -1,21 +1,30 @@ import { create } from "zustand"; 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"; interface NoteState { folderTree: FolderTreeResponse | null; selectedFolder: number | null; + selectedNote: NoteRead | null; loadFolderTree: () => Promise; createNote: (note: NoteCreate) => Promise; - createFolder: (folder: FolderCreate) => Promise; updateNote: (id: number, note: Partial) => Promise; + createFolder: (folder: FolderCreate) => Promise; + setSelectedFolder: (id: number | null) => void; + setSelectedNote: (id: NoteRead | null) => void; } export const useNoteStore = create()((set, get) => ({ folderTree: null, selectedFolder: null, + selectedNote: null, loadFolderTree: async () => { const data = await folderApi.tree(); @@ -36,4 +45,12 @@ export const useNoteStore = create()((set, get) => ({ await notesApi.update(id, note); await get().loadFolderTree(); }, + + setSelectedFolder: (id: number | null) => { + set({ selectedFolder: id }); + }, + + setSelectedNote: (id: NoteRead | null) => { + set({ selectedNote: id }); + }, }));