create-nextjs-cms 0.5.8

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.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +395 -0
  6. package/dist/lib/utils.d.ts +11 -0
  7. package/dist/lib/utils.d.ts.map +1 -0
  8. package/dist/lib/utils.js +48 -0
  9. package/package.json +44 -0
  10. package/templates/default/.env +24 -0
  11. package/templates/default/.env.development +8 -0
  12. package/templates/default/.eslintrc.json +5 -0
  13. package/templates/default/.prettierignore +7 -0
  14. package/templates/default/.prettierrc.json +19 -0
  15. package/templates/default/CHANGELOG.md +77 -0
  16. package/templates/default/README.md +45 -0
  17. package/templates/default/app/(auth)/auth/login/LoginPage.tsx +175 -0
  18. package/templates/default/app/(auth)/auth/login/page.tsx +12 -0
  19. package/templates/default/app/(rootLayout)/admins/page.tsx +5 -0
  20. package/templates/default/app/(rootLayout)/advanced/page.tsx +5 -0
  21. package/templates/default/app/(rootLayout)/analytics/page.tsx +7 -0
  22. package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +7 -0
  23. package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +7 -0
  24. package/templates/default/app/(rootLayout)/dashboard/page.tsx +7 -0
  25. package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +7 -0
  26. package/templates/default/app/(rootLayout)/emails/page.tsx +6 -0
  27. package/templates/default/app/(rootLayout)/layout.tsx +5 -0
  28. package/templates/default/app/(rootLayout)/loading.tsx +10 -0
  29. package/templates/default/app/(rootLayout)/log/page.tsx +7 -0
  30. package/templates/default/app/(rootLayout)/new/[section]/page.tsx +7 -0
  31. package/templates/default/app/(rootLayout)/page.tsx +9 -0
  32. package/templates/default/app/(rootLayout)/section/[section]/page.tsx +7 -0
  33. package/templates/default/app/(rootLayout)/settings/page.tsx +7 -0
  34. package/templates/default/app/_trpc/client.ts +4 -0
  35. package/templates/default/app/api/auth/csrf/route.ts +25 -0
  36. package/templates/default/app/api/auth/refresh/route.ts +10 -0
  37. package/templates/default/app/api/auth/route.ts +23 -0
  38. package/templates/default/app/api/auth/session/route.ts +20 -0
  39. package/templates/default/app/api/editor/photo/route.ts +42 -0
  40. package/templates/default/app/api/photo/route.ts +27 -0
  41. package/templates/default/app/api/placeholder/route.ts +7 -0
  42. package/templates/default/app/api/submit/section/item/[slug]/route.ts +63 -0
  43. package/templates/default/app/api/submit/section/item/route.ts +53 -0
  44. package/templates/default/app/api/submit/section/simple/route.ts +54 -0
  45. package/templates/default/app/api/trpc/[trpc]/route.ts +33 -0
  46. package/templates/default/app/api/video/route.ts +174 -0
  47. package/templates/default/app/dictionaries.ts +14 -0
  48. package/templates/default/app/layout.tsx +28 -0
  49. package/templates/default/app/providers.tsx +151 -0
  50. package/templates/default/cli.ts +4 -0
  51. package/templates/default/components/AdminCard.tsx +163 -0
  52. package/templates/default/components/AdminEditPage.tsx +123 -0
  53. package/templates/default/components/AdminPrivilegeCard.tsx +184 -0
  54. package/templates/default/components/AdminsPage.tsx +43 -0
  55. package/templates/default/components/AdvancedSettingsPage.tsx +167 -0
  56. package/templates/default/components/AnalyticsPage.tsx +127 -0
  57. package/templates/default/components/BarChartBox.tsx +43 -0
  58. package/templates/default/components/BrowsePage.tsx +119 -0
  59. package/templates/default/components/CategorizedSectionPage.tsx +36 -0
  60. package/templates/default/components/CategoryDeleteConfirmPage.tsx +129 -0
  61. package/templates/default/components/CategorySectionSelectInput.tsx +139 -0
  62. package/templates/default/components/ConditionalFields.tsx +49 -0
  63. package/templates/default/components/ContainerBox.tsx +24 -0
  64. package/templates/default/components/DashboardPage.tsx +187 -0
  65. package/templates/default/components/DashboardPageAlt.tsx +43 -0
  66. package/templates/default/components/DefaultNavItems.tsx +3 -0
  67. package/templates/default/components/Dropzone.tsx +153 -0
  68. package/templates/default/components/EmailCard.tsx +137 -0
  69. package/templates/default/components/EmailPasswordForm.tsx +84 -0
  70. package/templates/default/components/EmailQuotaForm.tsx +72 -0
  71. package/templates/default/components/EmailsPage.tsx +48 -0
  72. package/templates/default/components/GalleryPhoto.tsx +93 -0
  73. package/templates/default/components/InfoCard.tsx +94 -0
  74. package/templates/default/components/ItemEditPage.tsx +217 -0
  75. package/templates/default/components/Layout.tsx +70 -0
  76. package/templates/default/components/LoadingSpinners.tsx +67 -0
  77. package/templates/default/components/LogPage.tsx +17 -0
  78. package/templates/default/components/Modal.tsx +99 -0
  79. package/templates/default/components/Navbar.tsx +29 -0
  80. package/templates/default/components/NavbarAlt.tsx +182 -0
  81. package/templates/default/components/NewAdminForm.tsx +172 -0
  82. package/templates/default/components/NewEmailForm.tsx +131 -0
  83. package/templates/default/components/NewPage.tsx +206 -0
  84. package/templates/default/components/NewVariantComponent.tsx +228 -0
  85. package/templates/default/components/PhotoGallery.tsx +35 -0
  86. package/templates/default/components/PieChartBox.tsx +101 -0
  87. package/templates/default/components/ProgressBar.tsx +24 -0
  88. package/templates/default/components/ProtectedDocument.tsx +78 -0
  89. package/templates/default/components/ProtectedImage.tsx +143 -0
  90. package/templates/default/components/ProtectedVideo.tsx +76 -0
  91. package/templates/default/components/SectionItemCard.tsx +143 -0
  92. package/templates/default/components/SectionItemStatusBadge.tsx +16 -0
  93. package/templates/default/components/SectionPage.tsx +124 -0
  94. package/templates/default/components/SelectBox.tsx +99 -0
  95. package/templates/default/components/SelectInputButtons.tsx +124 -0
  96. package/templates/default/components/SettingsPage.tsx +238 -0
  97. package/templates/default/components/Sidebar.tsx +209 -0
  98. package/templates/default/components/SidebarDropdownItem.tsx +74 -0
  99. package/templates/default/components/SidebarItem.tsx +19 -0
  100. package/templates/default/components/TempPage.tsx +12 -0
  101. package/templates/default/components/ThemeProvider.tsx +8 -0
  102. package/templates/default/components/TooltipComponent.tsx +27 -0
  103. package/templates/default/components/VariantCard.tsx +123 -0
  104. package/templates/default/components/VariantEditPage.tsx +229 -0
  105. package/templates/default/components/analytics/BounceRate.tsx +69 -0
  106. package/templates/default/components/analytics/LivePageViews.tsx +54 -0
  107. package/templates/default/components/analytics/LiveUsersCount.tsx +32 -0
  108. package/templates/default/components/analytics/MonthlyPageViews.tsx +41 -0
  109. package/templates/default/components/analytics/TopCountries.tsx +51 -0
  110. package/templates/default/components/analytics/TopDevices.tsx +45 -0
  111. package/templates/default/components/analytics/TopMediums.tsx +57 -0
  112. package/templates/default/components/analytics/TopSources.tsx +44 -0
  113. package/templates/default/components/analytics/TotalPageViews.tsx +40 -0
  114. package/templates/default/components/analytics/TotalSessions.tsx +40 -0
  115. package/templates/default/components/analytics/TotalUniqueUsers.tsx +40 -0
  116. package/templates/default/components/custom/RightHomeRoomVariantCard.tsx +137 -0
  117. package/templates/default/components/dndKit/Draggable.tsx +21 -0
  118. package/templates/default/components/dndKit/Droppable.tsx +20 -0
  119. package/templates/default/components/dndKit/SortableItem.tsx +18 -0
  120. package/templates/default/components/form/DateRangeFormInput.tsx +55 -0
  121. package/templates/default/components/form/Form.tsx +298 -0
  122. package/templates/default/components/form/FormInputElement.tsx +68 -0
  123. package/templates/default/components/form/FormInputs.tsx +108 -0
  124. package/templates/default/components/form/helpers/util.ts +20 -0
  125. package/templates/default/components/form/inputs/CheckboxFormInput.tsx +33 -0
  126. package/templates/default/components/form/inputs/ColorFormInput.tsx +44 -0
  127. package/templates/default/components/form/inputs/DateFormInput.tsx +107 -0
  128. package/templates/default/components/form/inputs/DocumentFormInput.tsx +124 -0
  129. package/templates/default/components/form/inputs/MapFormInput.tsx +139 -0
  130. package/templates/default/components/form/inputs/MultipleSelectFormInput.tsx +150 -0
  131. package/templates/default/components/form/inputs/NumberFormInput.tsx +42 -0
  132. package/templates/default/components/form/inputs/PasswordFormInput.tsx +47 -0
  133. package/templates/default/components/form/inputs/PhotoFormInput.tsx +218 -0
  134. package/templates/default/components/form/inputs/RichTextFormInput.tsx +133 -0
  135. package/templates/default/components/form/inputs/SelectFormInput.tsx +164 -0
  136. package/templates/default/components/form/inputs/TagsFormInput.tsx +63 -0
  137. package/templates/default/components/form/inputs/TextFormInput.tsx +48 -0
  138. package/templates/default/components/form/inputs/TextareaFormInput.tsx +47 -0
  139. package/templates/default/components/form/inputs/VideoFormInput.tsx +117 -0
  140. package/templates/default/components/pagination/Pagination.tsx +36 -0
  141. package/templates/default/components/pagination/PaginationButtons.tsx +145 -0
  142. package/templates/default/components/ui/accordion.tsx +57 -0
  143. package/templates/default/components/ui/alert.tsx +46 -0
  144. package/templates/default/components/ui/badge.tsx +33 -0
  145. package/templates/default/components/ui/button.tsx +57 -0
  146. package/templates/default/components/ui/calendar.tsx +68 -0
  147. package/templates/default/components/ui/card.tsx +76 -0
  148. package/templates/default/components/ui/checkbox.tsx +29 -0
  149. package/templates/default/components/ui/dropdown-menu.tsx +205 -0
  150. package/templates/default/components/ui/input.tsx +25 -0
  151. package/templates/default/components/ui/label.tsx +26 -0
  152. package/templates/default/components/ui/popover.tsx +31 -0
  153. package/templates/default/components/ui/scroll-area.tsx +42 -0
  154. package/templates/default/components/ui/select.tsx +164 -0
  155. package/templates/default/components/ui/sheet.tsx +107 -0
  156. package/templates/default/components/ui/switch.tsx +29 -0
  157. package/templates/default/components/ui/table.tsx +120 -0
  158. package/templates/default/components/ui/tabs.tsx +55 -0
  159. package/templates/default/components/ui/toast.tsx +113 -0
  160. package/templates/default/components/ui/toaster.tsx +35 -0
  161. package/templates/default/components/ui/tooltip.tsx +30 -0
  162. package/templates/default/components/ui/use-toast.ts +188 -0
  163. package/templates/default/components.json +16 -0
  164. package/templates/default/context/ModalProvider.tsx +53 -0
  165. package/templates/default/drizzle.config.ts +4 -0
  166. package/templates/default/dynamic-schemas/schema.ts +373 -0
  167. package/templates/default/env/env.js +130 -0
  168. package/templates/default/envConfig.ts +4 -0
  169. package/templates/default/hooks/useModal.ts +8 -0
  170. package/templates/default/lib/apiHelpers.ts +106 -0
  171. package/templates/default/lz.config.ts +40 -0
  172. package/templates/default/middleware.ts +33 -0
  173. package/templates/default/next.config.ts +46 -0
  174. package/templates/default/package.json +134 -0
  175. package/templates/default/postcss.config.js +6 -0
  176. package/templates/default/postinstall.js +14 -0
  177. package/templates/default/public/blank_avatar.png +0 -0
  178. package/templates/default/public/favicon.ico +0 -0
  179. package/templates/default/public/img/placeholder.svg +1 -0
  180. package/templates/default/public/lazemni_logo.png +0 -0
  181. package/templates/default/public/next.svg +1 -0
  182. package/templates/default/public/vercel.svg +1 -0
  183. package/templates/default/section-tests.ts +92 -0
  184. package/templates/default/styles/globals.css +88 -0
  185. package/templates/default/tailwind.config.js +95 -0
  186. package/templates/default/test.ts +77 -0
  187. package/templates/default/tsconfig.json +44 -0
@@ -0,0 +1,218 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import getString from 'nextjs-cms/translations'
3
+ import FormInputElement from '@/components/form/FormInputElement'
4
+ import ProtectedImage from '@/components/ProtectedImage'
5
+ import Image from 'next/image'
6
+ import useModal from '@/hooks/useModal'
7
+ import { ChevronRight, InfoIcon, Trash2Icon, UploadIcon, X } from 'lucide-react'
8
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
9
+ import { Badge } from '@/components/ui/badge'
10
+ import { PhotoFieldClientConfig } from 'nextjs-cms/core/fields'
11
+ import { useController, useFormContext } from 'react-hook-form'
12
+
13
+ export default function PhotoFormInput({ input, sectionName }: { input: PhotoFieldClientConfig; sectionName: string }) {
14
+ const { control } = useFormContext()
15
+ const {
16
+ field,
17
+ fieldState: { invalid, isTouched, isDirty, error },
18
+ } = useController({
19
+ name: input.name,
20
+ control,
21
+ defaultValue: input.value,
22
+ })
23
+
24
+ const [fileName, setFileName] = useState(getString('no_file_selected'))
25
+ const fileInputContainerRef = useRef<HTMLDivElement>(null)
26
+ const [image, setImage] = useState<string | null>(null)
27
+ const { setModal, modal, modalResponse, setModalResponse } = useModal()
28
+
29
+ const onImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
30
+ if (event.target.files && event.target.files[0]) {
31
+ setFileName(event.target.files?.[0].name || getString('no_file_selected'))
32
+ setImage(URL.createObjectURL(event.target.files[0]))
33
+ } else {
34
+ setFileName(getString('no_file_selected'))
35
+ setImage(null)
36
+ }
37
+ }
38
+
39
+ const imageWidth = input.thumbnail?.width ?? 200
40
+ const imageHeight = input.thumbnail?.height ?? 200
41
+
42
+ const protectedImage = (
43
+ <ProtectedImage
44
+ section={sectionName}
45
+ photo={input.value}
46
+ isThumb={true}
47
+ alt={'admin'}
48
+ width={imageWidth}
49
+ height={imageHeight}
50
+ className='rounded p-1 ring ring-gray-400'
51
+ />
52
+ )
53
+
54
+ const handleImageDelete = () => {}
55
+ return (
56
+ <FormInputElement
57
+ validationError={error}
58
+ value={protectedImage}
59
+ readonly={input.readonly}
60
+ label={input.label}
61
+ required={input.required}
62
+ >
63
+ <Card className='flex max-w-full flex-col'>
64
+ <CardHeader>
65
+ <CardTitle className='flex flex-wrap content-center items-center gap-1.5'>
66
+ <span>{input.label} </span>
67
+ {['true', 1, true].includes(input.required) ? <Badge>{getString('mandatory')}</Badge> : null}
68
+ </CardTitle>
69
+ </CardHeader>
70
+ <CardContent className='flex flex-col gap-1'>
71
+ <div className='mb-2 flex flex-col text-sm'>
72
+ {input.size ? (
73
+ <div className='flex flex-wrap items-center gap-2'>
74
+ <InfoIcon size={14} />
75
+ <span>
76
+ {getString(
77
+ input.size.crop ? 'imageRecommendedDimensions' : 'imageDimensionsMustBe',
78
+ )}
79
+ :
80
+ </span>
81
+ <Badge variant={'secondary'}>{`${input.size.width} x ${input.size.height}`}</Badge>
82
+ </div>
83
+ ) : null}
84
+ {input.maxFileSize ? (
85
+ <div className='flex flex-wrap items-center gap-2'>
86
+ <InfoIcon size={14} />
87
+ <span>{getString('maxFileSize')}:</span>
88
+ <Badge
89
+ variant={'secondary'}
90
+ >{`${input.maxFileSize.size} ${input.maxFileSize.unit.toUpperCase()}`}</Badge>
91
+ </div>
92
+ ) : null}
93
+ {input.extensions ? (
94
+ <div className='flex flex-wrap items-center gap-2'>
95
+ <InfoIcon size={14} />
96
+ <span>{getString('allowedExtensions')}:</span>
97
+ <Badge variant={'secondary'}>{input.extensions.join(', ')}</Badge>
98
+ </div>
99
+ ) : null}
100
+ </div>
101
+ <div className='flex flex-col gap-4'>
102
+ {input.value ? protectedImage : null}
103
+ {image ? (
104
+ <div className='relative flex items-center'>
105
+ <ChevronRight fontSize='large' />
106
+ <div className='relative h-[150px] w-[150px]'>
107
+ <X
108
+ className='absolute -right-3 -top-3 z-10 cursor-pointer rounded-full border-2 border-gray-500 bg-white p-1'
109
+ onClick={() => {
110
+ setImage(null)
111
+ setFileName(getString('no_file_selected'))
112
+ field.onChange(undefined)
113
+ if (fileInputContainerRef.current) {
114
+ /**
115
+ * Clear the child input value
116
+ */
117
+ if (fileInputContainerRef.current?.firstChild) {
118
+ ;(
119
+ fileInputContainerRef.current.firstChild as HTMLInputElement
120
+ ).value = ''
121
+ }
122
+ }
123
+ }}
124
+ />
125
+ <Image
126
+ className='mb-4 rounded p-1 ring ring-green-600'
127
+ src={image}
128
+ alt='new'
129
+ fill={true}
130
+ style={{
131
+ objectFit: 'contain',
132
+ }}
133
+ />
134
+ </div>
135
+ </div>
136
+ ) : null}
137
+ </div>
138
+ <div ref={fileInputContainerRef}>
139
+ <input
140
+ type='file'
141
+ className='hidden'
142
+ name={field.name}
143
+ ref={field.ref}
144
+ onChange={(e) => {
145
+ onImageChange(e)
146
+ if (!input.value) {
147
+ field.onChange(e.target?.files?.length ? e.target.files[0] : undefined)
148
+ }
149
+ }}
150
+ />
151
+ </div>
152
+ <div className='flex flex-col items-center gap-2 md:flex-row'>
153
+ <div className='flex w-full flex-col'>
154
+ {[false, 'false', 0].includes(input.required) &&
155
+ !['', null, undefined].includes(input.value) ? (
156
+ <div className='w-[400px] max-w-full'>
157
+ <button
158
+ type='button'
159
+ className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-gradient-to-r from-red-600 to-amber-500 p-2 text-center text-sm font-bold uppercase text-white drop-shadow hover:from-red-400 hover:to-amber-400'
160
+ onClick={() => {
161
+ setModal({
162
+ title: getString('deletePhoto'),
163
+ body: (
164
+ <div className='p-4'>
165
+ <div className='flex flex-col gap-4'>
166
+ <div>{getString('deletePhotoText')}</div>
167
+ <div className='flex gap-2'>
168
+ <button
169
+ className='rounded bg-green-600 px-2 py-1 text-white'
170
+ onClick={handleImageDelete}
171
+ >
172
+ {getString('yes')}
173
+ </button>
174
+ <button
175
+ className='rounded bg-red-800 px-2 py-1 text-white'
176
+ onClick={() => {
177
+ setModal(null)
178
+ }}
179
+ >
180
+ {getString('no')}
181
+ </button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ ),
186
+ headerColor: 'bg-red-700',
187
+ titleColor: 'text-white',
188
+ lang: 'en',
189
+ })
190
+ }}
191
+ >
192
+ <Trash2Icon size={20} /> {getString('delete')}
193
+ </button>
194
+ </div>
195
+ ) : null}
196
+ <div className='dark:border-neutral my-2 flex w-[400px] max-w-full flex-col gap-1 rounded-lg border border-gray-400 p-2'>
197
+ <button
198
+ type='button'
199
+ className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-gradient-to-r from-blue-700 to-sky-500 p-2 text-center text-sm font-bold uppercase text-white drop-shadow hover:from-blue-500 hover:to-sky-400'
200
+ onClick={() => {
201
+ if (fileInputContainerRef.current?.firstChild) {
202
+ ;(fileInputContainerRef.current.firstChild as HTMLInputElement).click()
203
+ }
204
+ }}
205
+ >
206
+ <UploadIcon size={20} /> {getString('selectFile')}
207
+ </button>
208
+ <div className='flex w-full ps-2'>
209
+ <div className='break-all'>{fileName}</div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </CardContent>
215
+ </Card>
216
+ </FormInputElement>
217
+ )
218
+ }
@@ -0,0 +1,133 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import { Editor } from '@tinymce/tinymce-react'
3
+ import FormInputElement from '@/components/form/FormInputElement'
4
+ // import { useTheme } from 'next-themes'
5
+ import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
6
+ import { RichTextFieldClientConfig } from 'nextjs-cms/core/fields'
7
+ import { FieldError, useFormContext } from 'react-hook-form'
8
+
9
+ export default function RichTextFormInput({ input }: { input: RichTextFieldClientConfig }) {
10
+ const { setValue, register, formState } = useFormContext()
11
+ register(input.name)
12
+ const editorRef = useRef(null)
13
+ const value = input.value ? input.value : ''
14
+ const [initialValue, setInitialValue] = useState('')
15
+ const [key, setKey] = useState(0) // Step 1: Add key state
16
+ // const [editorPhotos, setEditorPhotos] = useState<string[]>([])
17
+ // const { theme } = useTheme()
18
+ const axiosPrivate = useAxiosPrivate()
19
+
20
+ /*useEffect(() => {
21
+ // Step 2: Update the key when the theme changes
22
+ setKey((prevKey) => prevKey + 1)
23
+ }, [theme])*/
24
+
25
+ const image_upload_handler = (blobInfo: any, progress: any) =>
26
+ new Promise<string>(async (resolve, reject) => {
27
+ const formData = new FormData()
28
+ formData.append('photo', blobInfo.blob(), blobInfo.filename())
29
+ formData.append('sectionName', 'editor')
30
+ formData.append('itemId', '123')
31
+
32
+ axiosPrivate
33
+ .post('/editor/photo', Object.fromEntries(formData.entries()), {
34
+ headers: {
35
+ 'Content-Type': 'multipart/form-data',
36
+ },
37
+ })
38
+ .then((res) => {
39
+ // resolve(getPublicPhotoUrl(res.data.name, 'editor', 'large'))
40
+ resolve(res.data)
41
+ return
42
+ })
43
+ .catch((error) => {
44
+ if (error.response) {
45
+ reject({ message: 'Error: ' + error.response.data.error, remove: true })
46
+ } else if (error.request) {
47
+ reject({ message: 'Error: ' + error.request, remove: true })
48
+ } else {
49
+ reject({ message: 'Error: ' + error.message, remove: true })
50
+ }
51
+ })
52
+ })
53
+
54
+ const plugins = [
55
+ 'directionality ',
56
+ 'advlist',
57
+ 'autolink',
58
+ 'lists',
59
+ 'link',
60
+ 'charmap',
61
+ 'anchor',
62
+ 'searchreplace',
63
+ 'visualblocks',
64
+ 'code',
65
+ 'fullscreen',
66
+ 'insertdatetime',
67
+ 'table',
68
+ 'preview',
69
+ 'help',
70
+ 'wordcount',
71
+ ]
72
+
73
+ let toolbar =
74
+ 'undo redo | bold italic underline strikethrough formatselect | fontfamily fontsize blocks | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor permanentpen formatpainter removeformat | pagebreak | charmap emoticons | fullscreen preview save print | pageembed template link anchor codesample | a11ycheck ltr rtl | showcomments addcomment | footnotes '
75
+ if (input.allowMedia) {
76
+ plugins.push('media', 'image')
77
+ toolbar += ' | insertfile image media'
78
+ }
79
+
80
+ return (
81
+ <FormInputElement
82
+ validationError={formState.errors[input.name] as FieldError}
83
+ value={<div style={{ maxHeight: '200px', overflow: 'auto', wordBreak: 'break-all' }}>{input.value}</div>}
84
+ readonly={input.readonly}
85
+ label={input.label}
86
+ required={input.required}
87
+ >
88
+ <Editor
89
+ // TODO: Is this expensive?
90
+ onEditorChange={(content) => {
91
+ setValue(input.name, content, { shouldValidate: true })
92
+ }}
93
+ key={key} // Step 3: Pass the key as a prop
94
+ /*
95
+ onEditorChange={(content, editor) => {
96
+ console.log('Content was updated:', content)
97
+ setValue(content)
98
+ }}*/
99
+ // onChange={(e) => setValue(e.target.getContent())}
100
+ tinymceScriptSrc={'/tinymce/tinymce.min.js?v=1.1'}
101
+ onInit={(evt, editor) => {
102
+ // @ts-ignore
103
+ editorRef.current = editor
104
+ setInitialValue(value ? value : input.value ? input.value : '')
105
+ }}
106
+ textareaName={input.name}
107
+ initialValue={initialValue}
108
+ init={{
109
+ promotion: false,
110
+ height: 500,
111
+ menubar: 'file edit view insert format tools table tc help',
112
+ plugins: plugins,
113
+ toolbar: toolbar,
114
+
115
+ content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
116
+ // skin: theme === 'light' ? 'oxide' : 'oxide-dark',
117
+ // content_css: theme === 'light' ? 'default' : 'dark',
118
+ skin: 'oxide-dark',
119
+ content_css: 'dark',
120
+ relative_urls: false,
121
+ remove_script_host: false,
122
+ convert_urls: true,
123
+ extended_valid_elements: 'iframe[src|width|height|name|align|class|style],*[id|class|style]',
124
+ // valid_children: '+body[style],+div[*],+p[*]', // Allow any element to be a child of body, div, and p elements
125
+ // valid_elements: '*[*]', // Allow all elements and attributes
126
+ images_file_types: 'jpg,png,webp',
127
+ block_unsupported_drop: true,
128
+ images_upload_handler: image_upload_handler,
129
+ }}
130
+ />
131
+ </FormInputElement>
132
+ )
133
+ }
@@ -0,0 +1,164 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import FormInputElement from '@/components/form/FormInputElement'
3
+ import SelectBox from '@/components/SelectBox'
4
+ import LoadingSpinners from '@/components/LoadingSpinners'
5
+ import { useAutoAnimate } from '@formkit/auto-animate/react'
6
+ import getString from 'nextjs-cms/translations'
7
+ import { trpc } from '@/app/_trpc/client'
8
+ import { SelectFieldClientConfig, SelectOption } from 'nextjs-cms/core/fields'
9
+ import { ConditionalFields } from '@/components/ConditionalFields'
10
+ import { useController, useFormContext } from 'react-hook-form'
11
+
12
+ export default function SelectFormInput({
13
+ input,
14
+ sectionName,
15
+ refetch,
16
+ level = 1,
17
+ }: {
18
+ input: SelectFieldClientConfig
19
+ sectionName: string
20
+ refetch?: any
21
+ level?: number
22
+ }) {
23
+ /**
24
+ * Check and add the select option if it does not exist, the select option has a value of undefined
25
+ */
26
+ const exists = input.options?.filter((option) => option.value === undefined)
27
+ if (!exists || exists.length === 0) {
28
+ if (input.options) {
29
+ // @ts-ignore - This is a type-hack to add the placeholder `select` option to the options array
30
+ input.options.unshift({ value: undefined, label: getString('select') })
31
+ }
32
+ }
33
+
34
+ const depth = input.section ? input.section.depth : 1
35
+ const [parent] = useAutoAnimate(/* optional config */)
36
+ const [isLoading, setIsLoading] = useState<boolean>(false)
37
+ const [value, setValue] = useState<SelectOption | undefined>(getValue(input.value))
38
+ const [child, setChild] = useState<{
39
+ input: SelectFieldClientConfig
40
+ level: number
41
+ } | null>(null)
42
+
43
+ const childrenMutation = trpc.categorySections.getChildren.useMutation({
44
+ onSuccess: (data) => {
45
+ if (data && data.options) {
46
+ const childInput = {
47
+ input: {
48
+ type: 'select' as const,
49
+ required: false,
50
+ readonly: false,
51
+ conditionalFields: [],
52
+ options: data.options,
53
+ section: input.section,
54
+ label: '|->',
55
+ name: input.name,
56
+ value:
57
+ input.value && Array.isArray(input.value) && input.value[1]
58
+ ? input.value.slice(1)
59
+ : undefined,
60
+ },
61
+ level: data.level,
62
+ }
63
+ setChild(childInput)
64
+ } else {
65
+ setChild(null)
66
+ }
67
+
68
+ setIsLoading(false)
69
+ },
70
+ onError: (error) => {},
71
+ // console.log('Error', error)
72
+ })
73
+
74
+ function getValue(value: SelectOption[] | undefined): SelectOption | undefined {
75
+ if (value && !Array.isArray(value)) {
76
+ return value
77
+ }
78
+ if (Array.isArray(value) && value.length > 0) {
79
+ return value[0]
80
+ }
81
+ return undefined
82
+ }
83
+
84
+ const { control } = useFormContext()
85
+ const {
86
+ field,
87
+ fieldState: { invalid, isTouched, isDirty, error },
88
+ } = useController({
89
+ name: input.name,
90
+ control,
91
+ defaultValue: value?.value,
92
+ })
93
+
94
+ useEffect(() => {
95
+ setChild(null)
96
+ if (!input.section || !input.section.name) return
97
+ if (!value?.value) {
98
+ return
99
+ }
100
+
101
+ /**
102
+ * If the depth is less than or equal to 1, return
103
+ * If the level is greater than or equal to the depth, return
104
+ */
105
+ if (depth <= 1 || level >= depth) return
106
+
107
+ setIsLoading(true)
108
+ childrenMutation.mutate({
109
+ parentId: value.value,
110
+ sectionName: input.section.name,
111
+ level: level,
112
+ })
113
+ }, [value?.value])
114
+
115
+ return (
116
+ <>
117
+ <FormInputElement validationError={error} label={input.label} required={input.required}>
118
+ <input
119
+ type='hidden'
120
+ disabled={field.disabled}
121
+ name={field.name}
122
+ onBlur={field.onBlur}
123
+ ref={field.ref}
124
+ value={field.value}
125
+ />
126
+ <SelectBox
127
+ defaultValue={value?.value ? value?.value.toString() : undefined}
128
+ items={input.options ? input.options : []}
129
+ onChange={(value: SelectOption) => {
130
+ field.onChange(value.value)
131
+ if (value) setValue(value)
132
+ }}
133
+ classname='w-full shadow-sm'
134
+ />
135
+ </FormInputElement>
136
+ <div ref={parent}>
137
+ {isLoading && <LoadingSpinners />}
138
+ {child && (
139
+ <div className='ps-4'>
140
+ {/* @ts-ignore no validators for child selects */}
141
+ <SelectFormInput
142
+ sectionName={sectionName}
143
+ refetch={() => {
144
+ refetch().then(() => {
145
+ setValue(undefined)
146
+ })
147
+ }}
148
+ level={child.level}
149
+ input={child.input}
150
+ />
151
+ </div>
152
+ )}
153
+
154
+ {input.conditionalFields ? (
155
+ <ConditionalFields
156
+ sectionName={sectionName}
157
+ conditionalFields={input.conditionalFields}
158
+ value={value?.value}
159
+ />
160
+ ) : null}
161
+ </div>
162
+ </>
163
+ )
164
+ }
@@ -0,0 +1,63 @@
1
+ import FormInputElement from '@/components/form/FormInputElement'
2
+ import { Autocomplete, Chip, TextField } from '@mui/material'
3
+ import getString from 'nextjs-cms/translations'
4
+ import { TagsFieldClientConfig } from 'nextjs-cms/core/fields'
5
+ import { useController, useFormContext } from 'react-hook-form'
6
+
7
+ export default function TagsFormInput({ input }: { input: TagsFieldClientConfig }) {
8
+ const { control } = useFormContext()
9
+ const {
10
+ field,
11
+ fieldState: { invalid, isTouched, isDirty, error },
12
+ } = useController({
13
+ name: input.name,
14
+ control,
15
+ defaultValue: input.value ?? undefined,
16
+ })
17
+
18
+ return (
19
+ <FormInputElement
20
+ validationError={error}
21
+ value={field.value}
22
+ readonly={input.readonly}
23
+ label={input.label}
24
+ required={input.required}
25
+ >
26
+ <input
27
+ type='hidden'
28
+ className='hidden'
29
+ disabled={field.disabled}
30
+ name={field.name}
31
+ onBlur={field.onBlur}
32
+ value={field.value}
33
+ ref={field.ref}
34
+ />
35
+
36
+ <Autocomplete
37
+ multiple
38
+ // limitTags={2}
39
+ options={[]}
40
+ getOptionLabel={(option) => option}
41
+ renderInput={(params) => (
42
+ <TextField
43
+ {...params}
44
+ label={input.label}
45
+ // className='ring-2 outline-0 focus:ring-blue-500 hover:ring-gray-400 ring-gray-300 shadow-sm rounded w-full bg-input text-foreground'
46
+ placeholder={input.placeholder ? input.placeholder : getString('start_typing')}
47
+ />
48
+ )}
49
+ freeSolo={true}
50
+ onChange={(event, newInputValue) => {
51
+ field.onChange(newInputValue.toString())
52
+ }}
53
+ defaultValue={input.value ? input.value.split(',') : []}
54
+ disableClearable={true}
55
+ renderTags={(tagValue, getTagProps) => {
56
+ return tagValue.map((option, index) => (
57
+ <Chip {...getTagProps({ index })} key={option} label={option} />
58
+ ))
59
+ }}
60
+ />
61
+ </FormInputElement>
62
+ )
63
+ }
@@ -0,0 +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='w-full rounded bg-input p-3 text-foreground shadow-sm outline-0 ring-2 ring-gray-300 hover:ring-gray-400 focus:ring-blue-500'
45
+ />
46
+ </FormInputElement>
47
+ )
48
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import FormInputElement from '@/components/form/FormInputElement'
3
+ import { TextAreaFieldClientConfig } from 'nextjs-cms/core/fields'
4
+ import { FieldError, useController, useFormContext } from 'react-hook-form'
5
+
6
+ export default function TextareaFormInput({
7
+ input,
8
+ defaultValue,
9
+ direction,
10
+ }: {
11
+ input: TextAreaFieldClientConfig
12
+ defaultValue?: string | null
13
+ direction?: 'row' | 'col'
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: defaultValue ?? input.value ?? undefined,
23
+ disabled: input.readonly,
24
+ })
25
+
26
+ return (
27
+ <FormInputElement
28
+ validationError={error}
29
+ value={<div style={{ maxHeight: '200px', overflow: 'auto', wordBreak: 'break-all' }}>{input.value}</div>}
30
+ readonly={input.readonly}
31
+ label={input.label}
32
+ required={input.required}
33
+ >
34
+ <textarea
35
+ placeholder={input.label}
36
+ readOnly={input.readonly}
37
+ disabled={field.disabled}
38
+ name={field.name}
39
+ onChange={field.onChange}
40
+ onBlur={field.onBlur}
41
+ value={field.value}
42
+ ref={field.ref}
43
+ className='h-[150px] w-full rounded bg-input p-3 text-foreground shadow-sm outline-0 ring-2 ring-gray-300 hover:ring-gray-400 focus:ring-blue-500'
44
+ ></textarea>
45
+ </FormInputElement>
46
+ )
47
+ }