2025-12-13 13:04:13 +00:00
|
|
|
import React, { useState, useRef, useEffect } from "react";
|
2025-12-11 22:20:23 +00:00
|
|
|
|
2025-12-08 22:08:30 +00:00
|
|
|
// @ts-ignore
|
2025-12-11 22:20:23 +00:00
|
|
|
import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react";
|
2025-12-18 18:12:23 +00:00
|
|
|
// @ts-ignore
|
|
|
|
|
import TagsIcon from "@/assets/fontawesome/svg/tags.svg?react";
|
2025-12-11 22:20:23 +00:00
|
|
|
import { DraggableNote } from "./subcomponents/DraggableNote";
|
|
|
|
|
|
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-12-11 22:20:23 +00:00
|
|
|
import { FolderTree } from "./subcomponents/FolderTree.tsx";
|
|
|
|
|
import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx";
|
|
|
|
|
import { useAuthStore } from "@/stores/authStore.ts";
|
|
|
|
|
import { useNoteStore } from "@/stores/notesStore.ts";
|
|
|
|
|
import { useUIStore } from "@/stores/uiStore.ts";
|
2025-12-18 18:12:23 +00:00
|
|
|
import { TagSelector } from "../../Home.tsx";
|
2025-11-29 12:45:41 +00:00
|
|
|
|
2025-12-11 22:20:23 +00:00
|
|
|
export const Sidebar = () => {
|
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-11 22:20:23 +00:00
|
|
|
const { encryptionKey } = useAuthStore();
|
2025-12-08 22:08:30 +00:00
|
|
|
|
2025-12-18 18:12:23 +00:00
|
|
|
const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } =
|
|
|
|
|
useUIStore();
|
2025-11-29 12:45:41 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (newFolder && newFolderRef.current) {
|
|
|
|
|
newFolderRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [newFolder]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-11 22:20:23 +00:00
|
|
|
if (!encryptionKey) return;
|
2025-11-29 12:45:41 +00:00
|
|
|
loadFolderTree();
|
2025-12-11 22:20:23 +00:00
|
|
|
}, [encryptionKey]);
|
2025-11-29 12:45:41 +00:00
|
|
|
|
|
|
|
|
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` }}
|
|
|
|
|
>
|
2025-12-11 22:20:23 +00:00
|
|
|
<SidebarHeader setNewFolder={setNewFolder} />
|
2025-12-18 18:12:23 +00:00
|
|
|
{sideBarView == "folders" ? (
|
|
|
|
|
<>
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Folder tree */}
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
{folderTree?.folders.map((folder) => (
|
|
|
|
|
<FolderTree key={folder.id} folder={folder} depth={0} />
|
2025-12-08 22:08:30 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-18 18:12:23 +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-12-08 22:08:30 +00:00
|
|
|
</div>
|
2025-12-18 18:12:23 +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 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">
|
|
|
|
|
<TagSelector />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-08 22:08:30 +00:00
|
|
|
</div>
|
2025-11-30 19:40:10 +00:00
|
|
|
</div>
|
|
|
|
|
</DndContext>
|
2025-11-29 12:45:41 +00:00
|
|
|
);
|
|
|
|
|
};
|