@betterstart/cli 0.1.2 → 0.1.4
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 +133 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +13484 -354
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -260
- package/dist/index.js +4 -11373
- package/dist/index.js.map +1 -1
- package/package.json +29 -42
- package/templates/schema.json +959 -0
- package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
- package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
- package/templates/tiptap/hooks/use-element-rect.ts +166 -0
- package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
- package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
- package/templates/tiptap/hooks/use-scrolling.ts +64 -0
- package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
- package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
- package/templates/tiptap/hooks/use-unmount.ts +21 -0
- package/templates/tiptap/hooks/use-window-size.ts +87 -0
- package/templates/tiptap/lib/tiptap-utils.ts +587 -0
- package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
- package/templates/tiptap/styles/_variables.scss +296 -0
- package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
- package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
- package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
- package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
- package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
- package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
- package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
- package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
- package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
- package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
- package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
- package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
- package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
- package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
- package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
- package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
- package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
- package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
- package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
- package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
- package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
- package/templates/ui/accordion.tsx +52 -0
- package/templates/ui/alert-dialog.tsx +116 -0
- package/templates/ui/alert.tsx +48 -0
- package/templates/ui/aspect-ratio.tsx +7 -0
- package/templates/ui/avatar.tsx +46 -0
- package/templates/ui/badge.tsx +32 -0
- package/templates/ui/breadcrumb.tsx +98 -0
- package/templates/ui/button-group.tsx +77 -0
- package/templates/ui/button.tsx +48 -0
- package/templates/ui/calendar.tsx +176 -0
- package/templates/ui/card.tsx +54 -0
- package/templates/ui/carousel.tsx +234 -0
- package/templates/ui/chart.tsx +349 -0
- package/templates/ui/checkbox.tsx +27 -0
- package/templates/ui/collapsible.tsx +11 -0
- package/templates/ui/command.tsx +142 -0
- package/templates/ui/context-menu.tsx +188 -0
- package/templates/ui/curriculum-editor.tsx +601 -0
- package/templates/ui/date-picker.tsx +70 -0
- package/templates/ui/dialog.tsx +103 -0
- package/templates/ui/drawer.tsx +99 -0
- package/templates/ui/dropdown-menu.tsx +185 -0
- package/templates/ui/dynamic-list-field.tsx +95 -0
- package/templates/ui/empty.tsx +90 -0
- package/templates/ui/field.tsx +231 -0
- package/templates/ui/file-upload-example.tsx +113 -0
- package/templates/ui/form.tsx +172 -0
- package/templates/ui/hover-card.tsx +28 -0
- package/templates/ui/icon-picker.tsx +435 -0
- package/templates/ui/icons-data.ts +6 -0
- package/templates/ui/image-upload-field.tsx +360 -0
- package/templates/ui/input-group.tsx +160 -0
- package/templates/ui/input-otp.tsx +70 -0
- package/templates/ui/input.tsx +21 -0
- package/templates/ui/item.tsx +171 -0
- package/templates/ui/kbd.tsx +28 -0
- package/templates/ui/label.tsx +20 -0
- package/templates/ui/logo.tsx +113 -0
- package/templates/ui/markdown-editor.tsx +303 -0
- package/templates/ui/markdown-utils.ts +128 -0
- package/templates/ui/media-upload-field.tsx +255 -0
- package/templates/ui/menubar.tsx +230 -0
- package/templates/ui/navigation-menu.tsx +119 -0
- package/templates/ui/pagination.tsx +96 -0
- package/templates/ui/placeholder.tsx +25 -0
- package/templates/ui/popover.tsx +32 -0
- package/templates/ui/progress.tsx +24 -0
- package/templates/ui/radio-group.tsx +37 -0
- package/templates/ui/resizable.tsx +41 -0
- package/templates/ui/rich-text-editor.tsx +374 -0
- package/templates/ui/scroll-area.tsx +45 -0
- package/templates/ui/select.tsx +151 -0
- package/templates/ui/separator.tsx +25 -0
- package/templates/ui/sheet.tsx +120 -0
- package/templates/ui/sidebar.tsx +684 -0
- package/templates/ui/skeleton.tsx +7 -0
- package/templates/ui/slider.tsx +24 -0
- package/templates/ui/sonner.tsx +29 -0
- package/templates/ui/spinner.tsx +15 -0
- package/templates/ui/switch.tsx +28 -0
- package/templates/ui/table.tsx +93 -0
- package/templates/ui/tabs.tsx +54 -0
- package/templates/ui/textarea.tsx +20 -0
- package/templates/ui/toast.tsx +127 -0
- package/templates/ui/toggle-group.tsx +56 -0
- package/templates/ui/toggle.tsx +43 -0
- package/templates/ui/tooltip.tsx +31 -0
- package/templates/ui/use-mobile.tsx +19 -0
- package/templates/ui/video-upload-field.tsx +368 -0
- package/dist/chunk-G4KI4DVB.js +0 -179
- package/dist/chunk-G4KI4DVB.js.map +0 -1
- package/dist/chunk-NKRQYAS6.js +0 -260
- package/dist/chunk-NKRQYAS6.js.map +0 -1
- package/dist/chunk-QLVSHP7X.js +0 -235
- package/dist/chunk-QLVSHP7X.js.map +0 -1
- package/dist/chunk-WY6BC55D.js +0 -357
- package/dist/chunk-WY6BC55D.js.map +0 -1
- package/dist/config/index.d.ts +0 -93
- package/dist/config/index.js +0 -58
- package/dist/config/index.js.map +0 -1
- package/dist/core/index.d.ts +0 -415
- package/dist/core/index.js +0 -906
- package/dist/core/index.js.map +0 -1
- package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
- package/dist/logger-awLb347n.d.ts +0 -81
- package/dist/plugins/index.d.ts +0 -213
- package/dist/plugins/index.js +0 -365
- package/dist/plugins/index.js.map +0 -1
- package/dist/types-ByX_gl6y.d.ts +0 -232
- package/dist/types-eI549DEG.d.ts +0 -331
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useUpload } from '@cms/hooks/use-upload'
|
|
4
|
+
import { cn } from '@cms/utils/cn'
|
|
5
|
+
import { Check, Loader2, Upload, X } from 'lucide-react'
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
import { Input } from './input'
|
|
10
|
+
import { Progress } from './progress'
|
|
11
|
+
|
|
12
|
+
function isValidUrl(url: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
new URL(url)
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createPreviewUrl(file: File): string {
|
|
22
|
+
return URL.createObjectURL(file)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function revokePreviewUrl(url: string): void {
|
|
26
|
+
if (url.startsWith('blob:')) URL.revokeObjectURL(url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useFetchVideo({ maxSizeInMB }: { accept: string; maxSizeInMB: number }) {
|
|
30
|
+
const [isFetching, setIsFetching] = React.useState(false)
|
|
31
|
+
|
|
32
|
+
const fetchVideoFromUrl = React.useCallback(
|
|
33
|
+
async (url: string): Promise<File | null> => {
|
|
34
|
+
setIsFetching(true)
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url)
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
toast.error('Failed to fetch video from URL')
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const blob = await response.blob()
|
|
42
|
+
if (blob.size > maxSizeInMB * 1024 * 1024) {
|
|
43
|
+
toast.error(`Video exceeds ${maxSizeInMB}MB limit`)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
const filename = url.split('/').pop() || 'video'
|
|
47
|
+
return new File([blob], filename, { type: blob.type })
|
|
48
|
+
} catch {
|
|
49
|
+
toast.error('Failed to fetch video from URL')
|
|
50
|
+
return null
|
|
51
|
+
} finally {
|
|
52
|
+
setIsFetching(false)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[maxSizeInMB]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return { fetchVideoFromUrl, isFetching }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface VideoUploadFieldProps {
|
|
62
|
+
value?: string
|
|
63
|
+
onChange: (value: string) => void
|
|
64
|
+
onBlur?: () => void
|
|
65
|
+
disabled?: boolean
|
|
66
|
+
accept?: string
|
|
67
|
+
maxSizeInMB?: number
|
|
68
|
+
className?: string
|
|
69
|
+
label?: string
|
|
70
|
+
description?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface UrlInputState {
|
|
74
|
+
value: string
|
|
75
|
+
preview: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Video upload field component with R2 integration
|
|
80
|
+
* Designed for use in forms with react-hook-form
|
|
81
|
+
*/
|
|
82
|
+
export function VideoUploadField({
|
|
83
|
+
value,
|
|
84
|
+
onChange,
|
|
85
|
+
onBlur,
|
|
86
|
+
disabled = false,
|
|
87
|
+
accept = 'video/*',
|
|
88
|
+
maxSizeInMB = 100,
|
|
89
|
+
className,
|
|
90
|
+
label,
|
|
91
|
+
description
|
|
92
|
+
}: VideoUploadFieldProps) {
|
|
93
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
|
94
|
+
const [previewUrl, setPreviewUrl] = React.useState<string | null>(value || null)
|
|
95
|
+
const [urlState, setUrlState] = React.useState<UrlInputState>({
|
|
96
|
+
value: '',
|
|
97
|
+
preview: null
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const { fetchVideoFromUrl, isFetching: isFetchingUrl } = useFetchVideo({
|
|
101
|
+
accept,
|
|
102
|
+
maxSizeInMB
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const { upload, mutation, progress } = useUpload({
|
|
106
|
+
validationConfig: {
|
|
107
|
+
maxSizeInBytes: maxSizeInMB * 1024 * 1024,
|
|
108
|
+
allowedTypes:
|
|
109
|
+
accept === 'video/*'
|
|
110
|
+
? ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo']
|
|
111
|
+
: accept.split(',').map((t) => t.trim()),
|
|
112
|
+
maxFiles: 1
|
|
113
|
+
},
|
|
114
|
+
onSuccess: (result) => {
|
|
115
|
+
if (result.success && result.files?.[0]) {
|
|
116
|
+
const uploadedUrl = result.files[0].url
|
|
117
|
+
onChange(uploadedUrl)
|
|
118
|
+
setPreviewUrl(uploadedUrl)
|
|
119
|
+
toast.success('Video uploaded successfully')
|
|
120
|
+
} else {
|
|
121
|
+
const errorMsg = result.error || 'Upload failed'
|
|
122
|
+
toast.error(errorMsg)
|
|
123
|
+
console.error('Upload failed:', errorMsg)
|
|
124
|
+
setPreviewUrl(value || null)
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
onError: (error) => {
|
|
128
|
+
const errorMsg = error.message || 'Network error during upload'
|
|
129
|
+
toast.error(errorMsg)
|
|
130
|
+
console.error('Upload error:', error)
|
|
131
|
+
setPreviewUrl(value || null)
|
|
132
|
+
},
|
|
133
|
+
prefix: 'videos'
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Update preview when value changes externally
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
if (value) {
|
|
139
|
+
setPreviewUrl(value)
|
|
140
|
+
}
|
|
141
|
+
}, [value])
|
|
142
|
+
|
|
143
|
+
// Cleanup object URLs on unmount
|
|
144
|
+
React.useEffect(() => {
|
|
145
|
+
return () => {
|
|
146
|
+
if (previewUrl) {
|
|
147
|
+
revokePreviewUrl(previewUrl)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}, [previewUrl])
|
|
151
|
+
|
|
152
|
+
const handleFileSelect = React.useCallback(
|
|
153
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
154
|
+
const file = e.target.files?.[0]
|
|
155
|
+
if (!file) return
|
|
156
|
+
|
|
157
|
+
if (file.size === 0) {
|
|
158
|
+
toast.error('Cannot upload empty file')
|
|
159
|
+
if (fileInputRef.current) {
|
|
160
|
+
fileInputRef.current.value = ''
|
|
161
|
+
}
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create local preview
|
|
166
|
+
try {
|
|
167
|
+
const localPreview = createPreviewUrl(file)
|
|
168
|
+
setPreviewUrl(localPreview)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('[VideoUploadField] Failed to create preview:', error)
|
|
171
|
+
toast.error('Failed to preview video')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
upload([file])
|
|
176
|
+
},
|
|
177
|
+
[upload]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const handleUrlChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
181
|
+
const url = e.target.value.trim()
|
|
182
|
+
const preview = url && isValidUrl(url) ? url : null
|
|
183
|
+
setUrlState({ value: url, preview })
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
const handleUrlConfirm = React.useCallback(async () => {
|
|
187
|
+
if (!urlState.value.trim()) {
|
|
188
|
+
toast.error('Please enter a valid video URL')
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const file = await fetchVideoFromUrl(urlState.value.trim())
|
|
193
|
+
if (!file) return
|
|
194
|
+
|
|
195
|
+
// Create local preview
|
|
196
|
+
try {
|
|
197
|
+
const localPreview = createPreviewUrl(file)
|
|
198
|
+
setPreviewUrl(localPreview)
|
|
199
|
+
setUrlState({ value: '', preview: null })
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('[VideoUploadField] Failed to create preview:', error)
|
|
202
|
+
toast.error('Failed to preview video')
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
upload([file])
|
|
207
|
+
}, [urlState.value, fetchVideoFromUrl, upload])
|
|
208
|
+
|
|
209
|
+
const handleRemove = React.useCallback(() => {
|
|
210
|
+
if (previewUrl) {
|
|
211
|
+
revokePreviewUrl(previewUrl)
|
|
212
|
+
}
|
|
213
|
+
setPreviewUrl(null)
|
|
214
|
+
setUrlState({ value: '', preview: null })
|
|
215
|
+
onChange('')
|
|
216
|
+
mutation.reset()
|
|
217
|
+
if (fileInputRef.current) {
|
|
218
|
+
fileInputRef.current.value = ''
|
|
219
|
+
}
|
|
220
|
+
}, [previewUrl, onChange, mutation])
|
|
221
|
+
|
|
222
|
+
const handleClick = React.useCallback(() => {
|
|
223
|
+
fileInputRef.current?.click()
|
|
224
|
+
}, [])
|
|
225
|
+
|
|
226
|
+
const isUploading = mutation.isPending
|
|
227
|
+
const uploadProgress = progress[0]?.progress || 0
|
|
228
|
+
const displayPreview = urlState.preview || previewUrl
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className={cn('space-y-2', className)}>
|
|
232
|
+
{label && (
|
|
233
|
+
<label htmlFor="video-input" className="text-sm font-medium">
|
|
234
|
+
{label}
|
|
235
|
+
</label>
|
|
236
|
+
)}
|
|
237
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
238
|
+
|
|
239
|
+
<div className="space-y-4">
|
|
240
|
+
<input
|
|
241
|
+
id="video-input"
|
|
242
|
+
ref={fileInputRef}
|
|
243
|
+
type="file"
|
|
244
|
+
accept={accept}
|
|
245
|
+
onChange={handleFileSelect}
|
|
246
|
+
onBlur={onBlur}
|
|
247
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
248
|
+
className="hidden"
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
{/* URL Input Section */}
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
<div className="flex gap-2">
|
|
254
|
+
<Input
|
|
255
|
+
type="url"
|
|
256
|
+
placeholder="Paste video URL here or Click below to upload"
|
|
257
|
+
value={urlState.value}
|
|
258
|
+
onChange={handleUrlChange}
|
|
259
|
+
onBlur={onBlur}
|
|
260
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
261
|
+
className="flex-1"
|
|
262
|
+
/>
|
|
263
|
+
{urlState.preview && (
|
|
264
|
+
<Button
|
|
265
|
+
type="button"
|
|
266
|
+
onClick={handleUrlConfirm}
|
|
267
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
268
|
+
className="shrink-0"
|
|
269
|
+
size="lg"
|
|
270
|
+
>
|
|
271
|
+
{isFetchingUrl ? (
|
|
272
|
+
<Loader2 className="size-4 animate-spin" />
|
|
273
|
+
) : (
|
|
274
|
+
<>
|
|
275
|
+
<Check className="size-4" />
|
|
276
|
+
Confirm
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
</Button>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{/* Preview or Upload Area */}
|
|
285
|
+
{displayPreview ? (
|
|
286
|
+
<div className="relative w-full rounded-lg corner-squircle border border-dashed border-border bg-secondary h-50 flex items-center justify-center p-10 group overflow-hidden">
|
|
287
|
+
<video
|
|
288
|
+
src={displayPreview}
|
|
289
|
+
controls
|
|
290
|
+
className="w-full h-auto max-h-60 object-contain rounded-lg corner-squircle aspect-[3/4]"
|
|
291
|
+
>
|
|
292
|
+
<track kind="captions" />
|
|
293
|
+
</video>
|
|
294
|
+
|
|
295
|
+
{/* Upload Progress Overlay */}
|
|
296
|
+
{isUploading && (
|
|
297
|
+
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center gap-2 rounded-lg corner-squircle">
|
|
298
|
+
<Loader2 className="size-8 text-white animate-spin" />
|
|
299
|
+
<Progress value={uploadProgress} className="w-3/4" />
|
|
300
|
+
<span className="text-white text-sm">{uploadProgress}%</span>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{/* Remove Button */}
|
|
305
|
+
{!isUploading && !isFetchingUrl && (
|
|
306
|
+
<Button
|
|
307
|
+
type="button"
|
|
308
|
+
variant="destructive"
|
|
309
|
+
size="icon"
|
|
310
|
+
className="absolute top-2 right-2 hidden group-hover:flex"
|
|
311
|
+
onClick={handleRemove}
|
|
312
|
+
disabled={disabled}
|
|
313
|
+
>
|
|
314
|
+
<X className="size-4" />
|
|
315
|
+
</Button>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* URL Preview Indicator */}
|
|
319
|
+
{urlState.preview && !previewUrl && (
|
|
320
|
+
<div className="absolute bottom-2 left-2 bg-primary/90 text-primary-foreground text-xs px-2 py-1 rounded">
|
|
321
|
+
Click Confirm to upload
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={handleClick}
|
|
329
|
+
disabled={disabled || isUploading}
|
|
330
|
+
className={cn(
|
|
331
|
+
'w-full rounded-lg corner-squircle border border-dashed border-border bg-secondary/50',
|
|
332
|
+
'hover:border-primary/50 transition-colors',
|
|
333
|
+
'flex flex-col items-center justify-center gap-2 p-8',
|
|
334
|
+
'disabled:opacity-50 disabled:cursor-not-allowed'
|
|
335
|
+
)}
|
|
336
|
+
>
|
|
337
|
+
{isUploading ? (
|
|
338
|
+
<>
|
|
339
|
+
<Loader2 className="size-10 text-muted-foreground animate-spin" />
|
|
340
|
+
<Progress value={uploadProgress} className="w-3/4" />
|
|
341
|
+
<span className="text-sm text-muted-foreground">{uploadProgress}%</span>
|
|
342
|
+
</>
|
|
343
|
+
) : (
|
|
344
|
+
<>
|
|
345
|
+
<div className="rounded-full corner-squircle bg-primary/10 p-3">
|
|
346
|
+
<Upload className="size-6 text-primary" />
|
|
347
|
+
</div>
|
|
348
|
+
<div className="text-center">
|
|
349
|
+
<p className="text-sm font-medium">Click to upload video</p>
|
|
350
|
+
<p className="text-xs text-muted-foreground mt-1">Max size: {maxSizeInMB}MB</p>
|
|
351
|
+
</div>
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
</button>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Error Message */}
|
|
358
|
+
{mutation.isError && (
|
|
359
|
+
<div className="text-sm text-destructive">Upload failed: {mutation.error.message}</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{mutation.data && !mutation.data.success && (
|
|
363
|
+
<div className="text-sm text-destructive">{mutation.data.error}</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
}
|
package/dist/chunk-G4KI4DVB.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ImportResolver,
|
|
3
|
-
MONOREPO_IMPORT_PATHS
|
|
4
|
-
} from "./chunk-NKRQYAS6.js";
|
|
5
|
-
|
|
6
|
-
// src/utils.ts
|
|
7
|
-
import fs from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
var _paths = null;
|
|
10
|
-
function initPaths(startDir) {
|
|
11
|
-
const cwd = startDir || process.cwd();
|
|
12
|
-
let root = cwd;
|
|
13
|
-
while (root !== "/") {
|
|
14
|
-
if (fs.existsSync(path.join(root, "turbo.json")) || fs.existsSync(path.join(root, "pnpm-workspace.yaml"))) {
|
|
15
|
-
break;
|
|
16
|
-
}
|
|
17
|
-
root = path.dirname(root);
|
|
18
|
-
}
|
|
19
|
-
if (root === "/") {
|
|
20
|
-
throw new Error("Could not find monorepo root (no turbo.json or pnpm-workspace.yaml found)");
|
|
21
|
-
}
|
|
22
|
-
_paths = {
|
|
23
|
-
root,
|
|
24
|
-
app: path.join(root, "apps/web"),
|
|
25
|
-
database: path.join(root, "packages/database"),
|
|
26
|
-
lib: path.join(root, "packages/lib"),
|
|
27
|
-
hooks: path.join(root, "packages/hooks"),
|
|
28
|
-
schemas: path.join(root, "packages/codegen/src/schemas"),
|
|
29
|
-
data: path.join(root, "packages/data")
|
|
30
|
-
};
|
|
31
|
-
return _paths;
|
|
32
|
-
}
|
|
33
|
-
function getPaths() {
|
|
34
|
-
if (!_paths) {
|
|
35
|
-
return initPaths();
|
|
36
|
-
}
|
|
37
|
-
return _paths;
|
|
38
|
-
}
|
|
39
|
-
function getProjectRoot() {
|
|
40
|
-
return getPaths().app;
|
|
41
|
-
}
|
|
42
|
-
var _importResolver = null;
|
|
43
|
-
function initImportResolver(config) {
|
|
44
|
-
_importResolver = new ImportResolver(config.imports);
|
|
45
|
-
return _importResolver;
|
|
46
|
-
}
|
|
47
|
-
function getImportResolver() {
|
|
48
|
-
if (!_importResolver) {
|
|
49
|
-
_importResolver = new ImportResolver(MONOREPO_IMPORT_PATHS);
|
|
50
|
-
}
|
|
51
|
-
return _importResolver;
|
|
52
|
-
}
|
|
53
|
-
function toPascalCase(str) {
|
|
54
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
55
|
-
}
|
|
56
|
-
function toCamelCase(str) {
|
|
57
|
-
const pascal = toPascalCase(str);
|
|
58
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
59
|
-
}
|
|
60
|
-
function toKebabCase(str) {
|
|
61
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
62
|
-
}
|
|
63
|
-
function toSnakeCase(str) {
|
|
64
|
-
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
function toScreamingSnakeCase(str) {
|
|
67
|
-
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toUpperCase();
|
|
68
|
-
}
|
|
69
|
-
function ensureDir(dirPath) {
|
|
70
|
-
if (!fs.existsSync(dirPath)) {
|
|
71
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function getMigrationTimestamp() {
|
|
75
|
-
const now = /* @__PURE__ */ new Date();
|
|
76
|
-
const year = now.getFullYear();
|
|
77
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
78
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
79
|
-
const hours = String(now.getHours()).padStart(2, "0");
|
|
80
|
-
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
81
|
-
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
82
|
-
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
83
|
-
}
|
|
84
|
-
function isPlural(str) {
|
|
85
|
-
const pluralPatterns = [
|
|
86
|
-
/ies$/,
|
|
87
|
-
/ves$/,
|
|
88
|
-
/ses$/,
|
|
89
|
-
/xes$/,
|
|
90
|
-
/zes$/,
|
|
91
|
-
/ches$/,
|
|
92
|
-
/shes$/,
|
|
93
|
-
/men$/,
|
|
94
|
-
/people$/
|
|
95
|
-
];
|
|
96
|
-
for (const pattern of pluralPatterns) {
|
|
97
|
-
if (pattern.test(str)) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (str.endsWith("s") && !str.endsWith("ss")) {
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
function pluralize(str) {
|
|
107
|
-
if (isPlural(str)) {
|
|
108
|
-
return str;
|
|
109
|
-
}
|
|
110
|
-
if (str.endsWith("y") && !["ay", "ey", "iy", "oy", "uy"].some((v) => str.endsWith(v))) {
|
|
111
|
-
return `${str.slice(0, -1)}ies`;
|
|
112
|
-
}
|
|
113
|
-
if (str.endsWith("s") || str.endsWith("x") || str.endsWith("ch") || str.endsWith("sh")) {
|
|
114
|
-
return `${str}es`;
|
|
115
|
-
}
|
|
116
|
-
if (str.endsWith("f")) {
|
|
117
|
-
return `${str.slice(0, -1)}ves`;
|
|
118
|
-
}
|
|
119
|
-
if (str.endsWith("fe")) {
|
|
120
|
-
return `${str.slice(0, -2)}ves`;
|
|
121
|
-
}
|
|
122
|
-
return `${str}s`;
|
|
123
|
-
}
|
|
124
|
-
function singularize(str) {
|
|
125
|
-
if (!isPlural(str)) {
|
|
126
|
-
return str;
|
|
127
|
-
}
|
|
128
|
-
if (str.endsWith("ies")) {
|
|
129
|
-
return `${str.slice(0, -3)}y`;
|
|
130
|
-
}
|
|
131
|
-
if (str.endsWith("ves")) {
|
|
132
|
-
return `${str.slice(0, -3)}f`;
|
|
133
|
-
}
|
|
134
|
-
if (str.endsWith("sses") || str.endsWith("xes") || str.endsWith("ches") || str.endsWith("shes") || str.endsWith("zes")) {
|
|
135
|
-
return str.slice(0, -2);
|
|
136
|
-
}
|
|
137
|
-
if (str.endsWith("s") && !str.endsWith("ss")) {
|
|
138
|
-
return str.slice(0, -1);
|
|
139
|
-
}
|
|
140
|
-
return str;
|
|
141
|
-
}
|
|
142
|
-
function quotePropertyName(name) {
|
|
143
|
-
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
144
|
-
if (validIdentifier.test(name)) {
|
|
145
|
-
return name;
|
|
146
|
-
}
|
|
147
|
-
return `'${name}'`;
|
|
148
|
-
}
|
|
149
|
-
function singularizeLabel(label) {
|
|
150
|
-
const words = label.split(" ");
|
|
151
|
-
if (words.length === 0) {
|
|
152
|
-
return label;
|
|
153
|
-
}
|
|
154
|
-
const lastWord = words[words.length - 1];
|
|
155
|
-
const lastWordLower = lastWord.toLowerCase();
|
|
156
|
-
const singularLastWordLower = singularize(lastWordLower);
|
|
157
|
-
const capitalizedSingular = lastWord[0] === lastWord[0].toUpperCase() ? singularLastWordLower.charAt(0).toUpperCase() + singularLastWordLower.slice(1) : singularLastWordLower;
|
|
158
|
-
return [...words.slice(0, -1), capitalizedSingular].join(" ");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export {
|
|
162
|
-
initPaths,
|
|
163
|
-
getPaths,
|
|
164
|
-
getProjectRoot,
|
|
165
|
-
initImportResolver,
|
|
166
|
-
getImportResolver,
|
|
167
|
-
toPascalCase,
|
|
168
|
-
toCamelCase,
|
|
169
|
-
toKebabCase,
|
|
170
|
-
toSnakeCase,
|
|
171
|
-
toScreamingSnakeCase,
|
|
172
|
-
ensureDir,
|
|
173
|
-
getMigrationTimestamp,
|
|
174
|
-
pluralize,
|
|
175
|
-
singularize,
|
|
176
|
-
quotePropertyName,
|
|
177
|
-
singularizeLabel
|
|
178
|
-
};
|
|
179
|
-
//# sourceMappingURL=chunk-G4KI4DVB.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils.ts"],"sourcesContent":["// Utility functions for the codemod system\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { ImportResolver } from './config/import-resolver'\nimport { MONOREPO_IMPORT_PATHS } from './config/presets'\nimport type { BetterstartConfig } from './config/types'\nimport type { MonorepoPaths } from './types'\n\n// Store monorepo paths globally (set during CLI initialization)\nlet _paths: MonorepoPaths | null = null\n\n/**\n * Initialize paths for the monorepo\n * Detects monorepo root and sets up all package paths\n */\nexport function initPaths(startDir?: string): MonorepoPaths {\n const cwd = startDir || process.cwd()\n\n // Find monorepo root by looking for turbo.json or pnpm-workspace.yaml\n let root = cwd\n while (root !== '/') {\n if (\n fs.existsSync(path.join(root, 'turbo.json')) ||\n fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))\n ) {\n break\n }\n root = path.dirname(root)\n }\n\n if (root === '/') {\n throw new Error('Could not find monorepo root (no turbo.json or pnpm-workspace.yaml found)')\n }\n\n _paths = {\n root,\n app: path.join(root, 'apps/web'),\n database: path.join(root, 'packages/database'),\n lib: path.join(root, 'packages/lib'),\n hooks: path.join(root, 'packages/hooks'),\n schemas: path.join(root, 'packages/codegen/src/schemas'),\n data: path.join(root, 'packages/data')\n }\n\n return _paths\n}\n\n/**\n * Get monorepo paths (must call initPaths first)\n */\nexport function getPaths(): MonorepoPaths {\n if (!_paths) {\n return initPaths()\n }\n return _paths\n}\n\n/**\n * Get the monorepo root directory\n * @deprecated Use getPaths().root instead\n */\nexport function getProjectRoot(): string {\n return getPaths().app\n}\n\n// Store import resolver globally (set during CLI initialization)\nlet _importResolver: ImportResolver | null = null\n\n/**\n * Initialize the import resolver from config\n */\nexport function initImportResolver(config: BetterstartConfig): ImportResolver {\n _importResolver = new ImportResolver(config.imports)\n return _importResolver\n}\n\n/**\n * Get the import resolver (must call initImportResolver first, or falls back to monorepo defaults)\n */\nexport function getImportResolver(): ImportResolver {\n if (!_importResolver) {\n _importResolver = new ImportResolver(MONOREPO_IMPORT_PATHS)\n }\n return _importResolver\n}\n\n/**\n * Convert a string to PascalCase\n */\nexport function toPascalCase(str: string): string {\n return str\n .split(/[-_\\s]+/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join('')\n}\n\n/**\n * Convert a string to camelCase\n */\nexport function toCamelCase(str: string): string {\n const pascal = toPascalCase(str)\n return pascal.charAt(0).toLowerCase() + pascal.slice(1)\n}\n\n/**\n * Convert a string to kebab-case\n */\nexport function toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/[\\s_]+/g, '-')\n .toLowerCase()\n}\n\n/**\n * Convert a string to snake_case\n */\nexport function toSnakeCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .replace(/[\\s-]+/g, '_')\n .toLowerCase()\n}\n\n/**\n * Convert a string to SCREAMING_SNAKE_CASE\n */\nexport function toScreamingSnakeCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .replace(/[\\s-]+/g, '_')\n .toUpperCase()\n}\n\n/**\n * Ensure a directory exists, create it if it doesn't\n */\nexport function ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true })\n }\n}\n\n/**\n * Format a timestamp for migration files\n */\nexport function getMigrationTimestamp(): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n const hours = String(now.getHours()).padStart(2, '0')\n const minutes = String(now.getMinutes()).padStart(2, '0')\n const seconds = String(now.getSeconds()).padStart(2, '0')\n return `${year}${month}${day}${hours}${minutes}${seconds}`\n}\n\n/**\n * Check if a word is already plural\n */\nfunction isPlural(str: string): boolean {\n const pluralPatterns = [\n /ies$/,\n /ves$/,\n /ses$/,\n /xes$/,\n /zes$/,\n /ches$/,\n /shes$/,\n /men$/,\n /people$/\n ]\n\n for (const pattern of pluralPatterns) {\n if (pattern.test(str)) {\n return true\n }\n }\n\n if (str.endsWith('s') && !str.endsWith('ss')) {\n return true\n }\n\n return false\n}\n\n/**\n * Pluralize a string\n */\nexport function pluralize(str: string): string {\n if (isPlural(str)) {\n return str\n }\n\n if (str.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].some((v) => str.endsWith(v))) {\n return `${str.slice(0, -1)}ies`\n }\n if (str.endsWith('s') || str.endsWith('x') || str.endsWith('ch') || str.endsWith('sh')) {\n return `${str}es`\n }\n if (str.endsWith('f')) {\n return `${str.slice(0, -1)}ves`\n }\n if (str.endsWith('fe')) {\n return `${str.slice(0, -2)}ves`\n }\n return `${str}s`\n}\n\n/**\n * Singularize a string\n */\nexport function singularize(str: string): string {\n if (!isPlural(str)) {\n return str\n }\n\n if (str.endsWith('ies')) {\n return `${str.slice(0, -3)}y`\n }\n if (str.endsWith('ves')) {\n return `${str.slice(0, -3)}f`\n }\n if (\n str.endsWith('sses') ||\n str.endsWith('xes') ||\n str.endsWith('ches') ||\n str.endsWith('shes') ||\n str.endsWith('zes')\n ) {\n return str.slice(0, -2)\n }\n if (str.endsWith('s') && !str.endsWith('ss')) {\n return str.slice(0, -1)\n }\n return str\n}\n\n/**\n * Quote a property name if it's not a valid JavaScript identifier.\n * Properties with hyphens, spaces, or starting with numbers need quotes.\n */\nexport function quotePropertyName(name: string): string {\n // Valid JS identifier: starts with letter/underscore/$, contains only letters/numbers/underscore/$\n const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/\n if (validIdentifier.test(name)) {\n return name\n }\n return `'${name}'`\n}\n\n/**\n * Singularize a label (e.g., \"Trusted Companies\" -> \"Trusted Company\")\n * Handles display labels with proper capitalization\n */\nexport function singularizeLabel(label: string): string {\n const words = label.split(' ')\n if (words.length === 0) {\n return label\n }\n\n // Singularize the last word\n const lastWord = words[words.length - 1]\n const lastWordLower = lastWord.toLowerCase()\n const singularLastWordLower = singularize(lastWordLower)\n\n // Preserve capitalization of the last word\n const capitalizedSingular =\n lastWord[0] === lastWord[0].toUpperCase()\n ? singularLastWordLower.charAt(0).toUpperCase() + singularLastWordLower.slice(1)\n : singularLastWordLower\n\n return [...words.slice(0, -1), capitalizedSingular].join(' ')\n}\n"],"mappings":"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","names":[]}
|