import { BoldItalicUnderlineToggles, codeBlockPlugin, codeMirrorPlugin, diffSourcePlugin, headingsPlugin, imagePlugin, linkPlugin, listsPlugin, markdownShortcutPlugin, MDXEditor, quotePlugin, SandpackConfig, sandpackPlugin, tablePlugin, thematicBreakPlugin, toolbarPlugin, UndoRedo, DiffSourceToggleWrapper, } from "@mdxeditor/editor"; import { SetStateAction, useEffect, useRef, useState } from "react"; import { folderApi, FolderCreate, FolderTreeNode, FolderTreeResponse, NoteRead, } from "../api/folders"; import { NoteCreate, notesApi } from "../api/notes"; import "../main.css"; import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import "@mdxeditor/editor/style.css"; 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"; 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(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 [encrypted, setEncrypted] = useState(false); const [updating, setUpdating] = useState(false); const pointer = useSensor(PointerSensor, { activationConstraint: { distance: 30, }, }); const sensors = useSensors(pointer); const newFolderRef = useRef(null); useEffect(() => { loadFolderTree(); }, []); useEffect(() => { if (newFolder && newFolderRef.current) { newFolderRef.current.focus(); } }, [newFolder]); useEffect(() => { if (!selectedNote) return; const timer = setTimeout(async () => { setUpdating(true); handleUpdate(); }, 2000); 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(); }; 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 }); loadFolderTree(); setTimeout(() => { setUpdating(false); }, 1000); }; const handleDelete = async (id: number) => { await notesApi.delete(id); loadFolderTree(); 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(""); setContent(""); }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over) return; await notesApi.update(active.id as number, { folder_id: over.id as number, }); loadFolderTree(); }; return (
{/* 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 */}
{/* Top accent bar */}
{/* Content area with padding */}
{/* Title input */} 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 */}
( <> ), }), 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", }), ]} />
{/* Action bar */}
{selectedNote ? ( <> ) : ( )} {/* Encryption toggle */} {/**/}
{/* Status indicator */}
{updating ? ( <> Saving... ) : ( <> Saved )}
); } export default Home; interface RenderFolderProps { folder: FolderTreeNode; depth?: number; setSelectedFolder: React.Dispatch>; 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) => ( ))} )}
); };