yet. more cleanup needs to be done. - added react query - moved to openapi instead of axios - added case translator from frontend to backend
271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
import React, { useState, useRef, useEffect } from "react";
|
|
|
|
// @ts-ignore
|
|
import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react";
|
|
// @ts-ignore
|
|
import TagsIcon from "@/assets/fontawesome/svg/tags.svg?react";
|
|
import { DraggableNote } from "./subcomponents/DraggableNote";
|
|
|
|
import {
|
|
DndContext,
|
|
DragEndEvent,
|
|
DragOverlay,
|
|
DragStartEvent,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
} from "@dnd-kit/core";
|
|
|
|
import { FolderTree } from "./subcomponents/FolderTree.tsx";
|
|
import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx";
|
|
import { useAuthStore } from "@/stores/authStore.ts";
|
|
import { useUIStore } from "@/stores/uiStore.ts";
|
|
import { TagSelector } from "../../Home.tsx";
|
|
import {
|
|
useCreateFolder,
|
|
useFolderTree,
|
|
useUpdateFolder,
|
|
useUpdateNote,
|
|
} from "@/hooks/useFolders.ts";
|
|
|
|
export const Sidebar = () => {
|
|
const [newFolder, setNewFolder] = useState(false);
|
|
const [newFolderText, setNewFolderText] = useState("");
|
|
const [activeItem, setActiveItem] = useState<{
|
|
type: "note" | "folder";
|
|
data: any;
|
|
} | null>(null);
|
|
const newFolderRef = useRef<HTMLInputElement>(null);
|
|
|
|
const { data: folderTree, isLoading, error } = useFolderTree();
|
|
const createFolder = useCreateFolder();
|
|
|
|
const { encryptionKey } = useAuthStore();
|
|
|
|
const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } =
|
|
useUIStore();
|
|
useEffect(() => {
|
|
if (newFolder && newFolderRef.current) {
|
|
newFolderRef.current.focus();
|
|
}
|
|
}, [newFolder]);
|
|
|
|
useEffect(() => {
|
|
if (!encryptionKey) return;
|
|
}, [encryptionKey]);
|
|
|
|
const handleCreateFolder = async () => {
|
|
if (!newFolderText.trim()) return;
|
|
createFolder.mutate({ name: newFolderText, parentId: null });
|
|
};
|
|
|
|
const pointer = useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 30,
|
|
},
|
|
});
|
|
const sensors = useSensors(pointer);
|
|
|
|
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 });
|
|
}
|
|
};
|
|
|
|
const updateNote = useUpdateNote();
|
|
const updateFolder = useUpdateFolder();
|
|
|
|
const handleDragEnd = async (event: DragEndEvent) => {
|
|
setActiveItem(null);
|
|
const { active, over } = event;
|
|
if (!over) return;
|
|
|
|
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);
|
|
updateNote.mutate({
|
|
noteId: active.id as number,
|
|
note: { folderId: over.id as number },
|
|
});
|
|
} 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 {
|
|
updateFolder.mutate({
|
|
folderId: active.data.current.folder.id,
|
|
folder: { parentId: over.id as number },
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to update folder:", error);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
setIsResizing(true);
|
|
e.preventDefault();
|
|
};
|
|
|
|
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]);
|
|
|
|
return (
|
|
<DndContext
|
|
onDragStart={handleDragStart}
|
|
onDragEnd={handleDragEnd}
|
|
autoScroll={false}
|
|
sensors={sensors}
|
|
>
|
|
<div className="flex-row-reverse flex h-screen">
|
|
<div
|
|
className="h-full bg-ctp-surface0 w-0.5 hover:cursor-ew-resize hover:bg-ctp-mauve transition-colors"
|
|
onMouseDown={handleMouseDown}
|
|
></div>
|
|
<div
|
|
className="flex flex-col h-full"
|
|
style={{ width: `${sideBarResize}px` }}
|
|
>
|
|
<SidebarHeader setNewFolder={setNewFolder} />
|
|
<div className="flex-1 overflow-y-auto bg-ctp-mantle border-r border-ctp-surface2">
|
|
{sideBarView == "folders" ? (
|
|
<>
|
|
<div
|
|
className="w-full p-4 sm:block hidden"
|
|
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>
|
|
)}
|
|
|
|
{/* Loading state */}
|
|
{isLoading && (
|
|
<div className="flex items-center justify-center py-8 text-ctp-subtext0">
|
|
<div className="text-sm">Loading folders...</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error state */}
|
|
{error && (
|
|
<div className="flex items-center justify-center py-8 text-ctp-red">
|
|
<div className="text-sm">Failed to load folders</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Folder tree */}
|
|
{!isLoading && !error && (
|
|
<>
|
|
<div className="flex flex-col gap-1">
|
|
{folderTree?.folders.map((folder) => (
|
|
<FolderTree
|
|
key={folder.id}
|
|
folder={folder}
|
|
depth={0}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Orphaned notes */}
|
|
{folderTree?.orphanedNotes &&
|
|
folderTree.orphanedNotes.length > 0 && (
|
|
<div className="mt-4 flex flex-col gap-1">
|
|
{folderTree.orphanedNotes.map((note) => (
|
|
<DraggableNote key={note.id} note={note} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<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="w-full p-4 sm:block hidden">
|
|
<TagSelector />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DndContext>
|
|
);
|
|
};
|