create-fluxstack 1.8.3 → 1.10.1
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/LIVE_COMPONENTS_REVIEW.md +781 -0
- package/README.md +653 -275
- package/app/client/src/App.tsx +39 -43
- package/app/client/src/lib/eden-api.ts +2 -7
- package/app/client/src/live/FileUploadExample.tsx +359 -0
- package/app/client/src/live/MinimalLiveClock.tsx +47 -0
- package/app/client/src/live/QuickUploadTest.tsx +193 -0
- package/app/client/src/main.tsx +10 -10
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +45 -44
- package/app/client/tsconfig.node.json +25 -25
- package/app/server/index.ts +30 -103
- package/app/server/live/LiveFileUploadComponent.ts +77 -0
- package/app/server/live/register-components.ts +19 -19
- package/core/build/bundler.ts +202 -55
- package/core/build/index.ts +126 -2
- package/core/build/live-components-generator.ts +68 -1
- package/core/cli/generators/plugin.ts +6 -6
- package/core/cli/index.ts +232 -4
- package/core/client/LiveComponentsProvider.tsx +3 -9
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
- package/core/client/hooks/useChunkedUpload.ts +112 -61
- package/core/client/hooks/useHybridLiveComponent.ts +80 -26
- package/core/client/hooks/useTypedLiveComponent.ts +133 -0
- package/core/client/hooks/useWebSocket.ts +4 -16
- package/core/client/index.ts +20 -2
- package/core/framework/server.ts +181 -8
- package/core/live/ComponentRegistry.ts +5 -1
- package/core/plugins/built-in/index.ts +8 -5
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +55 -63
- package/core/plugins/built-in/vite/index.ts +75 -187
- package/core/plugins/built-in/vite/vite-dev.ts +88 -0
- package/core/plugins/registry.ts +54 -2
- package/core/plugins/types.ts +86 -2
- package/core/server/index.ts +1 -2
- package/core/server/live/ComponentRegistry.ts +14 -5
- package/core/server/live/FileUploadManager.ts +22 -25
- package/core/server/live/auto-generated-components.ts +29 -26
- package/core/server/live/websocket-plugin.ts +19 -5
- package/core/server/plugins/static-files-plugin.ts +49 -240
- package/core/server/plugins/swagger.ts +33 -33
- package/core/types/build.ts +22 -0
- package/core/types/plugin.ts +9 -1
- package/core/types/types.ts +137 -0
- package/core/utils/logger/startup-banner.ts +20 -4
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +7 -7
- package/eslint.config.js +23 -23
- package/package.json +3 -2
- package/plugins/crypto-auth/server/middlewares.ts +19 -19
- package/tsconfig.json +52 -51
- package/workspace.json +5 -5
package/app/client/src/App.tsx
CHANGED
|
@@ -1,49 +1,13 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { api } from './lib/eden-api'
|
|
3
|
-
import { FaFire, FaBook, FaGithub, FaClock } from 'react-icons/fa'
|
|
4
|
-
import { LiveComponentsProvider
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
currentTime: string
|
|
8
|
-
timeZone: string
|
|
9
|
-
format: '12h' | '24h'
|
|
10
|
-
showSeconds: boolean
|
|
11
|
-
showDate: boolean
|
|
12
|
-
lastSync: Date
|
|
13
|
-
serverUptime: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const initialClockState: LiveClockState = {
|
|
17
|
-
currentTime: "Loading...",
|
|
18
|
-
timeZone: "America/Sao_Paulo",
|
|
19
|
-
format: "24h",
|
|
20
|
-
showSeconds: true,
|
|
21
|
-
showDate: true,
|
|
22
|
-
lastSync: new Date(),
|
|
23
|
-
serverUptime: 0,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Minimal Live Clock Component
|
|
27
|
-
function MinimalLiveClock() {
|
|
28
|
-
const { state } = useHybridLiveComponent<LiveClockState>(
|
|
29
|
-
'LiveClock',
|
|
30
|
-
initialClockState
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className="bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-xl p-4 border border-blue-400/20">
|
|
35
|
-
<div className="text-4xl font-mono font-bold text-white text-center tracking-wider">
|
|
36
|
-
{state.currentTime}
|
|
37
|
-
</div>
|
|
38
|
-
<div className="text-center mt-2">
|
|
39
|
-
<span className="text-xs text-gray-400">{state.timeZone}</span>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
3
|
+
import { FaFire, FaBook, FaGithub, FaClock, FaImage } from 'react-icons/fa'
|
|
4
|
+
import { LiveComponentsProvider } from '@/core/client'
|
|
5
|
+
import { FileUploadExample } from './live/FileUploadExample'
|
|
6
|
+
import { MinimalLiveClock } from './live/MinimalLiveClock'
|
|
44
7
|
|
|
45
8
|
function AppContent() {
|
|
46
9
|
const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('checking')
|
|
10
|
+
const [showDemo, setShowDemo] = useState(false)
|
|
47
11
|
|
|
48
12
|
useEffect(() => {
|
|
49
13
|
checkApiStatus()
|
|
@@ -58,6 +22,31 @@ function AppContent() {
|
|
|
58
22
|
}
|
|
59
23
|
}
|
|
60
24
|
|
|
25
|
+
// If demo mode is active, show demo content
|
|
26
|
+
if (showDemo) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
|
|
29
|
+
<div className="container mx-auto px-4 py-8">
|
|
30
|
+
{/* Header with back button */}
|
|
31
|
+
<div className="flex items-center gap-4 mb-8">
|
|
32
|
+
<button
|
|
33
|
+
onClick={() => setShowDemo(false)}
|
|
34
|
+
className="px-4 py-2 bg-white/10 backdrop-blur-sm border border-white/20 text-white rounded-lg font-medium hover:bg-white/20 transition-all"
|
|
35
|
+
>
|
|
36
|
+
← Back
|
|
37
|
+
</button>
|
|
38
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
|
39
|
+
FluxStack Demos
|
|
40
|
+
</h1>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Demo Content */}
|
|
44
|
+
<FileUploadExample />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
61
50
|
return (
|
|
62
51
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
|
|
63
52
|
<div className="flex flex-col items-center justify-center min-h-screen px-6 text-center">
|
|
@@ -130,8 +119,15 @@ function AppContent() {
|
|
|
130
119
|
|
|
131
120
|
{/* Action Buttons */}
|
|
132
121
|
<div className="flex flex-wrap gap-4 justify-center">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => setShowDemo(true)}
|
|
124
|
+
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-emerald-500 to-teal-600 text-white rounded-xl font-medium hover:shadow-lg hover:shadow-emerald-500/50 transition-all"
|
|
125
|
+
>
|
|
126
|
+
<FaImage />
|
|
127
|
+
View Demos
|
|
128
|
+
</button>
|
|
133
129
|
<a
|
|
134
|
-
href="
|
|
130
|
+
href="/swagger"
|
|
135
131
|
target="_blank"
|
|
136
132
|
rel="noopener noreferrer"
|
|
137
133
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl font-medium hover:shadow-lg hover:shadow-purple-500/50 transition-all"
|
|
@@ -149,7 +145,7 @@ function AppContent() {
|
|
|
149
145
|
GitHub
|
|
150
146
|
</a>
|
|
151
147
|
<a
|
|
152
|
-
href="
|
|
148
|
+
href="/api"
|
|
153
149
|
target="_blank"
|
|
154
150
|
rel="noopener noreferrer"
|
|
155
151
|
className="inline-flex items-center gap-2 px-6 py-3 bg-white/10 backdrop-blur-sm border border-white/20 text-white rounded-xl font-medium hover:bg-white/20 transition-all"
|
|
@@ -8,13 +8,8 @@ import type { App } from '../../../server/app'
|
|
|
8
8
|
export const getBaseUrl = () => {
|
|
9
9
|
if (typeof window === 'undefined') return 'http://localhost:3000'
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
return window.location.origin
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Development: use backend server
|
|
17
|
-
return 'http://localhost:3000'
|
|
11
|
+
// Always use current origin - works for both dev and production
|
|
12
|
+
return window.location.origin
|
|
18
13
|
}
|
|
19
14
|
|
|
20
15
|
/**
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// 📤 File Upload Example - Demonstrates chunked file upload with Live Components
|
|
2
|
+
import { useState, useRef } from 'react'
|
|
3
|
+
import { useTypedLiveComponent, useChunkedUpload, useLiveComponents } from '@/core/client'
|
|
4
|
+
|
|
5
|
+
// Import component type DIRECTLY from backend - full type inference!
|
|
6
|
+
import type { LiveFileUploadComponent } from '@/server/live/LiveFileUploadComponent'
|
|
7
|
+
|
|
8
|
+
export function FileUploadExample() {
|
|
9
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
10
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
11
|
+
|
|
12
|
+
// Get sendMessageAndWait from LiveComponents context
|
|
13
|
+
const { sendMessageAndWait } = useLiveComponents()
|
|
14
|
+
|
|
15
|
+
// Setup Live Component with full type inference
|
|
16
|
+
const {
|
|
17
|
+
state,
|
|
18
|
+
call,
|
|
19
|
+
componentId,
|
|
20
|
+
connected
|
|
21
|
+
} = useTypedLiveComponent<LiveFileUploadComponent>('LiveFileUpload', {
|
|
22
|
+
uploadedFiles: [],
|
|
23
|
+
maxFiles: 10
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Setup Chunked Upload Hook with Adaptive Chunking
|
|
27
|
+
const {
|
|
28
|
+
uploading,
|
|
29
|
+
progress,
|
|
30
|
+
error: uploadError,
|
|
31
|
+
uploadFile,
|
|
32
|
+
cancelUpload,
|
|
33
|
+
reset: resetUpload,
|
|
34
|
+
bytesUploaded,
|
|
35
|
+
totalBytes
|
|
36
|
+
} = useChunkedUpload(componentId || '', {
|
|
37
|
+
chunkSize: 64 * 1024, // 64KB initial chunk size
|
|
38
|
+
maxFileSize: 500 * 1024 * 1024, // 500MB max (aceita qualquer arquivo)
|
|
39
|
+
allowedTypes: [], // Aceita todos os tipos de arquivo
|
|
40
|
+
sendMessageAndWait,
|
|
41
|
+
|
|
42
|
+
// Enable Adaptive Chunking for optimal upload speed
|
|
43
|
+
adaptiveChunking: true,
|
|
44
|
+
adaptiveConfig: {
|
|
45
|
+
minChunkSize: 16 * 1024, // 16KB minimum
|
|
46
|
+
maxChunkSize: 512 * 1024, // 512KB maximum (safer for web)
|
|
47
|
+
initialChunkSize: 64 * 1024, // 64KB start
|
|
48
|
+
targetLatency: 200, // Target 200ms per chunk
|
|
49
|
+
adjustmentFactor: 1.5, // Moderate adjustment
|
|
50
|
+
measurementWindow: 3 // Measure last 3 chunks
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
onProgress: (progress, uploaded, total) => {
|
|
54
|
+
console.log(`📤 Upload progress: ${progress.toFixed(1)}% (${uploaded}/${total} bytes)`)
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
onComplete: async (response) => {
|
|
58
|
+
console.log('✅ Upload complete:', response)
|
|
59
|
+
|
|
60
|
+
// Notify the Live Component about the successful upload
|
|
61
|
+
if (selectedFile && response.fileUrl) {
|
|
62
|
+
await call('onFileUploaded', {
|
|
63
|
+
filename: selectedFile.name,
|
|
64
|
+
fileUrl: response.fileUrl
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Reset state
|
|
69
|
+
setSelectedFile(null)
|
|
70
|
+
resetUpload()
|
|
71
|
+
if (fileInputRef.current) {
|
|
72
|
+
fileInputRef.current.value = ''
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
onError: (error) => {
|
|
77
|
+
console.error('❌ Upload error:', error)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
82
|
+
const file = e.target.files?.[0]
|
|
83
|
+
if (file) {
|
|
84
|
+
setSelectedFile(file)
|
|
85
|
+
resetUpload()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleUpload = async () => {
|
|
90
|
+
if (!selectedFile) return
|
|
91
|
+
await uploadFile(selectedFile)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleRemoveFile = async (fileId: string) => {
|
|
95
|
+
await call('removeFile', { fileId })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleClearAll = async () => {
|
|
99
|
+
await call('clearAll', {})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const formatBytes = (bytes: number): string => {
|
|
103
|
+
if (bytes === 0) return '0 Bytes'
|
|
104
|
+
const k = 1024
|
|
105
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
106
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
107
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getFileIcon = (filename: string): string => {
|
|
111
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))
|
|
112
|
+
const iconMap: Record<string, string> = {
|
|
113
|
+
'.pdf': '📄',
|
|
114
|
+
'.doc': '📝',
|
|
115
|
+
'.docx': '📝',
|
|
116
|
+
'.xls': '📊',
|
|
117
|
+
'.xlsx': '📊',
|
|
118
|
+
'.ppt': '📽️',
|
|
119
|
+
'.pptx': '📽️',
|
|
120
|
+
'.zip': '🗜️',
|
|
121
|
+
'.rar': '🗜️',
|
|
122
|
+
'.7z': '🗜️',
|
|
123
|
+
'.jpg': '🖼️',
|
|
124
|
+
'.jpeg': '🖼️',
|
|
125
|
+
'.png': '🖼️',
|
|
126
|
+
'.gif': '🖼️',
|
|
127
|
+
'.webp': '🖼️',
|
|
128
|
+
'.mp4': '🎥',
|
|
129
|
+
'.avi': '🎥',
|
|
130
|
+
'.mov': '🎥',
|
|
131
|
+
'.mp3': '🎵',
|
|
132
|
+
'.wav': '🎵',
|
|
133
|
+
'.jar': '☕',
|
|
134
|
+
'.java': '☕',
|
|
135
|
+
'.js': '📜',
|
|
136
|
+
'.ts': '📜',
|
|
137
|
+
'.json': '📋',
|
|
138
|
+
'.xml': '📋',
|
|
139
|
+
'.txt': '📃',
|
|
140
|
+
'.md': '📰'
|
|
141
|
+
}
|
|
142
|
+
return iconMap[ext] || '📎'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isImageFile = (filename: string): boolean => {
|
|
146
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))
|
|
147
|
+
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'].includes(ext)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!connected) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
153
|
+
<p className="text-yellow-800">🔌 Connecting to Live Components...</p>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const remainingSlots = state.maxFiles - state.uploadedFiles.length
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
162
|
+
<div className="bg-white rounded-lg shadow-lg">
|
|
163
|
+
{/* Header */}
|
|
164
|
+
<div className="p-6 border-b border-gray-200">
|
|
165
|
+
<h2 className="text-2xl font-bold text-gray-900">
|
|
166
|
+
📤 Universal File Upload
|
|
167
|
+
</h2>
|
|
168
|
+
<p className="mt-2 text-gray-600">
|
|
169
|
+
Upload any type of file with real-time progress tracking and adaptive chunking
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Upload Section */}
|
|
174
|
+
<div className="p-6 border-b border-gray-200">
|
|
175
|
+
<div className="space-y-4">
|
|
176
|
+
{/* File Input */}
|
|
177
|
+
<div>
|
|
178
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
179
|
+
Select Any File
|
|
180
|
+
</label>
|
|
181
|
+
<input
|
|
182
|
+
ref={fileInputRef}
|
|
183
|
+
type="file"
|
|
184
|
+
onChange={handleFileSelect}
|
|
185
|
+
disabled={uploading || remainingSlots === 0}
|
|
186
|
+
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 disabled:opacity-50"
|
|
187
|
+
/>
|
|
188
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
189
|
+
Maximum file size: 500 MB • All file types supported
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Selected File Info */}
|
|
194
|
+
{selectedFile && !uploading && (
|
|
195
|
+
<div className="p-4 bg-gray-50 rounded-lg">
|
|
196
|
+
<p className="text-sm text-gray-700">
|
|
197
|
+
<strong>Selected:</strong> {getFileIcon(selectedFile.name)} {selectedFile.name} ({formatBytes(selectedFile.size)})
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Upload Progress */}
|
|
203
|
+
{uploading && (
|
|
204
|
+
<div className="space-y-2">
|
|
205
|
+
<div className="flex justify-between text-sm text-gray-600">
|
|
206
|
+
<span>Uploading {selectedFile?.name}...</span>
|
|
207
|
+
<span>{progress.toFixed(1)}%</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
210
|
+
<div
|
|
211
|
+
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
|
212
|
+
style={{ width: `${progress}%` }}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
<p className="text-xs text-gray-500">
|
|
216
|
+
{formatBytes(bytesUploaded)} / {formatBytes(totalBytes)}
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Error Display */}
|
|
222
|
+
{uploadError && (
|
|
223
|
+
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
224
|
+
<p className="text-sm text-red-800">❌ {uploadError}</p>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{/* Action Buttons */}
|
|
229
|
+
<div className="flex gap-2">
|
|
230
|
+
<button
|
|
231
|
+
onClick={handleUpload}
|
|
232
|
+
disabled={!selectedFile || uploading || remainingSlots === 0}
|
|
233
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
|
234
|
+
>
|
|
235
|
+
{uploading ? '⏳ Uploading...' : '📤 Upload'}
|
|
236
|
+
</button>
|
|
237
|
+
|
|
238
|
+
{uploading && (
|
|
239
|
+
<button
|
|
240
|
+
onClick={cancelUpload}
|
|
241
|
+
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 font-medium"
|
|
242
|
+
>
|
|
243
|
+
❌ Cancel
|
|
244
|
+
</button>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{state.uploadedFiles.length > 0 && (
|
|
248
|
+
<button
|
|
249
|
+
onClick={handleClearAll}
|
|
250
|
+
disabled={uploading}
|
|
251
|
+
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50 font-medium ml-auto"
|
|
252
|
+
>
|
|
253
|
+
🗑️ Clear All
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Slots Info */}
|
|
259
|
+
<div className="text-sm text-gray-600">
|
|
260
|
+
{remainingSlots > 0 ? (
|
|
261
|
+
<span>✅ {remainingSlots} upload slot(s) remaining</span>
|
|
262
|
+
) : (
|
|
263
|
+
<span className="text-red-600">⚠️ Maximum files reached</span>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Uploaded Files Grid */}
|
|
270
|
+
<div className="p-6">
|
|
271
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
272
|
+
Uploaded Files ({state.uploadedFiles.length}/{state.maxFiles})
|
|
273
|
+
</h3>
|
|
274
|
+
|
|
275
|
+
{state.uploadedFiles.length === 0 ? (
|
|
276
|
+
<div className="text-center py-12 bg-gray-50 rounded-lg">
|
|
277
|
+
<p className="text-gray-500">No files uploaded yet</p>
|
|
278
|
+
</div>
|
|
279
|
+
) : (
|
|
280
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
281
|
+
{state.uploadedFiles.map((file) => (
|
|
282
|
+
<div
|
|
283
|
+
key={file.id}
|
|
284
|
+
className="relative bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
|
|
285
|
+
>
|
|
286
|
+
{/* File Preview */}
|
|
287
|
+
<div className="aspect-video bg-gray-100 flex items-center justify-center">
|
|
288
|
+
{isImageFile(file.filename) ? (
|
|
289
|
+
<img
|
|
290
|
+
src={file.url}
|
|
291
|
+
alt={file.filename}
|
|
292
|
+
className="max-w-full max-h-full object-contain"
|
|
293
|
+
onError={(e) => {
|
|
294
|
+
const target = e.target as HTMLImageElement
|
|
295
|
+
target.style.display = 'none'
|
|
296
|
+
target.parentElement!.innerHTML = `<div class="text-6xl">${getFileIcon(file.filename)}</div>`
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
) : (
|
|
300
|
+
<div className="text-6xl">{getFileIcon(file.filename)}</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Info */}
|
|
305
|
+
<div className="p-3">
|
|
306
|
+
<p className="text-sm font-medium text-gray-900 truncate" title={file.filename}>
|
|
307
|
+
{file.filename}
|
|
308
|
+
</p>
|
|
309
|
+
<p className="text-xs text-gray-500">
|
|
310
|
+
{new Date(file.uploadedAt).toLocaleString()}
|
|
311
|
+
</p>
|
|
312
|
+
<a
|
|
313
|
+
href={file.url}
|
|
314
|
+
target="_blank"
|
|
315
|
+
rel="noopener noreferrer"
|
|
316
|
+
className="text-xs text-blue-600 hover:underline mt-1 inline-block"
|
|
317
|
+
>
|
|
318
|
+
Download
|
|
319
|
+
</a>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{/* Remove Button */}
|
|
323
|
+
<button
|
|
324
|
+
onClick={() => handleRemoveFile(file.id)}
|
|
325
|
+
className="absolute top-2 right-2 p-2 bg-red-600 text-white rounded-full hover:bg-red-700 transition-colors shadow-lg"
|
|
326
|
+
title="Remove file"
|
|
327
|
+
>
|
|
328
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
329
|
+
<path
|
|
330
|
+
fillRule="evenodd"
|
|
331
|
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
332
|
+
clipRule="evenodd"
|
|
333
|
+
/>
|
|
334
|
+
</svg>
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
))}
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* Technical Info */}
|
|
344
|
+
<div className="mt-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
|
345
|
+
<h4 className="font-semibold text-gray-900 mb-2">📋 Technical Details</h4>
|
|
346
|
+
<ul className="text-sm text-gray-600 space-y-1">
|
|
347
|
+
<li>✅ <strong>Adaptive Chunking:</strong> Enabled (16KB - 512KB)</li>
|
|
348
|
+
<li>✅ <strong>Initial Chunk Size:</strong> 64KB</li>
|
|
349
|
+
<li>✅ <strong>Max File Size:</strong> 500MB</li>
|
|
350
|
+
<li>✅ <strong>Allowed Types:</strong> All file types supported</li>
|
|
351
|
+
<li>✅ <strong>Real-time Progress:</strong> Shows bytes uploaded and percentage</li>
|
|
352
|
+
<li>✅ <strong>State Sync:</strong> Uploaded files synced via Live Component</li>
|
|
353
|
+
<li>✅ <strong>Component ID:</strong> {componentId || 'Not connected'}</li>
|
|
354
|
+
<li>🚀 <strong>Optimization:</strong> Chunk size adjusts based on connection speed</li>
|
|
355
|
+
</ul>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 🔥 MinimalLiveClock - Live Component
|
|
2
|
+
import { useTypedLiveComponent } from '@/core/client';
|
|
3
|
+
|
|
4
|
+
// Import component type DIRECTLY from backend - full type inference!
|
|
5
|
+
import type { LiveClockComponent } from '@/server/live/LiveClockComponent';
|
|
6
|
+
|
|
7
|
+
export function MinimalLiveClock() {
|
|
8
|
+
const { state, setValue } = useTypedLiveComponent<LiveClockComponent>(
|
|
9
|
+
'LiveClock',
|
|
10
|
+
{
|
|
11
|
+
currentTime: "Loading...",
|
|
12
|
+
timeZone: "America/Sao_Paulo",
|
|
13
|
+
format: "12h",
|
|
14
|
+
showSeconds: true,
|
|
15
|
+
showDate: true,
|
|
16
|
+
lastSync: new Date(),
|
|
17
|
+
serverUptime: 0,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// Called when WebSocket connects (component not mounted yet - can't use setValue)
|
|
21
|
+
onConnect: () => {
|
|
22
|
+
console.log('onConnect called - WebSocket connected')
|
|
23
|
+
},
|
|
24
|
+
// Called after fresh mount (no prior state)
|
|
25
|
+
onMount: () => {
|
|
26
|
+
console.log('onMount called - changing format to 12h')
|
|
27
|
+
setValue('format', '12h')
|
|
28
|
+
},
|
|
29
|
+
// Called after successful rehydration (restoring prior state)
|
|
30
|
+
onRehydrate: () => {
|
|
31
|
+
console.log('onRehydrate called - keeping format 24h')
|
|
32
|
+
setValue('format', '24h')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-xl p-4 border border-blue-400/20">
|
|
39
|
+
<div className="text-4xl font-mono font-bold text-white text-center tracking-wider">
|
|
40
|
+
{state.currentTime}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="text-center mt-2">
|
|
43
|
+
<span className="text-xs text-gray-400">{state.timeZone} ({state.format})</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|