@codaijs/keel 0.2.2 → 0.2.4
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/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
import { useFiles } from "@/hooks/useFiles";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Format a file size in bytes to a human-readable string.
|
|
6
|
-
*/
|
|
7
|
-
function formatFileSize(bytes: number | null): string {
|
|
8
|
-
if (bytes === null || bytes === 0) return "--";
|
|
9
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
10
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
11
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
12
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Format a date string to a short locale representation.
|
|
18
|
-
*/
|
|
19
|
-
function formatDate(dateString: string): string {
|
|
20
|
-
return new Date(dateString).toLocaleDateString(undefined, {
|
|
21
|
-
year: "numeric",
|
|
22
|
-
month: "short",
|
|
23
|
-
day: "numeric",
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Return a simple icon/label for a content type.
|
|
29
|
-
*/
|
|
30
|
-
function fileTypeIcon(contentType: string | null): string {
|
|
31
|
-
if (!contentType) return "FILE";
|
|
32
|
-
if (contentType.startsWith("image/")) return "IMG";
|
|
33
|
-
if (contentType.startsWith("video/")) return "VID";
|
|
34
|
-
if (contentType.startsWith("audio/")) return "AUD";
|
|
35
|
-
if (contentType === "application/pdf") return "PDF";
|
|
36
|
-
if (contentType.includes("spreadsheet") || contentType.includes("excel"))
|
|
37
|
-
return "XLS";
|
|
38
|
-
if (contentType.includes("document") || contentType.includes("word"))
|
|
39
|
-
return "DOC";
|
|
40
|
-
if (contentType.includes("zip") || contentType.includes("compressed"))
|
|
41
|
-
return "ZIP";
|
|
42
|
-
return "FILE";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* File browser component.
|
|
47
|
-
*
|
|
48
|
-
* Displays the current user's files in a list view with download and delete
|
|
49
|
-
* actions per file. Includes loading, empty, and error states.
|
|
50
|
-
*/
|
|
51
|
-
export function FileList() {
|
|
52
|
-
const { files, isLoading, error, deleteFile, getDownloadUrl, refresh } =
|
|
53
|
-
useFiles();
|
|
54
|
-
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
55
|
-
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
|
56
|
-
|
|
57
|
-
const handleDownload = useCallback(
|
|
58
|
-
async (id: string, fileName: string) => {
|
|
59
|
-
setDownloadingId(id);
|
|
60
|
-
try {
|
|
61
|
-
const url = await getDownloadUrl(id);
|
|
62
|
-
if (url) {
|
|
63
|
-
const a = document.createElement("a");
|
|
64
|
-
a.href = url;
|
|
65
|
-
a.download = fileName;
|
|
66
|
-
a.target = "_blank";
|
|
67
|
-
a.rel = "noopener noreferrer";
|
|
68
|
-
document.body.appendChild(a);
|
|
69
|
-
a.click();
|
|
70
|
-
document.body.removeChild(a);
|
|
71
|
-
}
|
|
72
|
-
} finally {
|
|
73
|
-
setDownloadingId(null);
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
[getDownloadUrl],
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const handleDelete = useCallback(
|
|
80
|
-
async (id: string) => {
|
|
81
|
-
if (!window.confirm("Are you sure you want to delete this file?")) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
setDeletingId(id);
|
|
85
|
-
try {
|
|
86
|
-
await deleteFile(id);
|
|
87
|
-
} finally {
|
|
88
|
-
setDeletingId(null);
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
[deleteFile],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// -- Loading state --------------------------------------------------------
|
|
95
|
-
if (isLoading) {
|
|
96
|
-
return (
|
|
97
|
-
<div className="flex items-center justify-center py-16">
|
|
98
|
-
<div className="h-8 w-8 animate-spin rounded-full border-4 border-keel-gray-600 border-t-keel-blue" />
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// -- Error state ----------------------------------------------------------
|
|
104
|
-
if (error) {
|
|
105
|
-
return (
|
|
106
|
-
<div className="rounded-xl border border-red-500/30 bg-red-500/10 px-6 py-8 text-center">
|
|
107
|
-
<p className="text-sm text-red-400">{error}</p>
|
|
108
|
-
<button
|
|
109
|
-
onClick={refresh}
|
|
110
|
-
className="mt-3 text-sm font-medium text-keel-blue hover:underline"
|
|
111
|
-
>
|
|
112
|
-
Try again
|
|
113
|
-
</button>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// -- Empty state ----------------------------------------------------------
|
|
119
|
-
if (files.length === 0) {
|
|
120
|
-
return (
|
|
121
|
-
<div className="rounded-xl border border-keel-gray-800 px-6 py-16 text-center">
|
|
122
|
-
<svg
|
|
123
|
-
className="mx-auto mb-4 h-12 w-12 text-keel-gray-600"
|
|
124
|
-
fill="none"
|
|
125
|
-
viewBox="0 0 24 24"
|
|
126
|
-
stroke="currentColor"
|
|
127
|
-
strokeWidth={1}
|
|
128
|
-
>
|
|
129
|
-
<path
|
|
130
|
-
strokeLinecap="round"
|
|
131
|
-
strokeLinejoin="round"
|
|
132
|
-
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
|
133
|
-
/>
|
|
134
|
-
</svg>
|
|
135
|
-
<p className="text-sm text-keel-gray-400">No files uploaded yet.</p>
|
|
136
|
-
<p className="mt-1 text-xs text-keel-gray-600">
|
|
137
|
-
Upload a file to get started.
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// -- File list ------------------------------------------------------------
|
|
144
|
-
return (
|
|
145
|
-
<div className="overflow-hidden rounded-xl border border-keel-gray-800">
|
|
146
|
-
<table className="w-full text-left text-sm">
|
|
147
|
-
<thead>
|
|
148
|
-
<tr className="border-b border-keel-gray-800 bg-keel-navy/50">
|
|
149
|
-
<th className="px-4 py-3 font-medium text-keel-gray-400">Type</th>
|
|
150
|
-
<th className="px-4 py-3 font-medium text-keel-gray-400">Name</th>
|
|
151
|
-
<th className="hidden px-4 py-3 font-medium text-keel-gray-400 sm:table-cell">
|
|
152
|
-
Size
|
|
153
|
-
</th>
|
|
154
|
-
<th className="hidden px-4 py-3 font-medium text-keel-gray-400 md:table-cell">
|
|
155
|
-
Date
|
|
156
|
-
</th>
|
|
157
|
-
<th className="px-4 py-3 text-right font-medium text-keel-gray-400">
|
|
158
|
-
Actions
|
|
159
|
-
</th>
|
|
160
|
-
</tr>
|
|
161
|
-
</thead>
|
|
162
|
-
<tbody>
|
|
163
|
-
{files.map((file) => (
|
|
164
|
-
<tr
|
|
165
|
-
key={file.id}
|
|
166
|
-
className="border-b border-keel-gray-800/50 last:border-b-0 hover:bg-keel-navy/30"
|
|
167
|
-
>
|
|
168
|
-
{/* Type badge */}
|
|
169
|
-
<td className="px-4 py-3">
|
|
170
|
-
<span className="inline-flex items-center rounded bg-keel-gray-800 px-2 py-0.5 text-xs font-medium text-keel-gray-300">
|
|
171
|
-
{fileTypeIcon(file.contentType)}
|
|
172
|
-
</span>
|
|
173
|
-
</td>
|
|
174
|
-
|
|
175
|
-
{/* Name */}
|
|
176
|
-
<td className="max-w-[200px] truncate px-4 py-3 font-medium text-keel-gray-200">
|
|
177
|
-
{file.fileName}
|
|
178
|
-
</td>
|
|
179
|
-
|
|
180
|
-
{/* Size */}
|
|
181
|
-
<td className="hidden px-4 py-3 text-keel-gray-400 sm:table-cell">
|
|
182
|
-
{formatFileSize(file.sizeBytes)}
|
|
183
|
-
</td>
|
|
184
|
-
|
|
185
|
-
{/* Date */}
|
|
186
|
-
<td className="hidden px-4 py-3 text-keel-gray-400 md:table-cell">
|
|
187
|
-
{formatDate(file.createdAt)}
|
|
188
|
-
</td>
|
|
189
|
-
|
|
190
|
-
{/* Actions */}
|
|
191
|
-
<td className="px-4 py-3 text-right">
|
|
192
|
-
<div className="flex items-center justify-end gap-2">
|
|
193
|
-
{/* Download */}
|
|
194
|
-
<button
|
|
195
|
-
onClick={() => handleDownload(file.id, file.fileName)}
|
|
196
|
-
disabled={downloadingId === file.id}
|
|
197
|
-
className="rounded-lg p-1.5 text-keel-gray-400 transition-colors hover:bg-keel-gray-800 hover:text-keel-blue disabled:opacity-50"
|
|
198
|
-
title="Download"
|
|
199
|
-
>
|
|
200
|
-
<svg
|
|
201
|
-
className="h-4 w-4"
|
|
202
|
-
fill="none"
|
|
203
|
-
viewBox="0 0 24 24"
|
|
204
|
-
stroke="currentColor"
|
|
205
|
-
strokeWidth={2}
|
|
206
|
-
>
|
|
207
|
-
<path
|
|
208
|
-
strokeLinecap="round"
|
|
209
|
-
strokeLinejoin="round"
|
|
210
|
-
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
|
211
|
-
/>
|
|
212
|
-
</svg>
|
|
213
|
-
</button>
|
|
214
|
-
|
|
215
|
-
{/* Delete */}
|
|
216
|
-
<button
|
|
217
|
-
onClick={() => handleDelete(file.id)}
|
|
218
|
-
disabled={deletingId === file.id}
|
|
219
|
-
className="rounded-lg p-1.5 text-keel-gray-400 transition-colors hover:bg-red-500/10 hover:text-red-400 disabled:opacity-50"
|
|
220
|
-
title="Delete"
|
|
221
|
-
>
|
|
222
|
-
{deletingId === file.id ? (
|
|
223
|
-
<div className="h-4 w-4 animate-spin rounded-full border-2 border-keel-gray-600 border-t-red-400" />
|
|
224
|
-
) : (
|
|
225
|
-
<svg
|
|
226
|
-
className="h-4 w-4"
|
|
227
|
-
fill="none"
|
|
228
|
-
viewBox="0 0 24 24"
|
|
229
|
-
stroke="currentColor"
|
|
230
|
-
strokeWidth={2}
|
|
231
|
-
>
|
|
232
|
-
<path
|
|
233
|
-
strokeLinecap="round"
|
|
234
|
-
strokeLinejoin="round"
|
|
235
|
-
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
|
236
|
-
/>
|
|
237
|
-
</svg>
|
|
238
|
-
)}
|
|
239
|
-
</button>
|
|
240
|
-
</div>
|
|
241
|
-
</td>
|
|
242
|
-
</tr>
|
|
243
|
-
))}
|
|
244
|
-
</tbody>
|
|
245
|
-
</table>
|
|
246
|
-
</div>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { useFiles } from "@/hooks/useFiles";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format a file size in bytes to a human-readable string.
|
|
6
|
+
*/
|
|
7
|
+
function formatFileSize(bytes: number | null): string {
|
|
8
|
+
if (bytes === null || bytes === 0) return "--";
|
|
9
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
11
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
12
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format a date string to a short locale representation.
|
|
18
|
+
*/
|
|
19
|
+
function formatDate(dateString: string): string {
|
|
20
|
+
return new Date(dateString).toLocaleDateString(undefined, {
|
|
21
|
+
year: "numeric",
|
|
22
|
+
month: "short",
|
|
23
|
+
day: "numeric",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Return a simple icon/label for a content type.
|
|
29
|
+
*/
|
|
30
|
+
function fileTypeIcon(contentType: string | null): string {
|
|
31
|
+
if (!contentType) return "FILE";
|
|
32
|
+
if (contentType.startsWith("image/")) return "IMG";
|
|
33
|
+
if (contentType.startsWith("video/")) return "VID";
|
|
34
|
+
if (contentType.startsWith("audio/")) return "AUD";
|
|
35
|
+
if (contentType === "application/pdf") return "PDF";
|
|
36
|
+
if (contentType.includes("spreadsheet") || contentType.includes("excel"))
|
|
37
|
+
return "XLS";
|
|
38
|
+
if (contentType.includes("document") || contentType.includes("word"))
|
|
39
|
+
return "DOC";
|
|
40
|
+
if (contentType.includes("zip") || contentType.includes("compressed"))
|
|
41
|
+
return "ZIP";
|
|
42
|
+
return "FILE";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* File browser component.
|
|
47
|
+
*
|
|
48
|
+
* Displays the current user's files in a list view with download and delete
|
|
49
|
+
* actions per file. Includes loading, empty, and error states.
|
|
50
|
+
*/
|
|
51
|
+
export function FileList() {
|
|
52
|
+
const { files, isLoading, error, deleteFile, getDownloadUrl, refresh } =
|
|
53
|
+
useFiles();
|
|
54
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
55
|
+
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const handleDownload = useCallback(
|
|
58
|
+
async (id: string, fileName: string) => {
|
|
59
|
+
setDownloadingId(id);
|
|
60
|
+
try {
|
|
61
|
+
const url = await getDownloadUrl(id);
|
|
62
|
+
if (url) {
|
|
63
|
+
const a = document.createElement("a");
|
|
64
|
+
a.href = url;
|
|
65
|
+
a.download = fileName;
|
|
66
|
+
a.target = "_blank";
|
|
67
|
+
a.rel = "noopener noreferrer";
|
|
68
|
+
document.body.appendChild(a);
|
|
69
|
+
a.click();
|
|
70
|
+
document.body.removeChild(a);
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
setDownloadingId(null);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[getDownloadUrl],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const handleDelete = useCallback(
|
|
80
|
+
async (id: string) => {
|
|
81
|
+
if (!window.confirm("Are you sure you want to delete this file?")) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setDeletingId(id);
|
|
85
|
+
try {
|
|
86
|
+
await deleteFile(id);
|
|
87
|
+
} finally {
|
|
88
|
+
setDeletingId(null);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[deleteFile],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// -- Loading state --------------------------------------------------------
|
|
95
|
+
if (isLoading) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex items-center justify-center py-16">
|
|
98
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-keel-gray-600 border-t-keel-blue" />
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// -- Error state ----------------------------------------------------------
|
|
104
|
+
if (error) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="rounded-xl border border-red-500/30 bg-red-500/10 px-6 py-8 text-center">
|
|
107
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
108
|
+
<button
|
|
109
|
+
onClick={refresh}
|
|
110
|
+
className="mt-3 text-sm font-medium text-keel-blue hover:underline"
|
|
111
|
+
>
|
|
112
|
+
Try again
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// -- Empty state ----------------------------------------------------------
|
|
119
|
+
if (files.length === 0) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="rounded-xl border border-keel-gray-800 px-6 py-16 text-center">
|
|
122
|
+
<svg
|
|
123
|
+
className="mx-auto mb-4 h-12 w-12 text-keel-gray-600"
|
|
124
|
+
fill="none"
|
|
125
|
+
viewBox="0 0 24 24"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
strokeWidth={1}
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
strokeLinecap="round"
|
|
131
|
+
strokeLinejoin="round"
|
|
132
|
+
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
|
133
|
+
/>
|
|
134
|
+
</svg>
|
|
135
|
+
<p className="text-sm text-keel-gray-400">No files uploaded yet.</p>
|
|
136
|
+
<p className="mt-1 text-xs text-keel-gray-600">
|
|
137
|
+
Upload a file to get started.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// -- File list ------------------------------------------------------------
|
|
144
|
+
return (
|
|
145
|
+
<div className="overflow-hidden rounded-xl border border-keel-gray-800">
|
|
146
|
+
<table className="w-full text-left text-sm">
|
|
147
|
+
<thead>
|
|
148
|
+
<tr className="border-b border-keel-gray-800 bg-keel-navy/50">
|
|
149
|
+
<th className="px-4 py-3 font-medium text-keel-gray-400">Type</th>
|
|
150
|
+
<th className="px-4 py-3 font-medium text-keel-gray-400">Name</th>
|
|
151
|
+
<th className="hidden px-4 py-3 font-medium text-keel-gray-400 sm:table-cell">
|
|
152
|
+
Size
|
|
153
|
+
</th>
|
|
154
|
+
<th className="hidden px-4 py-3 font-medium text-keel-gray-400 md:table-cell">
|
|
155
|
+
Date
|
|
156
|
+
</th>
|
|
157
|
+
<th className="px-4 py-3 text-right font-medium text-keel-gray-400">
|
|
158
|
+
Actions
|
|
159
|
+
</th>
|
|
160
|
+
</tr>
|
|
161
|
+
</thead>
|
|
162
|
+
<tbody>
|
|
163
|
+
{files.map((file) => (
|
|
164
|
+
<tr
|
|
165
|
+
key={file.id}
|
|
166
|
+
className="border-b border-keel-gray-800/50 last:border-b-0 hover:bg-keel-navy/30"
|
|
167
|
+
>
|
|
168
|
+
{/* Type badge */}
|
|
169
|
+
<td className="px-4 py-3">
|
|
170
|
+
<span className="inline-flex items-center rounded bg-keel-gray-800 px-2 py-0.5 text-xs font-medium text-keel-gray-300">
|
|
171
|
+
{fileTypeIcon(file.contentType)}
|
|
172
|
+
</span>
|
|
173
|
+
</td>
|
|
174
|
+
|
|
175
|
+
{/* Name */}
|
|
176
|
+
<td className="max-w-[200px] truncate px-4 py-3 font-medium text-keel-gray-200">
|
|
177
|
+
{file.fileName}
|
|
178
|
+
</td>
|
|
179
|
+
|
|
180
|
+
{/* Size */}
|
|
181
|
+
<td className="hidden px-4 py-3 text-keel-gray-400 sm:table-cell">
|
|
182
|
+
{formatFileSize(file.sizeBytes)}
|
|
183
|
+
</td>
|
|
184
|
+
|
|
185
|
+
{/* Date */}
|
|
186
|
+
<td className="hidden px-4 py-3 text-keel-gray-400 md:table-cell">
|
|
187
|
+
{formatDate(file.createdAt)}
|
|
188
|
+
</td>
|
|
189
|
+
|
|
190
|
+
{/* Actions */}
|
|
191
|
+
<td className="px-4 py-3 text-right">
|
|
192
|
+
<div className="flex items-center justify-end gap-2">
|
|
193
|
+
{/* Download */}
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => handleDownload(file.id, file.fileName)}
|
|
196
|
+
disabled={downloadingId === file.id}
|
|
197
|
+
className="rounded-lg p-1.5 text-keel-gray-400 transition-colors hover:bg-keel-gray-800 hover:text-keel-blue disabled:opacity-50"
|
|
198
|
+
title="Download"
|
|
199
|
+
>
|
|
200
|
+
<svg
|
|
201
|
+
className="h-4 w-4"
|
|
202
|
+
fill="none"
|
|
203
|
+
viewBox="0 0 24 24"
|
|
204
|
+
stroke="currentColor"
|
|
205
|
+
strokeWidth={2}
|
|
206
|
+
>
|
|
207
|
+
<path
|
|
208
|
+
strokeLinecap="round"
|
|
209
|
+
strokeLinejoin="round"
|
|
210
|
+
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
|
211
|
+
/>
|
|
212
|
+
</svg>
|
|
213
|
+
</button>
|
|
214
|
+
|
|
215
|
+
{/* Delete */}
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => handleDelete(file.id)}
|
|
218
|
+
disabled={deletingId === file.id}
|
|
219
|
+
className="rounded-lg p-1.5 text-keel-gray-400 transition-colors hover:bg-red-500/10 hover:text-red-400 disabled:opacity-50"
|
|
220
|
+
title="Delete"
|
|
221
|
+
>
|
|
222
|
+
{deletingId === file.id ? (
|
|
223
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-keel-gray-600 border-t-red-400" />
|
|
224
|
+
) : (
|
|
225
|
+
<svg
|
|
226
|
+
className="h-4 w-4"
|
|
227
|
+
fill="none"
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
stroke="currentColor"
|
|
230
|
+
strokeWidth={2}
|
|
231
|
+
>
|
|
232
|
+
<path
|
|
233
|
+
strokeLinecap="round"
|
|
234
|
+
strokeLinejoin="round"
|
|
235
|
+
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
|
236
|
+
/>
|
|
237
|
+
</svg>
|
|
238
|
+
)}
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
</td>
|
|
242
|
+
</tr>
|
|
243
|
+
))}
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|