deepspace 0.3.9 → 0.3.11
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/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Manager Page
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - useR2Files for upload, list, download, delete
|
|
6
|
+
* - Drag-and-drop upload zone
|
|
7
|
+
* - Inline image preview
|
|
8
|
+
* - File size formatting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
12
|
+
import { useUser, useR2Files, Button, EmptyState, Badge, Modal } from 'deepspace'
|
|
13
|
+
import type { R2FileInfo } from 'deepspace'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helpers
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
function formatBytes(bytes: number): string {
|
|
20
|
+
if (bytes === 0) return '0 B'
|
|
21
|
+
const k = 1024
|
|
22
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
23
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
24
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isImage(name: string): boolean {
|
|
28
|
+
return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i.test(name)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fileIcon(name: string): string {
|
|
32
|
+
if (isImage(name)) return 'image'
|
|
33
|
+
if (/\.(pdf)$/i.test(name)) return 'pdf'
|
|
34
|
+
if (/\.(doc|docx|txt|md)$/i.test(name)) return 'doc'
|
|
35
|
+
if (/\.(mp4|mov|webm|avi)$/i.test(name)) return 'video'
|
|
36
|
+
if (/\.(mp3|wav|ogg|flac)$/i.test(name)) return 'audio'
|
|
37
|
+
if (/\.(zip|tar|gz|rar|7z)$/i.test(name)) return 'archive'
|
|
38
|
+
return 'file'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Drop Zone
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
interface DropZoneProps {
|
|
46
|
+
onFiles: (files: File[]) => void
|
|
47
|
+
isUploading: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function DropZone({ onFiles, isUploading }: DropZoneProps) {
|
|
51
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
52
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
53
|
+
|
|
54
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
setIsDragging(true)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const handleDragLeave = useCallback(() => {
|
|
60
|
+
setIsDragging(false)
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
64
|
+
e.preventDefault()
|
|
65
|
+
setIsDragging(false)
|
|
66
|
+
const files = Array.from(e.dataTransfer.files)
|
|
67
|
+
if (files.length > 0) onFiles(files)
|
|
68
|
+
}, [onFiles])
|
|
69
|
+
|
|
70
|
+
const handleClick = () => inputRef.current?.click()
|
|
71
|
+
|
|
72
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
73
|
+
const files = Array.from(e.target.files ?? [])
|
|
74
|
+
if (files.length > 0) onFiles(files)
|
|
75
|
+
e.target.value = ''
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
onDragOver={handleDragOver}
|
|
81
|
+
onDragLeave={handleDragLeave}
|
|
82
|
+
onDrop={handleDrop}
|
|
83
|
+
onClick={handleClick}
|
|
84
|
+
className={`
|
|
85
|
+
relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors
|
|
86
|
+
${isDragging
|
|
87
|
+
? 'border-primary bg-primary/5'
|
|
88
|
+
: 'border-border hover:border-primary/50 hover:bg-muted/30'
|
|
89
|
+
}
|
|
90
|
+
${isUploading ? 'pointer-events-none opacity-60' : ''}
|
|
91
|
+
`}
|
|
92
|
+
>
|
|
93
|
+
<input
|
|
94
|
+
ref={inputRef}
|
|
95
|
+
type="file"
|
|
96
|
+
multiple
|
|
97
|
+
onChange={handleInputChange}
|
|
98
|
+
className="hidden"
|
|
99
|
+
/>
|
|
100
|
+
{isUploading ? (
|
|
101
|
+
<div className="flex flex-col items-center gap-2">
|
|
102
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
103
|
+
<p className="text-sm text-muted-foreground">Uploading...</p>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<div className="flex flex-col items-center gap-2">
|
|
107
|
+
<svg className="w-10 h-10 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
108
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
|
|
109
|
+
</svg>
|
|
110
|
+
<p className="text-sm text-muted-foreground">
|
|
111
|
+
<span className="font-medium text-foreground">Click to upload</span> or drag and drop
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// File Card
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
interface FileCardProps {
|
|
124
|
+
file: R2FileInfo
|
|
125
|
+
onDownload: () => void
|
|
126
|
+
onDelete: () => void
|
|
127
|
+
onPreview: () => void
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function FileCard({ file, onDownload, onDelete, onPreview }: FileCardProps) {
|
|
131
|
+
const name = file.originalName ?? file.key.split('/').pop() ?? 'Unnamed'
|
|
132
|
+
const type = fileIcon(name)
|
|
133
|
+
const previewable = isImage(name)
|
|
134
|
+
|
|
135
|
+
const iconColor: Record<string, string> = {
|
|
136
|
+
image: 'text-emerald-500',
|
|
137
|
+
pdf: 'text-red-500',
|
|
138
|
+
doc: 'text-blue-500',
|
|
139
|
+
video: 'text-purple-500',
|
|
140
|
+
audio: 'text-orange-500',
|
|
141
|
+
archive: 'text-yellow-500',
|
|
142
|
+
file: 'text-muted-foreground',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="group p-4 bg-card/60 rounded-xl border border-border hover:border-primary/30 transition-colors">
|
|
147
|
+
<div className="flex items-start gap-3">
|
|
148
|
+
<div
|
|
149
|
+
className={`shrink-0 w-10 h-10 rounded-lg bg-muted/60 flex items-center justify-center ${iconColor[type] ?? ''} ${previewable ? 'cursor-pointer' : ''}`}
|
|
150
|
+
onClick={previewable ? onPreview : undefined}
|
|
151
|
+
>
|
|
152
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
153
|
+
{type === 'image' ? (
|
|
154
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
155
|
+
) : (
|
|
156
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
157
|
+
)}
|
|
158
|
+
</svg>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="flex-1 min-w-0">
|
|
162
|
+
<p className="text-sm font-medium text-foreground truncate">{name}</p>
|
|
163
|
+
<div className="flex items-center gap-2 mt-1">
|
|
164
|
+
<Badge variant="secondary" size="sm">{formatBytes(file.size)}</Badge>
|
|
165
|
+
<span className="text-xs text-muted-foreground">
|
|
166
|
+
{new Date(file.uploaded).toLocaleDateString()}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
172
|
+
<button
|
|
173
|
+
onClick={onDownload}
|
|
174
|
+
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-lg transition-colors"
|
|
175
|
+
title="Download"
|
|
176
|
+
>
|
|
177
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
178
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
179
|
+
</svg>
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
onClick={onDelete}
|
|
183
|
+
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors"
|
|
184
|
+
title="Delete"
|
|
185
|
+
>
|
|
186
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
187
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
188
|
+
</svg>
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Preview Modal
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
interface PreviewModalProps {
|
|
201
|
+
file: R2FileInfo | null
|
|
202
|
+
imageUrl: string | null
|
|
203
|
+
onClose: () => void
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function PreviewModal({ file, imageUrl, onClose }: PreviewModalProps) {
|
|
207
|
+
const name = file?.originalName ?? file?.key.split('/').pop() ?? ''
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Modal open={!!file} onClose={onClose} size="lg">
|
|
211
|
+
<Modal.Header onClose={onClose}>
|
|
212
|
+
<Modal.Title>{name}</Modal.Title>
|
|
213
|
+
</Modal.Header>
|
|
214
|
+
<Modal.Body>
|
|
215
|
+
{imageUrl && (
|
|
216
|
+
<img src={imageUrl} alt={name} className="max-w-full max-h-[70vh] mx-auto rounded-lg" />
|
|
217
|
+
)}
|
|
218
|
+
</Modal.Body>
|
|
219
|
+
</Modal>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Main Page
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
export default function FileManagerPage({ className }: { className?: string }) {
|
|
228
|
+
const { user } = useUser()
|
|
229
|
+
const { upload, downloadFile, deleteFile, list, getUrl, isUploading } = useR2Files()
|
|
230
|
+
const [files, setFiles] = useState<R2FileInfo[]>([])
|
|
231
|
+
const [loading, setLoading] = useState(true)
|
|
232
|
+
const [previewFile, setPreviewFile] = useState<R2FileInfo | null>(null)
|
|
233
|
+
|
|
234
|
+
const refreshFiles = useCallback(async () => {
|
|
235
|
+
const result = await list()
|
|
236
|
+
setFiles(result)
|
|
237
|
+
setLoading(false)
|
|
238
|
+
}, [list])
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
refreshFiles()
|
|
242
|
+
}, [refreshFiles])
|
|
243
|
+
|
|
244
|
+
const handleUpload = async (fileList: File[]) => {
|
|
245
|
+
for (const file of fileList) {
|
|
246
|
+
await upload(file)
|
|
247
|
+
}
|
|
248
|
+
await refreshFiles()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const handleDownload = async (file: R2FileInfo) => {
|
|
252
|
+
await downloadFile(file)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const handleDelete = async (file: R2FileInfo) => {
|
|
256
|
+
const name = file.originalName ?? file.key.split('/').pop() ?? 'this file'
|
|
257
|
+
if (!confirm(`Delete "${name}"?`)) return
|
|
258
|
+
await deleteFile(file)
|
|
259
|
+
await refreshFiles()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const handlePreview = (file: R2FileInfo) => {
|
|
263
|
+
setPreviewFile(file)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className={`h-full bg-background overflow-y-auto ${className ?? ''}`}>
|
|
268
|
+
{/* Header */}
|
|
269
|
+
<div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
|
|
270
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
271
|
+
<div className="flex items-center justify-between">
|
|
272
|
+
<div>
|
|
273
|
+
<h1 className="text-2xl font-bold text-foreground">Files</h1>
|
|
274
|
+
<p className="text-muted-foreground mt-1">
|
|
275
|
+
{files.length} {files.length === 1 ? 'file' : 'files'} stored
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
{files.length > 0 && (
|
|
279
|
+
<Badge variant="secondary">
|
|
280
|
+
{formatBytes(files.reduce((sum, f) => sum + f.size, 0))} used
|
|
281
|
+
</Badge>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Content */}
|
|
288
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
|
289
|
+
{/* Upload zone */}
|
|
290
|
+
<DropZone onFiles={handleUpload} isUploading={isUploading} />
|
|
291
|
+
|
|
292
|
+
{/* File list */}
|
|
293
|
+
{loading ? (
|
|
294
|
+
<div className="flex items-center justify-center py-16">
|
|
295
|
+
<div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
296
|
+
</div>
|
|
297
|
+
) : files.length === 0 ? (
|
|
298
|
+
<EmptyState
|
|
299
|
+
title="No files yet"
|
|
300
|
+
description="Upload your first file using the drop zone above"
|
|
301
|
+
icon={
|
|
302
|
+
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
303
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
304
|
+
</svg>
|
|
305
|
+
}
|
|
306
|
+
/>
|
|
307
|
+
) : (
|
|
308
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
309
|
+
{files.map((file) => (
|
|
310
|
+
<FileCard
|
|
311
|
+
key={file.key}
|
|
312
|
+
file={file}
|
|
313
|
+
onDownload={() => handleDownload(file)}
|
|
314
|
+
onDelete={() => handleDelete(file)}
|
|
315
|
+
onPreview={() => handlePreview(file)}
|
|
316
|
+
/>
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{/* Image preview modal */}
|
|
323
|
+
<PreviewModal
|
|
324
|
+
file={previewFile}
|
|
325
|
+
imageUrl={previewFile ? getUrl(previewFile) : null}
|
|
326
|
+
onClose={() => setPreviewFile(null)}
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "integration-test",
|
|
3
|
+
"name": "Integration Tester",
|
|
4
|
+
"category": "data",
|
|
5
|
+
"description": "Test page for exercising integration API endpoints",
|
|
6
|
+
"details": "Lets you call any integration endpoint (OpenAI, weather, search, etc.) and see the result. Used by Playwright tests to verify the full integration proxy flow.",
|
|
7
|
+
"files": [
|
|
8
|
+
{ "src": "src/IntegrationTestPage.tsx", "dest": "src/pages/integration-test.tsx" },
|
|
9
|
+
{ "src": "src/integration.spec.ts", "dest": "tests/integration.spec.ts" }
|
|
10
|
+
],
|
|
11
|
+
"route": {
|
|
12
|
+
"path": "/integration-test",
|
|
13
|
+
"protected": false
|
|
14
|
+
},
|
|
15
|
+
"requires": [],
|
|
16
|
+
"instructions": [],
|
|
17
|
+
"patterns": []
|
|
18
|
+
}
|