2025-11-30 18:01:57 +00:00
|
|
|
import React, { useState, useRef, useEffect, SetStateAction } from "react";
|
2025-12-08 22:08:30 +00:00
|
|
|
// @ts-ignore
|
|
|
|
|
import FolderPlusIcon from "../../assets/fontawesome/svg/folder-plus.svg?react";
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import FileCirclePlusIcon from "../../assets/fontawesome/svg/file-circle-plus.svg?react";
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import FolderIcon from "../../assets/fontawesome/svg/folder.svg?react";
|
2025-11-29 12:45:41 +00:00
|
|
|
import { DraggableNote } from "./DraggableNote";
|
2025-11-30 18:01:57 +00:00
|
|
|
import { useNoteStore } from "../../stores/notesStore";
|
2025-11-30 19:40:10 +00:00
|
|
|
import {
|
|
|
|
|
DndContext,
|
|
|
|
|
DragEndEvent,
|
2025-12-08 22:08:30 +00:00
|
|
|
DragOverlay,
|
|
|
|
|
DragStartEvent,
|
2025-11-30 19:40:10 +00:00
|
|
|
PointerSensor,
|
|
|
|
|
useSensor,
|
|
|
|
|
useSensors,
|
|
|
|
|
} from "@dnd-kit/core";
|
2025-12-08 22:08:30 +00:00
|
|
|
|
2025-11-30 20:32:22 +00:00
|
|
|
import { RecursiveFolder } from "./RecursiveFolder";
|
2025-12-08 22:08:30 +00:00
|
|
|
import { useAuthStore } from "../../stores/authStore";
|
|
|
|
|
import { useUIStore } from "../../stores/uiStore";
|
|
|
|
|
import { NoteRead } from "../../api/folders";
|
2025-11-29 12:45:41 +00:00
|
|
|
|
2025-11-30 18:01:57 +00:00
|
|
|
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
|
2025-11-29 12:45:41 +00:00
|
|
|
const [newFolder, setNewFolder] = useState(false);
|
|
|
|
|
const [newFolderText, setNewFolderText] = useState("");
|
2025-12-08 22:08:30 +00:00
|
|
|
const [activeItem, setActiveItem] = useState<{
|
|
|
|
|
type: "note" | "folder";
|
|
|
|
|
data: any;
|
|
|
|
|
} | null>(null);
|
2025-11-29 12:45:41 +00:00
|
|
|
const newFolderRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
2025-11-30 18:01:57 +00:00
|
|
|
const {
|
|
|
|
|
folderTree,
|
|
|
|
|
loadFolderTree,
|
2025-12-08 22:08:30 +00:00
|
|
|
moveNoteToFolder,
|
|
|
|
|
moveFolderToFolder,
|
|
|
|
|
createFolder,
|
2025-11-30 18:01:57 +00:00
|
|
|
} = useNoteStore();
|
|
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
const { isAuthenticated } = useAuthStore();
|
|
|
|
|
|
|
|
|
|
const { setSideBarResize, sideBarResize } = useUIStore();
|
2025-11-29 12:45:41 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (newFolder && newFolderRef.current) {
|
|
|
|
|
newFolderRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [newFolder]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-08 22:08:30 +00:00
|
|
|
// if (!isAuthenticated) return;
|
2025-11-29 12:45:41 +00:00
|
|
|
loadFolderTree();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleCreateFolder = async () => {
|
|
|
|
|
if (!newFolderText.trim()) return;
|
2025-12-08 22:08:30 +00:00
|
|
|
await createFolder({
|
2025-11-29 12:45:41 +00:00
|
|
|
name: newFolderText,
|
|
|
|
|
parent_id: null,
|
2025-12-08 22:08:30 +00:00
|
|
|
});
|
2025-11-29 12:45:41 +00:00
|
|
|
setNewFolderText("");
|
|
|
|
|
setNewFolder(false);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-30 19:40:10 +00:00
|
|
|
const pointer = useSensor(PointerSensor, {
|
|
|
|
|
activationConstraint: {
|
|
|
|
|
distance: 30,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const sensors = useSensors(pointer);
|
2025-11-29 12:45:41 +00:00
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|
|
|
|
const { active } = event;
|
|
|
|
|
if (active.data.current?.type === "note") {
|
|
|
|
|
setActiveItem({ type: "note", data: active.data.current.note });
|
|
|
|
|
} else if (active.data.current?.type === "folder") {
|
|
|
|
|
setActiveItem({ type: "folder", data: active.data.current.folder });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-30 19:40:10 +00:00
|
|
|
const handleDragEnd = async (event: DragEndEvent) => {
|
2025-12-08 22:08:30 +00:00
|
|
|
setActiveItem(null);
|
2025-11-30 19:40:10 +00:00
|
|
|
const { active, over } = event;
|
|
|
|
|
if (!over) return;
|
2025-11-29 12:45:41 +00:00
|
|
|
|
2025-11-30 19:40:10 +00:00
|
|
|
console.log("Drag ended:", {
|
|
|
|
|
activeId: active.id,
|
|
|
|
|
activeType: active.data.current?.type,
|
|
|
|
|
activeFolder: active.data.current?.folder,
|
|
|
|
|
overId: over.id,
|
|
|
|
|
overType: over.data.current?.type,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (active.data.current?.type === "note") {
|
|
|
|
|
console.log("Updating note", active.id, "to folder", over.id);
|
2025-12-08 22:08:30 +00:00
|
|
|
await moveNoteToFolder(active.id as number, over.id as number);
|
2025-11-30 19:40:10 +00:00
|
|
|
} else if (active.data.current?.type === "folder") {
|
|
|
|
|
// Prevent dropping folder into itself
|
|
|
|
|
if (active.data.current.folder.id === over.id) {
|
|
|
|
|
console.log("Cannot drop folder into itself");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
"Updating folder",
|
|
|
|
|
active.data.current.folder.id,
|
|
|
|
|
"parent to",
|
|
|
|
|
over.id,
|
|
|
|
|
);
|
|
|
|
|
try {
|
2025-12-08 22:08:30 +00:00
|
|
|
await moveFolderToFolder(
|
|
|
|
|
active.data.current.folder.id,
|
|
|
|
|
over.id as number,
|
|
|
|
|
);
|
2025-11-30 19:40:10 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to update folder:", error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 22:08:30 +00:00
|
|
|
};
|
2025-11-30 19:40:10 +00:00
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
|
|
|
setIsResizing(true);
|
|
|
|
|
e.preventDefault();
|
2025-11-30 19:40:10 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
|
|
|
if (!isResizing) return;
|
|
|
|
|
|
|
|
|
|
// Calculate new width based on mouse position from the left edge
|
|
|
|
|
const newWidth = e.clientX;
|
|
|
|
|
|
|
|
|
|
if (newWidth >= 200 && newWidth <= 500) {
|
|
|
|
|
setSideBarResize(newWidth);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
setIsResizing(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isResizing) {
|
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
|
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
|
};
|
|
|
|
|
}, [isResizing]);
|
|
|
|
|
|
2025-11-30 19:40:10 +00:00
|
|
|
return (
|
2025-12-08 22:08:30 +00:00
|
|
|
<DndContext
|
|
|
|
|
onDragStart={handleDragStart}
|
|
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
|
autoScroll={false}
|
|
|
|
|
sensors={sensors}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex-row-reverse flex">
|
|
|
|
|
<div
|
|
|
|
|
className="h-screen bg-ctp-surface0 w-0.5 hover:cursor-ew-resize hover:bg-ctp-mauve transition-colors"
|
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
className="flex flex-col min-h-full"
|
|
|
|
|
style={{ width: `${sideBarResize}px` }}
|
|
|
|
|
>
|
|
|
|
|
<SidebarHeader
|
|
|
|
|
clearSelection={clearSelection}
|
|
|
|
|
setNewFolder={setNewFolder}
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3"
|
|
|
|
|
onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
onTouchMove={(e) => e.preventDefault()}
|
|
|
|
|
>
|
|
|
|
|
{/* 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="standard-input"
|
|
|
|
|
ref={newFolderRef}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
handleCreateFolder();
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
setNewFolder(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-30 19:40:10 +00:00
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
{/* Folder tree */}
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
{folderTree?.folders.map((folder) => (
|
|
|
|
|
<RecursiveFolder key={folder.id} folder={folder} depth={0} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-11-30 19:40:10 +00:00
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
{/* Orphaned notes */}
|
|
|
|
|
{folderTree?.orphaned_notes &&
|
|
|
|
|
folderTree.orphaned_notes.length > 0 && (
|
|
|
|
|
<div className="mt-4 flex flex-col gap-1">
|
|
|
|
|
{folderTree.orphaned_notes.map((note) => (
|
|
|
|
|
<DraggableNote key={note.id} note={note} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-30 19:40:10 +00:00
|
|
|
</div>
|
2025-12-08 22:08:30 +00:00
|
|
|
|
|
|
|
|
<DragOverlay>
|
|
|
|
|
{activeItem?.type === "note" && (
|
|
|
|
|
<div className="bg-ctp-surface0 rounded-md px-2 py-1 shadow-lg border border-ctp-mauve">
|
|
|
|
|
{activeItem.data.title}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{activeItem?.type === "folder" && (
|
|
|
|
|
<div className="bg-ctp-surface0 rounded-md px-1 py-0.5 shadow-lg flex items-center gap-1 text-sm">
|
|
|
|
|
<FolderIcon className="w-3 h-3 fill-ctp-mauve mr-1" />
|
|
|
|
|
{activeItem.data.name}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</DragOverlay>
|
|
|
|
|
</div>
|
2025-11-30 19:40:10 +00:00
|
|
|
</div>
|
|
|
|
|
</DndContext>
|
2025-11-29 12:45:41 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-30 18:01:57 +00:00
|
|
|
export const SidebarHeader = ({
|
|
|
|
|
clearSelection,
|
|
|
|
|
setNewFolder,
|
|
|
|
|
}: {
|
|
|
|
|
clearSelection: () => void;
|
|
|
|
|
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
|
|
|
|
|
}) => {
|
2025-12-08 22:08:30 +00:00
|
|
|
const { createNote, selectedFolder } = useNoteStore();
|
|
|
|
|
const handleCreate = async () => {
|
|
|
|
|
await createNote({
|
|
|
|
|
title: "Untitled",
|
|
|
|
|
content: "",
|
|
|
|
|
folder_id: selectedFolder,
|
|
|
|
|
});
|
|
|
|
|
};
|
2025-11-29 12:45:41 +00:00
|
|
|
return (
|
2025-12-08 22:08:30 +00:00
|
|
|
<div className="flex items-center justify-center w-full gap-2 bg-ctp-mantle border-b border-ctp-surface0 p-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setNewFolder(true)}
|
|
|
|
|
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2"
|
|
|
|
|
title="New folder"
|
|
|
|
|
>
|
|
|
|
|
<FolderPlusIcon className="w-4 h-4 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCreate}
|
|
|
|
|
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2 fill-ctp-mauve hover:fill-ctp-base"
|
|
|
|
|
title="New note"
|
|
|
|
|
>
|
|
|
|
|
<FileCirclePlusIcon className="w-4 h-4 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
|
|
|
|
|
</button>
|
2025-11-29 12:45:41 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|