Refactor frontend: extract Editor and wire stores
This commit is contained in:
parent
b27604130a
commit
89fecc5c08
10 changed files with 265 additions and 335 deletions
|
|
@ -82,45 +82,18 @@ def update_folder(
|
|||
folder_id: int, folder_update: FolderUpdate, session: Session = Depends(get_session)
|
||||
):
|
||||
"""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)
|
||||
if not folder:
|
||||
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)
|
||||
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():
|
||||
print(f"Setting {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.commit()
|
||||
print(f"Committed changes to database")
|
||||
|
||||
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
|
||||
|
|
|
|||
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
119
frontend/src/components/editor/Editor.tsx
Normal file
119
frontend/src/components/editor/Editor.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,16 +2,11 @@ import React from "react";
|
|||
import { useDraggable } from "@dnd-kit/core";
|
||||
import { Note } from "../../api/notes";
|
||||
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({
|
||||
id: note.id,
|
||||
data: { type: "note", note },
|
||||
|
|
@ -26,14 +21,16 @@ export const DraggableNote = ({
|
|||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
<div
|
||||
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 ${
|
||||
selectedNote?.id === note.id
|
||||
? "bg-ctp-mauve text-ctp-base"
|
||||
: "hover:bg-ctp-surface1"
|
||||
}`}
|
||||
>
|
||||
<span>{note.title}</span>
|
||||
<span>
|
||||
{selectedNote?.id == note.id ? selectedNote.title : note.title}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
import React from "react";
|
||||
import { useDroppable, useDraggable } from "@dnd-kit/core";
|
||||
import { Folder, NoteRead } from "../../api/folders";
|
||||
import { useNoteStore } from "../../stores/notesStore";
|
||||
|
||||
export const DroppableFolder = ({
|
||||
folder,
|
||||
setSelectedFolder,
|
||||
selectedFolder,
|
||||
selectedNote,
|
||||
setCollapse,
|
||||
collapse,
|
||||
}: {
|
||||
folder: Partial<Folder>;
|
||||
setSelectedFolder: (id: number | null) => void;
|
||||
selectedFolder: number | null;
|
||||
selectedNote: NoteRead | null;
|
||||
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
collapse: boolean;
|
||||
}) => {
|
||||
const { setSelectedFolder, selectedFolder, selectedNote } = useNoteStore();
|
||||
|
||||
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
||||
id: folder.id!,
|
||||
data: { type: "folder", folder },
|
||||
|
|
|
|||
42
frontend/src/components/sidebar/RecursiveFolder.tsx
Normal file
42
frontend/src/components/sidebar/RecursiveFolder.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { notesApi } from "../../api/notes";
|
||||
import { RecursiveFolder } from "./RecursiveFolder";
|
||||
|
||||
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||
|
|
@ -143,15 +144,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
|||
{/* 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={setSelectedNote}
|
||||
/>
|
||||
<RecursiveFolder key={folder.id} folder={folder} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -162,12 +155,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
|||
Unsorted
|
||||
</div>*/}
|
||||
{folderTree.orphaned_notes.map((note) => (
|
||||
<DraggableNote
|
||||
key={note.id}
|
||||
note={note}
|
||||
selectNote={setSelectedNote}
|
||||
selectedNote={selectedNote}
|
||||
/>
|
||||
<DraggableNote key={note.id} note={note} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -205,65 +193,3 @@ export const SidebarHeader = ({
|
|||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,21 +46,8 @@ 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",
|
||||
presets: [
|
||||
{
|
||||
label: "React",
|
||||
name: "react",
|
||||
meta: "live react",
|
||||
sandpackTemplate: "react",
|
||||
sandpackTheme: "dark",
|
||||
snippetFileName: "/App.js",
|
||||
snippetLanguage: "jsx",
|
||||
},
|
||||
],
|
||||
};
|
||||
import { Editor } from "../components/editor/Editor";
|
||||
import { useUIStore } from "../stores/uiStore";
|
||||
|
||||
function Home() {
|
||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||
|
|
@ -71,7 +58,7 @@ function Home() {
|
|||
const [newFolderText, setNewFolderText] = useState("");
|
||||
// const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
|
||||
const [encrypted, setEncrypted] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
// const [updating, setUpdating] = useState(false);
|
||||
|
||||
const {
|
||||
setSelectedFolder,
|
||||
|
|
@ -85,7 +72,7 @@ function Home() {
|
|||
selectedNote,
|
||||
} = useNoteStore();
|
||||
|
||||
|
||||
const { updating } = useUIStore();
|
||||
|
||||
const newFolderRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
|
@ -99,31 +86,11 @@ function Home() {
|
|||
}
|
||||
}, [newFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedNote) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
setUpdating(true);
|
||||
handleUpdate();
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [content, title]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!title.trim()) return;
|
||||
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) => {
|
||||
await notesApi.delete(id);
|
||||
loadFolderTree();
|
||||
|
|
@ -136,193 +103,71 @@ function Home() {
|
|||
setContent("");
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
|
||||
<div className="flex bg-ctp-base h-screen text-ctp-text overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<Sidebar clearSelection={clearSelection} />
|
||||
|
||||
<Sidebar clearSelection={clearSelection} />
|
||||
{/* Main editor area */}
|
||||
<div className="flex flex-col w-full h-screen overflow-hidden">
|
||||
{/* Top accent bar */}
|
||||
<div className="w-full bg-ctp-crust h-1 shrink-0"></div>
|
||||
|
||||
{/* Main editor area */}
|
||||
<div className="flex flex-col w-full h-screen overflow-hidden">
|
||||
{/* Top accent bar */}
|
||||
<div className="w-full bg-ctp-crust h-1 shrink-0"></div>
|
||||
<Editor />
|
||||
|
||||
{/* Content area with padding */}
|
||||
<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 */}
|
||||
<div className="flex items-center gap-3 px-8 py-4 border-t border-ctp-surface2 bg-ctp-mantle shrink-0">
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="px-2 py-0.5 bg-ctp-surface0 text-ctp-text rounded-lg hover:bg-ctp-surface1 transition-colors font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="px-2 py-0.5 bg-ctp-green text-ctp-base rounded-lg hover:bg-ctp-teal transition-colors font-medium shadow-sm"
|
||||
>
|
||||
Create Note
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="fixed bottom-4 right-4 bg-ctp-surface0 border border-ctp-surface2 rounded-lg px-2 py-0.5 flex items-center gap-2.5 shadow-lg backdrop-blur-sm">
|
||||
{updating ? (
|
||||
{/* Action bar */}
|
||||
<div className="flex items-center gap-3 px-8 py-4 border-t border-ctp-surface2 bg-ctp-mantle shrink-0">
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" />
|
||||
<span className="text-sm text-ctp-subtext0 font-medium">
|
||||
Saving...
|
||||
</span>
|
||||
{/*<button
|
||||
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"
|
||||
>
|
||||
Save
|
||||
</button>*/}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="px-2 py-0.5 bg-ctp-surface0 text-ctp-text rounded-lg hover:bg-ctp-surface1 transition-colors font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
Saved
|
||||
</span>
|
||||
</>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="px-2 py-0.5 bg-ctp-green text-ctp-base rounded-lg hover:bg-ctp-teal transition-colors font-medium shadow-sm"
|
||||
>
|
||||
Create Note
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="fixed bottom-4 right-4 bg-ctp-surface0 border border-ctp-surface2 rounded-lg px-2 py-0.5 flex items-center gap-2.5 shadow-lg backdrop-blur-sm">
|
||||
{updating ? (
|
||||
<>
|
||||
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" />
|
||||
<span className="text-sm text-ctp-subtext0 font-medium">
|
||||
Saving...
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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">Saved</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ import {
|
|||
NoteRead,
|
||||
} from "../api/folders";
|
||||
import { Note, NoteCreate, notesApi } from "../api/notes";
|
||||
import { getSelectedNode } from "@mdxeditor/editor";
|
||||
|
||||
interface NoteState {
|
||||
folderTree: FolderTreeResponse | null;
|
||||
selectedFolder: number | null;
|
||||
selectedNote: NoteRead | null;
|
||||
|
||||
setContent: (content: string) => void;
|
||||
setTitle: (title: string) => void;
|
||||
loadFolderTree: () => 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>;
|
||||
setSelectedFolder: (id: number | null) => void;
|
||||
setSelectedNote: (id: NoteRead | null) => void;
|
||||
|
|
@ -26,6 +29,20 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
|
|||
selectedFolder: 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 () => {
|
||||
const data = await folderApi.tree();
|
||||
set({ folderTree: data });
|
||||
|
|
@ -41,7 +58,8 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
|
|||
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 get().loadFolderTree();
|
||||
},
|
||||
|
|
|
|||
13
frontend/src/stores/uiStore.ts
Normal file
13
frontend/src/stores/uiStore.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
Loading…
Reference in a new issue