@unciatech/file-manager 0.0.42 → 0.0.43
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 +291 -410
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,517 +1,398 @@
|
|
|
1
1
|
# 🗂️ File Manager
|
|
2
2
|
|
|
3
|
-
A robust, production-ready file management component for
|
|
3
|
+
A robust, production-ready file management component for **React applications**.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It works with **Vite, Next.js, Remix, CRA**, and other React frameworks.
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
7
|
+
The library supports deep folder hierarchies, drag-and-drop uploads, metadata management for multiple file types (Images, Videos, Audio, Documents), and a responsive grid interface optimized for large media libraries.
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
- **Framework**: React (any — Vite, Next.js, Remix, CRA)
|
|
18
|
-
- **Styling**: Tailwind CSS
|
|
19
|
-
- **Icons**: Custom SVG Icons
|
|
20
|
-
- **Notifications**: Sonner
|
|
9
|
+
---
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
> This library is currently in **BETA**. Please report any bugs or feature requests on the GitHub issues page.
|
|
11
|
+
# 🌟 Key Features
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
* **Dual Operating Modes** - Use as a full-page media library or as a picker modal.
|
|
14
|
+
* **Unified Grid View** - Responsive layout that intelligently renders thumbnails, icons, and metadata.
|
|
15
|
+
* **Nested Folder Structure** - Infinite folder depth with smooth paginated fetching.
|
|
16
|
+
* **Provider Agnostic** - Integrates with any backend through the `IFileManagerProvider` interface.
|
|
17
|
+
* **Bulk Actions** - Select and move/delete multiple files or folders.
|
|
18
|
+
* **Optimistic UI Updates** - Instant UI feedback with background synchronization.
|
|
19
|
+
* **Error Boundary Protection** - Built-in `<FileManagerErrorBoundary>` prevents crashes.
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
---
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
If you are using **Tailwind CSS v4** (like in newer Next.js or Vite projects), add this to your main CSS file (`globals.css` or `index.css`). This pulls in the required theme configuration and ensures the library is scanned:
|
|
23
|
+
# 📦 Installation
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@
|
|
25
|
+
Install the file manager and required utilities:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @unciatech/file-manager class-variance-authority clsx tailwind-merge
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
If
|
|
31
|
+
> ⚠️ This library relies on **Tailwind CSS** for styling.
|
|
32
|
+
> If your project does not use Tailwind yet, install it in Step 3.
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# ⚡ Quick Start
|
|
37
|
+
|
|
38
|
+
You can instantly test the UI using the built-in `MockProvider`.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { FileManager, MockProvider } from "@unciatech/file-manager"
|
|
42
|
+
|
|
43
|
+
const provider = new MockProvider()
|
|
44
|
+
|
|
45
|
+
export default function App() {
|
|
46
|
+
return (
|
|
47
|
+
<FileManager
|
|
48
|
+
provider={provider}
|
|
49
|
+
basePath="/"
|
|
50
|
+
viewMode="grid"
|
|
51
|
+
allowedFileTypes={["images", "videos", "files"]}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
46
55
|
```
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
---
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
# 🛠 Setup & Requirements
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
## Step 1 - Install the Library
|
|
53
62
|
|
|
54
|
-
Install the core library along with its Radix UI and Tailwind dependencies:
|
|
55
63
|
```bash
|
|
56
64
|
npm install @unciatech/file-manager class-variance-authority clsx tailwind-merge
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Step 2 - Import Styles
|
|
70
|
+
|
|
71
|
+
Import the file manager styles in your root file.
|
|
72
|
+
|
|
73
|
+
### Vite / React
|
|
60
74
|
|
|
61
|
-
**(CRITICAL) Import the styles:**
|
|
62
|
-
Import the styles in your root layout (`layout.tsx` for Next.js) or top-level file (`main.tsx` / `App.tsx` for Vite):
|
|
63
75
|
```ts
|
|
64
|
-
import
|
|
76
|
+
import "@unciatech/file-manager/styles"
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
Place this inside:
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
```
|
|
82
|
+
main.tsx or App.tsx
|
|
83
|
+
```
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
### Next.js
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
// lib/my-api-provider.ts
|
|
75
|
-
import {
|
|
76
|
-
IFileManagerProvider,
|
|
77
|
-
FolderId,
|
|
78
|
-
FileUploadInput
|
|
79
|
-
} from "@/types/file-manager"; // Or "@unciatech/file-manager" for external users
|
|
87
|
+
Import inside your root layout:
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
```ts
|
|
90
|
+
import "@unciatech/file-manager/styles"
|
|
91
|
+
```
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
async getFolders(folderId: FolderId, page = 1, limit = 24) {
|
|
86
|
-
const parentQuery = folderId ? `&parentId=${folderId}` : "&isRoot=true";
|
|
87
|
-
|
|
88
|
-
// Simulate real API Call
|
|
89
|
-
const res = await fetch(`${this.baseUrl}/folders?page=${page}&limit=${limit}${parentQuery}`);
|
|
90
|
-
|
|
91
|
-
if (!res.ok) throw new Error("Failed to fetch folders");
|
|
92
|
-
|
|
93
|
-
const data = await res.json();
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
folders: data.folders, // Array of Folder objects matching our interface
|
|
97
|
-
pagination: data.pagination // { currentPage, totalPages, totalFiles, filesPerPage }
|
|
98
|
-
};
|
|
99
|
-
}
|
|
93
|
+
Place this in:
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (folderId) formData.append("folderId", String(folderId));
|
|
105
|
-
|
|
106
|
-
filesInput.forEach(({ file }) => {
|
|
107
|
-
formData.append("files", file);
|
|
108
|
-
});
|
|
95
|
+
```
|
|
96
|
+
app/layout.tsx
|
|
97
|
+
```
|
|
109
98
|
|
|
110
|
-
|
|
111
|
-
method: 'POST',
|
|
112
|
-
body: formData
|
|
113
|
-
});
|
|
99
|
+
---
|
|
114
100
|
|
|
115
|
-
|
|
116
|
-
}
|
|
101
|
+
# Step 3 - Configure Tailwind CSS
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
```
|
|
103
|
+
This library relies heavily on **Tailwind CSS**.
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
If your project **already uses Tailwind**, follow Option A.
|
|
106
|
+
If **not**, follow Option B.
|
|
124
107
|
|
|
125
|
-
|
|
108
|
+
---
|
|
126
109
|
|
|
127
|
-
|
|
110
|
+
## Option A - Project Already Uses Tailwind
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
<summary><b>Example A: Vite (React Router)</b></summary>
|
|
131
|
-
<br>
|
|
112
|
+
### Tailwind v4
|
|
132
113
|
|
|
133
|
-
|
|
134
|
-
Wrap your application in `BrowserRouter`:
|
|
135
|
-
```tsx
|
|
136
|
-
import React from 'react';
|
|
137
|
-
import ReactDOM from 'react-dom/client';
|
|
138
|
-
import { BrowserRouter } from 'react-router-dom';
|
|
139
|
-
import App from './App';
|
|
140
|
-
import './index.css';
|
|
114
|
+
Add this to your main CSS file (`globals.css` or `index.css`):
|
|
141
115
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<App />
|
|
146
|
-
</BrowserRouter>
|
|
147
|
-
</React.StrictMode>
|
|
148
|
-
);
|
|
149
|
-
```
|
|
116
|
+
```css
|
|
117
|
+
@import "tailwindcss";
|
|
118
|
+
@import "@unciatech/file-manager/styles";
|
|
150
119
|
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
import { useState } from 'react';
|
|
154
|
-
import { useNavigate, Routes, Route, Link } from 'react-router-dom';
|
|
155
|
-
import { FileManager, FileManagerModal, MockProvider } from '@unciatech/file-manager';
|
|
120
|
+
@source "../node_modules/@unciatech/file-manager";
|
|
121
|
+
```
|
|
156
122
|
|
|
157
|
-
|
|
123
|
+
---
|
|
158
124
|
|
|
159
|
-
|
|
160
|
-
const navigate = useNavigate();
|
|
161
|
-
return (
|
|
162
|
-
<div className="h-screen w-full flex flex-col">
|
|
163
|
-
<div className="flex justify-between p-4 border-b">
|
|
164
|
-
<h1 className="text-xl font-bold">Media Library</h1>
|
|
165
|
-
<Link to="/" className="text-blue-600 hover:underline">Back to Demo</Link>
|
|
166
|
-
</div>
|
|
167
|
-
<div className="flex-1 overflow-hidden relative">
|
|
168
|
-
<FileManager
|
|
169
|
-
provider={provider}
|
|
170
|
-
basePath="full"
|
|
171
|
-
allowedFileTypes={["images", "videos", "audios", "files"]}
|
|
172
|
-
onNavigate={(url, opts) => navigate(url, { replace: opts?.replace })}
|
|
173
|
-
/>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
125
|
+
### Tailwind v3
|
|
178
126
|
|
|
179
|
-
|
|
180
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
181
|
-
return (
|
|
182
|
-
<div className="p-10 flex flex-col gap-4">
|
|
183
|
-
<div className="flex gap-4">
|
|
184
|
-
<button
|
|
185
|
-
onClick={() => setIsOpen(true)}
|
|
186
|
-
className="px-6 py-2 bg-blue-600 text-white rounded-md"
|
|
187
|
-
>
|
|
188
|
-
Open File Picker
|
|
189
|
-
</button>
|
|
190
|
-
<Link to="/full" className="px-6 py-2 bg-zinc-200 rounded-md">
|
|
191
|
-
Go to Full Page View
|
|
192
|
-
</Link>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<FileManagerModal
|
|
196
|
-
open={isOpen}
|
|
197
|
-
onClose={() => setIsOpen(false)}
|
|
198
|
-
provider={provider}
|
|
199
|
-
basePath="/"
|
|
200
|
-
onFilesSelected={(files) => console.log(files)}
|
|
201
|
-
/>
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
127
|
+
Update your `tailwind.config.ts`:
|
|
205
128
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<Route path="/full/*" element={<FullPage />} />
|
|
211
|
-
</Routes>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
129
|
+
```ts
|
|
130
|
+
content: [
|
|
131
|
+
"./node_modules/@unciatech/file-manager/dist/**/*.{js,ts,jsx,tsx}",
|
|
132
|
+
]
|
|
214
133
|
```
|
|
215
134
|
|
|
216
|
-
|
|
135
|
+
---
|
|
217
136
|
|
|
218
|
-
|
|
219
|
-
<summary><b>Example B: Next.js (App Router)</b></summary>
|
|
220
|
-
<br>
|
|
137
|
+
## Option B - Install Tailwind CSS
|
|
221
138
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
import { useRouter } from 'next/navigation';
|
|
227
|
-
import { FileManager, MockProvider } from '@unciatech/file-manager';
|
|
139
|
+
```bash
|
|
140
|
+
npm install -D tailwindcss postcss autoprefixer
|
|
141
|
+
npx tailwindcss init -p
|
|
142
|
+
```
|
|
228
143
|
|
|
229
|
-
|
|
230
|
-
|
|
144
|
+
Update `tailwind.config.ts`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
content: [
|
|
148
|
+
"./index.html",
|
|
149
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
150
|
+
"./node_modules/@unciatech/file-manager/dist/**/*.{js,ts,jsx,tsx}",
|
|
151
|
+
]
|
|
152
|
+
```
|
|
231
153
|
|
|
232
|
-
|
|
233
|
-
const router = useRouter();
|
|
154
|
+
Add to `index.css` or `globals.css`:
|
|
234
155
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<FileManager
|
|
242
|
-
provider={provider}
|
|
243
|
-
basePath="/media"
|
|
244
|
-
allowedFileTypes={["images", "videos", "audios", "files"]}
|
|
245
|
-
viewMode="grid"
|
|
246
|
-
onNavigate={(url, opts) =>
|
|
247
|
-
opts?.replace ? router.replace(url) : router.push(url)
|
|
248
|
-
}
|
|
249
|
-
/>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
156
|
+
```css
|
|
157
|
+
@tailwind base;
|
|
158
|
+
@tailwind components;
|
|
159
|
+
@tailwind utilities;
|
|
160
|
+
|
|
161
|
+
@import "@unciatech/file-manager/styles";
|
|
254
162
|
```
|
|
255
163
|
|
|
256
|
-
|
|
257
|
-
```tsx
|
|
258
|
-
'use client';
|
|
259
|
-
import { useState } from 'react';
|
|
260
|
-
import { FileManagerModal, MockProvider } from '@unciatech/file-manager';
|
|
164
|
+
---
|
|
261
165
|
|
|
262
|
-
|
|
166
|
+
# Step 4 - Create Your Custom API Provider
|
|
263
167
|
|
|
264
|
-
|
|
265
|
-
|
|
168
|
+
> 💡 **Pro Tip - The Mock Provider**
|
|
169
|
+
> If you are just prototyping and don't have a backend ready yet, you can skip this step entirely.
|
|
170
|
+
>
|
|
171
|
+
> The library includes a fully functional `MockProvider` that simulates network latency and stores data in memory.
|
|
266
172
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<FileManagerModal
|
|
277
|
-
open={isOpen}
|
|
278
|
-
onClose={() => setIsOpen(false)}
|
|
279
|
-
provider={provider}
|
|
280
|
-
basePath="/"
|
|
281
|
-
onFilesSelected={(files) => {
|
|
282
|
-
console.log("Selected:", files);
|
|
283
|
-
setIsOpen(false);
|
|
284
|
-
}}
|
|
285
|
-
/>
|
|
286
|
-
</div>
|
|
287
|
-
);
|
|
288
|
-
}
|
|
173
|
+
Example:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { FileManager, MockProvider } from "@unciatech/file-manager"
|
|
177
|
+
|
|
178
|
+
const provider = new MockProvider()
|
|
179
|
+
|
|
180
|
+
<FileManager provider={provider} basePath="/" viewMode="grid" />
|
|
289
181
|
```
|
|
290
|
-
</details>
|
|
291
182
|
|
|
292
183
|
---
|
|
293
184
|
|
|
294
|
-
|
|
185
|
+
### Using Your Own Backend
|
|
295
186
|
|
|
296
|
-
|
|
187
|
+
The file manager is **backend-agnostic**.
|
|
297
188
|
|
|
298
|
-
|
|
189
|
+
Create a class implementing `IFileManagerProvider`.
|
|
299
190
|
|
|
300
|
-
|
|
191
|
+
```ts
|
|
192
|
+
import {
|
|
193
|
+
IFileManagerProvider,
|
|
194
|
+
FolderId,
|
|
195
|
+
FileUploadInput,
|
|
196
|
+
} from "@unciatech/file-manager"
|
|
301
197
|
|
|
302
|
-
|
|
303
|
-
|
|
198
|
+
export class MyCustomApiProvider implements IFileManagerProvider {
|
|
199
|
+
private baseUrl = "https://api.mybackend.com/v1"
|
|
304
200
|
|
|
305
|
-
|
|
306
|
-
|
|
201
|
+
async getFolders(folderId: FolderId, page = 1, limit = 24) {
|
|
202
|
+
const parentQuery = folderId ? `&parentId=${folderId}` : "&isRoot=true"
|
|
307
203
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
allowedFileTypes={['images', 'videos']}
|
|
312
|
-
onNavigate={(url) => navigate(url)}
|
|
313
|
-
basePath="/media"
|
|
314
|
-
/>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
```
|
|
204
|
+
const res = await fetch(
|
|
205
|
+
`${this.baseUrl}/folders?page=${page}&limit=${limit}${parentQuery}`
|
|
206
|
+
)
|
|
318
207
|
|
|
319
|
-
|
|
208
|
+
if (!res.ok) throw new Error("Failed to fetch folders")
|
|
320
209
|
|
|
321
|
-
|
|
322
|
-
'use client';
|
|
323
|
-
import { useRouter } from 'next/navigation';
|
|
210
|
+
const data = await res.json()
|
|
324
211
|
|
|
325
|
-
|
|
326
|
-
|
|
212
|
+
return {
|
|
213
|
+
folders: data.folders,
|
|
214
|
+
pagination: data.pagination,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
327
217
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
provider={provider}
|
|
331
|
-
allowedFileTypes={['images', 'videos']}
|
|
332
|
-
onNavigate={(url) => router.push(url)}
|
|
333
|
-
basePath="/media"
|
|
334
|
-
/>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
```
|
|
218
|
+
async uploadFiles(filesInput: FileUploadInput[], folderId?: FolderId) {
|
|
219
|
+
const formData = new FormData()
|
|
338
220
|
|
|
339
|
-
|
|
221
|
+
if (folderId) {
|
|
222
|
+
formData.append("folderId", String(folderId))
|
|
223
|
+
}
|
|
340
224
|
|
|
341
|
-
|
|
342
|
-
|
|
225
|
+
filesInput.forEach(({ file }) => {
|
|
226
|
+
formData.append("files", file)
|
|
227
|
+
})
|
|
343
228
|
|
|
344
|
-
|
|
345
|
-
|
|
229
|
+
const res = await fetch(`${this.baseUrl}/upload`, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
body: formData,
|
|
232
|
+
})
|
|
346
233
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
provider={provider}
|
|
350
|
-
allowedFileTypes={['images', 'videos']}
|
|
351
|
-
onNavigate={(url) => router.navigate({ href: url })}
|
|
352
|
-
basePath="/media"
|
|
353
|
-
/>
|
|
354
|
-
);
|
|
234
|
+
return res.json()
|
|
235
|
+
}
|
|
355
236
|
}
|
|
356
237
|
```
|
|
357
238
|
|
|
358
|
-
|
|
359
|
-
<FileManager
|
|
360
|
-
provider={provider}
|
|
361
|
-
allowedFileTypes={['images', 'videos']}
|
|
362
|
-
basePath="/media"
|
|
363
|
-
// no onNavigate needed
|
|
364
|
-
/>
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
## 🎮 Playgrounds & Reference Implementations
|
|
239
|
+
---
|
|
368
240
|
|
|
369
|
-
|
|
241
|
+
# 🔀 Router Integration
|
|
370
242
|
|
|
371
|
-
|
|
372
|
-
- **[TanStack Router Playground](file:///Users/avi/Developer/Uncia/file-manager/playgrounds/test-tanstack)**: Demonstrates integration with `@tanstack/react-router` using the `router` object and `href` based navigation.
|
|
243
|
+
By default the file manager uses `history.pushState`.
|
|
373
244
|
|
|
374
|
-
|
|
375
|
-
- Styling with Tailwind CSS v4
|
|
376
|
-
- Mapping `onNavigate` to your framework's router
|
|
377
|
-
- Using the `MockProvider` for rapid prototyping
|
|
378
|
-
- Configuring `FileManagerModal` v/s full-page `FileManager`
|
|
245
|
+
If your app uses a router, pass the `onNavigate` prop.
|
|
379
246
|
|
|
380
247
|
---
|
|
381
248
|
|
|
382
|
-
|
|
383
|
-
|
|
249
|
+
# Example - React Router
|
|
250
|
+
|
|
251
|
+
### Install
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
npm install react-router-dom
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### main.tsx
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import React from "react"
|
|
261
|
+
import ReactDOM from "react-dom/client"
|
|
262
|
+
import { BrowserRouter } from "react-router-dom"
|
|
263
|
+
import App from "./App"
|
|
264
|
+
import "./index.css"
|
|
265
|
+
|
|
266
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
267
|
+
<React.StrictMode>
|
|
268
|
+
<BrowserRouter>
|
|
269
|
+
<App />
|
|
270
|
+
</BrowserRouter>
|
|
271
|
+
</React.StrictMode>
|
|
272
|
+
)
|
|
273
|
+
```
|
|
384
274
|
|
|
385
275
|
---
|
|
386
276
|
|
|
387
|
-
|
|
277
|
+
# Example - TanStack Router
|
|
388
278
|
|
|
389
|
-
|
|
279
|
+
### Install
|
|
390
280
|
|
|
391
|
-
|
|
281
|
+
```bash
|
|
282
|
+
npm install @tanstack/react-router
|
|
283
|
+
```
|
|
392
284
|
|
|
393
|
-
###
|
|
285
|
+
### main.tsx
|
|
394
286
|
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
|
|
287
|
+
```tsx
|
|
288
|
+
import React from "react"
|
|
289
|
+
import ReactDOM from "react-dom/client"
|
|
290
|
+
import {
|
|
291
|
+
RouterProvider,
|
|
292
|
+
createRouter,
|
|
293
|
+
createRoute,
|
|
294
|
+
createRootRoute,
|
|
295
|
+
} from "@tanstack/react-router"
|
|
296
|
+
|
|
297
|
+
import App, { ModalPage } from "./App"
|
|
298
|
+
import "./index.css"
|
|
299
|
+
|
|
300
|
+
const rootRoute = createRootRoute()
|
|
301
|
+
|
|
302
|
+
const indexRoute = createRoute({
|
|
303
|
+
getParentRoute: () => rootRoute,
|
|
304
|
+
path: "/",
|
|
305
|
+
component: ModalPage,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const fullRoute = createRoute({
|
|
309
|
+
getParentRoute: () => rootRoute,
|
|
310
|
+
path: "/full",
|
|
311
|
+
component: App,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const catchAllRoute = createRoute({
|
|
315
|
+
getParentRoute: () => rootRoute,
|
|
316
|
+
path: "$",
|
|
317
|
+
component: App,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const routeTree = rootRoute.addChildren([
|
|
321
|
+
indexRoute,
|
|
322
|
+
fullRoute,
|
|
323
|
+
catchAllRoute,
|
|
324
|
+
])
|
|
325
|
+
|
|
326
|
+
const router = createRouter({ routeTree })
|
|
327
|
+
|
|
328
|
+
declare module "@tanstack/react-router" {
|
|
329
|
+
interface Register {
|
|
330
|
+
router: typeof router
|
|
331
|
+
}
|
|
398
332
|
}
|
|
399
333
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
334
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
335
|
+
<React.StrictMode>
|
|
336
|
+
<RouterProvider router={router} />
|
|
337
|
+
</React.StrictMode>
|
|
338
|
+
)
|
|
339
|
+
```
|
|
404
340
|
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
# 🗄️ Database Schema (Optional)
|
|
344
|
+
|
|
345
|
+
Using **PostgreSQL** with JSON support is recommended.
|
|
346
|
+
|
|
347
|
+
Example Prisma schema:
|
|
348
|
+
|
|
349
|
+
```prisma
|
|
405
350
|
model Folder {
|
|
406
|
-
id Int
|
|
351
|
+
id Int @id @default(autoincrement())
|
|
407
352
|
name String
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
pathId Int @default(0)
|
|
420
|
-
path String? // e.g. "/1/5/12"
|
|
421
|
-
|
|
422
|
-
createdAt DateTime @default(now())
|
|
423
|
-
updatedAt DateTime @updatedAt
|
|
424
|
-
|
|
353
|
+
parentId Int?
|
|
354
|
+
|
|
355
|
+
folderCount Int @default(0)
|
|
356
|
+
fileCount Int @default(0)
|
|
357
|
+
|
|
358
|
+
pathId Int @default(0)
|
|
359
|
+
path String?
|
|
360
|
+
|
|
361
|
+
createdAt DateTime @default(now())
|
|
362
|
+
updatedAt DateTime @updatedAt
|
|
363
|
+
|
|
425
364
|
files File[]
|
|
426
365
|
|
|
427
366
|
@@index([parentId])
|
|
428
367
|
}
|
|
429
368
|
|
|
430
369
|
model File {
|
|
431
|
-
id
|
|
432
|
-
name
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
ext String?
|
|
445
|
-
hash String?
|
|
446
|
-
|
|
447
|
-
// Common Media Details
|
|
448
|
-
alternativeText String?
|
|
449
|
-
caption String?
|
|
450
|
-
width Int?
|
|
451
|
-
height Int?
|
|
452
|
-
|
|
453
|
-
// JSONB storage for flexible metadata
|
|
454
|
-
// (e.g., formats: { thumbnail: {...}, small: {...} })
|
|
455
|
-
formats Json?
|
|
456
|
-
// (e.g., metaData: { duration: 120, bitrate: 320, pageCount: 15 })
|
|
457
|
-
metaData Json?
|
|
458
|
-
|
|
459
|
-
createdAt DateTime @default(now())
|
|
460
|
-
updatedAt DateTime @updatedAt
|
|
461
|
-
|
|
462
|
-
@@index([folderId])
|
|
370
|
+
id Int @id @default(autoincrement())
|
|
371
|
+
name String
|
|
372
|
+
folderId Int?
|
|
373
|
+
|
|
374
|
+
size Int
|
|
375
|
+
url String
|
|
376
|
+
mime String
|
|
377
|
+
|
|
378
|
+
formats Json?
|
|
379
|
+
metaData Json?
|
|
380
|
+
|
|
381
|
+
createdAt DateTime @default(now())
|
|
382
|
+
updatedAt DateTime @updatedAt
|
|
463
383
|
}
|
|
464
384
|
```
|
|
465
385
|
|
|
386
|
+
---
|
|
466
387
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Nullable parentId allows root-level folders
|
|
479
|
-
parentId: integer("parent_id"), // Needs recursive FK setup in relations
|
|
480
|
-
|
|
481
|
-
folderCount: integer("folder_count").default(0),
|
|
482
|
-
fileCount: integer("file_count").default(0),
|
|
483
|
-
|
|
484
|
-
pathId: integer("path_id").default(0),
|
|
485
|
-
path: varchar("path", { length: 255 }),
|
|
486
|
-
|
|
487
|
-
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
488
|
-
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
export const files = pgTable("files", {
|
|
492
|
-
id: serial("id").primaryKey(),
|
|
493
|
-
name: varchar("name", { length: 255 }).notNull(),
|
|
494
|
-
|
|
495
|
-
folderId: integer("folder_id").references(() => folders.id, { onDelete: "cascade" }),
|
|
496
|
-
folderPath: varchar("folder_path", { length: 255 }),
|
|
497
|
-
|
|
498
|
-
size: integer("size").notNull(),
|
|
499
|
-
url: text("url").notNull(),
|
|
500
|
-
previewUrl: text("preview_url"),
|
|
501
|
-
mime: varchar("mime", { length: 100 }).notNull(),
|
|
502
|
-
ext: varchar("ext", { length: 20 }),
|
|
503
|
-
hash: varchar("hash", { length: 255 }),
|
|
504
|
-
|
|
505
|
-
alternativeText: text("alternative_text"),
|
|
506
|
-
caption: text("caption"),
|
|
507
|
-
width: integer("width"),
|
|
508
|
-
height: integer("height"),
|
|
509
|
-
|
|
510
|
-
// JSONB is perfect for storing our dynamic metadata & responsive formats
|
|
511
|
-
formats: jsonb("formats"),
|
|
512
|
-
metaData: jsonb("meta_data"),
|
|
513
|
-
|
|
514
|
-
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
515
|
-
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
516
|
-
});
|
|
517
|
-
```
|
|
388
|
+
# ⚠️ Status
|
|
389
|
+
|
|
390
|
+
> This library is currently in **BETA**.
|
|
391
|
+
|
|
392
|
+
Please report bugs or feature requests through **GitHub Issues**.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
# License
|
|
397
|
+
|
|
398
|
+
MIT
|