@codaijs/keel 0.1.0
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__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- package/sails/stripe/install.ts +378 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { useFileUpload } from "@/hooks/useFileUpload";
|
|
3
|
+
|
|
4
|
+
interface FileUploadButtonProps {
|
|
5
|
+
/** Accepted MIME types (e.g., "image/*,.pdf"). Defaults to all files. */
|
|
6
|
+
accept?: string;
|
|
7
|
+
/** Maximum file size in bytes. Defaults to 50 MB. */
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
/** Callback invoked when an upload completes successfully. */
|
|
10
|
+
onUploadComplete?: (file: { id: string; fileName: string }) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MAX_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* File upload button with drag-and-drop support.
|
|
17
|
+
*
|
|
18
|
+
* Displays an upload zone that accepts clicks and drag-and-drop. Shows upload
|
|
19
|
+
* progress while a file is being transferred.
|
|
20
|
+
*/
|
|
21
|
+
export function FileUploadButton({
|
|
22
|
+
accept,
|
|
23
|
+
maxSize = DEFAULT_MAX_SIZE,
|
|
24
|
+
onUploadComplete,
|
|
25
|
+
}: FileUploadButtonProps) {
|
|
26
|
+
const { upload, isUploading, progress, error } = useFileUpload();
|
|
27
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
28
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
29
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
30
|
+
|
|
31
|
+
const handleFile = useCallback(
|
|
32
|
+
async (file: File) => {
|
|
33
|
+
setValidationError(null);
|
|
34
|
+
|
|
35
|
+
if (file.size > maxSize) {
|
|
36
|
+
const mb = Math.round(maxSize / 1024 / 1024);
|
|
37
|
+
setValidationError(`File size must be less than ${mb} MB.`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await upload(file);
|
|
42
|
+
if (result) {
|
|
43
|
+
onUploadComplete?.(result);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[upload, maxSize, onUploadComplete],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
const file = e.target.files?.[0];
|
|
51
|
+
if (file) {
|
|
52
|
+
handleFile(file);
|
|
53
|
+
}
|
|
54
|
+
// Reset the input so the same file can be selected again.
|
|
55
|
+
e.target.value = "";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setIsDragOver(false);
|
|
61
|
+
const file = e.dataTransfer.files?.[0];
|
|
62
|
+
if (file) {
|
|
63
|
+
handleFile(file);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
setIsDragOver(true);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
setIsDragOver(false);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const displayError = validationError ?? error;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="w-full">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => fileInputRef.current?.click()}
|
|
84
|
+
onDrop={handleDrop}
|
|
85
|
+
onDragOver={handleDragOver}
|
|
86
|
+
onDragLeave={handleDragLeave}
|
|
87
|
+
disabled={isUploading}
|
|
88
|
+
className={`flex w-full flex-col items-center justify-center rounded-xl border-2 border-dashed px-6 py-10 transition-colors ${
|
|
89
|
+
isDragOver
|
|
90
|
+
? "border-keel-blue bg-keel-blue/10"
|
|
91
|
+
: "border-keel-gray-700 hover:border-keel-gray-500"
|
|
92
|
+
} ${isUploading ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`}
|
|
93
|
+
>
|
|
94
|
+
{isUploading ? (
|
|
95
|
+
<>
|
|
96
|
+
{/* Progress indicator */}
|
|
97
|
+
<div className="mb-3 h-10 w-10 animate-spin rounded-full border-4 border-keel-gray-600 border-t-keel-blue" />
|
|
98
|
+
<p className="text-sm font-medium text-keel-gray-300">
|
|
99
|
+
Uploading... {progress}%
|
|
100
|
+
</p>
|
|
101
|
+
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-keel-gray-800">
|
|
102
|
+
<div
|
|
103
|
+
className="h-full rounded-full bg-keel-blue transition-all duration-300"
|
|
104
|
+
style={{ width: `${progress}%` }}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
{/* Upload icon */}
|
|
111
|
+
<svg
|
|
112
|
+
className="mb-3 h-10 w-10 text-keel-gray-500"
|
|
113
|
+
fill="none"
|
|
114
|
+
viewBox="0 0 24 24"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
strokeWidth={1.5}
|
|
117
|
+
>
|
|
118
|
+
<path
|
|
119
|
+
strokeLinecap="round"
|
|
120
|
+
strokeLinejoin="round"
|
|
121
|
+
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
<p className="text-sm font-medium text-keel-gray-300">
|
|
125
|
+
Click to upload or drag and drop
|
|
126
|
+
</p>
|
|
127
|
+
<p className="mt-1 text-xs text-keel-gray-500">
|
|
128
|
+
Max file size: {Math.round(maxSize / 1024 / 1024)} MB
|
|
129
|
+
</p>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
|
|
134
|
+
<input
|
|
135
|
+
ref={fileInputRef}
|
|
136
|
+
type="file"
|
|
137
|
+
accept={accept}
|
|
138
|
+
onChange={handleInputChange}
|
|
139
|
+
className="hidden"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
{displayError && (
|
|
143
|
+
<p className="mt-2 text-sm text-red-400">{displayError}</p>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
interface UploadUrlResponse {
|
|
4
|
+
uploadUrl: string;
|
|
5
|
+
file: {
|
|
6
|
+
id: string;
|
|
7
|
+
key: string;
|
|
8
|
+
fileName: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UseFileUploadResult {
|
|
13
|
+
/** Upload a file. Returns the file record on success. */
|
|
14
|
+
upload: (file: File) => Promise<{ id: string; fileName: string } | null>;
|
|
15
|
+
/** Whether an upload is currently in progress. */
|
|
16
|
+
isUploading: boolean;
|
|
17
|
+
/** Upload progress as a percentage (0-100). */
|
|
18
|
+
progress: number;
|
|
19
|
+
/** Last error message, if any. */
|
|
20
|
+
error: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hook for uploading files via presigned URLs.
|
|
25
|
+
*
|
|
26
|
+
* 1. Requests a presigned upload URL from the backend
|
|
27
|
+
* 2. PUTs the file directly to S3-compatible storage
|
|
28
|
+
* 3. Returns the file metadata
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* const { upload, isUploading, progress, error } = useFileUpload();
|
|
32
|
+
* const result = await upload(file);
|
|
33
|
+
*/
|
|
34
|
+
export function useFileUpload(): UseFileUploadResult {
|
|
35
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
36
|
+
const [progress, setProgress] = useState(0);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const upload = useCallback(async (file: File) => {
|
|
40
|
+
setIsUploading(true);
|
|
41
|
+
setProgress(0);
|
|
42
|
+
setError(null);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Step 1: Get presigned upload URL from the backend.
|
|
46
|
+
const response = await fetch("/api/files/upload-url", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
credentials: "include",
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
fileName: file.name,
|
|
52
|
+
contentType: file.type,
|
|
53
|
+
maxSize: file.size,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const data = await response.json().catch(() => ({}));
|
|
59
|
+
throw new Error(data.error ?? `Upload failed (${response.status})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { uploadUrl, file: fileRecord } =
|
|
63
|
+
(await response.json()) as UploadUrlResponse;
|
|
64
|
+
|
|
65
|
+
// Step 2: Upload the file directly to storage via presigned URL.
|
|
66
|
+
// Use XMLHttpRequest for progress tracking.
|
|
67
|
+
await new Promise<void>((resolve, reject) => {
|
|
68
|
+
const xhr = new XMLHttpRequest();
|
|
69
|
+
|
|
70
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
71
|
+
if (e.lengthComputable) {
|
|
72
|
+
setProgress(Math.round((e.loaded / e.total) * 100));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
xhr.addEventListener("load", () => {
|
|
77
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
78
|
+
resolve();
|
|
79
|
+
} else {
|
|
80
|
+
reject(new Error(`Storage upload failed (${xhr.status})`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
xhr.addEventListener("error", () => {
|
|
85
|
+
reject(new Error("Network error during upload"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
xhr.open("PUT", uploadUrl);
|
|
89
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
90
|
+
xhr.send(file);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
setProgress(100);
|
|
94
|
+
return { id: fileRecord.id, fileName: fileRecord.fileName };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message =
|
|
97
|
+
err instanceof Error ? err.message : "Upload failed";
|
|
98
|
+
setError(message);
|
|
99
|
+
return null;
|
|
100
|
+
} finally {
|
|
101
|
+
setIsUploading(false);
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
return { upload, isUploading, progress, error };
|
|
106
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
interface FileRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
fileName: string;
|
|
6
|
+
contentType: string | null;
|
|
7
|
+
sizeBytes: number | null;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseFilesResult {
|
|
12
|
+
/** Array of the current user's files. */
|
|
13
|
+
files: FileRecord[];
|
|
14
|
+
/** Whether the file list is loading. */
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
/** Last error message, if any. */
|
|
17
|
+
error: string | null;
|
|
18
|
+
/** Delete a file by ID. */
|
|
19
|
+
deleteFile: (id: string) => Promise<boolean>;
|
|
20
|
+
/** Get a temporary download URL for a file. */
|
|
21
|
+
getDownloadUrl: (id: string) => Promise<string | null>;
|
|
22
|
+
/** Manually refresh the file list. */
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook for managing the current user's files.
|
|
28
|
+
*
|
|
29
|
+
* Automatically fetches the file list on mount.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* const { files, isLoading, deleteFile, getDownloadUrl, refresh } = useFiles();
|
|
33
|
+
*/
|
|
34
|
+
export function useFiles(): UseFilesResult {
|
|
35
|
+
const [files, setFiles] = useState<FileRecord[]>([]);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const fetchFiles = useCallback(async () => {
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch("/api/files", {
|
|
45
|
+
credentials: "include",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
setFiles(data.files ?? []);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError(
|
|
56
|
+
err instanceof Error ? err.message : "Failed to load files",
|
|
57
|
+
);
|
|
58
|
+
setFiles([]);
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fetchFiles();
|
|
66
|
+
}, [fetchFiles]);
|
|
67
|
+
|
|
68
|
+
const deleteFile = useCallback(async (id: string): Promise<boolean> => {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`/api/files/${id}`, {
|
|
71
|
+
method: "DELETE",
|
|
72
|
+
credentials: "include",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error("Failed to delete file");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Remove from local state.
|
|
80
|
+
setFiles((prev) => prev.filter((f) => f.id !== id));
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(
|
|
84
|
+
err instanceof Error ? err.message : "Failed to delete file",
|
|
85
|
+
);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const getDownloadUrl = useCallback(
|
|
91
|
+
async (id: string): Promise<string | null> => {
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`/api/files/${id}`, {
|
|
94
|
+
credentials: "include",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error("Failed to get download URL");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
return data.file?.downloadUrl ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
files,
|
|
112
|
+
isLoading,
|
|
113
|
+
error,
|
|
114
|
+
deleteFile,
|
|
115
|
+
getDownloadUrl,
|
|
116
|
+
refresh: fetchFiles,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { FileUploadButton } from "@/components/files/FileUploadButton";
|
|
3
|
+
import { FileList } from "@/components/files/FileList";
|
|
4
|
+
import { useFiles } from "@/hooks/useFiles";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Files page.
|
|
8
|
+
*
|
|
9
|
+
* Combines the file upload button and file browser into a single page.
|
|
10
|
+
*/
|
|
11
|
+
export function FilesPage() {
|
|
12
|
+
const { refresh } = useFiles();
|
|
13
|
+
|
|
14
|
+
const handleUploadComplete = useCallback(() => {
|
|
15
|
+
// Refresh the file list after a successful upload.
|
|
16
|
+
refresh();
|
|
17
|
+
}, [refresh]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="mx-auto max-w-4xl px-4 py-10 sm:px-6 lg:px-8">
|
|
21
|
+
<div className="mb-8">
|
|
22
|
+
<h1 className="text-2xl font-bold text-white">Files</h1>
|
|
23
|
+
<p className="mt-1 text-sm text-keel-gray-400">
|
|
24
|
+
Upload, manage, and download your files.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Upload zone */}
|
|
29
|
+
<div className="mb-8">
|
|
30
|
+
<FileUploadButton onUploadComplete={handleUploadComplete} />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* File browser */}
|
|
34
|
+
<FileList />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|