2025-11-23 09:08:01 +00:00
|
|
|
import {
|
|
|
|
|
codeBlockPlugin,
|
|
|
|
|
codeMirrorPlugin,
|
|
|
|
|
headingsPlugin,
|
|
|
|
|
linkPlugin,
|
|
|
|
|
listsPlugin,
|
|
|
|
|
markdownShortcutPlugin,
|
|
|
|
|
MDXEditor,
|
|
|
|
|
quotePlugin,
|
|
|
|
|
SandpackConfig,
|
|
|
|
|
sandpackPlugin,
|
|
|
|
|
thematicBreakPlugin,
|
|
|
|
|
} from "@mdxeditor/editor";
|
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
import {
|
|
|
|
|
folderApi,
|
|
|
|
|
FolderCreate,
|
|
|
|
|
FolderTreeNode,
|
|
|
|
|
FolderTreeResponse,
|
|
|
|
|
NoteRead,
|
|
|
|
|
} from "../api/folders";
|
|
|
|
|
import { NoteCreate, notesApi } from "../api/notes";
|
|
|
|
|
import "../main.css";
|
2025-11-23 15:14:48 +00:00
|
|
|
import {
|
|
|
|
|
DndContext,
|
|
|
|
|
DragEndEvent,
|
|
|
|
|
PointerSensor,
|
|
|
|
|
useSensor,
|
|
|
|
|
useSensors,
|
|
|
|
|
} from "@dnd-kit/core";
|
2025-11-23 09:08:01 +00:00
|
|
|
|
|
|
|
|
import "@mdxeditor/editor/style.css";
|
|
|
|
|
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
|
|
|
|
|
import { DraggableNote } from "../components/sidebar/DraggableNote";
|
|
|
|
|
|
|
|
|
|
const simpleSandpackConfig: SandpackConfig = {
|
|
|
|
|
defaultPreset: "react",
|
|
|
|
|
presets: [
|
|
|
|
|
{
|
|
|
|
|
label: "React",
|
|
|
|
|
name: "react",
|
|
|
|
|
meta: "live react",
|
|
|
|
|
sandpackTemplate: "react",
|
|
|
|
|
sandpackTheme: "dark",
|
|
|
|
|
snippetFileName: "/App.js",
|
|
|
|
|
snippetLanguage: "jsx",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function Home() {
|
|
|
|
|
const [folderTree, setFolderTree] = useState<FolderTreeResponse | null>(null);
|
|
|
|
|
const [selectedNote, setSelectedNote] = useState<NoteRead | null>(null);
|
|
|
|
|
const [title, setTitle] = useState("");
|
|
|
|
|
const [content, setContent] = useState("#");
|
|
|
|
|
const [newFolder, setNewFolder] = useState(false);
|
|
|
|
|
const [newFolderText, setNewFolderText] = useState("");
|
|
|
|
|
const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
|
2025-11-23 15:14:48 +00:00
|
|
|
const [encrypted, setEncrypted] = useState(false);
|
|
|
|
|
const pointer = useSensor(PointerSensor, {
|
|
|
|
|
activationConstraint: {
|
|
|
|
|
distance: 30,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const sensors = useSensors(pointer);
|
2025-11-23 09:08:01 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
loadFolderTree();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const loadFolderTree = async () => {
|
2025-11-24 19:48:46 +00:00
|
|
|
const data = await folderApi.tree();
|
2025-11-23 09:08:01 +00:00
|
|
|
setFolderTree(data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreate = async () => {
|
|
|
|
|
if (!title.trim()) return;
|
2025-11-23 15:14:48 +00:00
|
|
|
const newNote: NoteCreate = {
|
|
|
|
|
title,
|
|
|
|
|
content,
|
|
|
|
|
folder_id: selectedFolder,
|
|
|
|
|
encrypted,
|
|
|
|
|
};
|
2025-11-23 09:08:01 +00:00
|
|
|
await notesApi.create(newNote);
|
|
|
|
|
setTitle("");
|
|
|
|
|
setContent("#");
|
|
|
|
|
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 handleUpdate = async () => {
|
|
|
|
|
if (!selectedNote) return;
|
|
|
|
|
await notesApi.update(selectedNote.id, { title, content });
|
|
|
|
|
setSelectedNote(null);
|
|
|
|
|
setTitle("");
|
|
|
|
|
setContent("#");
|
|
|
|
|
loadFolderTree();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: number) => {
|
|
|
|
|
await notesApi.delete(id);
|
|
|
|
|
loadFolderTree();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectNote = (note: NoteRead) => {
|
2025-11-24 19:48:46 +00:00
|
|
|
console.log(note);
|
2025-11-23 09:08:01 +00:00
|
|
|
setSelectedNote(note);
|
|
|
|
|
setTitle(note.title);
|
|
|
|
|
setContent(note.content);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearSelection = () => {
|
|
|
|
|
setSelectedNote(null);
|
|
|
|
|
setTitle("");
|
|
|
|
|
setContent("#");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const newFolderRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (newFolder && newFolderRef.current) {
|
|
|
|
|
newFolderRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [newFolder]);
|
|
|
|
|
|
|
|
|
|
const renderFolder = (folder: FolderTreeNode, depth: number = 0) => (
|
|
|
|
|
<div
|
|
|
|
|
key={folder.id}
|
|
|
|
|
style={{ marginLeft: depth > 0 ? "1rem" : "0" }}
|
|
|
|
|
className="flex flex-col"
|
|
|
|
|
>
|
|
|
|
|
<DroppableFolder
|
|
|
|
|
key={folder.id}
|
|
|
|
|
folder={folder}
|
|
|
|
|
setSelectedFolder={setSelectedFolder}
|
|
|
|
|
selectedFolder={selectedFolder}
|
|
|
|
|
selectedNote={selectedNote}
|
|
|
|
|
/>
|
2025-11-24 19:48:46 +00:00
|
|
|
<div className="flex flex-col ml-5">
|
|
|
|
|
{folder.notes.map((note) => (
|
|
|
|
|
<DraggableNote
|
|
|
|
|
key={note.id}
|
|
|
|
|
note={note}
|
|
|
|
|
selectNote={selectNote}
|
|
|
|
|
selectedNote={selectedNote}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-11-23 09:08:01 +00:00
|
|
|
{folder.children.map((child) => renderFolder(child, depth + 1))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = async (event: DragEndEvent) => {
|
|
|
|
|
const { active, over } = event;
|
|
|
|
|
if (!over) return;
|
|
|
|
|
|
|
|
|
|
console.log(active.data);
|
|
|
|
|
console.log(over.data);
|
|
|
|
|
|
|
|
|
|
await notesApi.update(active.id as number, {
|
|
|
|
|
folder_id: over.id as number,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
loadFolderTree();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-23 15:14:48 +00:00
|
|
|
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
|
2025-11-23 09:08:01 +00:00
|
|
|
<div className="flex bg-ctp-base min-h-screen text-ctp-text">
|
|
|
|
|
<div
|
2025-11-24 19:48:46 +00:00
|
|
|
className="bg-ctp-mantle border-r-ctp-surface2 border-r overflow-hidden w-[300px] p-4 overflow-y-auto sm:block hidden"
|
|
|
|
|
onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
onTouchMove={(e) => e.preventDefault()}
|
2025-11-23 09:08:01 +00:00
|
|
|
>
|
|
|
|
|
<h2>Notes</h2>
|
|
|
|
|
<button
|
|
|
|
|
onClick={clearSelection}
|
|
|
|
|
style={{ marginBottom: "1rem", width: "100%" }}
|
|
|
|
|
>
|
|
|
|
|
New Note
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex gap-2 mb-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (newFolder && newFolderRef.current) {
|
|
|
|
|
newFolderRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
setNewFolder(true);
|
|
|
|
|
}}
|
|
|
|
|
className="hover:bg-ctp-mauve group transition-colors rounded-md p-1 text-center flex"
|
|
|
|
|
>
|
|
|
|
|
<i className="fadr fa-folder-plus text-xl 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 text-center flex"
|
|
|
|
|
>
|
|
|
|
|
<i className="fadr fa-file-circle-plus text-xl text-ctp-mauve group-hover:text-ctp-base transition-colors"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{newFolder && (
|
|
|
|
|
<div className="px-1 mb-1">
|
|
|
|
|
<input
|
|
|
|
|
onBlur={() => setNewFolder(false)}
|
|
|
|
|
onChange={(e) => setNewFolderText(e.target.value)}
|
|
|
|
|
value={newFolderText}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="new folder"
|
|
|
|
|
className="border-ctp-mauve border rounded-md px-2 w-full focus:outline-none focus:ring-1 focus:ring-ctp-mauve focus:border-ctp-mauve bg-ctp-base"
|
|
|
|
|
ref={newFolderRef}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
handleCreateFolder();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Render folder tree */}
|
|
|
|
|
{folderTree?.folders.map((folder) => renderFolder(folder))}
|
|
|
|
|
|
|
|
|
|
{/* Render orphaned notes */}
|
|
|
|
|
{folderTree?.orphaned_notes &&
|
|
|
|
|
folderTree.orphaned_notes.length > 0 && (
|
2025-11-24 19:48:46 +00:00
|
|
|
<div className="mt-4 flex flex-col">
|
2025-11-23 09:08:01 +00:00
|
|
|
<div className="text-ctp-subtext0 text-sm mb-1">Unsorted</div>
|
|
|
|
|
{folderTree.orphaned_notes.map((note) => (
|
2025-11-24 19:48:46 +00:00
|
|
|
<DraggableNote
|
2025-11-23 09:08:01 +00:00
|
|
|
key={note.id}
|
2025-11-24 19:48:46 +00:00
|
|
|
note={note}
|
|
|
|
|
selectNote={selectNote}
|
|
|
|
|
selectedNote={selectedNote}
|
|
|
|
|
/>
|
2025-11-23 09:08:01 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-11-24 19:48:46 +00:00
|
|
|
|
|
|
|
|
<div className="flex flex-col w-full">
|
|
|
|
|
<div className="w-full bg-ctp-crust h-4"></div>
|
2025-11-23 09:08:01 +00:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Note title..."
|
|
|
|
|
value={title}
|
|
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
|
|
style={{
|
|
|
|
|
padding: "0.5rem",
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
fontSize: "1.5rem",
|
|
|
|
|
border: "1px solid #ccc",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<MDXEditor
|
|
|
|
|
markdown={content}
|
|
|
|
|
key={selectedNote?.id || "new"}
|
|
|
|
|
onChange={setContent}
|
|
|
|
|
className="prose text-ctp-text"
|
|
|
|
|
plugins={[
|
|
|
|
|
headingsPlugin(),
|
|
|
|
|
listsPlugin(),
|
|
|
|
|
quotePlugin(),
|
|
|
|
|
thematicBreakPlugin(),
|
|
|
|
|
linkPlugin(),
|
|
|
|
|
codeBlockPlugin({ defaultCodeBlockLanguage: "js" }),
|
|
|
|
|
sandpackPlugin({ sandpackConfig: simpleSandpackConfig }),
|
|
|
|
|
codeMirrorPlugin({
|
|
|
|
|
codeBlockLanguages: { js: "JavaScript", css: "CSS" },
|
|
|
|
|
}),
|
|
|
|
|
markdownShortcutPlugin(),
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: "1rem", display: "flex", gap: "0.5rem" }}>
|
|
|
|
|
{selectedNote ? (
|
|
|
|
|
<>
|
|
|
|
|
<button onClick={handleUpdate}>Update Note</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleDelete(selectedNote.id)}
|
|
|
|
|
className="bg-ctp-red rounded-md px-1 text-ctp-crust"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={clearSelection}>Cancel</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<button onClick={handleCreate}>Create Note</button>
|
|
|
|
|
)}
|
2025-11-23 15:14:48 +00:00
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={encrypted}
|
|
|
|
|
onChange={() => setEncrypted(!encrypted)}
|
|
|
|
|
/>
|
2025-11-23 09:08:01 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DndContext>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Home;
|