create-nextjs-cms 0.9.19 → 0.9.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/default/components/GalleryPhoto.tsx +109 -93
- package/templates/default/components/PhotoGallery.tsx +47 -35
- package/templates/default/components/form/Form.tsx +385 -370
- package/templates/default/components/form/inputs/DateFormInput.tsx +11 -10
- package/templates/default/components/form/inputs/DocumentFormInput.tsx +2 -2
- package/templates/default/components/form/inputs/PhotoFormInput.tsx +7 -9
- package/templates/default/components/form/inputs/VideoFormInput.tsx +2 -2
- package/templates/default/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,93 +1,109 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
import { trpc } from '@/app/_trpc/client'
|
|
2
|
+
import ProtectedImage from '@/components/ProtectedImage'
|
|
3
|
+
import { useToast } from '@/components/ui/use-toast'
|
|
4
|
+
import useModal from '@/hooks/useModal'
|
|
5
|
+
import { MinusIcon } from '@radix-ui/react-icons'
|
|
6
|
+
import { PhotoGalleryItem } from 'nextjs-cms/core/types'
|
|
7
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
8
|
+
|
|
9
|
+
type GalleryPhotoItem = PhotoGalleryItem & { locale?: string }
|
|
10
|
+
|
|
11
|
+
const GalleryPhoto = ({
|
|
12
|
+
item,
|
|
13
|
+
sectionName,
|
|
14
|
+
localized,
|
|
15
|
+
locale,
|
|
16
|
+
action,
|
|
17
|
+
}: {
|
|
18
|
+
item: GalleryPhotoItem
|
|
19
|
+
sectionName: string
|
|
20
|
+
localized?: boolean
|
|
21
|
+
locale?: string
|
|
22
|
+
action?: any
|
|
23
|
+
}) => {
|
|
24
|
+
const t = useI18n()
|
|
25
|
+
const { setModal, setModalResponse } = useModal()
|
|
26
|
+
const deleteMutation = trpc.gallery.deletePhoto.useMutation({
|
|
27
|
+
onError: (error) => {
|
|
28
|
+
toast({
|
|
29
|
+
variant: 'destructive',
|
|
30
|
+
title: t('deleteGalleryPhoto'),
|
|
31
|
+
description: error.message,
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
onSuccess: (data) => {
|
|
36
|
+
setModal(null)
|
|
37
|
+
setModalResponse(null)
|
|
38
|
+
toast({
|
|
39
|
+
variant: 'success',
|
|
40
|
+
title: t('galleryPhotoDeleted'),
|
|
41
|
+
})
|
|
42
|
+
action()
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
const { toast } = useToast()
|
|
46
|
+
const handlePhotoDelete = async () => {
|
|
47
|
+
const targetLocale = locale ?? item.locale
|
|
48
|
+
deleteMutation.mutate({
|
|
49
|
+
sectionName: sectionName,
|
|
50
|
+
photoName: item.photo,
|
|
51
|
+
referenceId: item.referenceId,
|
|
52
|
+
...(localized && targetLocale ? { locale: targetLocale } : {}),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
<div className='relative'>
|
|
57
|
+
{/* Delete Button */}
|
|
58
|
+
<button
|
|
59
|
+
type='button'
|
|
60
|
+
className='absolute -end-2 -top-2 z-10 h-6 w-6 rounded-full bg-red-500 p-1'
|
|
61
|
+
onClick={() => {
|
|
62
|
+
setModal({
|
|
63
|
+
title: t('deleteGalleryPhoto'),
|
|
64
|
+
body: (
|
|
65
|
+
<div className='p-4'>
|
|
66
|
+
<div className='flex flex-col gap-4'>
|
|
67
|
+
<div>{t('deleteGalleryPhotoText')}</div>
|
|
68
|
+
<div className='flex gap-2'>
|
|
69
|
+
<button
|
|
70
|
+
className='rounded bg-green-600 px-2 py-1 text-white'
|
|
71
|
+
onClick={handlePhotoDelete}
|
|
72
|
+
>
|
|
73
|
+
Yes
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
className='rounded bg-red-800 px-2 py-1 text-white'
|
|
77
|
+
onClick={() => {
|
|
78
|
+
setModal(null)
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
No
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
headerColor: 'bg-red-700',
|
|
88
|
+
titleColor: 'text-white',
|
|
89
|
+
lang: 'en',
|
|
90
|
+
})
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<MinusIcon className='text-white' />
|
|
94
|
+
</button>
|
|
95
|
+
<ProtectedImage
|
|
96
|
+
section={sectionName}
|
|
97
|
+
photo={item.photo}
|
|
98
|
+
isThumb={true}
|
|
99
|
+
alt={item.photo}
|
|
100
|
+
height={150}
|
|
101
|
+
width={150}
|
|
102
|
+
// fill={true}
|
|
103
|
+
className='ring-3 mb-4 rounded p-1 ring-gray-400'
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default GalleryPhoto
|
|
@@ -1,35 +1,47 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
const PhotoGallery = ({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
import ContainerBox from '@/components/ContainerBox'
|
|
2
|
+
import GalleryPhoto from '@/components/GalleryPhoto'
|
|
3
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
4
|
+
import { PhotoGalleryItem } from 'nextjs-cms/core/types'
|
|
5
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
6
|
+
|
|
7
|
+
const PhotoGallery = ({
|
|
8
|
+
sectionName,
|
|
9
|
+
gallery,
|
|
10
|
+
localized,
|
|
11
|
+
locale,
|
|
12
|
+
}: {
|
|
13
|
+
sectionName: string
|
|
14
|
+
gallery: PhotoGalleryItem[]
|
|
15
|
+
localized?: boolean
|
|
16
|
+
locale?: string
|
|
17
|
+
}) => {
|
|
18
|
+
const t = useI18n()
|
|
19
|
+
return (
|
|
20
|
+
<ContainerBox title={t('gallery')}>
|
|
21
|
+
{gallery && gallery.length > 0 ? (
|
|
22
|
+
<div className='flex flex-wrap gap-4'>
|
|
23
|
+
{gallery.map((photo: PhotoGalleryItem, index: number) => (
|
|
24
|
+
<GalleryPhoto
|
|
25
|
+
item={photo}
|
|
26
|
+
sectionName={sectionName}
|
|
27
|
+
localized={localized}
|
|
28
|
+
locale={locale}
|
|
29
|
+
key={photo.photo}
|
|
30
|
+
action={() => {
|
|
31
|
+
// This is the action that will be executed when the user removes a photo from the gallery
|
|
32
|
+
// Remove the removed photo from the gallery
|
|
33
|
+
gallery.splice(index, 1)
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<Alert variant='light' className='mt-4'>
|
|
40
|
+
<AlertDescription className='font-bold'>{t('noItems')}</AlertDescription>
|
|
41
|
+
</Alert>
|
|
42
|
+
)}
|
|
43
|
+
</ContainerBox>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default PhotoGallery
|
|
@@ -1,370 +1,385 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import ContainerBox from '@/components/ContainerBox'
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import ProgressBar from '@/components/ProgressBar'
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from 'nextjs-cms/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
richTextFieldSchema,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from '
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| RouterOutputs['hasItemsSections']['
|
|
78
|
-
| RouterOutputs['
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
break
|
|
164
|
-
|
|
165
|
-
case 'checkbox':
|
|
166
|
-
schema = schema.extend({
|
|
167
|
-
[input.name]: checkboxFieldSchema(input as CheckboxFieldClientConfig, language),
|
|
168
|
-
})
|
|
169
|
-
break
|
|
170
|
-
|
|
171
|
-
case 'text':
|
|
172
|
-
schema = schema.extend({
|
|
173
|
-
[input.name]: textFieldSchema(input as TextFieldClientConfig, language),
|
|
174
|
-
})
|
|
175
|
-
break
|
|
176
|
-
|
|
177
|
-
case 'textarea':
|
|
178
|
-
schema = schema.extend({
|
|
179
|
-
[input.name]: textareaFieldSchema(input as TextAreaFieldClientConfig, language),
|
|
180
|
-
})
|
|
181
|
-
break
|
|
182
|
-
|
|
183
|
-
case 'rich_text':
|
|
184
|
-
schema = schema.extend({
|
|
185
|
-
[input.name]: richTextFieldSchema(input as RichTextFieldClientConfig, language),
|
|
186
|
-
})
|
|
187
|
-
break
|
|
188
|
-
|
|
189
|
-
case 'photo':
|
|
190
|
-
if (formType === 'edit' && !!input.value) break
|
|
191
|
-
schema = schema.extend({
|
|
192
|
-
[input.name]: photoFieldSchema(input as PhotoFieldClientConfig, language),
|
|
193
|
-
})
|
|
194
|
-
break
|
|
195
|
-
|
|
196
|
-
case 'document':
|
|
197
|
-
if (formType === 'edit' && !!input.value) break
|
|
198
|
-
schema = schema.extend({
|
|
199
|
-
[input.name]: documentFieldSchema(input as DocumentFieldClientConfig, language),
|
|
200
|
-
})
|
|
201
|
-
break
|
|
202
|
-
|
|
203
|
-
case 'video':
|
|
204
|
-
if (formType === 'edit' && !!input.value) break
|
|
205
|
-
schema = schema.extend({
|
|
206
|
-
[input.name]: videoFieldSchema(input as VideoFieldClientConfig, language),
|
|
207
|
-
})
|
|
208
|
-
break
|
|
209
|
-
|
|
210
|
-
case 'number':
|
|
211
|
-
schema = schema.extend({
|
|
212
|
-
[input.name]: numberFieldSchema(input as NumberFieldClientConfig, language),
|
|
213
|
-
})
|
|
214
|
-
break
|
|
215
|
-
case 'color':
|
|
216
|
-
schema = schema.extend({
|
|
217
|
-
[input.name]: colorFieldSchema(input as ColorFieldClientConfig, language),
|
|
218
|
-
})
|
|
219
|
-
break
|
|
220
|
-
|
|
221
|
-
case 'map':
|
|
222
|
-
schema = schema.extend({
|
|
223
|
-
[input.name]: mapFieldSchema(input as MapFieldClientConfig, language),
|
|
224
|
-
})
|
|
225
|
-
break
|
|
226
|
-
|
|
227
|
-
case 'password':
|
|
228
|
-
schema = schema.extend({
|
|
229
|
-
[input.name]: passwordFieldSchema(input as PasswordFieldClientConfig, language),
|
|
230
|
-
})
|
|
231
|
-
break
|
|
232
|
-
|
|
233
|
-
case 'slug':
|
|
234
|
-
schema = schema.extend({
|
|
235
|
-
[input.name]: slugFieldSchema(input as SlugFieldClientConfig, language),
|
|
236
|
-
})
|
|
237
|
-
break
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
const methods = useForm({
|
|
243
|
-
resolver: zodResolver(schema),
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
const { dirtyFields } = methods.formState
|
|
247
|
-
const hasDirtyFields = Object.keys(dirtyFields).length > 0
|
|
248
|
-
useEffect(() => {
|
|
249
|
-
onDirtyChange?.(hasDirtyFields)
|
|
250
|
-
}, [hasDirtyFields, onDirtyChange])
|
|
251
|
-
|
|
252
|
-
return (
|
|
253
|
-
<LocalizationProvider value={contentLocale ?? null}>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1
|
+
import type { RouterOutputs } from 'nextjs-cms/api'
|
|
2
|
+
import React, { RefObject, useCallback, useEffect } from 'react'
|
|
3
|
+
import ContainerBox from '@/components/ContainerBox'
|
|
4
|
+
import Dropzone, { DropzoneHandles } from '@/components/Dropzone'
|
|
5
|
+
import { LocalizationProvider } from '@/components/form/ContentLocaleContext'
|
|
6
|
+
import FormInputs from '@/components/form/FormInputs'
|
|
7
|
+
import { configLastUpdated } from '@/components/form/helpers/util'
|
|
8
|
+
import NewVariantComponent, { VariantHandles } from '@/components/NewVariantComponent'
|
|
9
|
+
import ProgressBar from '@/components/ProgressBar'
|
|
10
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
11
|
+
import classNames from 'classnames'
|
|
12
|
+
import { useSession } from 'nextjs-cms/auth/react'
|
|
13
|
+
import {
|
|
14
|
+
CheckboxFieldClientConfig,
|
|
15
|
+
ColorFieldClientConfig,
|
|
16
|
+
DateFieldClientConfig,
|
|
17
|
+
DateRangeFieldClientConfig,
|
|
18
|
+
DocumentFieldClientConfig,
|
|
19
|
+
MapFieldClientConfig,
|
|
20
|
+
NumberFieldClientConfig,
|
|
21
|
+
PasswordFieldClientConfig,
|
|
22
|
+
PhotoFieldClientConfig,
|
|
23
|
+
RichTextFieldClientConfig,
|
|
24
|
+
SelectFieldClientConfig,
|
|
25
|
+
SelectMultipleFieldClientConfig,
|
|
26
|
+
SlugFieldClientConfig,
|
|
27
|
+
TextAreaFieldClientConfig,
|
|
28
|
+
TextFieldClientConfig,
|
|
29
|
+
VideoFieldClientConfig,
|
|
30
|
+
} from 'nextjs-cms/core/fields'
|
|
31
|
+
import { ConditionalField, FieldType } from 'nextjs-cms/core/types'
|
|
32
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
33
|
+
import {
|
|
34
|
+
checkboxFieldSchema,
|
|
35
|
+
colorFieldSchema,
|
|
36
|
+
dateFieldSchema,
|
|
37
|
+
dateRangeFieldSchema,
|
|
38
|
+
documentFieldSchema,
|
|
39
|
+
mapFieldSchema,
|
|
40
|
+
numberFieldSchema,
|
|
41
|
+
passwordFieldSchema,
|
|
42
|
+
photoFieldSchema,
|
|
43
|
+
richTextFieldSchema,
|
|
44
|
+
selectFieldSchema,
|
|
45
|
+
selectMultipleFieldSchema,
|
|
46
|
+
slugFieldSchema,
|
|
47
|
+
textareaFieldSchema,
|
|
48
|
+
textFieldSchema,
|
|
49
|
+
videoFieldSchema,
|
|
50
|
+
} from 'nextjs-cms/validators'
|
|
51
|
+
import { FormProvider, useForm } from 'react-hook-form'
|
|
52
|
+
import * as z from 'zod'
|
|
53
|
+
|
|
54
|
+
import PhotoGallery from '../PhotoGallery'
|
|
55
|
+
|
|
56
|
+
export const revalidate = 1
|
|
57
|
+
|
|
58
|
+
export default function Form({
|
|
59
|
+
formType,
|
|
60
|
+
data,
|
|
61
|
+
dropzoneRef,
|
|
62
|
+
variantRef,
|
|
63
|
+
handleSubmit,
|
|
64
|
+
isSubmitting,
|
|
65
|
+
response,
|
|
66
|
+
progress,
|
|
67
|
+
progressVariant,
|
|
68
|
+
buttonType = 'big',
|
|
69
|
+
submitSuccessCount = 0,
|
|
70
|
+
contentLocale,
|
|
71
|
+
defaultLocale,
|
|
72
|
+
onDirtyChange,
|
|
73
|
+
}: {
|
|
74
|
+
formType?: 'new' | 'edit'
|
|
75
|
+
data:
|
|
76
|
+
| RouterOutputs['hasItemsSections']['newItem']
|
|
77
|
+
| RouterOutputs['hasItemsSections']['editItem']
|
|
78
|
+
| RouterOutputs['simpleSections']['create']
|
|
79
|
+
| {
|
|
80
|
+
section: {
|
|
81
|
+
name: string
|
|
82
|
+
gallery?: { localized?: boolean } | null
|
|
83
|
+
title?: { section?: string; singular?: string; plural?: string }
|
|
84
|
+
configFile?: string
|
|
85
|
+
}
|
|
86
|
+
inputGroups:
|
|
87
|
+
| {
|
|
88
|
+
groupId: number | undefined
|
|
89
|
+
groupTitle: string
|
|
90
|
+
groupOrder: number
|
|
91
|
+
inputs: {
|
|
92
|
+
type: FieldType
|
|
93
|
+
name: string
|
|
94
|
+
label: string
|
|
95
|
+
required: boolean
|
|
96
|
+
conditionalFields: ConditionalField[]
|
|
97
|
+
placeholder?: string
|
|
98
|
+
readonly: boolean
|
|
99
|
+
value: any
|
|
100
|
+
}[]
|
|
101
|
+
}[]
|
|
102
|
+
| undefined
|
|
103
|
+
}
|
|
104
|
+
dropzoneRef?: RefObject<DropzoneHandles | null>
|
|
105
|
+
variantRef?: RefObject<VariantHandles[]>
|
|
106
|
+
handleSubmit: any
|
|
107
|
+
isSubmitting: boolean
|
|
108
|
+
response?: any
|
|
109
|
+
progress?: number
|
|
110
|
+
progressVariant?: 'determinate' | 'query'
|
|
111
|
+
buttonType?: 'big' | 'small'
|
|
112
|
+
submitSuccessCount?: number
|
|
113
|
+
contentLocale?: { code: string; label: string; rtl?: boolean }
|
|
114
|
+
defaultLocale?: { code: string; label: string; rtl?: boolean }
|
|
115
|
+
onDirtyChange?: (isDirty: boolean) => void
|
|
116
|
+
}) {
|
|
117
|
+
const t = useI18n()
|
|
118
|
+
const session = useSession()
|
|
119
|
+
const language = session?.data?.user?.language
|
|
120
|
+
|
|
121
|
+
// When editing a non-default locale, only show fields marked as localized
|
|
122
|
+
const isTranslationMode = !!(contentLocale && defaultLocale && contentLocale.code !== defaultLocale.code)
|
|
123
|
+
const sectionGallery = data.section?.gallery as { localized?: boolean } | null | undefined
|
|
124
|
+
const showGallery = !!sectionGallery && (!isTranslationMode || sectionGallery.localized === true)
|
|
125
|
+
|
|
126
|
+
const filterInputsForLocale = <T,>(inputs: T[]): T[] => {
|
|
127
|
+
if (!isTranslationMode) return inputs
|
|
128
|
+
return inputs.filter((input) => (input as any).localized)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hasNoLocalizedContent =
|
|
132
|
+
isTranslationMode &&
|
|
133
|
+
(data.inputGroups?.every((g) => filterInputsForLocale(g.inputs as any[]).length === 0) ?? true) &&
|
|
134
|
+
sectionGallery?.localized !== true
|
|
135
|
+
|
|
136
|
+
let schema = z.object({})
|
|
137
|
+
/**
|
|
138
|
+
* Construct the schema for the form
|
|
139
|
+
*/
|
|
140
|
+
data.inputGroups?.forEach((inputGroup) => {
|
|
141
|
+
filterInputsForLocale(inputGroup.inputs as any[]).forEach((input: any) => {
|
|
142
|
+
if (input.readonly) return
|
|
143
|
+
switch (input.type) {
|
|
144
|
+
case 'select_multiple':
|
|
145
|
+
schema = schema.extend({
|
|
146
|
+
[input.name]: selectMultipleFieldSchema(input as SelectMultipleFieldClientConfig, language),
|
|
147
|
+
})
|
|
148
|
+
break
|
|
149
|
+
case 'select':
|
|
150
|
+
schema = schema.extend({
|
|
151
|
+
[input.name]: selectFieldSchema(input as SelectFieldClientConfig, language),
|
|
152
|
+
})
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
case 'date':
|
|
156
|
+
schema = schema.extend({
|
|
157
|
+
[input.name]: dateFieldSchema(input as DateFieldClientConfig, language),
|
|
158
|
+
})
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
case 'date_range':
|
|
162
|
+
schema = schema.extend(dateRangeFieldSchema(input as DateRangeFieldClientConfig, language))
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
case 'checkbox':
|
|
166
|
+
schema = schema.extend({
|
|
167
|
+
[input.name]: checkboxFieldSchema(input as CheckboxFieldClientConfig, language),
|
|
168
|
+
})
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
case 'text':
|
|
172
|
+
schema = schema.extend({
|
|
173
|
+
[input.name]: textFieldSchema(input as TextFieldClientConfig, language),
|
|
174
|
+
})
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
case 'textarea':
|
|
178
|
+
schema = schema.extend({
|
|
179
|
+
[input.name]: textareaFieldSchema(input as TextAreaFieldClientConfig, language),
|
|
180
|
+
})
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
case 'rich_text':
|
|
184
|
+
schema = schema.extend({
|
|
185
|
+
[input.name]: richTextFieldSchema(input as RichTextFieldClientConfig, language),
|
|
186
|
+
})
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
case 'photo':
|
|
190
|
+
if (formType === 'edit' && !!input.value) break
|
|
191
|
+
schema = schema.extend({
|
|
192
|
+
[input.name]: photoFieldSchema(input as PhotoFieldClientConfig, language),
|
|
193
|
+
})
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
case 'document':
|
|
197
|
+
if (formType === 'edit' && !!input.value) break
|
|
198
|
+
schema = schema.extend({
|
|
199
|
+
[input.name]: documentFieldSchema(input as DocumentFieldClientConfig, language),
|
|
200
|
+
})
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
case 'video':
|
|
204
|
+
if (formType === 'edit' && !!input.value) break
|
|
205
|
+
schema = schema.extend({
|
|
206
|
+
[input.name]: videoFieldSchema(input as VideoFieldClientConfig, language),
|
|
207
|
+
})
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
case 'number':
|
|
211
|
+
schema = schema.extend({
|
|
212
|
+
[input.name]: numberFieldSchema(input as NumberFieldClientConfig, language),
|
|
213
|
+
})
|
|
214
|
+
break
|
|
215
|
+
case 'color':
|
|
216
|
+
schema = schema.extend({
|
|
217
|
+
[input.name]: colorFieldSchema(input as ColorFieldClientConfig, language),
|
|
218
|
+
})
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
case 'map':
|
|
222
|
+
schema = schema.extend({
|
|
223
|
+
[input.name]: mapFieldSchema(input as MapFieldClientConfig, language),
|
|
224
|
+
})
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
case 'password':
|
|
228
|
+
schema = schema.extend({
|
|
229
|
+
[input.name]: passwordFieldSchema(input as PasswordFieldClientConfig, language),
|
|
230
|
+
})
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
case 'slug':
|
|
234
|
+
schema = schema.extend({
|
|
235
|
+
[input.name]: slugFieldSchema(input as SlugFieldClientConfig, language),
|
|
236
|
+
})
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const methods = useForm({
|
|
243
|
+
resolver: zodResolver(schema),
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const { dirtyFields } = methods.formState
|
|
247
|
+
const hasDirtyFields = Object.keys(dirtyFields).length > 0
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
onDirtyChange?.(hasDirtyFields)
|
|
250
|
+
}, [hasDirtyFields, onDirtyChange])
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<LocalizationProvider value={contentLocale ?? null}>
|
|
254
|
+
<FormProvider {...methods}>
|
|
255
|
+
<form
|
|
256
|
+
method='post'
|
|
257
|
+
onSubmit={(e) => {
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
// e.stopPropagation()
|
|
260
|
+
methods.handleSubmit((data) => {
|
|
261
|
+
handleSubmit(new FormData(e.target as HTMLFormElement))
|
|
262
|
+
})(e)
|
|
263
|
+
}}
|
|
264
|
+
encType='multipart/form-data'
|
|
265
|
+
>
|
|
266
|
+
<div className='w-full' data-section-schema-version={configLastUpdated}>
|
|
267
|
+
{/*<ContainerBox title={formType ? t(formType === 'new' ? 'add_new' : 'edit') : undefined}>*/}
|
|
268
|
+
<div className='p-4'>
|
|
269
|
+
<div className=''>
|
|
270
|
+
{data.inputGroups ? (
|
|
271
|
+
<div className='flex flex-col gap-4'>
|
|
272
|
+
{hasNoLocalizedContent ? (
|
|
273
|
+
<div className='rounded border border-amber-300 bg-amber-50 p-4 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'>
|
|
274
|
+
<h3 className='font-semibold'>{t('noLocalizedFields')}</h3>
|
|
275
|
+
<p className='mt-1'>
|
|
276
|
+
{t('noLocalizedFieldsHint', {
|
|
277
|
+
section:
|
|
278
|
+
typeof data.section.title === 'object'
|
|
279
|
+
? (data.section.title?.section ?? '')
|
|
280
|
+
: '',
|
|
281
|
+
file: data.section.configFile,
|
|
282
|
+
})}
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
) : (
|
|
286
|
+
<>
|
|
287
|
+
{data.inputGroups.length > 0
|
|
288
|
+
? data.inputGroups.map((inputGroup, index: number) => {
|
|
289
|
+
const filteredInputs = filterInputsForLocale(
|
|
290
|
+
inputGroup.inputs as any[],
|
|
291
|
+
)
|
|
292
|
+
if (filteredInputs.length === 0) return null
|
|
293
|
+
return (
|
|
294
|
+
<ContainerBox title={inputGroup.groupTitle} key={index}>
|
|
295
|
+
<FormInputs
|
|
296
|
+
inputs={filteredInputs}
|
|
297
|
+
sectionName={data.section.name}
|
|
298
|
+
submitSuccessCount={submitSuccessCount}
|
|
299
|
+
/>
|
|
300
|
+
</ContainerBox>
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
: null}
|
|
304
|
+
{showGallery ? (
|
|
305
|
+
<>
|
|
306
|
+
<div className='w-full'>
|
|
307
|
+
<PhotoGallery
|
|
308
|
+
sectionName={data.section.name}
|
|
309
|
+
gallery={'gallery' in data ? data.gallery : []}
|
|
310
|
+
localized={sectionGallery?.localized === true}
|
|
311
|
+
locale={contentLocale?.code}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
<div className='w-full'>
|
|
315
|
+
<Dropzone ref={dropzoneRef} />
|
|
316
|
+
</div>
|
|
317
|
+
</>
|
|
318
|
+
) : null}
|
|
319
|
+
|
|
320
|
+
{/*{data.section.variants && data.section.variants.length > 0 ? (
|
|
321
|
+
<div className='w-full'>
|
|
322
|
+
<div className='flex flex-col gap-4'>
|
|
323
|
+
{data.section.variants.map((variant, index) => {
|
|
324
|
+
// Only one variant is allowed for now
|
|
325
|
+
// I have to find a way to make multiple variantRef in order to handle multiple variants
|
|
326
|
+
if (index > 0) return
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<NewVariantComponent
|
|
330
|
+
ref={(el) => (variantRef.current[index] = el)}
|
|
331
|
+
key={index}
|
|
332
|
+
section={section}
|
|
333
|
+
variantInfo={variant.info}
|
|
334
|
+
xsrfToken={xsrfToken}
|
|
335
|
+
/>
|
|
336
|
+
)
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
) : null}*/}
|
|
341
|
+
|
|
342
|
+
<div className='flex flex-col gap-3 pb-4'>
|
|
343
|
+
<div className=''>
|
|
344
|
+
<button
|
|
345
|
+
className={classNames({
|
|
346
|
+
'w-full': buttonType === 'big',
|
|
347
|
+
'float-end': buttonType === 'small',
|
|
348
|
+
'bg-linear-to-r rounded px-4 py-2 font-bold text-white drop-shadow-sm': true,
|
|
349
|
+
'from-emerald-700 via-green-700 to-green-500 dark:from-blue-800 dark:via-sky-800 dark:to-slate-500':
|
|
350
|
+
!isSubmitting,
|
|
351
|
+
'from-gray-600 via-gray-500 to-gray-400': isSubmitting,
|
|
352
|
+
})}
|
|
353
|
+
type='submit'
|
|
354
|
+
disabled={isSubmitting}
|
|
355
|
+
>
|
|
356
|
+
{isSubmitting
|
|
357
|
+
? t('loading')
|
|
358
|
+
: t(formType === 'new' ? 'create' : 'save')}
|
|
359
|
+
</button>
|
|
360
|
+
{progressVariant && progress ? (
|
|
361
|
+
isSubmitting ? (
|
|
362
|
+
<div className='mt-0.5'>
|
|
363
|
+
<ProgressBar
|
|
364
|
+
variant={progressVariant}
|
|
365
|
+
value={progress}
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
) : null
|
|
369
|
+
) : null}
|
|
370
|
+
</div>
|
|
371
|
+
{response ? <div className='w-full'>{response}</div> : null}
|
|
372
|
+
</div>
|
|
373
|
+
</>
|
|
374
|
+
)}
|
|
375
|
+
</div>
|
|
376
|
+
) : null}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
{/*</ContainerBox>*/}
|
|
380
|
+
</div>
|
|
381
|
+
</form>
|
|
382
|
+
</FormProvider>
|
|
383
|
+
</LocalizationProvider>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
@@ -70,6 +70,11 @@ function getTimeBoundForSelectedDate(selectedDate: Date | undefined, bound: Date
|
|
|
70
70
|
return dayjs(bound).format('HH:mm:ss')
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function formatHiddenValue(date: Date | undefined, format: DateFieldClientConfig['format']): string {
|
|
74
|
+
if (!date) return ''
|
|
75
|
+
return dayjs(date).format(format !== 'date' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
export default function DateFormInput({ input }: { input: DateFieldClientConfig }) {
|
|
74
79
|
const t = useI18n()
|
|
75
80
|
const { control } = useFormContext()
|
|
@@ -108,16 +113,12 @@ export default function DateFormInput({ input }: { input: DateFieldClientConfig
|
|
|
108
113
|
<input
|
|
109
114
|
type='hidden'
|
|
110
115
|
disabled={field.disabled}
|
|
111
|
-
name={field.name}
|
|
112
|
-
onBlur={field.onBlur}
|
|
113
|
-
value={
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
ref={field.ref}
|
|
119
|
-
className='rounded border p-2'
|
|
120
|
-
/>
|
|
116
|
+
name={field.name}
|
|
117
|
+
onBlur={field.onBlur}
|
|
118
|
+
value={formatHiddenValue(date, input.format)}
|
|
119
|
+
ref={field.ref}
|
|
120
|
+
className='rounded border p-2'
|
|
121
|
+
/>
|
|
121
122
|
|
|
122
123
|
<div className='flex flex-row items-start gap-2'>
|
|
123
124
|
<div>
|
|
@@ -88,7 +88,7 @@ export default function DocumentFormInput({
|
|
|
88
88
|
<CardContent className='flex flex-col gap-1'>
|
|
89
89
|
<div className='mb-2 flex flex-col text-sm'>
|
|
90
90
|
{input.maxFileSize ? (
|
|
91
|
-
<div className='flex flex-wrap items-center gap-2'>
|
|
91
|
+
<div dir='ltr' className='flex flex-wrap items-center gap-2'>
|
|
92
92
|
<InfoIcon size={14} />
|
|
93
93
|
<span>{t('maxFileSize')}:</span>
|
|
94
94
|
<Badge
|
|
@@ -97,7 +97,7 @@ export default function DocumentFormInput({
|
|
|
97
97
|
</div>
|
|
98
98
|
) : null}
|
|
99
99
|
{input.extensions ? (
|
|
100
|
-
<div className='flex flex-wrap items-center gap-2'>
|
|
100
|
+
<div dir='ltr' className='flex flex-wrap items-center gap-2'>
|
|
101
101
|
<InfoIcon size={14} />
|
|
102
102
|
<span>{t('allowedExtensions')}:</span>
|
|
103
103
|
<Badge variant={'secondary'}>{input.extensions.join(', ')}</Badge>
|
|
@@ -122,19 +122,17 @@ export default function PhotoFormInput({
|
|
|
122
122
|
<CardContent className='flex flex-col gap-1'>
|
|
123
123
|
<div className='mb-2 flex flex-col text-sm'>
|
|
124
124
|
{input.size ? (
|
|
125
|
-
<div className='flex flex-wrap items-center gap-2'>
|
|
125
|
+
<div dir='ltr' className='flex flex-wrap items-center gap-2'>
|
|
126
126
|
<InfoIcon size={14} />
|
|
127
127
|
<span>
|
|
128
|
-
{t(
|
|
129
|
-
'strict' in input.size ? 'imageDimensionsMustBe' : 'imageRecommendedDimensions',
|
|
130
|
-
)}
|
|
128
|
+
{t('strict' in input.size ? 'imageDimensionsMustBe' : 'imageRecommendedDimensions')}
|
|
131
129
|
:
|
|
132
130
|
</span>
|
|
133
131
|
<Badge variant={'secondary'}>{`${input.size.width} x ${input.size.height}`}</Badge>
|
|
134
132
|
</div>
|
|
135
133
|
) : null}
|
|
136
134
|
{input.maxFileSize ? (
|
|
137
|
-
<div className='flex flex-wrap items-center gap-2'>
|
|
135
|
+
<div dir='ltr' className='flex flex-wrap items-center gap-2'>
|
|
138
136
|
<InfoIcon size={14} />
|
|
139
137
|
<span>{t('maxFileSize')}:</span>
|
|
140
138
|
<Badge
|
|
@@ -143,7 +141,7 @@ export default function PhotoFormInput({
|
|
|
143
141
|
</div>
|
|
144
142
|
) : null}
|
|
145
143
|
{input.extensions ? (
|
|
146
|
-
<div className='flex flex-wrap items-center gap-2'>
|
|
144
|
+
<div dir='ltr' className='flex flex-wrap items-center gap-2'>
|
|
147
145
|
<InfoIcon size={14} />
|
|
148
146
|
<span>{t('allowedExtensions')}:</span>
|
|
149
147
|
<Badge variant={'secondary'}>{input.extensions.join(', ')}</Badge>
|
|
@@ -172,7 +170,7 @@ export default function PhotoFormInput({
|
|
|
172
170
|
<ChevronRight fontSize='large' />
|
|
173
171
|
<div className='relative h-[150px] w-[150px]'>
|
|
174
172
|
<X
|
|
175
|
-
className='absolute -
|
|
173
|
+
className='absolute -top-3 -right-3 z-10 cursor-pointer rounded-full border-2 border-gray-500 bg-white p-1'
|
|
176
174
|
onClick={clearSelectedFile}
|
|
177
175
|
/>
|
|
178
176
|
<Image
|
|
@@ -209,7 +207,7 @@ export default function PhotoFormInput({
|
|
|
209
207
|
<div className='w-[400px] max-w-full'>
|
|
210
208
|
<button
|
|
211
209
|
type='button'
|
|
212
|
-
className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-linear-to-r from-red-600 to-amber-500 p-2 text-center text-sm font-bold
|
|
210
|
+
className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-linear-to-r from-red-600 to-amber-500 p-2 text-center text-sm font-bold text-white uppercase drop-shadow-sm hover:from-red-400 hover:to-amber-400'
|
|
213
211
|
onClick={() => {
|
|
214
212
|
setModal({
|
|
215
213
|
title: t('deletePhoto'),
|
|
@@ -249,7 +247,7 @@ export default function PhotoFormInput({
|
|
|
249
247
|
<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'>
|
|
250
248
|
<button
|
|
251
249
|
type='button'
|
|
252
|
-
className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-linear-to-r from-blue-700 to-sky-500 p-2 text-center text-sm font-bold
|
|
250
|
+
className='flex w-full flex-wrap items-center justify-center gap-1.5 rounded border bg-linear-to-r from-blue-700 to-sky-500 p-2 text-center text-sm font-bold text-white uppercase drop-shadow-sm hover:from-blue-500 hover:to-sky-400'
|
|
253
251
|
onClick={() => {
|
|
254
252
|
if (fileInputContainerRef.current?.firstChild) {
|
|
255
253
|
;(
|
|
@@ -58,7 +58,7 @@ export default function VideoFormInput({ input, sectionName }: { input: VideoFie
|
|
|
58
58
|
<ChevronRight fontSize='large' />
|
|
59
59
|
<div className='relative'>
|
|
60
60
|
<X
|
|
61
|
-
className='absolute -
|
|
61
|
+
className='absolute -top-3 -right-3 cursor-pointer rounded-full border-2 border-gray-500 bg-white p-1'
|
|
62
62
|
onClick={() => {
|
|
63
63
|
setImage(null)
|
|
64
64
|
setFileName(t('noFileSelected'))
|
|
@@ -99,7 +99,7 @@ export default function VideoFormInput({ input, sectionName }: { input: VideoFie
|
|
|
99
99
|
<div className='w-full flex-1 md:flex-[0.5]'>
|
|
100
100
|
<button
|
|
101
101
|
type='button'
|
|
102
|
-
className='w-full rounded border bg-linear-to-r from-blue-700 to-sky-500 p-2 text-center text-sm font-bold
|
|
102
|
+
className='w-full rounded border bg-linear-to-r from-blue-700 to-sky-500 p-2 text-center text-sm font-bold text-white uppercase drop-shadow-sm'
|
|
103
103
|
onClick={() => {
|
|
104
104
|
if (fileInputContainerRef.current?.firstChild) {
|
|
105
105
|
;(fileInputContainerRef.current.firstChild as HTMLInputElement).click()
|