@betterstart/cli 0.1.47 → 0.1.49
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 +47 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/ui/media-upload-field.tsx +63 -27
package/package.json
CHANGED
|
@@ -25,34 +25,50 @@ const IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'imag
|
|
|
25
25
|
const VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo']
|
|
26
26
|
const MEDIA_TYPES = [...IMAGE_TYPES, ...VIDEO_TYPES]
|
|
27
27
|
|
|
28
|
+
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|avif|ico|bmp|tiff?)($|\?)/i
|
|
29
|
+
const VIDEO_EXTENSIONS = /\.(mp4|webm|ogg|mov|avi|mkv|m4v)($|\?)/i
|
|
30
|
+
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
32
|
+
* Check if an accept string only allows non-media file types (documents, archives, etc.)
|
|
30
33
|
*/
|
|
31
|
-
function
|
|
34
|
+
function isDocumentOnlyAccept(accept: string): boolean {
|
|
35
|
+
if (!accept || accept === 'image/*,video/*' || accept === '*/*' || accept === '*') return false
|
|
36
|
+
const types = accept.split(',').map((t) => t.trim().toLowerCase())
|
|
37
|
+
return types.every(
|
|
38
|
+
(t) =>
|
|
39
|
+
!t.startsWith('image/') &&
|
|
40
|
+
!t.startsWith('video/') &&
|
|
41
|
+
!IMAGE_EXTENSIONS.test(t) &&
|
|
42
|
+
!VIDEO_EXTENSIONS.test(t)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect if a URL or MIME type represents an image, video, or other file type
|
|
48
|
+
*/
|
|
49
|
+
function getMediaType(urlOrMime: string): 'image' | 'video' | 'document' {
|
|
32
50
|
const lower = urlOrMime.toLowerCase()
|
|
33
51
|
|
|
34
|
-
// Check for video MIME types or extensions
|
|
35
52
|
if (
|
|
36
53
|
VIDEO_TYPES.some((type) => lower.includes(type)) ||
|
|
37
|
-
|
|
54
|
+
VIDEO_EXTENSIONS.test(lower)
|
|
38
55
|
) {
|
|
39
56
|
return 'video'
|
|
40
57
|
}
|
|
41
58
|
|
|
42
|
-
// Check for image MIME types or extensions
|
|
43
59
|
if (
|
|
44
60
|
IMAGE_TYPES.some((type) => lower.includes(type)) ||
|
|
45
|
-
|
|
61
|
+
IMAGE_EXTENSIONS.test(lower)
|
|
46
62
|
) {
|
|
47
63
|
return 'image'
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
return '
|
|
66
|
+
return 'document'
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
/**
|
|
54
70
|
* Media upload field component with R2 integration
|
|
55
|
-
* Supports
|
|
71
|
+
* Supports images, videos, and document files (PDF, DOC, etc.)
|
|
56
72
|
* Designed for use in forms with react-hook-form
|
|
57
73
|
*/
|
|
58
74
|
export function MediaUploadField({
|
|
@@ -68,9 +84,11 @@ export function MediaUploadField({
|
|
|
68
84
|
}: MediaUploadFieldProps) {
|
|
69
85
|
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
|
70
86
|
const [previewUrl, setPreviewUrl] = React.useState<string | null>(value || null)
|
|
71
|
-
const [
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
const [fileName, setFileName] = React.useState<string | null>(null)
|
|
88
|
+
const [mediaType, setMediaType] = React.useState<'image' | 'video' | 'document'>(() => {
|
|
89
|
+
if (isDocumentOnlyAccept(accept)) return 'document'
|
|
90
|
+
return value ? getMediaType(value) : 'document'
|
|
91
|
+
})
|
|
74
92
|
|
|
75
93
|
const { upload, mutation, progress } = useUpload({
|
|
76
94
|
validationConfig: {
|
|
@@ -88,14 +106,19 @@ export function MediaUploadField({
|
|
|
88
106
|
const uploadedUrl = result.files[0].url
|
|
89
107
|
onChange(uploadedUrl)
|
|
90
108
|
setPreviewUrl(uploadedUrl)
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
if (!isDocumentOnlyAccept(accept)) {
|
|
110
|
+
setMediaType(getMediaType(uploadedUrl))
|
|
111
|
+
}
|
|
112
|
+
toast.success('File uploaded successfully')
|
|
93
113
|
} else {
|
|
94
114
|
const errorMsg = result.error || 'Upload failed'
|
|
95
115
|
toast.error(errorMsg)
|
|
96
116
|
console.error('Upload failed:', errorMsg)
|
|
97
117
|
setPreviewUrl(value || null)
|
|
98
|
-
|
|
118
|
+
setFileName(null)
|
|
119
|
+
if (!isDocumentOnlyAccept(accept)) {
|
|
120
|
+
setMediaType(value ? getMediaType(value) : 'document')
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
},
|
|
101
124
|
onError: (error) => {
|
|
@@ -103,7 +126,10 @@ export function MediaUploadField({
|
|
|
103
126
|
toast.error(errorMsg)
|
|
104
127
|
console.error('Upload error:', error)
|
|
105
128
|
setPreviewUrl(value || null)
|
|
106
|
-
|
|
129
|
+
setFileName(null)
|
|
130
|
+
if (!isDocumentOnlyAccept(accept)) {
|
|
131
|
+
setMediaType(value ? getMediaType(value) : 'document')
|
|
132
|
+
}
|
|
107
133
|
},
|
|
108
134
|
prefix: 'media'
|
|
109
135
|
})
|
|
@@ -120,14 +146,19 @@ export function MediaUploadField({
|
|
|
120
146
|
return
|
|
121
147
|
}
|
|
122
148
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
setFileName(file.name)
|
|
150
|
+
|
|
151
|
+
const detectedType = isDocumentOnlyAccept(accept) ? 'document' : getMediaType(file.type)
|
|
152
|
+
setMediaType(detectedType)
|
|
153
|
+
|
|
154
|
+
if (detectedType === 'image' || detectedType === 'video') {
|
|
155
|
+
try {
|
|
156
|
+
setPreviewUrl(URL.createObjectURL(file))
|
|
157
|
+
} catch {
|
|
158
|
+
setPreviewUrl(file.name)
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
setPreviewUrl(file.name)
|
|
131
162
|
}
|
|
132
163
|
|
|
133
164
|
upload([file])
|
|
@@ -135,7 +166,8 @@ export function MediaUploadField({
|
|
|
135
166
|
|
|
136
167
|
const handleRemove = () => {
|
|
137
168
|
setPreviewUrl(null)
|
|
138
|
-
|
|
169
|
+
setFileName(null)
|
|
170
|
+
setMediaType(isDocumentOnlyAccept(accept) ? 'document' : 'document')
|
|
139
171
|
onChange('')
|
|
140
172
|
mutation.reset()
|
|
141
173
|
if (fileInputRef.current) {
|
|
@@ -150,6 +182,10 @@ export function MediaUploadField({
|
|
|
150
182
|
const isUploading = mutation.isPending
|
|
151
183
|
const uploadProgress = progress[0]?.progress || 0
|
|
152
184
|
|
|
185
|
+
const displayName =
|
|
186
|
+
fileName ||
|
|
187
|
+
(previewUrl ? decodeURIComponent(previewUrl.split('/').pop()?.split('?')[0] || 'Uploaded file') : 'Uploaded file')
|
|
188
|
+
|
|
153
189
|
return (
|
|
154
190
|
<div className={cn('space-y-2', className)}>
|
|
155
191
|
{label && (
|
|
@@ -192,10 +228,10 @@ export function MediaUploadField({
|
|
|
192
228
|
<div className="flex items-center gap-3 p-4">
|
|
193
229
|
<FileText className="size-8 text-muted-foreground shrink-0" />
|
|
194
230
|
<div className="min-w-0 flex-1">
|
|
195
|
-
<p className="text-sm font-medium truncate">
|
|
196
|
-
|
|
231
|
+
<p className="text-sm font-medium truncate">{displayName}</p>
|
|
232
|
+
<p className="text-xs text-muted-foreground">
|
|
233
|
+
{isUploading ? 'Uploading...' : 'File uploaded'}
|
|
197
234
|
</p>
|
|
198
|
-
<p className="text-xs text-muted-foreground">File uploaded</p>
|
|
199
235
|
</div>
|
|
200
236
|
</div>
|
|
201
237
|
)}
|