@codaijs/keel 0.2.3 → 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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. package/sails/stripe/install.ts +378 -378
@@ -1,147 +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
- }
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
+ }
@@ -1,106 +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
- }
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
+ }