helix-lang 11.0.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/README.md +168 -0
- package/dist/architect.d.ts +14 -0
- package/dist/architect.d.ts.map +1 -0
- package/dist/architect.js +127 -0
- package/dist/architect.js.map +1 -0
- package/dist/bin/helix.d.ts +20 -0
- package/dist/bin/helix.d.ts.map +1 -0
- package/dist/bin/helix.js +921 -0
- package/dist/bin/helix.js.map +1 -0
- package/dist/commands/collaborate/index.d.ts +2 -0
- package/dist/commands/collaborate/index.d.ts.map +1 -0
- package/dist/commands/collaborate/index.js +129 -0
- package/dist/commands/collaborate/index.js.map +1 -0
- package/dist/commands/collaborate/server.d.ts +31 -0
- package/dist/commands/collaborate/server.d.ts.map +1 -0
- package/dist/commands/collaborate/server.js +159 -0
- package/dist/commands/collaborate/server.js.map +1 -0
- package/dist/commands/deploy/index.d.ts +25 -0
- package/dist/commands/deploy/index.d.ts.map +1 -0
- package/dist/commands/deploy/index.js +130 -0
- package/dist/commands/deploy/index.js.map +1 -0
- package/dist/commands/deploy/platforms/fly.d.ts +9 -0
- package/dist/commands/deploy/platforms/fly.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/fly.js +68 -0
- package/dist/commands/deploy/platforms/fly.js.map +1 -0
- package/dist/commands/deploy/platforms/railway.d.ts +9 -0
- package/dist/commands/deploy/platforms/railway.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/railway.js +115 -0
- package/dist/commands/deploy/platforms/railway.js.map +1 -0
- package/dist/commands/deploy/platforms/vercel.d.ts +10 -0
- package/dist/commands/deploy/platforms/vercel.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/vercel.js +126 -0
- package/dist/commands/deploy/platforms/vercel.js.map +1 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +56 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/evolve/analyzers/performance.d.ts +13 -0
- package/dist/commands/evolve/analyzers/performance.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/performance.js +591 -0
- package/dist/commands/evolve/analyzers/performance.js.map +1 -0
- package/dist/commands/evolve/analyzers/security.d.ts +21 -0
- package/dist/commands/evolve/analyzers/security.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/security.js +280 -0
- package/dist/commands/evolve/analyzers/security.js.map +1 -0
- package/dist/commands/evolve/index.d.ts +2 -0
- package/dist/commands/evolve/index.d.ts.map +1 -0
- package/dist/commands/evolve/index.js +122 -0
- package/dist/commands/evolve/index.js.map +1 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +277 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +176 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/library/index.d.ts +27 -0
- package/dist/commands/library/index.d.ts.map +1 -0
- package/dist/commands/library/index.js +126 -0
- package/dist/commands/library/index.js.map +1 -0
- package/dist/commands/migrate.d.ts +5 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +258 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/new.d.ts +6 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +195 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/preflight.d.ts +20 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +182 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preview.d.ts +13 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +260 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +96 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/spawn.d.ts +11 -0
- package/dist/commands/spawn.d.ts.map +1 -0
- package/dist/commands/spawn.js +916 -0
- package/dist/commands/spawn.js.map +1 -0
- package/dist/compiler.d.ts +12 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +92 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core/file-writer.d.ts +36 -0
- package/dist/core/file-writer.d.ts.map +1 -0
- package/dist/core/file-writer.js +268 -0
- package/dist/core/file-writer.js.map +1 -0
- package/dist/core/registry.d.ts +57 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +222 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/self-healing.d.ts +47 -0
- package/dist/core/self-healing.d.ts.map +1 -0
- package/dist/core/self-healing.js +250 -0
- package/dist/core/self-healing.js.map +1 -0
- package/dist/core/types.d.ts +126 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generators/databases/mongodb.d.ts +10 -0
- package/dist/generators/databases/mongodb.d.ts.map +1 -0
- package/dist/generators/databases/mongodb.js +83 -0
- package/dist/generators/databases/mongodb.js.map +1 -0
- package/dist/generators/databases/redis.d.ts +2 -0
- package/dist/generators/databases/redis.d.ts.map +1 -0
- package/dist/generators/databases/redis.js +140 -0
- package/dist/generators/databases/redis.js.map +1 -0
- package/dist/generators/flutter.d.ts +32 -0
- package/dist/generators/flutter.d.ts.map +1 -0
- package/dist/generators/flutter.js +628 -0
- package/dist/generators/flutter.js.map +1 -0
- package/dist/openrouter.d.ts +68 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +241 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/page-generator.d.ts +22 -0
- package/dist/page-generator.d.ts.map +1 -0
- package/dist/page-generator.js +192 -0
- package/dist/page-generator.js.map +1 -0
- package/dist/parser.d.ts +76 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +691 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompts/master-architect.d.ts +9 -0
- package/dist/prompts/master-architect.d.ts.map +1 -0
- package/dist/prompts/master-architect.js +150 -0
- package/dist/prompts/master-architect.js.map +1 -0
- package/dist/researcher.d.ts +12 -0
- package/dist/researcher.d.ts.map +1 -0
- package/dist/researcher.js +85 -0
- package/dist/researcher.js.map +1 -0
- package/dist/self-heal.d.ts +29 -0
- package/dist/self-heal.d.ts.map +1 -0
- package/dist/self-heal.js +260 -0
- package/dist/self-heal.js.map +1 -0
- package/dist/services/SupabaseDeployer.d.ts +9 -0
- package/dist/services/SupabaseDeployer.d.ts.map +1 -0
- package/dist/services/SupabaseDeployer.js +50 -0
- package/dist/services/SupabaseDeployer.js.map +1 -0
- package/dist/test-generator.d.ts +18 -0
- package/dist/test-generator.d.ts.map +1 -0
- package/dist/test-generator.js +180 -0
- package/dist/test-generator.js.map +1 -0
- package/dist/themes/index.d.ts +52 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +273 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/constitutional-validator.d.ts +73 -0
- package/dist/utils/constitutional-validator.d.ts.map +1 -0
- package/dist/utils/constitutional-validator.js +249 -0
- package/dist/utils/constitutional-validator.js.map +1 -0
- package/library/auth-flow/app/api/auth/[...nextauth]/route.ts +31 -0
- package/library/auth-flow/app/api/auth/login/route.ts +90 -0
- package/library/auth-flow/app/api/auth/register/route.ts +91 -0
- package/library/auth-flow/components/auth/AuthMiddleware.tsx +139 -0
- package/library/auth-flow/components/auth/LoginForm.tsx +125 -0
- package/library/auth-flow/components/auth/RegisterForm.tsx +168 -0
- package/library/auth-flow/components/auth/nextauth-config.ts +99 -0
- package/library/auth-flow/manifest.json +29 -0
- package/library/auth-flow/schema.prisma +45 -0
- package/library/dashboard-analytics/components/dashboard/ActivityFeed.tsx +109 -0
- package/library/dashboard-analytics/components/dashboard/LineChart.tsx +180 -0
- package/library/dashboard-analytics/components/dashboard/StatsCard.tsx +47 -0
- package/library/dashboard-analytics/components/dashboard/SummaryGrid.tsx +39 -0
- package/library/dashboard-analytics/manifest.json +19 -0
- package/library/data-table/components/table/BulkActions.tsx +59 -0
- package/library/data-table/components/table/ColumnToggle.tsx +65 -0
- package/library/data-table/components/table/DataTable.tsx +318 -0
- package/library/data-table/components/table/ExportCSV.tsx +52 -0
- package/library/data-table/components/table/SearchFilter.tsx +48 -0
- package/library/data-table/manifest.json +20 -0
- package/library/file-upload/app/api/upload/route.ts +107 -0
- package/library/file-upload/components/upload/DropZone.tsx +268 -0
- package/library/file-upload/components/upload/FilePreview.tsx +82 -0
- package/library/file-upload/components/upload/UploadProgress.tsx +92 -0
- package/library/file-upload/components/upload/fileStorage.ts +142 -0
- package/library/file-upload/manifest.json +21 -0
- package/library/notification-system/app/api/notifications/route.ts +121 -0
- package/library/notification-system/components/notifications/NotificationBell.tsx +154 -0
- package/library/notification-system/components/notifications/NotificationProvider.tsx +161 -0
- package/library/notification-system/components/notifications/Toast.tsx +112 -0
- package/library/notification-system/manifest.json +20 -0
- package/package.json +66 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, type DragEvent, type ChangeEvent } from 'react';
|
|
4
|
+
import FilePreview from './FilePreview';
|
|
5
|
+
import UploadProgress from './UploadProgress';
|
|
6
|
+
|
|
7
|
+
interface UploadedFile {
|
|
8
|
+
file: File;
|
|
9
|
+
previewUrl?: string;
|
|
10
|
+
progress: number;
|
|
11
|
+
status: 'pending' | 'uploading' | 'complete' | 'error';
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DropZoneProps {
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
maxFileSize?: number;
|
|
18
|
+
maxFiles?: number;
|
|
19
|
+
allowedTypes?: string[];
|
|
20
|
+
onUploadComplete?: (files: { name: string; url: string }[]) => void;
|
|
21
|
+
onError?: (error: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function DropZone({
|
|
25
|
+
endpoint = '/api/upload',
|
|
26
|
+
maxFileSize = 10 * 1024 * 1024,
|
|
27
|
+
maxFiles = 5,
|
|
28
|
+
allowedTypes,
|
|
29
|
+
onUploadComplete,
|
|
30
|
+
onError,
|
|
31
|
+
}: DropZoneProps) {
|
|
32
|
+
const [files, setFiles] = useState<UploadedFile[]>([]);
|
|
33
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
34
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
36
|
+
const validateFile = useCallback(
|
|
37
|
+
(file: File): string | null => {
|
|
38
|
+
if (file.size > maxFileSize) {
|
|
39
|
+
return `File too large. Max size: ${(maxFileSize / 1024 / 1024).toFixed(0)}MB`;
|
|
40
|
+
}
|
|
41
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
42
|
+
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
|
43
|
+
const typeMatch = allowedTypes.some(
|
|
44
|
+
(t) => file.type === t || `.${ext}` === t || t === `${file.type.split('/')[0]}/*`
|
|
45
|
+
);
|
|
46
|
+
if (!typeMatch) {
|
|
47
|
+
return `File type not allowed. Accepted: ${allowedTypes.join(', ')}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
[maxFileSize, allowedTypes]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const addFiles = useCallback(
|
|
56
|
+
(newFiles: FileList | File[]) => {
|
|
57
|
+
const fileArray = Array.from(newFiles);
|
|
58
|
+
const remaining = maxFiles - files.length;
|
|
59
|
+
|
|
60
|
+
if (remaining <= 0) {
|
|
61
|
+
onError?.(`Maximum ${maxFiles} files allowed`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const toAdd = fileArray.slice(0, remaining);
|
|
66
|
+
const uploadFiles: UploadedFile[] = [];
|
|
67
|
+
|
|
68
|
+
for (const file of toAdd) {
|
|
69
|
+
const error = validateFile(file);
|
|
70
|
+
if (error) {
|
|
71
|
+
onError?.(error);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const previewUrl = file.type.startsWith('image/')
|
|
76
|
+
? URL.createObjectURL(file)
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
uploadFiles.push({
|
|
80
|
+
file,
|
|
81
|
+
previewUrl,
|
|
82
|
+
progress: 0,
|
|
83
|
+
status: 'pending',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setFiles((prev) => [...prev, ...uploadFiles]);
|
|
88
|
+
|
|
89
|
+
// Auto-upload each file
|
|
90
|
+
uploadFiles.forEach((uf) => {
|
|
91
|
+
uploadFile(uf);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
[files.length, maxFiles, validateFile, onError]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const uploadFile = async (uploadedFile: UploadedFile) => {
|
|
98
|
+
const formData = new FormData();
|
|
99
|
+
formData.append('file', uploadedFile.file);
|
|
100
|
+
|
|
101
|
+
setFiles((prev) =>
|
|
102
|
+
prev.map((f) =>
|
|
103
|
+
f.file === uploadedFile.file ? { ...f, status: 'uploading' as const, progress: 0 } : f
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const xhr = new XMLHttpRequest();
|
|
109
|
+
|
|
110
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
111
|
+
if (e.lengthComputable) {
|
|
112
|
+
const pct = Math.round((e.loaded / e.total) * 100);
|
|
113
|
+
setFiles((prev) =>
|
|
114
|
+
prev.map((f) =>
|
|
115
|
+
f.file === uploadedFile.file ? { ...f, progress: pct } : f
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await new Promise<void>((resolve, reject) => {
|
|
122
|
+
xhr.addEventListener('load', () => {
|
|
123
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
124
|
+
const response = JSON.parse(xhr.responseText);
|
|
125
|
+
setFiles((prev) =>
|
|
126
|
+
prev.map((f) =>
|
|
127
|
+
f.file === uploadedFile.file
|
|
128
|
+
? { ...f, status: 'complete' as const, progress: 100 }
|
|
129
|
+
: f
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
onUploadComplete?.([{ name: uploadedFile.file.name, url: response.url || '' }]);
|
|
133
|
+
resolve();
|
|
134
|
+
} else {
|
|
135
|
+
reject(new Error(`Upload failed: ${xhr.statusText}`));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
xhr.addEventListener('error', () => reject(new Error('Network error')));
|
|
140
|
+
xhr.open('POST', endpoint);
|
|
141
|
+
xhr.send(formData);
|
|
142
|
+
});
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : 'Upload failed';
|
|
145
|
+
setFiles((prev) =>
|
|
146
|
+
prev.map((f) =>
|
|
147
|
+
f.file === uploadedFile.file
|
|
148
|
+
? { ...f, status: 'error' as const, error: msg }
|
|
149
|
+
: f
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const removeFile = (index: number) => {
|
|
156
|
+
setFiles((prev) => {
|
|
157
|
+
const file = prev[index];
|
|
158
|
+
if (file.previewUrl) URL.revokeObjectURL(file.previewUrl);
|
|
159
|
+
return prev.filter((_, i) => i !== index);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleDrop = (e: DragEvent) => {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
setIsDragOver(false);
|
|
166
|
+
if (e.dataTransfer.files.length > 0) {
|
|
167
|
+
addFiles(e.dataTransfer.files);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleDragOver = (e: DragEvent) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
setIsDragOver(true);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleDragLeave = (e: DragEvent) => {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
setIsDragOver(false);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
182
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
183
|
+
addFiles(e.target.files);
|
|
184
|
+
e.target.value = '';
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="space-y-4">
|
|
190
|
+
{/* Drop zone */}
|
|
191
|
+
<div
|
|
192
|
+
onDrop={handleDrop}
|
|
193
|
+
onDragOver={handleDragOver}
|
|
194
|
+
onDragLeave={handleDragLeave}
|
|
195
|
+
onClick={() => inputRef.current?.click()}
|
|
196
|
+
className={`relative cursor-pointer border-2 border-dashed rounded-2xl p-8 text-center transition-all ${
|
|
197
|
+
isDragOver
|
|
198
|
+
? 'border-cyan-500 bg-cyan-500/5'
|
|
199
|
+
: 'border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]'
|
|
200
|
+
}`}
|
|
201
|
+
>
|
|
202
|
+
<input
|
|
203
|
+
ref={inputRef}
|
|
204
|
+
type="file"
|
|
205
|
+
multiple={maxFiles > 1}
|
|
206
|
+
accept={allowedTypes?.join(',')}
|
|
207
|
+
onChange={handleInputChange}
|
|
208
|
+
className="hidden"
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<div className="flex flex-col items-center gap-3">
|
|
212
|
+
<div className={`p-3 rounded-xl transition-colors ${isDragOver ? 'bg-cyan-500/10' : 'bg-white/5'}`}>
|
|
213
|
+
<svg
|
|
214
|
+
className={`w-8 h-8 ${isDragOver ? 'text-cyan-400' : 'text-white/30'}`}
|
|
215
|
+
fill="none"
|
|
216
|
+
viewBox="0 0 24 24"
|
|
217
|
+
stroke="currentColor"
|
|
218
|
+
strokeWidth={1.5}
|
|
219
|
+
>
|
|
220
|
+
<path strokeLinecap="round" strokeLinejoin="round" 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" />
|
|
221
|
+
</svg>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<p className="text-sm text-white/70">
|
|
225
|
+
<span className="text-cyan-400 font-medium">Click to upload</span> or drag and drop
|
|
226
|
+
</p>
|
|
227
|
+
<p className="text-xs text-white/30 mt-1">
|
|
228
|
+
Max {maxFiles} file{maxFiles > 1 ? 's' : ''}, up to {(maxFileSize / 1024 / 1024).toFixed(0)}MB each
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* File previews */}
|
|
235
|
+
{files.some((f) => f.status === 'pending' || f.previewUrl) && (
|
|
236
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
237
|
+
{files.map((f, i) => (
|
|
238
|
+
<FilePreview
|
|
239
|
+
key={`${f.file.name}-${i}`}
|
|
240
|
+
file={f.file}
|
|
241
|
+
previewUrl={f.previewUrl}
|
|
242
|
+
onRemove={() => removeFile(i)}
|
|
243
|
+
/>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Upload progress */}
|
|
249
|
+
{files.some((f) => f.status !== 'pending') && (
|
|
250
|
+
<div className="space-y-2">
|
|
251
|
+
{files
|
|
252
|
+
.filter((f) => f.status !== 'pending')
|
|
253
|
+
.map((f, i) => (
|
|
254
|
+
<UploadProgress
|
|
255
|
+
key={`progress-${f.file.name}-${i}`}
|
|
256
|
+
filename={f.file.name}
|
|
257
|
+
progress={f.progress}
|
|
258
|
+
status={f.status === 'pending' ? 'uploading' : f.status}
|
|
259
|
+
error={f.error}
|
|
260
|
+
onCancel={() => removeFile(files.indexOf(f))}
|
|
261
|
+
onRetry={() => uploadFile(f)}
|
|
262
|
+
/>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface FilePreviewProps {
|
|
4
|
+
file: File;
|
|
5
|
+
previewUrl?: string;
|
|
6
|
+
onRemove?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatFileSize(bytes: number): string {
|
|
10
|
+
if (bytes === 0) return '0 B';
|
|
11
|
+
const k = 1024;
|
|
12
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
13
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
14
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getFileIcon(type: string): string {
|
|
18
|
+
if (type.startsWith('image/')) return 'image';
|
|
19
|
+
if (type === 'application/pdf') return 'pdf';
|
|
20
|
+
if (type.includes('spreadsheet') || type.includes('csv')) return 'spreadsheet';
|
|
21
|
+
if (type.includes('document') || type.includes('word')) return 'document';
|
|
22
|
+
return 'file';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function FilePreview({ file, previewUrl, onRemove }: FilePreviewProps) {
|
|
26
|
+
const fileType = getFileIcon(file.type);
|
|
27
|
+
const isImage = file.type.startsWith('image/');
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="relative group backdrop-blur-xl bg-white/5 border border-white/10 rounded-xl overflow-hidden">
|
|
31
|
+
{/* Preview area */}
|
|
32
|
+
<div className="aspect-square flex items-center justify-center bg-white/[0.02]">
|
|
33
|
+
{isImage && previewUrl ? (
|
|
34
|
+
<img
|
|
35
|
+
src={previewUrl}
|
|
36
|
+
alt={file.name}
|
|
37
|
+
className="w-full h-full object-cover"
|
|
38
|
+
/>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="flex flex-col items-center gap-2 p-4">
|
|
41
|
+
{fileType === 'pdf' && (
|
|
42
|
+
<svg className="w-10 h-10 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
43
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
44
|
+
</svg>
|
|
45
|
+
)}
|
|
46
|
+
{fileType === 'document' && (
|
|
47
|
+
<svg className="w-10 h-10 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
48
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
49
|
+
</svg>
|
|
50
|
+
)}
|
|
51
|
+
{(fileType === 'file' || fileType === 'spreadsheet' || fileType === 'image') && (
|
|
52
|
+
<svg className="w-10 h-10 text-white/30" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
54
|
+
</svg>
|
|
55
|
+
)}
|
|
56
|
+
<span className="text-xs text-white/30 uppercase font-medium">
|
|
57
|
+
{file.name.split('.').pop()}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* File info */}
|
|
64
|
+
<div className="p-3 border-t border-white/5">
|
|
65
|
+
<p className="text-sm text-white/70 truncate">{file.name}</p>
|
|
66
|
+
<p className="text-xs text-white/30 mt-0.5">{formatFileSize(file.size)}</p>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Remove button */}
|
|
70
|
+
{onRemove && (
|
|
71
|
+
<button
|
|
72
|
+
onClick={onRemove}
|
|
73
|
+
className="absolute top-2 right-2 p-1.5 bg-black/60 backdrop-blur rounded-lg text-white/60 hover:text-white opacity-0 group-hover:opacity-100 transition-all"
|
|
74
|
+
>
|
|
75
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
76
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface UploadProgressProps {
|
|
4
|
+
filename: string;
|
|
5
|
+
progress: number;
|
|
6
|
+
status: 'uploading' | 'complete' | 'error';
|
|
7
|
+
error?: string;
|
|
8
|
+
onCancel?: () => void;
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function UploadProgress({
|
|
13
|
+
filename,
|
|
14
|
+
progress,
|
|
15
|
+
status,
|
|
16
|
+
error,
|
|
17
|
+
onCancel,
|
|
18
|
+
onRetry,
|
|
19
|
+
}: UploadProgressProps) {
|
|
20
|
+
const clampedProgress = Math.min(100, Math.max(0, progress));
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex items-center gap-3 p-3 bg-white/5 border border-white/10 rounded-lg">
|
|
24
|
+
{/* File icon */}
|
|
25
|
+
<div className="flex-shrink-0">
|
|
26
|
+
{status === 'complete' ? (
|
|
27
|
+
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center">
|
|
28
|
+
<svg className="w-4 h-4 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
29
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
30
|
+
</svg>
|
|
31
|
+
</div>
|
|
32
|
+
) : status === 'error' ? (
|
|
33
|
+
<div className="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center">
|
|
34
|
+
<svg className="w-4 h-4 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
35
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
36
|
+
</svg>
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<div className="w-8 h-8 rounded-lg bg-cyan-500/10 flex items-center justify-center">
|
|
40
|
+
<svg className="w-4 h-4 text-cyan-400 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
41
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
42
|
+
</svg>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Info */}
|
|
48
|
+
<div className="flex-1 min-w-0">
|
|
49
|
+
<p className="text-sm text-white/80 truncate">{filename}</p>
|
|
50
|
+
{status === 'error' && error ? (
|
|
51
|
+
<p className="text-xs text-red-400 mt-0.5">{error}</p>
|
|
52
|
+
) : status === 'complete' ? (
|
|
53
|
+
<p className="text-xs text-emerald-400 mt-0.5">Upload complete</p>
|
|
54
|
+
) : (
|
|
55
|
+
<div className="mt-1.5">
|
|
56
|
+
<div className="w-full h-1.5 bg-white/5 rounded-full overflow-hidden">
|
|
57
|
+
<div
|
|
58
|
+
className="h-full bg-cyan-500 rounded-full transition-all duration-300"
|
|
59
|
+
style={{ width: `${clampedProgress}%` }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<p className="text-xs text-white/30 mt-0.5">{clampedProgress}%</p>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Actions */}
|
|
68
|
+
<div className="flex-shrink-0">
|
|
69
|
+
{status === 'uploading' && onCancel && (
|
|
70
|
+
<button
|
|
71
|
+
onClick={onCancel}
|
|
72
|
+
className="p-1 text-white/30 hover:text-white/60 transition-colors"
|
|
73
|
+
>
|
|
74
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
75
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
76
|
+
</svg>
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
79
|
+
{status === 'error' && onRetry && (
|
|
80
|
+
<button
|
|
81
|
+
onClick={onRetry}
|
|
82
|
+
className="p-1 text-cyan-400 hover:text-cyan-300 transition-colors"
|
|
83
|
+
>
|
|
84
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
85
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File storage utility for handling uploads.
|
|
3
|
+
*
|
|
4
|
+
* Default implementation stores files locally.
|
|
5
|
+
* Replace with S3, Cloudflare R2, or other cloud storage as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as crypto from 'crypto';
|
|
11
|
+
|
|
12
|
+
export interface StoredFile {
|
|
13
|
+
id: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
originalName: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
url: string;
|
|
19
|
+
storedAt: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StorageConfig {
|
|
23
|
+
uploadDir: string;
|
|
24
|
+
publicUrlPrefix: string;
|
|
25
|
+
maxFileSize: number;
|
|
26
|
+
allowedMimeTypes: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultConfig: StorageConfig = {
|
|
30
|
+
uploadDir: path.join(process.cwd(), 'public', 'uploads'),
|
|
31
|
+
publicUrlPrefix: '/uploads',
|
|
32
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
33
|
+
allowedMimeTypes: [
|
|
34
|
+
'image/jpeg',
|
|
35
|
+
'image/png',
|
|
36
|
+
'image/gif',
|
|
37
|
+
'image/webp',
|
|
38
|
+
'application/pdf',
|
|
39
|
+
'text/csv',
|
|
40
|
+
'application/json',
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function ensureDir(dir: string): void {
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateFilename(originalName: string): string {
|
|
51
|
+
const ext = path.extname(originalName);
|
|
52
|
+
const hash = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
53
|
+
const timestamp = Date.now();
|
|
54
|
+
return `${timestamp}-${hash}${ext}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store a file buffer to local disk.
|
|
59
|
+
*/
|
|
60
|
+
export async function storeFile(
|
|
61
|
+
buffer: Buffer,
|
|
62
|
+
originalName: string,
|
|
63
|
+
mimeType: string,
|
|
64
|
+
config: Partial<StorageConfig> = {}
|
|
65
|
+
): Promise<StoredFile> {
|
|
66
|
+
const cfg = { ...defaultConfig, ...config };
|
|
67
|
+
|
|
68
|
+
// Validate size
|
|
69
|
+
if (buffer.length > cfg.maxFileSize) {
|
|
70
|
+
throw new Error(`File exceeds maximum size of ${(cfg.maxFileSize / 1024 / 1024).toFixed(0)}MB`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate type
|
|
74
|
+
if (cfg.allowedMimeTypes.length > 0 && !cfg.allowedMimeTypes.includes(mimeType)) {
|
|
75
|
+
throw new Error(`File type ${mimeType} is not allowed`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ensureDir(cfg.uploadDir);
|
|
79
|
+
|
|
80
|
+
const filename = generateFilename(originalName);
|
|
81
|
+
const filePath = path.join(cfg.uploadDir, filename);
|
|
82
|
+
|
|
83
|
+
await fs.promises.writeFile(filePath, buffer);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: crypto.randomUUID(),
|
|
87
|
+
filename,
|
|
88
|
+
originalName,
|
|
89
|
+
mimeType,
|
|
90
|
+
size: buffer.length,
|
|
91
|
+
url: `${cfg.publicUrlPrefix}/${filename}`,
|
|
92
|
+
storedAt: new Date(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Delete a stored file.
|
|
98
|
+
*/
|
|
99
|
+
export async function deleteFile(
|
|
100
|
+
filename: string,
|
|
101
|
+
config: Partial<StorageConfig> = {}
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const cfg = { ...defaultConfig, ...config };
|
|
104
|
+
const filePath = path.join(cfg.uploadDir, filename);
|
|
105
|
+
|
|
106
|
+
if (fs.existsSync(filePath)) {
|
|
107
|
+
await fs.promises.unlink(filePath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List stored files.
|
|
113
|
+
*/
|
|
114
|
+
export async function listFiles(
|
|
115
|
+
config: Partial<StorageConfig> = {}
|
|
116
|
+
): Promise<StoredFile[]> {
|
|
117
|
+
const cfg = { ...defaultConfig, ...config };
|
|
118
|
+
|
|
119
|
+
ensureDir(cfg.uploadDir);
|
|
120
|
+
|
|
121
|
+
const entries = await fs.promises.readdir(cfg.uploadDir, { withFileTypes: true });
|
|
122
|
+
const files: StoredFile[] = [];
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!entry.isFile()) continue;
|
|
126
|
+
|
|
127
|
+
const filePath = path.join(cfg.uploadDir, entry.name);
|
|
128
|
+
const stat = await fs.promises.stat(filePath);
|
|
129
|
+
|
|
130
|
+
files.push({
|
|
131
|
+
id: entry.name,
|
|
132
|
+
filename: entry.name,
|
|
133
|
+
originalName: entry.name,
|
|
134
|
+
mimeType: 'application/octet-stream',
|
|
135
|
+
size: stat.size,
|
|
136
|
+
url: `${cfg.publicUrlPrefix}/${entry.name}`,
|
|
137
|
+
storedAt: stat.birthtime,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return files;
|
|
142
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "file-upload",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drag-and-drop file upload with preview, progress tracking, and server-side handling",
|
|
5
|
+
"author": "AD AI Engine",
|
|
6
|
+
"category": "media",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"files": {
|
|
9
|
+
"components": [
|
|
10
|
+
"components/upload/DropZone.tsx",
|
|
11
|
+
"components/upload/FilePreview.tsx",
|
|
12
|
+
"components/upload/UploadProgress.tsx",
|
|
13
|
+
"components/upload/fileStorage.ts"
|
|
14
|
+
],
|
|
15
|
+
"api": [
|
|
16
|
+
"app/api/upload/route.ts"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"schema": null,
|
|
20
|
+
"instructions": "1. Import DropZone into your page for drag-and-drop uploads\n2. DropZone handles file selection, previews, and progress internally\n3. The API route at /api/upload stores files to the configured directory\n4. Customize fileStorage.ts for S3/cloud storage integration\n5. Set MAX_FILE_SIZE and ALLOWED_TYPES in the DropZone props"
|
|
21
|
+
}
|