Introduce zustand store for notes
- Migrate Home to use store actions for creating notes and folders - Remove encrypted flag from NoteCreate interface - Add zustand to frontend dependencies and update package-lock Add Zustand store for notes
This commit is contained in:
parent
45dcbdaee9
commit
16313b961b
7 changed files with 223 additions and 31 deletions
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
51
frontend/package-lock.json
generated
51
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export interface NoteCreate {
|
|||
title: string;
|
||||
content: string;
|
||||
folder_id: number | null;
|
||||
encrypted: boolean;
|
||||
}
|
||||
|
||||
const createNote = async (note: NoteCreate) => {
|
||||
|
|
|
|||
125
frontend/src/components/sidebar/SideBar.tsx
Normal file
125
frontend/src/components/sidebar/SideBar.tsx
Normal file
|
|
@ -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<FolderTreeResponse | null>(null);
|
||||
const [newFolder, setNewFolder] = useState(false);
|
||||
const [newFolderText, setNewFolderText] = useState("");
|
||||
const newFolderRef = useRef<HTMLInputElement>(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 (
|
||||
<div
|
||||
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()}
|
||||
onTouchMove={(e) => e.preventDefault()}
|
||||
>
|
||||
<SidebarHeader />
|
||||
{/* New folder input */}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarHeader = () => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<FolderTreeResponse | null>(null);
|
||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||
const [selectedNote, setSelectedNote] = useState<NoteRead | null>(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);
|
||||
|
|
|
|||
39
frontend/src/stores/notesStore.ts
Normal file
39
frontend/src/stores/notesStore.ts
Normal file
|
|
@ -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<void>;
|
||||
createNote: (note: NoteCreate) => Promise<void>;
|
||||
createFolder: (folder: FolderCreate) => Promise<void>;
|
||||
updateNote: (id: number, note: Partial<Note>) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useNoteStore = create<NoteState>()((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<Note>) => {
|
||||
await notesApi.update(id, note);
|
||||
await get().loadFolderTree();
|
||||
},
|
||||
}));
|
||||
Loading…
Reference in a new issue