feat: implement Files page with persistent storage and version tracking

Three-panel file manager (nav, list, details) modeled after Stirling-PDF.
- Backend: user_files table, /api/v1/files/* CRUD routes, file storage
  helpers, thumbnail generation via Sharp, recursive CTE version chains
- Frontend: FilesNav, FileList, FileDetails, FileUploadArea components,
  Zustand store, mobile layout with bottom sheet
- Integration: tool-factory auto-saves results as new versions when
  fileId is provided, "Open File" loads file into tool processing flow
- Search, bulk select/delete/download, version badges, tool chain tags
This commit is contained in:
Siddharth Kumar Sah
2026-03-26 01:10:51 +08:00
parent 926b52d330
commit 62fbb5484c
24 changed files with 771 additions and 470 deletions
+12 -14
View File
@@ -1,16 +1,16 @@
import { TOOLS } from "@stirling-image/shared";
import { FileImage } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { FileImage } from "lucide-react";
import { TOOLS } from "@stirling-image/shared";
import {
apiGetFileDetails,
getFileThumbnailUrl,
getFileDownloadUrl,
getFileThumbnailUrl,
type UserFileDetail,
} from "@/lib/api";
import { useFilesPageStore } from "@/stores/files-page-store";
import { useFileStore } from "@/stores/file-store";
import { cn } from "@/lib/utils";
import { useFileStore } from "@/stores/file-store";
import { useFilesPageStore } from "@/stores/files-page-store";
function toolName(toolId: string): string {
return TOOLS.find((t) => t.id === toolId)?.name ?? toolId;
@@ -124,23 +124,20 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
</div>
<div className="divide-y divide-border">
<DetailRow label="Name" value={details.originalName} />
<DetailRow label="Format" value={details.mimeType.replace("image/", "").toUpperCase()} />
<DetailRow
label="Format"
value={details.mimeType.replace("image/", "").toUpperCase()}
/>
<DetailRow label="Size" value={formatSize(details.size)} />
<DetailRow
label="Dimensions"
value={
details.width && details.height
? `${details.width} × ${details.height}`
: "—"
}
value={details.width && details.height ? `${details.width} × ${details.height}` : "—"}
/>
<DetailRow label="Version" value={`V${details.version}`} />
<DetailRow
label="Tools Used"
value={
details.toolChain.length > 0
? details.toolChain.map(toolName).join(", ")
: "None"
details.toolChain.length > 0 ? details.toolChain.map(toolName).join(", ") : "None"
}
/>
</div>
@@ -150,6 +147,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
{/* Open File button */}
<div className={cn("border-t border-border", mobile ? "pt-3" : "pt-4")}>
<button
type="button"
onClick={handleOpenFile}
className="w-full px-4 py-2 bg-primary text-primary-foreground text-sm font-medium rounded-lg hover:bg-primary/90 transition-colors"
>