@unciatech/file-manager 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # 🗂️ File Manager
2
+
3
+ A robust, production-ready React / Next.js file management system designed to mirror the capabilities of professional asset managers (like Google Drive, macOS Finder, or Strapi Media Library).
4
+
5
+ It supports deep folder nesting, drag-and-drop file uploads, metadata management for various file types (Images, Videos, Audio, Documents), unified grid layouts, and fully optimized loading states.
6
+
7
+ ## 🌟 Key Features
8
+ - **Dual Operating Modes**: Use it as a standalone full-page media library or instantiate it as a picker modal for form inputs.
9
+ - **Unified Grid View**: Beautiful, responsive layout that intelligently renders thumbnails, icons, and metadata based on the file's MIME type.
10
+ - **Nested Folder Structure**: Infinite folder depth with smooth virtualized/paginated fetching.
11
+ - **Provider Agnostic**: Built on an `IFileManagerProvider` interface. You can easily hot-swap the mock data provider for a real backend (Node.js, Supabase, Strapi, etc.).
12
+ - **Bulk Actions**: Select multiple files/folders at once to bulk move or bulk delete.
13
+ - **Optimistic UI Updates**: Instant visual feedback when renaming folders or updating file descriptions, with silent background synchronization.
14
+ - **Graceful Error Handling**: Resilient `<FileManagerErrorBoundary>` that captures catastrophic failures and allows users to hard-reload safely without app crashes.
15
+
16
+ ## 🛠️ Tech Stack
17
+ - **Framework**: Next.js / React
18
+ - **Styling**: Tailwind CSS
19
+ - **Icons**: Lucide React
20
+ - **Notifications**: Sonner
21
+
22
+ ## 🚀 How to Install and Use in Your Project
23
+
24
+ If you want to integrate this File Manager into your own Next.js or React application, follow this step-by-step guide.
25
+
26
+ ### Step 1: Copy the Core Files
27
+ Copy the `components`, `types`, `hooks`, `context`, and `providers` folders related to the file manager into your project's source directory (e.g., `src/`).
28
+
29
+ ### Step 2: Create your Custom API Provider
30
+
31
+ The file manager is completely agnostic to your backend database. You simply need to create a class that implements the `IFileManagerProvider` interface.
32
+
33
+ Here is an example of what your custom provider might look like, making real API calls to your backend using `fetch`:
34
+
35
+ ```typescript
36
+ // lib/my-api-provider.ts
37
+ import {
38
+ IFileManagerProvider,
39
+ FolderId,
40
+ FileUploadInput
41
+ } from "@/types/file-manager";
42
+
43
+ export class MyCustomApiProvider implements IFileManagerProvider {
44
+ private baseUrl = "https://api.mybackend.com/v1";
45
+
46
+ // Example 1: Fetching folders from your Real API
47
+ async getFolders(folderId: FolderId, page = 1, limit = 24) {
48
+ const parentQuery = folderId ? `&parentId=${folderId}` : "&isRoot=true";
49
+
50
+ // Simulate real API Call
51
+ const res = await fetch(`${this.baseUrl}/folders?page=${page}&limit=${limit}${parentQuery}`);
52
+
53
+ if (!res.ok) throw new Error("Failed to fetch folders");
54
+
55
+ const data = await res.json();
56
+
57
+ return {
58
+ folders: data.folders, // Array of Folder objects matching our interface
59
+ pagination: data.pagination // { currentPage, totalPages, totalFiles, filesPerPage }
60
+ };
61
+ }
62
+
63
+ // Example 2: Uploading files via multipart/form-data
64
+ async uploadFiles(filesInput: FileUploadInput[], folderId?: FolderId) {
65
+ const formData = new FormData();
66
+ if (folderId) formData.append("folderId", String(folderId));
67
+
68
+ filesInput.forEach(({ file }) => {
69
+ formData.append("files", file);
70
+ });
71
+
72
+ const res = await fetch(`${this.baseUrl}/upload`, {
73
+ method: 'POST',
74
+ body: formData
75
+ });
76
+
77
+ return res.json(); // Returns array of FileMetaData
78
+ }
79
+
80
+ // ... Implement the remaining interface methods (getFiles, renameFolder, bulkMove, etc.)
81
+ }
82
+ ```
83
+
84
+ > **💡 Pro Tip - The Mock Provider:**
85
+ > If you are just prototyping and don't have a backend ready yet, you can skip Step 2 entirely! We included a fully functional `MockFileManagerProvider` that fakes network latency and stores data in memory. Just import it and use it right away to see the UI in action.
86
+
87
+ ### Step 3: Wrap Your Page with the Provider
88
+
89
+ Finally, import your provider and wrap the `<FileManager />` component in the Context Provider. Define which file types you want to allow.
90
+
91
+ ```tsx
92
+ // app/media/page.tsx
93
+ import { FileManagerProvider } from "@/context/file-manager-context";
94
+ import { FileManager } from "@/components/file-manager";
95
+
96
+ // Import your real provider OR the mock one
97
+ import { MyCustomApiProvider } from "@/lib/my-api-provider";
98
+ // import { MockFileManagerProvider } from "@/providers/mock-provider";
99
+
100
+ export default function MediaLibraryPage() {
101
+ // Instantiate the provider
102
+ const apiProvider = new MyCustomApiProvider();
103
+
104
+ return (
105
+ <div className="h-screen w-full">
106
+ <FileManagerProvider
107
+ mode="page"
108
+ selectionMode="multiple"
109
+ allowedFileTypes={["images", "videos", "audios", "files"]}
110
+ provider={apiProvider}
111
+ >
112
+ <FileManager />
113
+ </FileManagerProvider>
114
+ </div>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 💾 Database Schema Design
122
+
123
+ Because this application relies heavily on tree structures (Folders inside Folders) and varied JSON metadata (Video durations vs Document page counts), using a relational database with JSONB support (like PostgreSQL) is highly recommended.
124
+
125
+ Below are production-ready schema examples using **Prisma** and **Drizzle ORM**.
126
+
127
+ ### Option A: Prisma Schema (PostgreSQL/MySQL)
128
+
129
+ ```prisma
130
+ generator client {
131
+ provider = "prisma-client-js"
132
+ }
133
+
134
+ datasource db {
135
+ provider = "postgresql" // or "mysql"
136
+ url = env("DATABASE_URL")
137
+ }
138
+
139
+ model Folder {
140
+ id Int @id @default(autoincrement())
141
+ name String
142
+
143
+ // Self-referencing relationship for infinite folder depth
144
+ parentId Int?
145
+ parent Folder? @relation("FolderHierarchy", fields: [parentId], references: [id])
146
+ children Folder[] @relation("FolderHierarchy")
147
+
148
+ // Cached counts for fast UI rendering
149
+ folderCount Int @default(0)
150
+ fileCount Int @default(0)
151
+
152
+ // Path optimization
153
+ pathId Int @default(0)
154
+ path String? // e.g. "/1/5/12"
155
+
156
+ createdAt DateTime @default(now())
157
+ updatedAt DateTime @updatedAt
158
+
159
+ files File[]
160
+
161
+ @@index([parentId])
162
+ }
163
+
164
+ model File {
165
+ id Int @id @default(autoincrement())
166
+ name String
167
+
168
+ // Foreign Key to Folder
169
+ folderId Int?
170
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
171
+ folderPath String?
172
+
173
+ // Core Asset Data
174
+ size Int
175
+ url String
176
+ previewUrl String? // Lightweight thumbnail URL
177
+ mime String // e.g. "image/jpeg", "video/mp4"
178
+ ext String?
179
+ hash String?
180
+
181
+ // Common Media Details
182
+ alternativeText String?
183
+ caption String?
184
+ width Int?
185
+ height Int?
186
+
187
+ // JSONB storage for flexible metadata
188
+ // (e.g., formats: { thumbnail: {...}, small: {...} })
189
+ formats Json?
190
+ // (e.g., metaData: { duration: 120, bitrate: 320, pageCount: 15 })
191
+ metaData Json?
192
+
193
+ createdAt DateTime @default(now())
194
+ updatedAt DateTime @updatedAt
195
+
196
+ @@index([folderId])
197
+ }
198
+ ```
199
+
200
+
201
+ ### Option B: Drizzle ORM Schema (PostgreSQL)
202
+
203
+ If you prefer a lighter, TypeScript-first approach using Drizzle:
204
+
205
+ ```typescript
206
+ import { pgTable, serial, varchar, integer, timestamp, jsonb, text } from "drizzle-orm/pg-core";
207
+
208
+ export const folders = pgTable("folders", {
209
+ id: serial("id").primaryKey(),
210
+ name: varchar("name", { length: 255 }).notNull(),
211
+
212
+ // Nullable parentId allows root-level folders
213
+ parentId: integer("parent_id"), // Needs recursive FK setup in relations
214
+
215
+ folderCount: integer("folder_count").default(0),
216
+ fileCount: integer("file_count").default(0),
217
+
218
+ pathId: integer("path_id").default(0),
219
+ path: varchar("path", { length: 255 }),
220
+
221
+ createdAt: timestamp("created_at").defaultNow().notNull(),
222
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
223
+ });
224
+
225
+ export const files = pgTable("files", {
226
+ id: serial("id").primaryKey(),
227
+ name: varchar("name", { length: 255 }).notNull(),
228
+
229
+ folderId: integer("folder_id").references(() => folders.id, { onDelete: "cascade" }),
230
+ folderPath: varchar("folder_path", { length: 255 }),
231
+
232
+ size: integer("size").notNull(),
233
+ url: text("url").notNull(),
234
+ previewUrl: text("preview_url"),
235
+ mime: varchar("mime", { length: 100 }).notNull(),
236
+ ext: varchar("ext", { length: 20 }),
237
+ hash: varchar("hash", { length: 255 }),
238
+
239
+ alternativeText: text("alternative_text"),
240
+ caption: text("caption"),
241
+ width: integer("width"),
242
+ height: integer("height"),
243
+
244
+ // JSONB is perfect for storing our dynamic metadata & responsive formats
245
+ formats: jsonb("formats"),
246
+ metaData: jsonb("meta_data"),
247
+
248
+ createdAt: timestamp("created_at").defaultNow().notNull(),
249
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
250
+ });
251
+ ```
@@ -0,0 +1,292 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type FileUploadInput = {
4
+ file: File;
5
+ metadata: Partial<FileMetaData>;
6
+ videoSource?: VideoSource;
7
+ };
8
+ interface IFileManagerProvider {
9
+ getFolder(folderId: FolderId): Promise<Folder | null>;
10
+ getFolders(folderId: FolderId, page?: number, limit?: number, query?: string): Promise<{
11
+ folders: Folder[];
12
+ pagination: PaginationInfo;
13
+ }>;
14
+ getTags(): Promise<string[]>;
15
+ getFiles(folderId: FolderId, fileTypes?: FileType[] | null, page?: number, limit?: number, query?: string): Promise<{
16
+ files: FileMetaData[];
17
+ pagination: PaginationInfo;
18
+ }>;
19
+ /**
20
+ * Get files and folders separately (folders always come first)
21
+ * Folders are returned for the current page, followed by files
22
+ */
23
+ getItems(folderId: FolderId, fileTypes?: FileType[], page?: number, limit?: number, query?: string): Promise<{
24
+ folders: Folder[];
25
+ files: FileMetaData[];
26
+ pagination: PaginationInfo;
27
+ }>;
28
+ createFolder(name: string, parentId?: FolderId): Promise<Folder>;
29
+ uploadFiles(files: FileUploadInput[], folderId?: FolderId): Promise<FileMetaData[]>;
30
+ renameFolder(folderId: FolderId, newName: string): Promise<Folder>;
31
+ moveFiles(fileIds: EntityId[], newFolderId: FolderId): Promise<FileMetaData[]>;
32
+ moveFolders(folderIds: FolderId[], newParentId: FolderId): Promise<Folder[]>;
33
+ updateFileMetaData(fileId: EntityId, metaData: Partial<FileMetaData>): Promise<FileMetaData>;
34
+ deleteFiles(fileIds: EntityId[]): Promise<void>;
35
+ deleteFolders(folderIds: FolderId[]): Promise<void>;
36
+ findFiles(searchQuery: string): Promise<FileMetaData[]>;
37
+ findFolders(searchQuery: string): Promise<Folder[]>;
38
+ }
39
+
40
+ declare const MODE: {
41
+ readonly PAGE: "page";
42
+ readonly MODAL: "modal";
43
+ };
44
+ type Mode = (typeof MODE)[keyof typeof MODE];
45
+ declare const FILE_TYPE: {
46
+ readonly IMAGE: "images";
47
+ readonly VIDEO: "videos";
48
+ readonly AUDIO: "audios";
49
+ readonly FILE: "files";
50
+ };
51
+ type FileType = (typeof FILE_TYPE)[keyof typeof FILE_TYPE];
52
+ declare const SELECTION_MODE: {
53
+ readonly SINGLE: "single";
54
+ readonly MULTIPLE: "multiple";
55
+ };
56
+ type SelectionMode = (typeof SELECTION_MODE)[keyof typeof SELECTION_MODE];
57
+ declare const VIEW_MODE: {
58
+ readonly GRID: "grid";
59
+ readonly LIST: "list";
60
+ };
61
+ type ViewMode = (typeof VIEW_MODE)[keyof typeof VIEW_MODE];
62
+ declare const VIDEO_SOURCE: {
63
+ readonly LOCAL: "local";
64
+ readonly REMOTE: "remote";
65
+ readonly YOUTUBE: "youtube";
66
+ readonly VIMEO: "vimeo";
67
+ };
68
+ type VideoSource = (typeof VIDEO_SOURCE)[keyof typeof VIDEO_SOURCE];
69
+ interface MetaDataType {
70
+ /** Video or Audio duration in seconds. */
71
+ duration?: number;
72
+ /** Provider source for video content (e.g., 'local', 'youtube'). */
73
+ videoSource?: VideoSource;
74
+ /** Audio bitrate in kbps. */
75
+ bitrate?: number;
76
+ /** Total number of pages for document file types. */
77
+ pageCount?: number;
78
+ /** Original creator or author of the document. */
79
+ author?: string;
80
+ /** General description text used across multiple asset types. */
81
+ description?: string;
82
+ }
83
+ type EntityId = string | number;
84
+ type FolderId = string | number | null;
85
+ interface Folder {
86
+ id: FolderId;
87
+ name: string;
88
+ pathId: number;
89
+ path: string;
90
+ parent?: Folder | null;
91
+ folderCount?: number;
92
+ parentId: FolderId;
93
+ folderPath?: string;
94
+ color?: string;
95
+ fileCount?: number;
96
+ createdAt: Date;
97
+ updatedAt: Date;
98
+ tags?: string[];
99
+ }
100
+ interface FormatDetails {
101
+ ext: string;
102
+ url: string;
103
+ hash: string;
104
+ mime: string;
105
+ name: string;
106
+ path: string | null;
107
+ size: number;
108
+ width: number;
109
+ height: number;
110
+ }
111
+ /**
112
+ * Core interface representing a File entity in the file manager.
113
+ * Supports various formats (images, videos, audio, documents) via common fields
114
+ * and nested metadata structures.
115
+ */
116
+ interface FileMetaData {
117
+ /** Unique identifier for the file. */
118
+ id: EntityId;
119
+ /** Human-readable name of the file (including extension). */
120
+ name: string;
121
+ /** ID of the folder containing this file. Null represents the root directory. */
122
+ folderId: FolderId;
123
+ /** Path representation of the file's location (e.g., "/1/156"). */
124
+ folderPath?: string;
125
+ /** Size of the file in bytes. */
126
+ size: number;
127
+ /** Direct URL path to access or download the full asset. */
128
+ url: string;
129
+ /** URL to an optimized, lightweight thumbnail or preview of the asset. */
130
+ previewUrl?: string;
131
+ /** Content-Type HTTP header representation (e.g., "image/jpeg"). */
132
+ mime: string;
133
+ /** File extension including the dot (e.g., ".jpg"). */
134
+ ext?: string;
135
+ /** Content hash for deduplication and cache busting. */
136
+ hash?: string;
137
+ /** Accessible alt text for images to display when images are disabled. */
138
+ alternativeText?: string;
139
+ /** Caption text commonly used in images and videos. */
140
+ caption?: string;
141
+ /** Intrinsic width in pixels for image/video assets. */
142
+ width?: number;
143
+ /** Intrinsic height in pixels for image/video assets. */
144
+ height?: number;
145
+ /** Collection of generated optimized formats for images. */
146
+ formats?: Record<string, FormatDetails>;
147
+ /** Dynamic metadata payload containing properties specific to the asset type. */
148
+ metaData: MetaDataType;
149
+ /** Timestamp of file creation. */
150
+ createdAt: Date;
151
+ /** Timestamp of last file modification. */
152
+ updatedAt: Date;
153
+ /** Categorization tags for sorting and discovery. */
154
+ tags?: string[];
155
+ }
156
+ interface PaginationInfo {
157
+ currentPage: number;
158
+ totalPages: number;
159
+ totalFiles: number;
160
+ filesPerPage: number;
161
+ }
162
+ interface FileManagerPageProps {
163
+ allowedFileTypes: FileType[];
164
+ viewMode: ViewMode;
165
+ initialFolderId?: FolderId;
166
+ provider: IFileManagerProvider;
167
+ basePath?: string;
168
+ }
169
+ interface FileManagerModalProps {
170
+ open: boolean;
171
+ onClose: () => void;
172
+ onFilesSelected: (files: FileMetaData[]) => void;
173
+ fileSelectionMode?: SelectionMode;
174
+ allowedFileTypes: FileType[];
175
+ acceptedFileTypes?: FileType[];
176
+ viewMode?: ViewMode;
177
+ initialFolderId?: FolderId;
178
+ provider: IFileManagerProvider;
179
+ basePath?: string;
180
+ }
181
+ interface FileManagerRootProps {
182
+ mode: Mode;
183
+ selectionMode: SelectionMode;
184
+ allowedFileTypes: FileType[];
185
+ viewMode: ViewMode;
186
+ initialFolderId?: FolderId;
187
+ acceptedFileTypesForModal?: FileType[];
188
+ provider: IFileManagerProvider;
189
+ basePath?: string;
190
+ onFilesSelected?: (files: FileMetaData[]) => void;
191
+ onClose?: () => void;
192
+ maxUploadFiles?: number;
193
+ maxUploadSize?: number;
194
+ }
195
+
196
+ declare function FileManager(props: FileManagerPageProps): react_jsx_runtime.JSX.Element;
197
+
198
+ declare function FileManagerModal({ open, onClose, ...props }: FileManagerModalProps): react_jsx_runtime.JSX.Element;
199
+
200
+ interface FileManagerContextType {
201
+ files: FileMetaData[];
202
+ folders: Folder[];
203
+ selectedFiles: FileMetaData[];
204
+ selectedFolders: Folder[];
205
+ currentFolder: Folder | null;
206
+ isLoading: boolean;
207
+ pagination: PaginationInfo;
208
+ isUploadModalOpen: boolean;
209
+ isCreateFolderModalOpen: boolean;
210
+ isSearchModalOpen: boolean;
211
+ isMoveFileModalOpen: boolean;
212
+ isRenameFolderModalOpen: boolean;
213
+ fileDetailsModalFile: FileMetaData | null;
214
+ folderToRename: Folder | null;
215
+ mode: Mode;
216
+ selectionMode: SelectionMode;
217
+ allowedFileTypes: FileType[];
218
+ acceptedFileTypesForModal?: FileType[];
219
+ maxUploadFiles: number;
220
+ maxUploadSize: number;
221
+ provider: IFileManagerProvider;
222
+ basePath?: string;
223
+ setSelectedFiles: (files: FileMetaData[]) => void;
224
+ setSelectedFolders: (folders: Folder[]) => void;
225
+ setIsUploadModalOpen: (isOpen: boolean) => void;
226
+ setIsCreateFolderModalOpen: (isOpen: boolean) => void;
227
+ setIsSearchModalOpen: (isOpen: boolean) => void;
228
+ setIsMoveFileModalOpen: (isOpen: boolean) => void;
229
+ setIsRenameFolderModalOpen: (isOpen: boolean) => void;
230
+ setFileDetailsModalFile: (file: FileMetaData | null) => void;
231
+ setFolderToRename: (folder: Folder | null) => void;
232
+ handleFileClick: (file: FileMetaData, event?: React.MouseEvent, isCheckboxClick?: boolean) => void;
233
+ handleFolderClick: (folder: Folder | null, event?: React.MouseEvent, isCheckboxClick?: boolean) => void;
234
+ handleClearSelection: () => void;
235
+ handleSelectAllGlobal: (checked: boolean) => void;
236
+ handlePageChange: (page: number) => void;
237
+ searchQuery: string;
238
+ updateSearchQuery: (query: string) => void;
239
+ uploadFiles: (fileUploadInput: FileUploadInput[]) => Promise<void>;
240
+ createFolder: (name: string) => Promise<void>;
241
+ bulkMove: (targetFolderId: FolderId) => Promise<void>;
242
+ renameFolder: (folderId: EntityId, newName: string) => Promise<void>;
243
+ updateFileMetadata: (fileId: EntityId, metadata: Partial<FileMetaData>) => Promise<void>;
244
+ bulkDelete: () => Promise<void>;
245
+ refreshData: () => Promise<void>;
246
+ isInSelectionMode: () => boolean;
247
+ getCurrentFolder: () => Folder | null;
248
+ getSelectionState: () => boolean | "indeterminate";
249
+ onClose?: () => void;
250
+ onFilesSelected?: (files: FileMetaData[]) => void;
251
+ }
252
+ declare function FileManagerProvider({ children, mode, selectionMode, allowedFileTypes, onFilesSelected, onClose, acceptedFileTypesForModal, initialFolderId, provider, basePath, maxUploadFiles, maxUploadSize, }: FileManagerRootProps & {
253
+ children: React.ReactNode;
254
+ }): react_jsx_runtime.JSX.Element;
255
+ declare function useFileManager(): FileManagerContextType;
256
+
257
+ declare class MockProvider implements IFileManagerProvider {
258
+ getFolder(folderId: FolderId): Promise<Folder | null>;
259
+ getFolders(folderId: FolderId, page?: number, limit?: number, query?: string): Promise<{
260
+ folders: Folder[];
261
+ pagination: PaginationInfo;
262
+ }>;
263
+ getTags(): Promise<string[]>;
264
+ getFiles(folderId: FolderId, fileTypes?: FileType[], page?: number, limit?: number, query?: string): Promise<{
265
+ files: FileMetaData[];
266
+ pagination: PaginationInfo;
267
+ }>;
268
+ /**
269
+ * Get files and folders separately (folders always come first)
270
+ * Folders are not paginated (all folders in current directory are returned)
271
+ * Files are paginated after folders
272
+ */
273
+ getItems(folderId: FolderId, fileTypes?: FileType[], page?: number, limit?: number, query?: string): Promise<{
274
+ folders: Folder[];
275
+ files: FileMetaData[];
276
+ pagination: PaginationInfo;
277
+ }>;
278
+ createFolder(name: string, parentId?: FolderId): Promise<Folder>;
279
+ private getMetaDataType;
280
+ private getFileType;
281
+ uploadFiles(files: FileUploadInput[], folderId?: FolderId): Promise<FileMetaData[]>;
282
+ renameFolder(folderId: EntityId, newName: string): Promise<Folder>;
283
+ updateFileMetaData(fileId: EntityId, updates: Partial<FileMetaData>): Promise<FileMetaData>;
284
+ deleteFiles(fileIds: EntityId[]): Promise<void>;
285
+ deleteFolders(folderIds: EntityId[]): Promise<void>;
286
+ findFiles(searchQuery: string): Promise<FileMetaData[]>;
287
+ findFolders(searchQuery: string): Promise<Folder[]>;
288
+ moveFiles(fileIds: EntityId[], newFolderId: FolderId): Promise<FileMetaData[]>;
289
+ moveFolders(folderIds: FolderId[], newParentId: FolderId): Promise<Folder[]>;
290
+ }
291
+
292
+ export { FileManager, FileManagerModal, type FileManagerModalProps, type FileManagerPageProps, FileManagerProvider, type FileMetaData, type FileType, type Folder, type IFileManagerProvider, MockProvider, type SelectionMode, type ViewMode, useFileManager };