create-nextjs-cms 0.7.0 → 0.7.2
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/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/helpers/utils.js +16 -16
- package/dist/lib/section-creators.js +166 -166
- package/package.json +3 -3
- package/templates/default/.eslintrc.json +5 -5
- package/templates/default/.prettierignore +7 -7
- package/templates/default/.prettierrc.json +27 -27
- package/templates/default/CHANGELOG.md +140 -140
- package/templates/default/_gitignore +57 -57
- package/templates/default/app/(auth)/auth/login/LoginPage.tsx +192 -192
- package/templates/default/app/(auth)/auth/login/page.tsx +11 -11
- package/templates/default/app/(auth)/auth-locale-provider.tsx +34 -34
- package/templates/default/app/(auth)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/page.tsx +40 -40
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/plugin-server-registry.ts +22 -22
- package/templates/default/app/(rootLayout)/admins/page.tsx +10 -10
- package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +22 -22
- package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/dashboard/page.tsx +63 -63
- package/templates/default/app/(rootLayout)/dashboard-new/page.tsx +7 -7
- package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +17 -17
- package/templates/default/app/(rootLayout)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/loading.tsx +10 -10
- package/templates/default/app/(rootLayout)/log/page.tsx +7 -7
- package/templates/default/app/(rootLayout)/new/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/section/[section]/page.tsx +16 -16
- package/templates/default/app/(rootLayout)/settings/page.tsx +13 -13
- package/templates/default/app/_trpc/client.ts +3 -3
- package/templates/default/app/api/auth/csrf/route.ts +25 -25
- package/templates/default/app/api/auth/refresh/route.ts +10 -10
- package/templates/default/app/api/auth/session/route.ts +20 -20
- package/templates/default/app/api/editor/photo/route.ts +49 -49
- package/templates/default/app/api/photo/route.ts +27 -27
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +66 -66
- package/templates/default/app/api/submit/section/item/route.ts +56 -56
- package/templates/default/app/api/submit/section/simple/route.ts +57 -57
- package/templates/default/app/api/trpc/[trpc]/route.ts +33 -33
- package/templates/default/app/api/video/route.ts +174 -174
- package/templates/default/app/globals.css +219 -219
- package/templates/default/app/providers.tsx +152 -152
- package/templates/default/cms.config.ts +49 -52
- package/templates/default/components/AdminCard.tsx +166 -166
- package/templates/default/components/AdminEditPage.tsx +124 -124
- package/templates/default/components/AdminPrivilegeCard.tsx +185 -185
- package/templates/default/components/AdminsPage.tsx +43 -43
- package/templates/default/components/AnalyticsPage.tsx +128 -128
- package/templates/default/components/BarChartBox.tsx +42 -42
- package/templates/default/components/BrowsePage.tsx +106 -106
- package/templates/default/components/CategorizedSectionPage.tsx +31 -31
- package/templates/default/components/CategoryDeleteConfirmPage.tsx +130 -130
- package/templates/default/components/CategorySectionSelectInput.tsx +140 -140
- package/templates/default/components/ConditionalFields.tsx +49 -49
- package/templates/default/components/ContainerBox.tsx +24 -24
- package/templates/default/components/DashboardNewPage.tsx +253 -253
- package/templates/default/components/DashboardPage.tsx +188 -188
- package/templates/default/components/DashboardPageAlt.tsx +45 -45
- package/templates/default/components/DefaultNavItems.tsx +3 -3
- package/templates/default/components/Dropzone.tsx +154 -154
- package/templates/default/components/EmailCard.tsx +138 -138
- package/templates/default/components/EmailPasswordForm.tsx +85 -85
- package/templates/default/components/EmailQuotaForm.tsx +73 -73
- package/templates/default/components/EmailsPage.tsx +49 -49
- package/templates/default/components/ErrorComponent.tsx +16 -16
- package/templates/default/components/GalleryPhoto.tsx +93 -93
- package/templates/default/components/InfoCard.tsx +93 -93
- package/templates/default/components/ItemEditPage.tsx +214 -214
- package/templates/default/components/Layout.tsx +84 -84
- package/templates/default/components/LoadingSpinners.tsx +67 -67
- package/templates/default/components/LogPage.tsx +107 -107
- package/templates/default/components/Modal.tsx +166 -166
- package/templates/default/components/Navbar.tsx +258 -258
- package/templates/default/components/NewAdminForm.tsx +173 -173
- package/templates/default/components/NewEmailForm.tsx +132 -132
- package/templates/default/components/NewPage.tsx +205 -205
- package/templates/default/components/NewVariantComponent.tsx +229 -229
- package/templates/default/components/PhotoGallery.tsx +35 -35
- package/templates/default/components/PieChartBox.tsx +101 -101
- package/templates/default/components/ProgressBar.tsx +48 -48
- package/templates/default/components/ProtectedDocument.tsx +78 -78
- package/templates/default/components/ProtectedImage.tsx +143 -143
- package/templates/default/components/ProtectedVideo.tsx +76 -76
- package/templates/default/components/SectionItemCard.tsx +144 -144
- package/templates/default/components/SectionItemStatusBadge.tsx +17 -17
- package/templates/default/components/SectionPage.tsx +125 -125
- package/templates/default/components/SelectBox.tsx +98 -98
- package/templates/default/components/SelectInputButtons.tsx +125 -125
- package/templates/default/components/SettingsPage.tsx +232 -232
- package/templates/default/components/Sidebar.tsx +201 -201
- package/templates/default/components/SidebarDropdownItem.tsx +80 -80
- package/templates/default/components/SidebarItem.tsx +20 -20
- package/templates/default/components/ThemeProvider.tsx +8 -8
- package/templates/default/components/TooltipComponent.tsx +27 -27
- package/templates/default/components/VariantCard.tsx +124 -124
- package/templates/default/components/VariantEditPage.tsx +230 -230
- package/templates/default/components/analytics/BounceRate.tsx +70 -70
- package/templates/default/components/analytics/LivePageViews.tsx +55 -55
- package/templates/default/components/analytics/LiveUsersCount.tsx +33 -33
- package/templates/default/components/analytics/MonthlyPageViews.tsx +42 -42
- package/templates/default/components/analytics/TopCountries.tsx +52 -52
- package/templates/default/components/analytics/TopDevices.tsx +46 -46
- package/templates/default/components/analytics/TopMediums.tsx +58 -58
- package/templates/default/components/analytics/TopSources.tsx +45 -45
- package/templates/default/components/analytics/TotalPageViews.tsx +41 -41
- package/templates/default/components/analytics/TotalSessions.tsx +41 -41
- package/templates/default/components/analytics/TotalUniqueUsers.tsx +41 -41
- package/templates/default/components/custom/RightHomeRoomVariantCard.tsx +138 -138
- package/templates/default/components/dndKit/Draggable.tsx +21 -21
- package/templates/default/components/dndKit/Droppable.tsx +20 -20
- package/templates/default/components/dndKit/SortableItem.tsx +18 -18
- package/templates/default/components/form/DateRangeFormInput.tsx +57 -57
- package/templates/default/components/form/Form.tsx +317 -317
- package/templates/default/components/form/FormInputElement.tsx +70 -70
- package/templates/default/components/form/FormInputs.tsx +112 -112
- package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
- package/templates/default/components/form/helpers/util.ts +17 -17
- package/templates/default/components/form/inputs/CheckboxFormInput.tsx +33 -33
- package/templates/default/components/form/inputs/ColorFormInput.tsx +44 -44
- package/templates/default/components/form/inputs/DateFormInput.tsx +156 -156
- package/templates/default/components/form/inputs/DocumentFormInput.tsx +222 -222
- package/templates/default/components/form/inputs/MapFormInput.tsx +140 -140
- package/templates/default/components/form/inputs/MultipleSelectFormInput.tsx +83 -83
- package/templates/default/components/form/inputs/NumberFormInput.tsx +42 -42
- package/templates/default/components/form/inputs/PasswordFormInput.tsx +47 -47
- package/templates/default/components/form/inputs/PhotoFormInput.tsx +219 -219
- package/templates/default/components/form/inputs/RichTextFormInput.tsx +135 -135
- package/templates/default/components/form/inputs/SelectFormInput.tsx +175 -175
- package/templates/default/components/form/inputs/SlugFormInput.tsx +129 -129
- package/templates/default/components/form/inputs/TagsFormInput.tsx +154 -154
- package/templates/default/components/form/inputs/TextFormInput.tsx +48 -48
- package/templates/default/components/form/inputs/TextareaFormInput.tsx +47 -47
- package/templates/default/components/form/inputs/VideoFormInput.tsx +118 -118
- package/templates/default/components/locale-dropdown.tsx +74 -74
- package/templates/default/components/locale-picker.tsx +85 -85
- package/templates/default/components/login-locale-dropdown.tsx +46 -46
- package/templates/default/components/multi-select.tsx +1144 -1144
- package/templates/default/components/pagination/Pagination.tsx +36 -36
- package/templates/default/components/pagination/PaginationButtons.tsx +147 -147
- package/templates/default/components/theme-toggle.tsx +37 -37
- package/templates/default/components/ui/accordion.tsx +53 -53
- package/templates/default/components/ui/alert-dialog.tsx +157 -157
- package/templates/default/components/ui/alert.tsx +46 -46
- package/templates/default/components/ui/badge.tsx +38 -38
- package/templates/default/components/ui/button.tsx +62 -62
- package/templates/default/components/ui/calendar.tsx +166 -166
- package/templates/default/components/ui/card.tsx +43 -43
- package/templates/default/components/ui/checkbox.tsx +29 -29
- package/templates/default/components/ui/command.tsx +137 -137
- package/templates/default/components/ui/custom-alert-dialog.tsx +113 -113
- package/templates/default/components/ui/custom-dialog.tsx +123 -123
- package/templates/default/components/ui/dialog.tsx +123 -123
- package/templates/default/components/ui/dropdown-menu.tsx +182 -182
- package/templates/default/components/ui/input-group.tsx +54 -54
- package/templates/default/components/ui/input.tsx +22 -22
- package/templates/default/components/ui/label.tsx +19 -19
- package/templates/default/components/ui/popover.tsx +42 -42
- package/templates/default/components/ui/progress.tsx +31 -31
- package/templates/default/components/ui/scroll-area.tsx +42 -42
- package/templates/default/components/ui/select.tsx +165 -165
- package/templates/default/components/ui/separator.tsx +28 -28
- package/templates/default/components/ui/sheet.tsx +103 -103
- package/templates/default/components/ui/switch.tsx +29 -29
- package/templates/default/components/ui/table.tsx +83 -83
- package/templates/default/components/ui/tabs.tsx +55 -55
- package/templates/default/components/ui/toast.tsx +113 -113
- package/templates/default/components/ui/toaster.tsx +35 -35
- package/templates/default/components/ui/tooltip.tsx +30 -30
- package/templates/default/components/ui/use-toast.ts +188 -188
- package/templates/default/components.json +21 -21
- package/templates/default/context/ModalProvider.tsx +53 -53
- package/templates/default/drizzle.config.ts +4 -4
- package/templates/default/dynamic-schemas/schema.ts +10 -0
- package/templates/default/env/env.js +130 -130
- package/templates/default/envConfig.ts +4 -4
- package/templates/default/hooks/useModal.ts +8 -8
- package/templates/default/lib/apiHelpers.ts +92 -92
- package/templates/default/lib/postinstall.js +14 -14
- package/templates/default/lib/utils.ts +6 -6
- package/templates/default/next-env.d.ts +6 -6
- package/templates/default/next.config.ts +23 -23
- package/templates/default/package.json +2 -4
- package/templates/default/postcss.config.mjs +6 -6
- package/templates/default/proxy.ts +32 -32
- package/templates/default/tsconfig.json +48 -48
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useEffect, useRef } from 'react'
|
|
4
|
-
import FormInputElement from '@/components/form/FormInputElement'
|
|
5
|
-
import type { SlugFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
6
|
-
import { useFormContext, useController, useWatch } from 'react-hook-form'
|
|
7
|
-
import { InputGroup, InputGroupInput, InputGroupAddon } from '@/components/ui/input-group'
|
|
8
|
-
import { Link2 } from 'lucide-react'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Convert a string to a URL-friendly slug (for real-time input).
|
|
12
|
-
* - Converts to lowercase
|
|
13
|
-
* - Replaces spaces with hyphens
|
|
14
|
-
* - Removes special characters (keeps letters from any language, numbers, and hyphens)
|
|
15
|
-
* - Collapses multiple consecutive hyphens into one
|
|
16
|
-
* - Keeps trailing hyphen (user might still be typing)
|
|
17
|
-
*/
|
|
18
|
-
function toSlugLive(value: string): string {
|
|
19
|
-
return value
|
|
20
|
-
.toLowerCase()
|
|
21
|
-
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
22
|
-
.replace(/[^\p{L}\p{N}-]/gu, '') // Remove special characters (keep Unicode letters/numbers)
|
|
23
|
-
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
24
|
-
.replace(/^-/, '') // Remove leading hyphen only
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Normalize a slug (for blur/final value).
|
|
29
|
-
* Same as toSlugLive but also trims and removes trailing hyphens.
|
|
30
|
-
*/
|
|
31
|
-
function toSlugFinal(value: string): string {
|
|
32
|
-
return toSlugLive(value.trim()).replace(/-$/, '') // Remove trailing hyphen
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default function SlugFormInput({
|
|
36
|
-
input,
|
|
37
|
-
direction,
|
|
38
|
-
disabled = false,
|
|
39
|
-
}: {
|
|
40
|
-
input: SlugFieldClientConfig
|
|
41
|
-
direction?: 'row' | 'col'
|
|
42
|
-
disabled?: boolean
|
|
43
|
-
}) {
|
|
44
|
-
const { control } = useFormContext()
|
|
45
|
-
const {
|
|
46
|
-
field,
|
|
47
|
-
fieldState: { error },
|
|
48
|
-
} = useController({
|
|
49
|
-
name: input.name,
|
|
50
|
-
control,
|
|
51
|
-
defaultValue: input.value ?? '',
|
|
52
|
-
disabled: disabled,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// Watch the source field value
|
|
56
|
-
const sourceFieldValue = useWatch({
|
|
57
|
-
control,
|
|
58
|
-
name: input.forFieldName,
|
|
59
|
-
defaultValue: '',
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// Track previous source value to only update when it actually changes
|
|
63
|
-
const previousSourceValueRef = useRef<string>(sourceFieldValue ?? '')
|
|
64
|
-
|
|
65
|
-
// Auto-generate slug from source field when it changes
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
const currentSourceValue = sourceFieldValue ?? ''
|
|
68
|
-
|
|
69
|
-
// Only update if the source field value actually changed
|
|
70
|
-
if (currentSourceValue !== previousSourceValueRef.current) {
|
|
71
|
-
previousSourceValueRef.current = currentSourceValue
|
|
72
|
-
|
|
73
|
-
if (currentSourceValue) {
|
|
74
|
-
const newSlug = toSlugFinal(currentSourceValue)
|
|
75
|
-
field.onChange(newSlug)
|
|
76
|
-
} else {
|
|
77
|
-
field.onChange('')
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}, [sourceFieldValue, field])
|
|
81
|
-
|
|
82
|
-
// Handle manual changes to the slug field
|
|
83
|
-
const handleChange = useCallback(
|
|
84
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
85
|
-
const newValue = e.target.value
|
|
86
|
-
// Apply slug normalization in real-time
|
|
87
|
-
field.onChange(toSlugLive(newValue))
|
|
88
|
-
},
|
|
89
|
-
[field],
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
// Handle blur - finalize the slug value (remove trailing hyphens)
|
|
93
|
-
const handleBlur = useCallback(() => {
|
|
94
|
-
field.onBlur()
|
|
95
|
-
if (field.value) {
|
|
96
|
-
const finalSlug = toSlugFinal(field.value)
|
|
97
|
-
if (finalSlug !== field.value) {
|
|
98
|
-
field.onChange(finalSlug)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}, [field])
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<FormInputElement
|
|
105
|
-
validationError={error}
|
|
106
|
-
value={input.value}
|
|
107
|
-
readonly={input.readonly}
|
|
108
|
-
label={input.label}
|
|
109
|
-
required={input.required}
|
|
110
|
-
>
|
|
111
|
-
<InputGroup className='bg-input'>
|
|
112
|
-
<InputGroupAddon align='inline-start' title='Auto-generated from linked field'>
|
|
113
|
-
<Link2 className='h-4 w-4' />
|
|
114
|
-
</InputGroupAddon>
|
|
115
|
-
<InputGroupInput
|
|
116
|
-
placeholder={input.placeholder ? input.placeholder : input.label}
|
|
117
|
-
type='text'
|
|
118
|
-
readOnly={disabled}
|
|
119
|
-
disabled={field.disabled}
|
|
120
|
-
name={field.name}
|
|
121
|
-
onChange={handleChange}
|
|
122
|
-
onBlur={handleBlur}
|
|
123
|
-
value={field.value ?? ''}
|
|
124
|
-
ref={field.ref}
|
|
125
|
-
/>
|
|
126
|
-
</InputGroup>
|
|
127
|
-
</FormInputElement>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
4
|
+
import FormInputElement from '@/components/form/FormInputElement'
|
|
5
|
+
import type { SlugFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
6
|
+
import { useFormContext, useController, useWatch } from 'react-hook-form'
|
|
7
|
+
import { InputGroup, InputGroupInput, InputGroupAddon } from '@/components/ui/input-group'
|
|
8
|
+
import { Link2 } from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert a string to a URL-friendly slug (for real-time input).
|
|
12
|
+
* - Converts to lowercase
|
|
13
|
+
* - Replaces spaces with hyphens
|
|
14
|
+
* - Removes special characters (keeps letters from any language, numbers, and hyphens)
|
|
15
|
+
* - Collapses multiple consecutive hyphens into one
|
|
16
|
+
* - Keeps trailing hyphen (user might still be typing)
|
|
17
|
+
*/
|
|
18
|
+
function toSlugLive(value: string): string {
|
|
19
|
+
return value
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
22
|
+
.replace(/[^\p{L}\p{N}-]/gu, '') // Remove special characters (keep Unicode letters/numbers)
|
|
23
|
+
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
24
|
+
.replace(/^-/, '') // Remove leading hyphen only
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a slug (for blur/final value).
|
|
29
|
+
* Same as toSlugLive but also trims and removes trailing hyphens.
|
|
30
|
+
*/
|
|
31
|
+
function toSlugFinal(value: string): string {
|
|
32
|
+
return toSlugLive(value.trim()).replace(/-$/, '') // Remove trailing hyphen
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function SlugFormInput({
|
|
36
|
+
input,
|
|
37
|
+
direction,
|
|
38
|
+
disabled = false,
|
|
39
|
+
}: {
|
|
40
|
+
input: SlugFieldClientConfig
|
|
41
|
+
direction?: 'row' | 'col'
|
|
42
|
+
disabled?: boolean
|
|
43
|
+
}) {
|
|
44
|
+
const { control } = useFormContext()
|
|
45
|
+
const {
|
|
46
|
+
field,
|
|
47
|
+
fieldState: { error },
|
|
48
|
+
} = useController({
|
|
49
|
+
name: input.name,
|
|
50
|
+
control,
|
|
51
|
+
defaultValue: input.value ?? '',
|
|
52
|
+
disabled: disabled,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Watch the source field value
|
|
56
|
+
const sourceFieldValue = useWatch({
|
|
57
|
+
control,
|
|
58
|
+
name: input.forFieldName,
|
|
59
|
+
defaultValue: '',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Track previous source value to only update when it actually changes
|
|
63
|
+
const previousSourceValueRef = useRef<string>(sourceFieldValue ?? '')
|
|
64
|
+
|
|
65
|
+
// Auto-generate slug from source field when it changes
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const currentSourceValue = sourceFieldValue ?? ''
|
|
68
|
+
|
|
69
|
+
// Only update if the source field value actually changed
|
|
70
|
+
if (currentSourceValue !== previousSourceValueRef.current) {
|
|
71
|
+
previousSourceValueRef.current = currentSourceValue
|
|
72
|
+
|
|
73
|
+
if (currentSourceValue) {
|
|
74
|
+
const newSlug = toSlugFinal(currentSourceValue)
|
|
75
|
+
field.onChange(newSlug)
|
|
76
|
+
} else {
|
|
77
|
+
field.onChange('')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, [sourceFieldValue, field])
|
|
81
|
+
|
|
82
|
+
// Handle manual changes to the slug field
|
|
83
|
+
const handleChange = useCallback(
|
|
84
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
85
|
+
const newValue = e.target.value
|
|
86
|
+
// Apply slug normalization in real-time
|
|
87
|
+
field.onChange(toSlugLive(newValue))
|
|
88
|
+
},
|
|
89
|
+
[field],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Handle blur - finalize the slug value (remove trailing hyphens)
|
|
93
|
+
const handleBlur = useCallback(() => {
|
|
94
|
+
field.onBlur()
|
|
95
|
+
if (field.value) {
|
|
96
|
+
const finalSlug = toSlugFinal(field.value)
|
|
97
|
+
if (finalSlug !== field.value) {
|
|
98
|
+
field.onChange(finalSlug)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [field])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<FormInputElement
|
|
105
|
+
validationError={error}
|
|
106
|
+
value={input.value}
|
|
107
|
+
readonly={input.readonly}
|
|
108
|
+
label={input.label}
|
|
109
|
+
required={input.required}
|
|
110
|
+
>
|
|
111
|
+
<InputGroup className='bg-input'>
|
|
112
|
+
<InputGroupAddon align='inline-start' title='Auto-generated from linked field'>
|
|
113
|
+
<Link2 className='h-4 w-4' />
|
|
114
|
+
</InputGroupAddon>
|
|
115
|
+
<InputGroupInput
|
|
116
|
+
placeholder={input.placeholder ? input.placeholder : input.label}
|
|
117
|
+
type='text'
|
|
118
|
+
readOnly={disabled}
|
|
119
|
+
disabled={field.disabled}
|
|
120
|
+
name={field.name}
|
|
121
|
+
onChange={handleChange}
|
|
122
|
+
onBlur={handleBlur}
|
|
123
|
+
value={field.value ?? ''}
|
|
124
|
+
ref={field.ref}
|
|
125
|
+
/>
|
|
126
|
+
</InputGroup>
|
|
127
|
+
</FormInputElement>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
import FormInputElement from '@/components/form/FormInputElement'
|
|
2
|
-
import { useI18n } from 'nextjs-cms/translations/client'
|
|
3
|
-
import { TagsFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
4
|
-
import { useController, useFormContext } from 'react-hook-form'
|
|
5
|
-
import { Badge } from '@/components/ui/badge'
|
|
6
|
-
import { Input } from '@/components/ui/input'
|
|
7
|
-
import { X } from 'lucide-react'
|
|
8
|
-
import { useState, useRef } from 'react'
|
|
9
|
-
import { cn } from '@/lib/utils'
|
|
10
|
-
|
|
11
|
-
export default function TagsFormInput({ input }: { input: TagsFieldClientConfig }) {
|
|
12
|
-
const t = useI18n()
|
|
13
|
-
const { control } = useFormContext()
|
|
14
|
-
const {
|
|
15
|
-
field,
|
|
16
|
-
fieldState: { invalid, isTouched, isDirty, error },
|
|
17
|
-
} = useController({
|
|
18
|
-
name: input.name,
|
|
19
|
-
control,
|
|
20
|
-
defaultValue: input.value ?? undefined,
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
const [inputValue, setInputValue] = useState('')
|
|
24
|
-
const [highlightedTag, setHighlightedTag] = useState<string | null>(null)
|
|
25
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
26
|
-
|
|
27
|
-
// Parse tags from comma-separated string
|
|
28
|
-
const getTags = (): string[] => {
|
|
29
|
-
if (!field.value || typeof field.value !== 'string') return []
|
|
30
|
-
return field.value.split(',').filter((tag) => tag.trim() !== '')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Convert tags array to comma-separated string
|
|
34
|
-
const setTags = (tags: string[]) => {
|
|
35
|
-
const filteredTags = tags.filter((tag) => tag.trim() !== '')
|
|
36
|
-
const value = filteredTags.length > 0 ? filteredTags.join(',') : ''
|
|
37
|
-
field.onChange(value)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const addTag = (tag: string) => {
|
|
41
|
-
const trimmedTag = tag.trim()
|
|
42
|
-
if (trimmedTag) {
|
|
43
|
-
const existingTags = getTags()
|
|
44
|
-
if (existingTags.includes(trimmedTag)) {
|
|
45
|
-
// Highlight the existing tag to show it already exists
|
|
46
|
-
setHighlightedTag(trimmedTag)
|
|
47
|
-
setTimeout(() => setHighlightedTag(null), 1000)
|
|
48
|
-
} else {
|
|
49
|
-
const newTags = [...existingTags, trimmedTag]
|
|
50
|
-
setTags(newTags)
|
|
51
|
-
setInputValue('')
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const removeTag = (tagToRemove: string) => {
|
|
57
|
-
const newTags = getTags().filter((tag) => tag !== tagToRemove)
|
|
58
|
-
setTags(newTags)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
62
|
-
if (event.key === 'Enter' || event.key === ',') {
|
|
63
|
-
event.preventDefault()
|
|
64
|
-
if (inputValue.trim()) {
|
|
65
|
-
addTag(inputValue)
|
|
66
|
-
}
|
|
67
|
-
} else if (event.key === 'Backspace' && !inputValue && getTags().length > 0) {
|
|
68
|
-
// Remove last tag when backspace is pressed on empty input
|
|
69
|
-
const lastTag = getTags()[getTags().length - 1]
|
|
70
|
-
if (lastTag) {
|
|
71
|
-
removeTag(lastTag)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
77
|
-
setInputValue(event.target.value)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const handleContainerClick = () => {
|
|
81
|
-
inputRef.current?.focus()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const tags = getTags()
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<FormInputElement
|
|
88
|
-
validationError={error}
|
|
89
|
-
value={field.value}
|
|
90
|
-
readonly={input.readonly}
|
|
91
|
-
label={input.label}
|
|
92
|
-
required={input.required}
|
|
93
|
-
>
|
|
94
|
-
<input
|
|
95
|
-
type='hidden'
|
|
96
|
-
className='hidden'
|
|
97
|
-
disabled={field.disabled}
|
|
98
|
-
name={field.name}
|
|
99
|
-
onBlur={field.onBlur}
|
|
100
|
-
value={field.value ?? ''}
|
|
101
|
-
ref={field.ref}
|
|
102
|
-
/>
|
|
103
|
-
|
|
104
|
-
<div
|
|
105
|
-
className={cn(
|
|
106
|
-
'bg-input text-foreground w-full rounded px-3 py-1 shadow-xs ring-2 ring-gray-300 outline-0 transition-colors hover:ring-gray-400 focus-within:ring-blue-500',
|
|
107
|
-
'flex min-h-9 flex-wrap items-center gap-1 text-sm',
|
|
108
|
-
'cursor-text',
|
|
109
|
-
field.disabled && 'cursor-not-allowed opacity-50',
|
|
110
|
-
)}
|
|
111
|
-
onClick={handleContainerClick}
|
|
112
|
-
>
|
|
113
|
-
{tags.map((tag, index) => (
|
|
114
|
-
<Badge
|
|
115
|
-
key={`${tag}-${index}`}
|
|
116
|
-
variant='secondary'
|
|
117
|
-
className={cn(
|
|
118
|
-
'flex items-center gap-1 px-2 py-1 text-xs transition-all duration-200 border-foreground/40',
|
|
119
|
-
highlightedTag === tag ? 'bg-success/20 border-success/50 animate-pulse' : '',
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
<span>{tag}</span>
|
|
123
|
-
{!field.disabled ? (
|
|
124
|
-
<button
|
|
125
|
-
type='button'
|
|
126
|
-
onClick={(e) => {
|
|
127
|
-
e.stopPropagation()
|
|
128
|
-
removeTag(tag)
|
|
129
|
-
}}
|
|
130
|
-
className='hover:bg-muted-foreground/20 focus:ring-ring ml-1 rounded-sm focus:ring-1 focus:outline-none'
|
|
131
|
-
aria-label={`Remove ${tag} tag`}
|
|
132
|
-
>
|
|
133
|
-
<X className='h-3 w-3' />
|
|
134
|
-
</button>
|
|
135
|
-
) : null}
|
|
136
|
-
</Badge>
|
|
137
|
-
))}
|
|
138
|
-
|
|
139
|
-
<Input
|
|
140
|
-
ref={inputRef}
|
|
141
|
-
type='text'
|
|
142
|
-
value={inputValue}
|
|
143
|
-
onChange={handleInputChange}
|
|
144
|
-
onKeyDown={handleKeyDown}
|
|
145
|
-
placeholder={
|
|
146
|
-
tags.length === 0 ? (input.placeholder ? input.placeholder : t('startTyping')) : ''
|
|
147
|
-
}
|
|
148
|
-
className='min-w-20 flex-1 border-0 p-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
|
149
|
-
disabled={field.disabled}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
</FormInputElement>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
1
|
+
import FormInputElement from '@/components/form/FormInputElement'
|
|
2
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
3
|
+
import { TagsFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
4
|
+
import { useController, useFormContext } from 'react-hook-form'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { X } from 'lucide-react'
|
|
8
|
+
import { useState, useRef } from 'react'
|
|
9
|
+
import { cn } from '@/lib/utils'
|
|
10
|
+
|
|
11
|
+
export default function TagsFormInput({ input }: { input: TagsFieldClientConfig }) {
|
|
12
|
+
const t = useI18n()
|
|
13
|
+
const { control } = useFormContext()
|
|
14
|
+
const {
|
|
15
|
+
field,
|
|
16
|
+
fieldState: { invalid, isTouched, isDirty, error },
|
|
17
|
+
} = useController({
|
|
18
|
+
name: input.name,
|
|
19
|
+
control,
|
|
20
|
+
defaultValue: input.value ?? undefined,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const [inputValue, setInputValue] = useState('')
|
|
24
|
+
const [highlightedTag, setHighlightedTag] = useState<string | null>(null)
|
|
25
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
26
|
+
|
|
27
|
+
// Parse tags from comma-separated string
|
|
28
|
+
const getTags = (): string[] => {
|
|
29
|
+
if (!field.value || typeof field.value !== 'string') return []
|
|
30
|
+
return field.value.split(',').filter((tag) => tag.trim() !== '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Convert tags array to comma-separated string
|
|
34
|
+
const setTags = (tags: string[]) => {
|
|
35
|
+
const filteredTags = tags.filter((tag) => tag.trim() !== '')
|
|
36
|
+
const value = filteredTags.length > 0 ? filteredTags.join(',') : ''
|
|
37
|
+
field.onChange(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const addTag = (tag: string) => {
|
|
41
|
+
const trimmedTag = tag.trim()
|
|
42
|
+
if (trimmedTag) {
|
|
43
|
+
const existingTags = getTags()
|
|
44
|
+
if (existingTags.includes(trimmedTag)) {
|
|
45
|
+
// Highlight the existing tag to show it already exists
|
|
46
|
+
setHighlightedTag(trimmedTag)
|
|
47
|
+
setTimeout(() => setHighlightedTag(null), 1000)
|
|
48
|
+
} else {
|
|
49
|
+
const newTags = [...existingTags, trimmedTag]
|
|
50
|
+
setTags(newTags)
|
|
51
|
+
setInputValue('')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const removeTag = (tagToRemove: string) => {
|
|
57
|
+
const newTags = getTags().filter((tag) => tag !== tagToRemove)
|
|
58
|
+
setTags(newTags)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
62
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
if (inputValue.trim()) {
|
|
65
|
+
addTag(inputValue)
|
|
66
|
+
}
|
|
67
|
+
} else if (event.key === 'Backspace' && !inputValue && getTags().length > 0) {
|
|
68
|
+
// Remove last tag when backspace is pressed on empty input
|
|
69
|
+
const lastTag = getTags()[getTags().length - 1]
|
|
70
|
+
if (lastTag) {
|
|
71
|
+
removeTag(lastTag)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
77
|
+
setInputValue(event.target.value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleContainerClick = () => {
|
|
81
|
+
inputRef.current?.focus()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tags = getTags()
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<FormInputElement
|
|
88
|
+
validationError={error}
|
|
89
|
+
value={field.value}
|
|
90
|
+
readonly={input.readonly}
|
|
91
|
+
label={input.label}
|
|
92
|
+
required={input.required}
|
|
93
|
+
>
|
|
94
|
+
<input
|
|
95
|
+
type='hidden'
|
|
96
|
+
className='hidden'
|
|
97
|
+
disabled={field.disabled}
|
|
98
|
+
name={field.name}
|
|
99
|
+
onBlur={field.onBlur}
|
|
100
|
+
value={field.value ?? ''}
|
|
101
|
+
ref={field.ref}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
'bg-input text-foreground w-full rounded px-3 py-1 shadow-xs ring-2 ring-gray-300 outline-0 transition-colors hover:ring-gray-400 focus-within:ring-blue-500',
|
|
107
|
+
'flex min-h-9 flex-wrap items-center gap-1 text-sm',
|
|
108
|
+
'cursor-text',
|
|
109
|
+
field.disabled && 'cursor-not-allowed opacity-50',
|
|
110
|
+
)}
|
|
111
|
+
onClick={handleContainerClick}
|
|
112
|
+
>
|
|
113
|
+
{tags.map((tag, index) => (
|
|
114
|
+
<Badge
|
|
115
|
+
key={`${tag}-${index}`}
|
|
116
|
+
variant='secondary'
|
|
117
|
+
className={cn(
|
|
118
|
+
'flex items-center gap-1 px-2 py-1 text-xs transition-all duration-200 border-foreground/40',
|
|
119
|
+
highlightedTag === tag ? 'bg-success/20 border-success/50 animate-pulse' : '',
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
<span>{tag}</span>
|
|
123
|
+
{!field.disabled ? (
|
|
124
|
+
<button
|
|
125
|
+
type='button'
|
|
126
|
+
onClick={(e) => {
|
|
127
|
+
e.stopPropagation()
|
|
128
|
+
removeTag(tag)
|
|
129
|
+
}}
|
|
130
|
+
className='hover:bg-muted-foreground/20 focus:ring-ring ml-1 rounded-sm focus:ring-1 focus:outline-none'
|
|
131
|
+
aria-label={`Remove ${tag} tag`}
|
|
132
|
+
>
|
|
133
|
+
<X className='h-3 w-3' />
|
|
134
|
+
</button>
|
|
135
|
+
) : null}
|
|
136
|
+
</Badge>
|
|
137
|
+
))}
|
|
138
|
+
|
|
139
|
+
<Input
|
|
140
|
+
ref={inputRef}
|
|
141
|
+
type='text'
|
|
142
|
+
value={inputValue}
|
|
143
|
+
onChange={handleInputChange}
|
|
144
|
+
onKeyDown={handleKeyDown}
|
|
145
|
+
placeholder={
|
|
146
|
+
tags.length === 0 ? (input.placeholder ? input.placeholder : t('startTyping')) : ''
|
|
147
|
+
}
|
|
148
|
+
className='min-w-20 flex-1 border-0 p-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
|
149
|
+
disabled={field.disabled}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
</FormInputElement>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import FormInputElement from '@/components/form/FormInputElement'
|
|
3
|
-
import type { TextFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
4
|
-
import { useFormContext, useController } from 'react-hook-form'
|
|
5
|
-
|
|
6
|
-
export default function TextFormInput({
|
|
7
|
-
input,
|
|
8
|
-
direction,
|
|
9
|
-
disabled = false,
|
|
10
|
-
}: {
|
|
11
|
-
input: TextFieldClientConfig
|
|
12
|
-
direction?: 'row' | 'col'
|
|
13
|
-
disabled?: boolean
|
|
14
|
-
}) {
|
|
15
|
-
const { control } = useFormContext()
|
|
16
|
-
const {
|
|
17
|
-
field,
|
|
18
|
-
fieldState: { invalid, isTouched, isDirty, error },
|
|
19
|
-
} = useController({
|
|
20
|
-
name: input.name,
|
|
21
|
-
control,
|
|
22
|
-
defaultValue: input.value ?? undefined,
|
|
23
|
-
disabled: disabled,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<FormInputElement
|
|
28
|
-
validationError={error}
|
|
29
|
-
value={input.value}
|
|
30
|
-
readonly={input.readonly}
|
|
31
|
-
label={input.label}
|
|
32
|
-
required={input.required}
|
|
33
|
-
>
|
|
34
|
-
<input
|
|
35
|
-
placeholder={input.placeholder ? input.placeholder : input.label}
|
|
36
|
-
type='text'
|
|
37
|
-
readOnly={disabled}
|
|
38
|
-
disabled={field.disabled}
|
|
39
|
-
name={field.name}
|
|
40
|
-
onChange={field.onChange}
|
|
41
|
-
onBlur={field.onBlur}
|
|
42
|
-
value={field.value}
|
|
43
|
-
ref={field.ref}
|
|
44
|
-
className='bg-input text-foreground w-full rounded p-3 shadow-xs ring-2 ring-gray-300 outline-0 hover:ring-gray-400 focus:ring-blue-500'
|
|
45
|
-
/>
|
|
46
|
-
</FormInputElement>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import FormInputElement from '@/components/form/FormInputElement'
|
|
3
|
+
import type { TextFieldClientConfig } from 'nextjs-cms/core/fields'
|
|
4
|
+
import { useFormContext, useController } from 'react-hook-form'
|
|
5
|
+
|
|
6
|
+
export default function TextFormInput({
|
|
7
|
+
input,
|
|
8
|
+
direction,
|
|
9
|
+
disabled = false,
|
|
10
|
+
}: {
|
|
11
|
+
input: TextFieldClientConfig
|
|
12
|
+
direction?: 'row' | 'col'
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}) {
|
|
15
|
+
const { control } = useFormContext()
|
|
16
|
+
const {
|
|
17
|
+
field,
|
|
18
|
+
fieldState: { invalid, isTouched, isDirty, error },
|
|
19
|
+
} = useController({
|
|
20
|
+
name: input.name,
|
|
21
|
+
control,
|
|
22
|
+
defaultValue: input.value ?? undefined,
|
|
23
|
+
disabled: disabled,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<FormInputElement
|
|
28
|
+
validationError={error}
|
|
29
|
+
value={input.value}
|
|
30
|
+
readonly={input.readonly}
|
|
31
|
+
label={input.label}
|
|
32
|
+
required={input.required}
|
|
33
|
+
>
|
|
34
|
+
<input
|
|
35
|
+
placeholder={input.placeholder ? input.placeholder : input.label}
|
|
36
|
+
type='text'
|
|
37
|
+
readOnly={disabled}
|
|
38
|
+
disabled={field.disabled}
|
|
39
|
+
name={field.name}
|
|
40
|
+
onChange={field.onChange}
|
|
41
|
+
onBlur={field.onBlur}
|
|
42
|
+
value={field.value}
|
|
43
|
+
ref={field.ref}
|
|
44
|
+
className='bg-input text-foreground w-full rounded p-3 shadow-xs ring-2 ring-gray-300 outline-0 hover:ring-gray-400 focus:ring-blue-500'
|
|
45
|
+
/>
|
|
46
|
+
</FormInputElement>
|
|
47
|
+
)
|
|
48
|
+
}
|