Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ VITE_SOCKET_URL=http://localhost:3001

# Python backend API
VITE_PYTHON_API_URL=http://localhost:8000

9 changes: 9 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import BugReportPage from "./components/feedback/BugReportPage";
import AdminPanel from "./components/admin/AdminPanel";
import AdminRoute from "./components/admin/AdminRoute";
import PdfChatbotPage from "./components/ai/PdfChatbotPage";
import VaultPage from "./components/vault/VaultPage";
import CertificateVerificationPage from "./pages/CertificateVerificationPage";
import Footer from "./components/layout/Footer";
import ScrollToTop from "./components/ui/ScrollToTop";
Expand Down Expand Up @@ -161,6 +162,14 @@ const App: React.FC<AppProps> = () => {
<Route path="/roadmaps" element={<RoadmapsPage />} />
<Route path="/roadmaps/:id" element={<RoadmapDetailPage />} />
<Route path="/pdf-chatbot" element={<PdfChatbotPage />} />
<Route
path="/vault/*"
element={
<ProtectedRoute authenticated={isLoggedIn}>
<VaultPage />
</ProtectedRoute>
}
/>
<Route
path="/suggest-feature"
element={<FeatureSuggestionPage />}
Expand Down
40 changes: 40 additions & 0 deletions client/src/components/vault/DropZone.tsx
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 }) => {

Copy link
Copy Markdown

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

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;
40 changes: 40 additions & 0 deletions client/src/components/vault/FileGrid.tsx
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<{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
33 changes: 33 additions & 0 deletions client/src/components/vault/FileList.tsx
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 }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
27 changes: 27 additions & 0 deletions client/src/components/vault/VaultBreadcrumb.tsx
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 }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
150 changes: 150 additions & 0 deletions client/src/components/vault/VaultPage.tsx
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 = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
43 changes: 43 additions & 0 deletions client/src/components/vault/VaultSidebar.tsx
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 }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
19 changes: 19 additions & 0 deletions client/src/components/vault/modals/ConfirmDeleteModal.tsx
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 }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
Loading