@betterstart/cli 0.1.48 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterstart/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "Scaffold a full-featured CMS into any Next.js 16 application",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- * Detect if a URL or file is an image or video based on extension or MIME type
32
+ * Check if an accept string only allows non-media file types (documents, archives, etc.)
30
33
  */
31
- function getMediaType(urlOrMime: string): 'image' | 'video' | 'unknown' {
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
- /\.(mp4|webm|ogg|mov|avi|mkv)($|\?)/.test(lower)
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
- /\.(jpg|jpeg|png|gif|webp|svg)($|\?)/.test(lower)
61
+ IMAGE_EXTENSIONS.test(lower)
46
62
  ) {
47
63
  return 'image'
48
64
  }
49
65
 
50
- return 'unknown'
66
+ return 'document'
51
67
  }
52
68
 
53
69
  /**
54
70
  * Media upload field component with R2 integration
55
- * Supports both images and videos
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 [mediaType, setMediaType] = React.useState<'image' | 'video' | 'unknown'>(
72
- value ? getMediaType(value) : 'unknown'
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
- setMediaType(getMediaType(uploadedUrl))
92
- toast.success('Media uploaded successfully')
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
- setMediaType(value ? getMediaType(value) : 'unknown')
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
- setMediaType(value ? getMediaType(value) : 'unknown')
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
- try {
124
- const localPreview = URL.createObjectURL(file)
125
- setPreviewUrl(localPreview)
126
- setMediaType(getMediaType(file.type))
127
- } catch (error) {
128
- console.error('[MediaUploadField] Failed to create preview:', error)
129
- toast.error('Failed to preview media')
130
- return
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
- setMediaType('unknown')
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
- {previewUrl.split('/').pop()?.split('?')[0] || 'Uploaded file'}
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
  )}