Files
SnapOtter/apps/web/src/stores/files-page-store.ts
T
Siddharth Kumar Sah 62fbb5484c 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
2026-03-26 01:10:51 +08:00

86 lines
2.5 KiB
TypeScript

import { create } from "zustand";
import { apiDeleteUserFiles, apiListFiles, apiUploadUserFiles, type UserFile } from "@/lib/api";
interface FilesPageState {
files: UserFile[];
total: number;
selectedFileId: string | null;
checkedIds: Set<string>;
activeTab: "recent" | "upload";
searchQuery: string;
loading: boolean;
error: string | null;
fetchFiles: () => Promise<void>;
uploadFiles: (files: File[]) => Promise<void>;
deleteChecked: () => Promise<void>;
selectFile: (id: string | null) => void;
toggleChecked: (id: string) => void;
toggleCheckAll: () => void;
setSearchQuery: (q: string) => void;
setActiveTab: (tab: "recent" | "upload") => void;
}
export const useFilesPageStore = create<FilesPageState>((set, get) => ({
files: [],
total: 0,
selectedFileId: null,
checkedIds: new Set(),
activeTab: "recent",
searchQuery: "",
loading: false,
error: null,
fetchFiles: async () => {
set({ loading: true, error: null });
try {
const { searchQuery } = get();
const result = await apiListFiles({ search: searchQuery || undefined, limit: 100 });
set({ files: result.files, total: result.total, loading: false });
} catch (err) {
set({ error: err instanceof Error ? err.message : "Failed to load files", loading: false });
}
},
uploadFiles: async (files) => {
set({ loading: true, error: null });
try {
await apiUploadUserFiles(files);
await get().fetchFiles();
set({ activeTab: "recent" });
} catch (err) {
set({ error: err instanceof Error ? err.message : "Upload failed", loading: false });
}
},
deleteChecked: async () => {
const { checkedIds } = get();
if (checkedIds.size === 0) return;
set({ loading: true, error: null });
try {
await apiDeleteUserFiles(Array.from(checkedIds));
set({ checkedIds: new Set(), selectedFileId: null });
await get().fetchFiles();
} catch (err) {
set({ error: err instanceof Error ? err.message : "Delete failed", loading: false });
}
},
selectFile: (id) => set({ selectedFileId: id }),
toggleChecked: (id) => {
const { checkedIds } = get();
const next = new Set(checkedIds);
if (next.has(id)) next.delete(id);
else next.add(id);
set({ checkedIds: next });
},
toggleCheckAll: () => {
const { files, checkedIds } = get();
set({
checkedIds: checkedIds.size === files.length ? new Set() : new Set(files.map((f) => f.id)),
});
},
setSearchQuery: (q) => set({ searchQuery: q }),
setActiveTab: (tab) => set({ activeTab: tab }),
}));