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)
|
folder_id: int, folder_update: FolderUpdate, session: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
"""Update a folder"""
|
"""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)
|
folder = session.get(Folder, folder_id)
|
||||||
if not folder:
|
if not folder:
|
||||||
raise HTTPException(status_code=404, detail="Folder not found")
|
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)
|
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():
|
for key, value in update_data.items():
|
||||||
print(f"Setting {key} = {value}")
|
|
||||||
setattr(folder, 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.add(folder)
|
||||||
session.commit()
|
session.commit()
|
||||||
print(f"Committed changes to database")
|
|
||||||
|
|
||||||
session.refresh(folder)
|
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
|
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 { useDraggable } from "@dnd-kit/core";
|
||||||
import { Note } from "../../api/notes";
|
import { Note } from "../../api/notes";
|
||||||
import { NoteRead } from "../../api/folders";
|
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({
|
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
data: { type: "note", note },
|
data: { type: "note", note },
|
||||||
|
|
@ -26,14 +21,16 @@ export const DraggableNote = ({
|
||||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||||
<div
|
<div
|
||||||
key={note.id}
|
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 ${
|
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
|
selectedNote?.id === note.id
|
||||||
? "bg-ctp-mauve text-ctp-base"
|
? "bg-ctp-mauve text-ctp-base"
|
||||||
: "hover:bg-ctp-surface1"
|
: "hover:bg-ctp-surface1"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{note.title}</span>
|
<span>
|
||||||
|
{selectedNote?.id == note.id ? selectedNote.title : note.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDroppable, useDraggable } from "@dnd-kit/core";
|
import { useDroppable, useDraggable } from "@dnd-kit/core";
|
||||||
import { Folder, NoteRead } from "../../api/folders";
|
import { Folder, NoteRead } from "../../api/folders";
|
||||||
|
import { useNoteStore } from "../../stores/notesStore";
|
||||||
|
|
||||||
export const DroppableFolder = ({
|
export const DroppableFolder = ({
|
||||||
folder,
|
folder,
|
||||||
setSelectedFolder,
|
|
||||||
selectedFolder,
|
|
||||||
selectedNote,
|
|
||||||
setCollapse,
|
setCollapse,
|
||||||
collapse,
|
collapse,
|
||||||
}: {
|
}: {
|
||||||
folder: Partial<Folder>;
|
folder: Partial<Folder>;
|
||||||
setSelectedFolder: (id: number | null) => void;
|
|
||||||
selectedFolder: number | null;
|
|
||||||
selectedNote: NoteRead | null;
|
|
||||||
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
setCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
collapse: boolean;
|
collapse: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { setSelectedFolder, selectedFolder, selectedNote } = useNoteStore();
|
||||||
|
|
||||||
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
const { isOver, setNodeRef: setDroppableRef } = useDroppable({
|
||||||
id: folder.id!,
|
id: folder.id!,
|
||||||
data: { type: "folder", folder },
|
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,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { notesApi } from "../../api/notes";
|
import { notesApi } from "../../api/notes";
|
||||||
|
import { RecursiveFolder } from "./RecursiveFolder";
|
||||||
|
|
||||||
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||||
|
|
@ -143,15 +144,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||||
{/* Folder tree */}
|
{/* Folder tree */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{folderTree?.folders.map((folder) => (
|
{folderTree?.folders.map((folder) => (
|
||||||
<RenderFolder
|
<RecursiveFolder key={folder.id} folder={folder} depth={0} />
|
||||||
key={folder.id}
|
|
||||||
folder={folder}
|
|
||||||
depth={0}
|
|
||||||
setSelectedFolder={setSelectedFolder}
|
|
||||||
selectedFolder={selectedFolder}
|
|
||||||
selectedNote={selectedNote}
|
|
||||||
selectNote={setSelectedNote}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -162,12 +155,7 @@ export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
||||||
Unsorted
|
Unsorted
|
||||||
</div>*/}
|
</div>*/}
|
||||||
{folderTree.orphaned_notes.map((note) => (
|
{folderTree.orphaned_notes.map((note) => (
|
||||||
<DraggableNote
|
<DraggableNote key={note.id} note={note} />
|
||||||
key={note.id}
|
|
||||||
note={note}
|
|
||||||
selectNote={setSelectedNote}
|
|
||||||
selectedNote={selectedNote}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -205,65 +193,3 @@ 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -46,21 +46,8 @@ 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";
|
import { Sidebar } from "../components/sidebar/SideBar";
|
||||||
|
import { Editor } from "../components/editor/Editor";
|
||||||
const simpleSandpackConfig: SandpackConfig = {
|
import { useUIStore } from "../stores/uiStore";
|
||||||
defaultPreset: "react",
|
|
||||||
presets: [
|
|
||||||
{
|
|
||||||
label: "React",
|
|
||||||
name: "react",
|
|
||||||
meta: "live react",
|
|
||||||
sandpackTemplate: "react",
|
|
||||||
sandpackTheme: "dark",
|
|
||||||
snippetFileName: "/App.js",
|
|
||||||
snippetLanguage: "jsx",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
// const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
||||||
|
|
@ -71,7 +58,7 @@ function Home() {
|
||||||
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 {
|
const {
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
|
|
@ -85,7 +72,7 @@ function Home() {
|
||||||
selectedNote,
|
selectedNote,
|
||||||
} = useNoteStore();
|
} = useNoteStore();
|
||||||
|
|
||||||
|
const { updating } = useUIStore();
|
||||||
|
|
||||||
const newFolderRef = useRef<HTMLInputElement>(null);
|
const newFolderRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -99,31 +86,11 @@ function Home() {
|
||||||
}
|
}
|
||||||
}, [newFolder]);
|
}, [newFolder]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedNote) return;
|
|
||||||
|
|
||||||
const timer = setTimeout(async () => {
|
|
||||||
setUpdating(true);
|
|
||||||
handleUpdate();
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [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 handleUpdate = async () => {
|
|
||||||
if (!selectedNote) return;
|
|
||||||
await updateNote(selectedNote.id, { title, content });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setUpdating(false);
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
await notesApi.delete(id);
|
await notesApi.delete(id);
|
||||||
loadFolderTree();
|
loadFolderTree();
|
||||||
|
|
@ -136,193 +103,71 @@ function Home() {
|
||||||
setContent("");
|
setContent("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
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 clearSelection={clearSelection} />
|
||||||
{/* Sidebar */}
|
|
||||||
|
|
||||||
<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 */}
|
<Editor />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Content area with padding */}
|
{/* Action bar */}
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto px-8 py-6">
|
<div className="flex items-center gap-3 px-8 py-4 border-t border-ctp-surface2 bg-ctp-mantle shrink-0">
|
||||||
{/* Title input */}
|
{selectedNote ? (
|
||||||
<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 ? (
|
|
||||||
<>
|
<>
|
||||||
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" />
|
{/*<button
|
||||||
<span className="text-sm text-ctp-subtext0 font-medium">
|
onClick={handleUpdate}
|
||||||
Saving...
|
className="px-2 py-0.5 bg-ctp-blue text-ctp-base rounded-lg hover:bg-ctp-sapphire transition-colors font-medium shadow-sm"
|
||||||
</span>
|
>
|
||||||
|
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
|
||||||
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" />
|
onClick={handleCreate}
|
||||||
<span className="text-sm text-ctp-subtext0 font-medium">
|
className="px-2 py-0.5 bg-ctp-green text-ctp-base rounded-lg hover:bg-ctp-teal transition-colors font-medium shadow-sm"
|
||||||
Saved
|
>
|
||||||
</span>
|
Create Note
|
||||||
</>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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;
|
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,
|
NoteRead,
|
||||||
} from "../api/folders";
|
} from "../api/folders";
|
||||||
import { Note, NoteCreate, notesApi } from "../api/notes";
|
import { Note, NoteCreate, notesApi } from "../api/notes";
|
||||||
|
import { getSelectedNode } from "@mdxeditor/editor";
|
||||||
|
|
||||||
interface NoteState {
|
interface NoteState {
|
||||||
folderTree: FolderTreeResponse | null;
|
folderTree: FolderTreeResponse | null;
|
||||||
selectedFolder: number | null;
|
selectedFolder: number | null;
|
||||||
selectedNote: NoteRead | null;
|
selectedNote: NoteRead | null;
|
||||||
|
|
||||||
|
setContent: (content: string) => void;
|
||||||
|
setTitle: (title: string) => void;
|
||||||
loadFolderTree: () => Promise<void>;
|
loadFolderTree: () => Promise<void>;
|
||||||
createNote: (note: NoteCreate) => 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>;
|
createFolder: (folder: FolderCreate) => Promise<void>;
|
||||||
setSelectedFolder: (id: number | null) => void;
|
setSelectedFolder: (id: number | null) => void;
|
||||||
setSelectedNote: (id: NoteRead | null) => void;
|
setSelectedNote: (id: NoteRead | null) => void;
|
||||||
|
|
@ -26,6 +29,20 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
|
||||||
selectedFolder: null,
|
selectedFolder: null,
|
||||||
selectedNote: 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 () => {
|
loadFolderTree: async () => {
|
||||||
const data = await folderApi.tree();
|
const data = await folderApi.tree();
|
||||||
set({ folderTree: data });
|
set({ folderTree: data });
|
||||||
|
|
@ -41,7 +58,8 @@ export const useNoteStore = create<NoteState>()((set, get) => ({
|
||||||
await get().loadFolderTree();
|
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 notesApi.update(id, note);
|
||||||
await get().loadFolderTree();
|
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