Jotzy/frontend/src/components/sidebar/SideBar.tsx

276 lines
8.3 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef, useEffect, SetStateAction } from "react";
// @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";
import { DraggableNote } from "./DraggableNote";
import { useNoteStore } from "../../stores/notesStore";
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { RecursiveFolder } from "./RecursiveFolder";
import { useAuthStore } from "../../stores/authStore";
import { useUIStore } from "../../stores/uiStore";
import { NoteRead } from "../../api/folders";
export const Sidebar = ({ clearSelection }: { clearSelection: () => void }) => {
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 {
folderTree,
loadFolderTree,
moveNoteToFolder,
moveFolderToFolder,
createFolder,
} = useNoteStore();
const { isAuthenticated } = useAuthStore();
const { setSideBarResize, sideBarResize } = useUIStore();
useEffect(() => {
if (newFolder && newFolderRef.current) {
newFolderRef.current.focus();
}
}, [newFolder]);
useEffect(() => {
// if (!isAuthenticated) return;
loadFolderTree();
}, []);
const handleCreateFolder = async () => {
if (!newFolderText.trim()) return;
await createFolder({
name: newFolderText,
parent_id: null,
});
setNewFolderText("");
setNewFolder(false);
};
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 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);
await moveNoteToFolder(active.id as number, 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 {
await moveFolderToFolder(
active.data.current.folder.id,
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">
<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>
)}
{/* Folder tree */}
<div className="flex flex-col gap-1">
{folderTree?.folders.map((folder) => (
<RecursiveFolder key={folder.id} folder={folder} depth={0} />
))}
</div>
{/* 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>
)}
</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>
</div>
</DndContext>
);
};
export const SidebarHeader = ({
clearSelection,
setNewFolder,
}: {
clearSelection: () => void;
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
}) => {
const { createNote, selectedFolder } = useNoteStore();
const handleCreate = async () => {
await createNote({
title: "Untitled",
content: "",
folder_id: selectedFolder,
});
};
return (
<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>
</div>
);
};