Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nayalsaurav/Storx/llms.txt

Use this file to discover all available pages before exploring further.

The file management surface in Storx is the core of the dashboard. It lets you upload new files, browse your library in grid or list view, search by name, download content, and permanently remove files you no longer need. All file metadata is stored in Neon PostgreSQL while the binary content lives in ImageKit CDN.

Supported File Types

The upload endpoint validates MIME types before accepting a file. Storx supports:
  • Images — any MIME type that starts with image/ (e.g. image/png, image/jpeg, image/webp)
  • PDFs — the exact MIME type application/pdf
Any other type is rejected by the server with a 400 response:
// app/api/files/upload/route.ts
if (!file.type.startsWith("image/") && file.type !== "application/pdf") {
  return NextResponse.json(
    { error: "Only images and PDF files are supported" },
    { status: 400 },
  );
}
Use grid view for image-heavy libraries so you can spot files visually via their thumbnails. Switch to list view when working with dense PDF archives where you need to scan names and modification dates quickly.

Uploading Files

File uploads flow through useFileOperations.uploadFile(), which builds a FormData payload and POSTs it to /api/files/upload.
// hooks/useFileOperations.ts
const uploadFile = async (file: File, parentId: string | null) => {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("userId", userId);
  if (parentId) {
    formData.append("parentId", parentId);
  }

  const response = await fetch("/api/files/upload", {
    method: "POST",
    body: formData,
  });
  // ...
};
The server derives the ImageKit folder path from the current context:
ContextImageKit path
Root (no folder)/storex/{userId}
Inside a folder/storex/{userId}/folder/{parentId}
The original filename is preserved in the database for display, while a UUID-based filename (e.g. 550e8400-e29b-41d4-a716-446655440000.pdf) is used in ImageKit to avoid collisions. Batch uploads are handled by handleFileUpload, which iterates over all selected files, calls uploadFile for each, and reports failures via toast notifications:
// hooks/useFileOperations.ts
const handleFileUpload = async (
  event: React.ChangeEvent<HTMLInputElement>,
  currentFolder: string | null,
  onSuccess: () => void,
) => {
  const files = event.target.files;
  if (!files || files.length === 0) return;

  setIsUploading(true);
  const fileArray = Array.from(files);
  let successCount = 0;

  for (const file of fileArray) {
    const success = await uploadFile(file, currentFolder);
    if (success) successCount++;
  }

  setIsUploading(false);
  if (successCount > 0) onSuccess();
};

Storage Limit

Every user has a 15 GB storage quota. The useStorage hook initialises the total as 15 * 1024 * 1024 * 1024 bytes, fetches current usage from GET /api/storage, and exposes it as a StorageInfo object:
// types/types.d.ts
export interface StorageInfo {
  used: number;       // bytes used across all files
  total: number;      // always 15 * 1024 * 1024 * 1024 (15 GB)
  percentage: number; // (used / total) * 100
}
// hooks/useStorage.ts
const [storageInfo, setStorageInfo] = useState<StorageInfo>({
  used: 0,
  total: 15 * 1024 * 1024 * 1024, // 15 GB
  percentage: 0,
});
The StorageIndicator component in the sidebar renders a progress bar using storageInfo.percentage, capped at 100%. File sizes are formatted for display using formatFileSize from lib/file-utils.ts:
// lib/file-utils.ts
export const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
The getFileIcon utility returns an appropriate Lucide React icon element based on the file’s MIME type or isFolder flag:
// lib/file-utils.ts
export const getFileIcon = (file: FileItem): React.ReactElement => {
  if (file.isFolder) return React.createElement(FolderOpen, { ... });
  if (file.type.startsWith("image/")) return React.createElement(Image, { ... });
  if (file.type.includes("pdf") || file.type.includes("document"))
    return React.createElement(FileText, { ... });
  if (file.type.startsWith("video/")) return React.createElement(Play, { ... });
  if (file.type.includes("zip") || file.type.includes("archive"))
    return React.createElement(Archive, { ... });
  return React.createElement(File, { ... }); // fallback
};

View Modes

The dashboard supports two display modes controlled by the ViewMode type:
// types/types.d.ts
export type ViewMode = "grid" | "list";

Grid View

Rendered by FileGrid, this mode lays files out in a responsive card grid (2 → 6 columns depending on viewport). Thumbnail images are shown for files that have a thumbnailUrl; other types fall back to a coloured icon from getFileIcon. Starred files show a yellow star badge on their thumbnail.

List View

Rendered by FileList, this mode displays files in a table with Name, Size, Type, and Modified columns. Each row has a dropdown menu for per-file actions including download, star, and delete.
Switch between modes using the Grid3X3 / List toggle buttons in ActionBar. The active mode is highlighted with the default button variant.

FileItem Interface

All file and folder records share the FileItem interface:
// types/types.d.ts
export interface FileItem {
  id: string;
  name: string;
  path: string;
  size: number;
  type: string;
  fileUrl: string;
  thumbnailUrl?: string;
  userId: string;
  parentId?: string;
  isFolder: boolean;
  isStarred: boolean;
  isTrash: boolean;
  createdAt?: string;
  updatedAt?: string;
}

File Actions

All per-file actions are available through the dropdown in list view, powered by useFileOperations:
ActionHook methodAPI endpoint
DownloadhandleDownload(fileId, fileName)GET /api/files/{fileId}/download?userId={userId}
Star / UnstarhandleStarToggle(fileId, isCurrentlyStarred)PATCH /api/files/{fileId}/star
Move to Trash(see Trash docs)PATCH /api/files/{fileId}/trash
DeletehandleDelete(fileId, fileName)DELETE /api/files/{fileId}
Download streams the file as a Blob and triggers a programmatic <a> click to save it:
// hooks/useFileOperations.ts
const handleDownload = async (fileId: string, fileName: string) => {
  const response = await fetch(`/api/files/${fileId}/download?userId=${userId}`);
  if (response.ok) {
    const blob = await response.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }
};
Delete prompts the user for confirmation before calling the API. A successful deletion also triggers a fetchStorageInfo() refresh so the storage indicator updates immediately.

Searching Files

Search is performed client-side inside the Dashboard component. The searchQuery state from useDashboardState is applied as a case-insensitive substring filter before passing files to either FileGrid or FileList:
// components/dashboard/Dashboard.tsx
const filteredFiles = files.filter((file) =>
  file.name.toLowerCase().includes(searchQuery.toLowerCase()),
);
The search input in the Header component calls setSearchQuery on every keystroke — no debounce, no server round-trip. Files already loaded for the current folder are filtered in memory.

Build docs developers (and LLMs) love