-
Notifications
You must be signed in to change notification settings - Fork 40
feat(vault): Vault File Browser UI — New menu, upload modals & mock service #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ VITE_SOCKET_URL=http://localhost:3001 | |
|
|
||
| # Python backend API | ||
| VITE_PYTHON_API_URL=http://localhost:8000 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import React, { useCallback, useState } from "react"; | ||
|
|
||
| const DropZone: React.FC<{ onFiles?: (files: FileList | null) => void; onDropFiles?: (files: FileList | null) => void }> = ({ onFiles, onDropFiles }) => { | ||
| const [isOver, setIsOver] = useState(false); | ||
|
|
||
| const handleDrop = useCallback( | ||
| (e: React.DragEvent) => { | ||
| e.preventDefault(); | ||
| setIsOver(false); | ||
| const dt = e.dataTransfer; | ||
| // Give parent a chance to show actions instead of auto-uploading | ||
| if (onDropFiles) onDropFiles(dt.files); | ||
| else if (onFiles) onFiles(dt.files); | ||
| }, | ||
| [onFiles, onDropFiles] | ||
| ); | ||
|
|
||
| const handleDragOver = (e: React.DragEvent) => { | ||
| e.preventDefault(); | ||
| setIsOver(true); | ||
| }; | ||
|
|
||
| const handleDragLeave = () => setIsOver(false); | ||
|
|
||
| return ( | ||
| <div | ||
| onDrop={handleDrop} | ||
| onDragOver={handleDragOver} | ||
| onDragLeave={handleDragLeave} | ||
| className={`w-full p-6 rounded border-2 border-dashed ${isOver ? "border-alien-green bg-[#04202f]" : "border-slate-700 bg-[#071122]"}`} | ||
| > | ||
| <div className="text-slate-300 text-center"> | ||
| <div className="text-2xl">Drag & drop files here</div> | ||
| <div className="text-sm text-slate-500">or click Upload to select files</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default DropZone; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import React from "react"; | ||
| import { FolderItem, FileItem } from "../../services/vaultService"; | ||
|
|
||
| const ItemCard: React.FC<{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — ItemCard and FileGrid are exported React components but lack JSDoc comments describing their purpose and props. Add JSDoc comments above ItemCard and FileGrid describing their purpose and the props they accept. documentation |
||
| item: FolderItem | FileItem; | ||
| onOpenFolder: (id: string) => void; | ||
| onRequestDelete: (id: string) => void; | ||
| }> = ({ item, onOpenFolder, onRequestDelete }) => { | ||
| const isFolder = item.type === "folder"; | ||
| return ( | ||
| <div className="w-40 p-3 bg-[#0b1220] rounded flex flex-col gap-2 cursor-pointer hover:shadow-lg" onDoubleClick={() => isFolder && onOpenFolder(item.id)}> | ||
| <div className="h-20 flex items-center justify-center text-3xl">{isFolder ? "📁" : "📄"}</div> | ||
| <div className="text-sm text-slate-200 truncate">{item.name}</div> | ||
| <div className="text-xs text-slate-500 flex justify-between"> | ||
| <span>{isFolder ? "Folder" : `${(item as FileItem).size} bytes`}</span> | ||
| <button className="text-red-400" onClick={(e) => { e.stopPropagation(); onRequestDelete(item.id); }}>Delete</button> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — The 'Delete' button is rendered for both files and folders, but the label does not clarify what is being deleted. This could be confusing in a mixed grid view. Consider updating the button label or adding a tooltip to clarify whether a file or folder will be deleted. readability |
||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| const FileGrid: React.FC<{ | ||
| folders: FolderItem[]; | ||
| files: FileItem[]; | ||
| onOpenFolder: (id: string) => void; | ||
| onRequestDelete: (id: string) => void; | ||
| }> = ({ folders, files, onOpenFolder, onRequestDelete }) => { | ||
| return ( | ||
| <div className="grid grid-cols-4 gap-4"> | ||
| {folders.map((f) => ( | ||
| <ItemCard key={f.id} item={f} onOpenFolder={onOpenFolder} onRequestDelete={onRequestDelete} /> | ||
| ))} | ||
| {files.map((fi) => ( | ||
| <ItemCard key={fi.id} item={fi} onOpenFolder={onOpenFolder} onRequestDelete={onRequestDelete} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default FileGrid; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import React from "react"; | ||
| import { FolderItem, FileItem } from "../../services/vaultService"; | ||
|
|
||
| const FileRow: React.FC<{ item: FolderItem | FileItem; onOpenFolder: (id: string) => void; onRequestDelete: (id: string) => void }> = ({ item, onOpenFolder, onRequestDelete }) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — FileRow and FileList are exported React components but lack JSDoc comments describing their purpose and props. Add JSDoc comments above FileRow and FileList describing their purpose and the props they accept. documentation |
||
| const isFolder = item.type === "folder"; | ||
| return ( | ||
| <div className="flex items-center justify-between py-2 px-3 hover:bg-slate-800 rounded"> | ||
| <div className="flex items-center gap-3"> | ||
| <div className="text-2xl">{isFolder ? "📁" : "📄"}</div> | ||
| <div className="text-sm text-slate-200 cursor-pointer" onDoubleClick={() => isFolder && onOpenFolder(item.id)}>{item.name}</div> | ||
| </div> | ||
| <div className="text-xs text-slate-400 flex gap-4 items-center"> | ||
| <div>{isFolder ? "Folder" : `${(item as FileItem).size} bytes`}</div> | ||
| <button className="text-red-400" onClick={() => onRequestDelete(item.id)}>Delete</button> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — The 'Delete' button is rendered for both files and folders, but the label does not clarify what is being deleted. This could be confusing in a mixed list view. Consider updating the button label or adding a tooltip to clarify whether a file or folder will be deleted. readability |
||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| const FileList: React.FC<{ folders: FolderItem[]; files: FileItem[]; onOpenFolder: (id: string) => void; onRequestDelete: (id: string) => void }> = ({ folders, files, onOpenFolder, onRequestDelete }) => { | ||
| return ( | ||
| <div className="flex flex-col"> | ||
| {folders.map((f) => ( | ||
| <FileRow key={f.id} item={f} onOpenFolder={onOpenFolder} onRequestDelete={onRequestDelete} /> | ||
| ))} | ||
| {files.map((fi) => ( | ||
| <FileRow key={fi.id} item={fi} onOpenFolder={onOpenFolder} onRequestDelete={onRequestDelete} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default FileList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import React, { useEffect, useState } from "react"; | ||
| import vaultService, { FolderItem } from "../../services/vaultService"; | ||
|
|
||
| const VaultBreadcrumb: React.FC<{ currentFolderId: string | null; onNavigate: (id: string) => void }> = ({ currentFolderId, onNavigate }) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — VaultBreadcrumb is an exported React component but lacks a JSDoc comment describing its purpose and props. Add a JSDoc comment above VaultBreadcrumb describing its purpose and the props it accepts. documentation |
||
| const [path, setPath] = useState<FolderItem[]>([]); | ||
|
|
||
| useEffect(() => { | ||
| if (!currentFolderId) return; | ||
| (async () => { | ||
| const p = await vaultService.getPath(currentFolderId); | ||
| setPath(p); | ||
| })(); | ||
| }, [currentFolderId]); | ||
|
|
||
| return ( | ||
| <div className="text-sm text-slate-300"> | ||
| {path.map((p, i) => ( | ||
| <span key={p.id} className="cursor-pointer hover:underline" onClick={() => onNavigate(p.id)}> | ||
| {i > 0 && <span className="mx-2 text-slate-500">/</span>} | ||
| {p.name} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default VaultBreadcrumb; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| import React, { useEffect, useState, useRef } from "react"; | ||
| import VaultSidebar from "./VaultSidebar"; | ||
| import VaultBreadcrumb from "./VaultBreadcrumb"; | ||
| import FileGrid from "./FileGrid"; | ||
| import FileList from "./FileList"; | ||
| import CreateFolderModal from "./modals/CreateFolderModal"; | ||
| import ConfirmDeleteModal from "./modals/ConfirmDeleteModal"; | ||
| import UploadFileModal from "./modals/UploadFileModal"; | ||
| import UploadFolderModal from "./modals/UploadFolderModal"; | ||
| import vaultService, { FolderItem, FileItem } from "../../services/vaultService"; | ||
|
|
||
| const VaultPage: React.FC = () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — VaultPage is an exported React component but lacks a JSDoc comment describing its purpose and behavior. Add a JSDoc comment above VaultPage describing its purpose and any important props or state. documentation |
||
| const [currentFolderId, setCurrentFolderId] = useState<string | null>(null); | ||
| const [folders, setFolders] = useState<FolderItem[]>([]); | ||
| const [files, setFiles] = useState<FileItem[]>([]); | ||
| const [view, setView] = useState<"grid" | "list">("grid"); | ||
| const [showCreate, setShowCreate] = useState(false); | ||
| const [showDelete, setShowDelete] = useState(false); | ||
| const [toDeleteId, setToDeleteId] = useState<string | null>(null); | ||
| const [showUploadFileModal, setShowUploadFileModal] = useState(false); | ||
| const [showUploadFolderModal, setShowUploadFolderModal] = useState(false); | ||
| const [showNewMenu, setShowNewMenu] = useState(false); | ||
| const newMenuRef = useRef<HTMLDivElement | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| (async () => { | ||
| const root = await vaultService.getRootId(); | ||
| setCurrentFolderId((id) => id ?? root); | ||
| })(); | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| if (!currentFolderId) return; | ||
| (async () => { | ||
| const { folders: f, files: fi } = await vaultService.getChildren(currentFolderId); | ||
| setFolders(f); | ||
| setFiles(fi); | ||
| })(); | ||
| }, [currentFolderId]); | ||
|
|
||
| async function handleCreate(name: string) { | ||
| if (!currentFolderId) return; | ||
| await vaultService.createFolder(currentFolderId, name); | ||
| const { folders: f } = await vaultService.getChildren(currentFolderId); | ||
| setFolders(f); | ||
| setShowCreate(false); | ||
| } | ||
|
|
||
| async function handleFiles(files: File[] | FileList | null) { | ||
| if (!files || (Array.isArray(files) ? files.length === 0 : files.length === 0) || !currentFolderId) return; | ||
| const list: File[] = Array.isArray(files) ? files : Array.from(files); | ||
| for (let i = 0; i < list.length; i++) { | ||
| const f = list[i]; | ||
| await vaultService.uploadFile(currentFolderId, f.name, f.size, f.type || "application/octet-stream"); | ||
| } | ||
| const { folders: fldr, files: fi } = await vaultService.getChildren(currentFolderId); | ||
| setFolders(fldr); | ||
| setFiles(fi); | ||
| setShowUploadFileModal(false); | ||
| setShowUploadFolderModal(false); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| function onDocClick(e: MouseEvent) { | ||
| if (newMenuRef.current && !newMenuRef.current.contains(e.target as Node)) { | ||
| setShowNewMenu(false); | ||
| } | ||
| } | ||
| document.addEventListener("mousedown", onDocClick); | ||
| return () => document.removeEventListener("mousedown", onDocClick); | ||
| }, []); | ||
|
|
||
| async function handleDelete(id: string) { | ||
| await vaultService.deleteItem(id); | ||
| const { folders: f, files: fi } = await vaultService.getChildren(currentFolderId); | ||
| setFolders(f); | ||
| setFiles(fi); | ||
| setShowDelete(false); | ||
| setToDeleteId(null); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="min-h-[70vh] p-6"> | ||
| <div className="flex gap-6"> | ||
| <div className="w-72"> | ||
| <VaultSidebar onNavigate={(id) => setCurrentFolderId(id)} /> | ||
| </div> | ||
| <div className="flex-1 bg-[#0f1724] rounded-lg p-4"> | ||
| <div className="flex items-center justify-between mb-4"> | ||
| <VaultBreadcrumb currentFolderId={currentFolderId} onNavigate={setCurrentFolderId} /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — The storage usage display is hardcoded as '120MB / 2GB'. This is misleading and not dynamic. Fetch and display the actual storage usage from the backend or mock service instead of hardcoding the values. readability |
||
| <div className="flex items-center gap-3"> | ||
| <div className="text-sm text-slate-300">Storage: 120MB / 2GB</div> | ||
| <div className="relative" ref={newMenuRef}> | ||
| <button onClick={() => setShowNewMenu((s) => !s)} className="px-3 py-1 bg-alien-green text-black rounded">New ▾</button> | ||
| {showNewMenu && ( | ||
| <div className="absolute right-0 mt-2 bg-[#071122] border border-slate-700 rounded shadow-lg w-48 z-30"> | ||
| <button className="w-full text-left px-4 py-2 hover:bg-slate-800" onClick={() => { setShowCreate(true); setShowNewMenu(false); }}>New Folder</button> | ||
| <button className="w-full text-left px-4 py-2 hover:bg-slate-800" onClick={() => { setShowUploadFileModal(true); setShowNewMenu(false); }}>Upload File</button> | ||
| <button className="w-full text-left px-4 py-2 hover:bg-slate-800" onClick={() => { setShowUploadFolderModal(true); setShowNewMenu(false); }}>Upload Folder</button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| <button | ||
| className="px-3 py-1 border border-slate-600 text-slate-200 rounded" | ||
| onClick={() => setView((v) => (v === "grid" ? "list" : "grid"))} | ||
| > | ||
| {view === "grid" ? "List" : "Grid"} | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div> | ||
| {view === "grid" ? ( | ||
| <FileGrid | ||
| folders={folders} | ||
| files={files} | ||
| onOpenFolder={(id) => setCurrentFolderId(id)} | ||
| onRequestDelete={(id) => { | ||
| setToDeleteId(id); | ||
| setShowDelete(true); | ||
| }} | ||
| /> | ||
| ) : ( | ||
| <FileList | ||
| folders={folders} | ||
| files={files} | ||
| onOpenFolder={(id) => setCurrentFolderId(id)} | ||
| onRequestDelete={(id) => { | ||
| setToDeleteId(id); | ||
| setShowDelete(true); | ||
| }} | ||
| /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <CreateFolderModal open={showCreate} onClose={() => setShowCreate(false)} onCreate={handleCreate} /> | ||
| <UploadFileModal open={showUploadFileModal} onClose={() => setShowUploadFileModal(false)} onUpload={handleFiles} /> | ||
| <UploadFolderModal open={showUploadFolderModal} onClose={() => setShowUploadFolderModal(false)} onUpload={handleFiles} /> | ||
| <ConfirmDeleteModal | ||
| open={showDelete} | ||
| onClose={() => setShowDelete(false)} | ||
| onConfirm={() => toDeleteId && handleDelete(toDeleteId)} | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default VaultPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import React, { useEffect, useState } from "react"; | ||
| import vaultService from "../../services/vaultService"; | ||
|
|
||
| type Node = { id: string; name: string; children?: Node[] }; | ||
|
|
||
| const TreeNode: React.FC<{ node: Node; onNavigate: (id: string) => void }> = ({ node, onNavigate }) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — TreeNode and VaultSidebar are exported React components but lack JSDoc comments describing their purpose and props. Add JSDoc comments above TreeNode and VaultSidebar describing their purpose and the props they accept. documentation |
||
| const [open, setOpen] = useState(false); | ||
| return ( | ||
| <div className="pl-2"> | ||
| <div className="flex items-center gap-2 py-1 cursor-pointer hover:bg-slate-800 rounded" onClick={() => { setOpen(!open); onNavigate(node.id); }}> | ||
| <div className="w-5 h-5 bg-slate-600 rounded flex items-center justify-center text-xs">F</div> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — The folder icon is rendered as a literal 'F' character, which is not visually descriptive and may confuse users. Replace the 'F' with an actual folder icon (e.g., an SVG or emoji like 📁) for better clarity and consistency with other parts of the UI. readability |
||
| <div className="text-sm text-slate-200">{node.name}</div> | ||
| </div> | ||
| {open && node.children && ( | ||
| <div className="pl-4"> | ||
| {node.children.map((c) => ( | ||
| <TreeNode key={c.id} node={c} onNavigate={onNavigate} /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| const VaultSidebar: React.FC<{ onNavigate: (id: string) => void }> = ({ onNavigate }) => { | ||
| const [tree, setTree] = useState<Node | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| (async () => { | ||
| const t = await vaultService.getFolderTree(); | ||
| setTree(t as Node); | ||
| })(); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <div className="bg-[#071126] rounded p-3 text-sm"> | ||
| <div className="text-slate-300 mb-2 font-semibold">My Vault</div> | ||
| {tree ? <TreeNode node={tree} onNavigate={onNavigate} /> : <div className="text-slate-500">Loading...</div>} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default VaultSidebar; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import React from "react"; | ||
|
|
||
| const ConfirmDeleteModal: React.FC<{ open: boolean; onClose: () => void; onConfirm: () => void }> = ({ open, onClose, onConfirm }) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Medium — ConfirmDeleteModal is an exported React component but lacks a JSDoc comment describing its purpose and props. Add a JSDoc comment above ConfirmDeleteModal describing its purpose and the props it accepts. documentation |
||
| if (!open) return null; | ||
| return ( | ||
| <div className="fixed inset-0 bg-black/60 flex items-center justify-center"> | ||
| <div className="bg-[#071122] p-6 rounded w-80"> | ||
| <div className="text-lg text-slate-200 mb-3">Confirm Delete</div> | ||
| <div className="text-sm text-slate-300 mb-4">Are you sure you want to delete this item? This action cannot be undone.</div> | ||
| <div className="flex justify-end gap-2"> | ||
| <button className="px-3 py-1 border rounded" onClick={onClose}>Cancel</button> | ||
| <button className="px-3 py-1 bg-red-600 text-white rounded" onClick={onConfirm}>Delete</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ConfirmDeleteModal; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Medium — DropZone is a new exported React component but lacks a JSDoc comment describing its purpose and props.
Add a JSDoc comment above DropZone describing its purpose and the props (onFiles, onDropFiles) it accepts.
documentation