create-nextjs-cms 0.9.30 → 0.9.31

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 (143) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/helpers/utils.js +16 -16
  4. package/dist/lib/section-creators.js +166 -166
  5. package/package.json +2 -2
  6. package/templates/default/.eslintrc.json +5 -5
  7. package/templates/default/.prettierignore +7 -7
  8. package/templates/default/.prettierrc.json +27 -27
  9. package/templates/default/CHANGELOG.md +140 -140
  10. package/templates/default/_gitignore +57 -57
  11. package/templates/default/app/(auth)/auth/login/LoginPage.tsx +192 -192
  12. package/templates/default/app/(auth)/auth/login/page.tsx +11 -11
  13. package/templates/default/app/(auth)/auth-language-provider.tsx +34 -34
  14. package/templates/default/app/(auth)/layout.tsx +81 -81
  15. package/templates/default/app/(rootLayout)/(plugins)/[...slug]/plugin-server-registry.ts +10 -6
  16. package/templates/default/app/(rootLayout)/admins/page.tsx +10 -10
  17. package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +22 -22
  18. package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +15 -15
  19. package/templates/default/app/(rootLayout)/dashboard/page.tsx +70 -70
  20. package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +20 -20
  21. package/templates/default/app/(rootLayout)/layout.tsx +81 -81
  22. package/templates/default/app/(rootLayout)/loading.tsx +10 -10
  23. package/templates/default/app/(rootLayout)/log/page.tsx +7 -7
  24. package/templates/default/app/(rootLayout)/new/[section]/page.tsx +15 -15
  25. package/templates/default/app/(rootLayout)/section/[section]/page.tsx +19 -19
  26. package/templates/default/app/(rootLayout)/settings/page.tsx +13 -13
  27. package/templates/default/app/api/auth/csrf/route.ts +25 -25
  28. package/templates/default/app/api/auth/refresh/route.ts +10 -10
  29. package/templates/default/app/api/auth/route.ts +49 -49
  30. package/templates/default/app/api/auth/session/route.ts +20 -20
  31. package/templates/default/app/api/document/route.ts +165 -165
  32. package/templates/default/app/api/editor/photo/route.ts +49 -49
  33. package/templates/default/app/api/photo/route.ts +27 -27
  34. package/templates/default/app/api/submit/section/item/[slug]/route.ts +95 -95
  35. package/templates/default/app/api/submit/section/item/route.ts +56 -56
  36. package/templates/default/app/api/submit/section/simple/route.ts +86 -86
  37. package/templates/default/app/api/video/route.ts +174 -174
  38. package/templates/default/app/globals.css +236 -236
  39. package/templates/default/cms.config.ts +56 -56
  40. package/templates/default/components/admin/admin-card.tsx +165 -165
  41. package/templates/default/components/admin/admin-edit-page.tsx +124 -124
  42. package/templates/default/components/admin/admin-privilege-card.tsx +184 -184
  43. package/templates/default/components/admin/new-admin-form.tsx +172 -172
  44. package/templates/default/components/container-box.tsx +24 -24
  45. package/templates/default/components/dnd-kit/draggable.tsx +21 -21
  46. package/templates/default/components/dnd-kit/droppable.tsx +20 -20
  47. package/templates/default/components/dnd-kit/sortable-item.tsx +18 -18
  48. package/templates/default/components/feedback/error-component.tsx +16 -16
  49. package/templates/default/components/feedback/info-card.tsx +93 -93
  50. package/templates/default/components/feedback/loading-spinners.tsx +67 -67
  51. package/templates/default/components/feedback/modal.tsx +166 -166
  52. package/templates/default/components/feedback/progress-bar.tsx +48 -48
  53. package/templates/default/components/feedback/tooltip-component.tsx +27 -27
  54. package/templates/default/components/form/form-input-element.tsx +70 -70
  55. package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
  56. package/templates/default/components/form/helpers/util.ts +17 -17
  57. package/templates/default/components/form/inputs/checkbox-form-input.tsx +46 -46
  58. package/templates/default/components/form/inputs/color-form-input.tsx +44 -44
  59. package/templates/default/components/form/inputs/date-form-input.tsx +93 -93
  60. package/templates/default/components/form/inputs/map-form-input.tsx +141 -141
  61. package/templates/default/components/form/inputs/multiple-select-form-input.tsx +85 -85
  62. package/templates/default/components/form/inputs/number-form-input.tsx +43 -43
  63. package/templates/default/components/form/inputs/password-form-input.tsx +47 -47
  64. package/templates/default/components/form/inputs/photo-form-input.tsx +279 -279
  65. package/templates/default/components/form/inputs/rich-text-form-input.tsx +148 -148
  66. package/templates/default/components/form/inputs/select-form-input.tsx +159 -159
  67. package/templates/default/components/form/inputs/slug-form-input.tsx +131 -131
  68. package/templates/default/components/form/inputs/tags-form-input.tsx +255 -255
  69. package/templates/default/components/form/inputs/text-form-input.tsx +61 -61
  70. package/templates/default/components/form/inputs/textarea-form-input.tsx +61 -61
  71. package/templates/default/components/layout/default-nav-items.tsx +3 -3
  72. package/templates/default/components/layout/layout.tsx +84 -84
  73. package/templates/default/components/layout/navbar.tsx +258 -258
  74. package/templates/default/components/layout/sidebar-dropdown-item.tsx +83 -83
  75. package/templates/default/components/layout/sidebar-item.tsx +24 -24
  76. package/templates/default/components/layout/sidebar.tsx +229 -229
  77. package/templates/default/components/layout/theme-provider.tsx +8 -8
  78. package/templates/default/components/layout/theme-toggle.tsx +39 -39
  79. package/templates/default/components/locale/locale-switcher.tsx +98 -98
  80. package/templates/default/components/media/dropzone.tsx +154 -154
  81. package/templates/default/components/media/protected-document.tsx +44 -44
  82. package/templates/default/components/media/protected-image.tsx +143 -143
  83. package/templates/default/components/media/protected-video.tsx +76 -76
  84. package/templates/default/components/multi-select.tsx +1150 -1150
  85. package/templates/default/components/pages/admins-page.tsx +43 -43
  86. package/templates/default/components/pages/browse-page.tsx +106 -106
  87. package/templates/default/components/pages/categorized-section-page.tsx +31 -31
  88. package/templates/default/components/pages/dashboard-page-alt.tsx +45 -45
  89. package/templates/default/components/pages/item-edit-page.tsx +267 -267
  90. package/templates/default/components/pages/log-page.tsx +107 -107
  91. package/templates/default/components/pages/new-page.tsx +183 -183
  92. package/templates/default/components/pages/section-page.tsx +203 -203
  93. package/templates/default/components/pages/settings-page.tsx +232 -232
  94. package/templates/default/components/pagination/pagination-buttons.tsx +147 -147
  95. package/templates/default/components/pagination/pagination.tsx +36 -36
  96. package/templates/default/components/sections/category-delete-confirm-page.tsx +130 -130
  97. package/templates/default/components/sections/category-section-select-input.tsx +139 -139
  98. package/templates/default/components/sections/conditional-fields.tsx +49 -49
  99. package/templates/default/components/sections/section-icon.tsx +8 -8
  100. package/templates/default/components/sections/section-item-card.tsx +143 -143
  101. package/templates/default/components/sections/section-item-status-badge.tsx +17 -17
  102. package/templates/default/components/sections/select-input-buttons.tsx +125 -125
  103. package/templates/default/components/select-box.tsx +98 -98
  104. package/templates/default/components/ui/accordion.tsx +53 -53
  105. package/templates/default/components/ui/alert-dialog.tsx +113 -113
  106. package/templates/default/components/ui/alert.tsx +47 -47
  107. package/templates/default/components/ui/badge.tsx +38 -38
  108. package/templates/default/components/ui/card.tsx +43 -43
  109. package/templates/default/components/ui/command.tsx +137 -137
  110. package/templates/default/components/ui/custom-alert-dialog.tsx +113 -113
  111. package/templates/default/components/ui/custom-dialog.tsx +123 -123
  112. package/templates/default/components/ui/dialog.tsx +123 -123
  113. package/templates/default/components/ui/direction.tsx +22 -22
  114. package/templates/default/components/ui/dropdown-menu.tsx +182 -182
  115. package/templates/default/components/ui/input-group.tsx +54 -54
  116. package/templates/default/components/ui/input.tsx +22 -22
  117. package/templates/default/components/ui/label.tsx +19 -19
  118. package/templates/default/components/ui/popover.tsx +42 -42
  119. package/templates/default/components/ui/progress.tsx +31 -31
  120. package/templates/default/components/ui/scroll-area.tsx +42 -42
  121. package/templates/default/components/ui/select.tsx +165 -165
  122. package/templates/default/components/ui/separator.tsx +28 -28
  123. package/templates/default/components/ui/sheet.tsx +103 -103
  124. package/templates/default/components/ui/spinner.tsx +16 -16
  125. package/templates/default/components/ui/switch.tsx +29 -29
  126. package/templates/default/components/ui/table.tsx +83 -83
  127. package/templates/default/components/ui/tabs.tsx +55 -55
  128. package/templates/default/components/ui/toast.tsx +113 -113
  129. package/templates/default/components/ui/toaster.tsx +35 -35
  130. package/templates/default/components/ui/tooltip.tsx +30 -30
  131. package/templates/default/components/ui/use-toast.ts +187 -187
  132. package/templates/default/drizzle.config.ts +4 -4
  133. package/templates/default/dynamic-schemas/schema.ts +225 -75
  134. package/templates/default/env/env.ts +46 -46
  135. package/templates/default/envConfig.ts +4 -4
  136. package/templates/default/lib/postinstall.js +14 -14
  137. package/templates/default/lib/utils.ts +6 -6
  138. package/templates/default/next-env.d.ts +6 -6
  139. package/templates/default/next.config.ts +24 -24
  140. package/templates/default/package.json +1 -1
  141. package/templates/default/postcss.config.mjs +6 -6
  142. package/templates/default/proxy.ts +32 -32
  143. package/templates/default/tsconfig.json +48 -48
@@ -1,98 +1,98 @@
1
- 'use client'
2
-
3
- import Link from 'next/link'
4
- import { cn } from '@/lib/utils'
5
- import { Check } from 'lucide-react'
6
- import { useI18n } from 'nextjs-cms/translations/client'
7
- import { useSession } from 'nextjs-cms/auth/react'
8
- import ContainerBox from '@/components/container-box'
9
- import { Alert, AlertDescription } from '@/components/ui/alert'
10
-
11
- type LocaleConfig = {
12
- code: string
13
- label: string
14
- rtl?: boolean
15
- }
16
-
17
- type Props = {
18
- section: string
19
- itemId?: string
20
- currentLocale: LocaleConfig
21
- defaultLocale: LocaleConfig
22
- existingTranslations: string[]
23
- locales: LocaleConfig[]
24
- hasUnsavedChanges?: boolean
25
- developerOnly?: boolean
26
- /** Base path for locale links. Defaults to `/edit/${section}/${itemId}` for hasItems sections. */
27
- basePath?: string
28
- }
29
-
30
- export default function LocaleSwitcher({
31
- section,
32
- itemId,
33
- currentLocale,
34
- defaultLocale,
35
- existingTranslations,
36
- locales,
37
- hasUnsavedChanges,
38
- developerOnly,
39
- basePath,
40
- }: Props) {
41
- const t = useI18n()
42
- const resolvedBasePath = basePath ?? `/edit/${section}/${itemId}`
43
-
44
- const getHref = (localeCode: string) => {
45
- if (localeCode === defaultLocale.code) {
46
- return resolvedBasePath
47
- }
48
- return `${resolvedBasePath}?locale=${localeCode}`
49
- }
50
-
51
- const handleClick = (e: React.MouseEvent, localeCode: string) => {
52
- if (localeCode === currentLocale.code) {
53
- e.preventDefault()
54
- return
55
- }
56
- if (hasUnsavedChanges) {
57
- if (!window.confirm(t('unsavedChangesConfirm'))) {
58
- e.preventDefault()
59
- }
60
- }
61
- }
62
-
63
- return (
64
- <ContainerBox title={t('localesHeading')}>
65
- <div className='flex flex-wrap items-center gap-2'>
66
- {locales.map((locale) => {
67
- const isActive = locale.code === currentLocale.code
68
- const hasTranslation =
69
- locale.code === defaultLocale.code || existingTranslations.includes(locale.code)
70
- const isDefault = locale.code === defaultLocale.code
71
-
72
- return (
73
- <Link
74
- key={locale.code}
75
- href={getHref(locale.code)}
76
- onClick={(e) => handleClick(e, locale.code)}
77
- className={cn(
78
- 'border-primary/20 inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors',
79
- isActive
80
- ? 'bg-success text-success-foreground'
81
- : 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
82
- )}
83
- >
84
- {locale.label}
85
- {isDefault && <span className='text-[10px] opacity-70'>{t('baseLocaleBadge')}</span>}
86
- {hasTranslation && !isDefault && <Check className='h-3 w-3' />}
87
- </Link>
88
- )
89
- })}
90
- </div>
91
- {developerOnly && (
92
- <Alert variant='info' className='mt-2 w-full'>
93
- <AlertDescription className='text-md font-bold'>{t('localeSwitcherDevOnly')}</AlertDescription>
94
- </Alert>
95
- )}
96
- </ContainerBox>
97
- )
98
- }
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { cn } from '@/lib/utils'
5
+ import { Check } from 'lucide-react'
6
+ import { useI18n } from 'nextjs-cms/translations/client'
7
+ import { useSession } from 'nextjs-cms/auth/react'
8
+ import ContainerBox from '@/components/container-box'
9
+ import { Alert, AlertDescription } from '@/components/ui/alert'
10
+
11
+ type LocaleConfig = {
12
+ code: string
13
+ label: string
14
+ rtl?: boolean
15
+ }
16
+
17
+ type Props = {
18
+ section: string
19
+ itemId?: string
20
+ currentLocale: LocaleConfig
21
+ defaultLocale: LocaleConfig
22
+ existingTranslations: string[]
23
+ locales: LocaleConfig[]
24
+ hasUnsavedChanges?: boolean
25
+ developerOnly?: boolean
26
+ /** Base path for locale links. Defaults to `/edit/${section}/${itemId}` for hasItems sections. */
27
+ basePath?: string
28
+ }
29
+
30
+ export default function LocaleSwitcher({
31
+ section,
32
+ itemId,
33
+ currentLocale,
34
+ defaultLocale,
35
+ existingTranslations,
36
+ locales,
37
+ hasUnsavedChanges,
38
+ developerOnly,
39
+ basePath,
40
+ }: Props) {
41
+ const t = useI18n()
42
+ const resolvedBasePath = basePath ?? `/edit/${section}/${itemId}`
43
+
44
+ const getHref = (localeCode: string) => {
45
+ if (localeCode === defaultLocale.code) {
46
+ return resolvedBasePath
47
+ }
48
+ return `${resolvedBasePath}?locale=${localeCode}`
49
+ }
50
+
51
+ const handleClick = (e: React.MouseEvent, localeCode: string) => {
52
+ if (localeCode === currentLocale.code) {
53
+ e.preventDefault()
54
+ return
55
+ }
56
+ if (hasUnsavedChanges) {
57
+ if (!window.confirm(t('unsavedChangesConfirm'))) {
58
+ e.preventDefault()
59
+ }
60
+ }
61
+ }
62
+
63
+ return (
64
+ <ContainerBox title={t('localesHeading')}>
65
+ <div className='flex flex-wrap items-center gap-2'>
66
+ {locales.map((locale) => {
67
+ const isActive = locale.code === currentLocale.code
68
+ const hasTranslation =
69
+ locale.code === defaultLocale.code || existingTranslations.includes(locale.code)
70
+ const isDefault = locale.code === defaultLocale.code
71
+
72
+ return (
73
+ <Link
74
+ key={locale.code}
75
+ href={getHref(locale.code)}
76
+ onClick={(e) => handleClick(e, locale.code)}
77
+ className={cn(
78
+ 'border-primary/20 inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors',
79
+ isActive
80
+ ? 'bg-success text-success-foreground'
81
+ : 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
82
+ )}
83
+ >
84
+ {locale.label}
85
+ {isDefault && <span className='text-[10px] opacity-70'>{t('baseLocaleBadge')}</span>}
86
+ {hasTranslation && !isDefault && <Check className='h-3 w-3' />}
87
+ </Link>
88
+ )
89
+ })}
90
+ </div>
91
+ {developerOnly && (
92
+ <Alert variant='info' className='mt-2 w-full'>
93
+ <AlertDescription className='text-md font-bold'>{t('localeSwitcherDevOnly')}</AlertDescription>
94
+ </Alert>
95
+ )}
96
+ </ContainerBox>
97
+ )
98
+ }
@@ -1,154 +1,154 @@
1
- import React, { CSSProperties, forwardRef, Ref, useEffect, useImperativeHandle, useState } from 'react'
2
- import { DropEvent, FileRejection, useDropzone } from 'react-dropzone'
3
- import { DropzoneFile } from 'nextjs-cms/core/types'
4
- import ContainerBox from '@/components/container-box'
5
- import { useI18n } from 'nextjs-cms/translations/client'
6
- import { MinusIcon } from '@radix-ui/react-icons'
7
- import { useToast } from '@/components/ui/use-toast'
8
-
9
- const thumb: CSSProperties = {
10
- display: 'inline-flex',
11
- borderRadius: 2,
12
- border: '1px solid #eaeaea',
13
- marginBottom: 8,
14
- marginRight: 8,
15
- width: 100,
16
- height: 100,
17
- padding: 4,
18
- boxSizing: 'border-box',
19
- }
20
-
21
- const thumbInner: CSSProperties = {
22
- display: 'flex',
23
- minWidth: 0,
24
- overflow: 'hidden',
25
- }
26
-
27
- const img: CSSProperties = {
28
- display: 'block',
29
- width: 'auto',
30
- height: '100%',
31
- }
32
-
33
- export interface DropzoneHandles {
34
- getFiles: () => File[]
35
- removeFiles: () => void
36
- }
37
-
38
- function Dropzone(props: any, ref: Ref<DropzoneHandles>) {
39
- const t = useI18n()
40
- const acceptedFileTypes = {
41
- 'image/png': ['.png'],
42
- 'image/jpeg': ['.jpg', '.jpeg'],
43
- 'image/webp': ['.webp'],
44
- }
45
- const [files, setFiles] = useState<DropzoneFile[]>([])
46
- const { toast } = useToast()
47
- const { getRootProps, getInputProps } = useDropzone({
48
- accept: acceptedFileTypes,
49
- maxFiles: 10,
50
- onDrop: (acceptedFiles: File[]) => {
51
- // First, check if the file is already in the list
52
- const filteredFiles = acceptedFiles.filter((file) => !files.some((prevFile) => prevFile.name === file.name))
53
- setFiles((prevFiles) => [
54
- ...prevFiles,
55
- ...filteredFiles.map((file: File) =>
56
- Object.assign(file, {
57
- preview: URL.createObjectURL(file),
58
- }),
59
- ),
60
- ])
61
- },
62
-
63
- onDropRejected: (rejectedFiles: FileRejection[], e: DropEvent) => {
64
- toast({
65
- variant: 'destructive',
66
- title: t('deleteGalleryPhoto'),
67
- description: rejectedFiles[0]?.errors[0]?.message
68
- ? rejectedFiles[0].errors[0].message
69
- : t('error'),
70
- })
71
- },
72
-
73
- onError: (error: any) => {
74
- toast({
75
- variant: 'destructive',
76
- title: t('deleteGalleryPhoto'),
77
- description: error.displayName,
78
- })
79
- },
80
- })
81
-
82
- useImperativeHandle(ref, () => ({
83
- getFiles: () => files,
84
- removeFiles: () => {
85
- files.forEach((file: any) => URL.revokeObjectURL(file.preview))
86
- setFiles([])
87
- },
88
- }))
89
-
90
- const thumbs = files.map((file: DropzoneFile, index: number) => (
91
- <div style={thumb} key={`${file.name}-${index}`} className='relative'>
92
- {/* Delete Button */}
93
- <button
94
- type='button'
95
- className='absolute -end-1 -top-1 rounded-full bg-red-500 p-1'
96
- onClick={() => {
97
- URL.revokeObjectURL(file.preview)
98
- setFiles((prevFiles) =>
99
- prevFiles.filter((prevFile) => {
100
- if (prevFile.name !== file.name) {
101
- // Create a new preview url for the file
102
- prevFile.preview = URL.createObjectURL(prevFile)
103
- return prevFile
104
- }
105
- }),
106
- )
107
- }}
108
- >
109
- <MinusIcon className='text-white' />
110
- </button>
111
- <div style={thumbInner}>
112
- <img
113
- alt={file.name}
114
- src={file.preview}
115
- style={img}
116
- // Revoke data uri after image is loaded, will prevent memory leaks
117
- // (see: https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL)
118
- // Notice: However, I can't use it because I'm re-rendering the thumbs when removing a file from the list
119
- /*
120
- onLoad={() => {
121
- URL.revokeObjectURL(file.preview)
122
- }}
123
- */
124
- />
125
- </div>
126
- </div>
127
- ))
128
-
129
- useEffect(() => {
130
- // Make sure to revoke the data uris to avoid memory leaks, will run on unmount
131
- return () => files.forEach((file: any) => URL.revokeObjectURL(file.preview))
132
- }, [])
133
-
134
- return (
135
- <ContainerBox title={t('uploadPhotosToGallery')}>
136
- <section className='container mt-4 rounded-2xl border-4 border-dashed border-gray-400 bg-accent py-8'>
137
- <div {...getRootProps({ className: 'dropzone' })}>
138
- <input {...getInputProps()} />
139
- <div className='flex flex-col items-center justify-center'>
140
- <div className='p-4 text-xl font-semibold text-card-foreground'>
141
- {t('dropzoneText')}
142
- </div>
143
- <div className='text-muted-foreground'>
144
- {t('accepts')}: {Object.values(acceptedFileTypes).flat().join(', ')}
145
- </div>
146
- </div>
147
- </div>
148
- {thumbs?.length > 0 && <aside className='mt-4 flex flex-row flex-wrap gap-2'>{thumbs}</aside>}
149
- </section>
150
- </ContainerBox>
151
- )
152
- }
153
-
154
- export default forwardRef(Dropzone)
1
+ import React, { CSSProperties, forwardRef, Ref, useEffect, useImperativeHandle, useState } from 'react'
2
+ import { DropEvent, FileRejection, useDropzone } from 'react-dropzone'
3
+ import { DropzoneFile } from 'nextjs-cms/core/types'
4
+ import ContainerBox from '@/components/container-box'
5
+ import { useI18n } from 'nextjs-cms/translations/client'
6
+ import { MinusIcon } from '@radix-ui/react-icons'
7
+ import { useToast } from '@/components/ui/use-toast'
8
+
9
+ const thumb: CSSProperties = {
10
+ display: 'inline-flex',
11
+ borderRadius: 2,
12
+ border: '1px solid #eaeaea',
13
+ marginBottom: 8,
14
+ marginRight: 8,
15
+ width: 100,
16
+ height: 100,
17
+ padding: 4,
18
+ boxSizing: 'border-box',
19
+ }
20
+
21
+ const thumbInner: CSSProperties = {
22
+ display: 'flex',
23
+ minWidth: 0,
24
+ overflow: 'hidden',
25
+ }
26
+
27
+ const img: CSSProperties = {
28
+ display: 'block',
29
+ width: 'auto',
30
+ height: '100%',
31
+ }
32
+
33
+ export interface DropzoneHandles {
34
+ getFiles: () => File[]
35
+ removeFiles: () => void
36
+ }
37
+
38
+ function Dropzone(props: any, ref: Ref<DropzoneHandles>) {
39
+ const t = useI18n()
40
+ const acceptedFileTypes = {
41
+ 'image/png': ['.png'],
42
+ 'image/jpeg': ['.jpg', '.jpeg'],
43
+ 'image/webp': ['.webp'],
44
+ }
45
+ const [files, setFiles] = useState<DropzoneFile[]>([])
46
+ const { toast } = useToast()
47
+ const { getRootProps, getInputProps } = useDropzone({
48
+ accept: acceptedFileTypes,
49
+ maxFiles: 10,
50
+ onDrop: (acceptedFiles: File[]) => {
51
+ // First, check if the file is already in the list
52
+ const filteredFiles = acceptedFiles.filter((file) => !files.some((prevFile) => prevFile.name === file.name))
53
+ setFiles((prevFiles) => [
54
+ ...prevFiles,
55
+ ...filteredFiles.map((file: File) =>
56
+ Object.assign(file, {
57
+ preview: URL.createObjectURL(file),
58
+ }),
59
+ ),
60
+ ])
61
+ },
62
+
63
+ onDropRejected: (rejectedFiles: FileRejection[], e: DropEvent) => {
64
+ toast({
65
+ variant: 'destructive',
66
+ title: t('deleteGalleryPhoto'),
67
+ description: rejectedFiles[0]?.errors[0]?.message
68
+ ? rejectedFiles[0].errors[0].message
69
+ : t('error'),
70
+ })
71
+ },
72
+
73
+ onError: (error: any) => {
74
+ toast({
75
+ variant: 'destructive',
76
+ title: t('deleteGalleryPhoto'),
77
+ description: error.displayName,
78
+ })
79
+ },
80
+ })
81
+
82
+ useImperativeHandle(ref, () => ({
83
+ getFiles: () => files,
84
+ removeFiles: () => {
85
+ files.forEach((file: any) => URL.revokeObjectURL(file.preview))
86
+ setFiles([])
87
+ },
88
+ }))
89
+
90
+ const thumbs = files.map((file: DropzoneFile, index: number) => (
91
+ <div style={thumb} key={`${file.name}-${index}`} className='relative'>
92
+ {/* Delete Button */}
93
+ <button
94
+ type='button'
95
+ className='absolute -end-1 -top-1 rounded-full bg-red-500 p-1'
96
+ onClick={() => {
97
+ URL.revokeObjectURL(file.preview)
98
+ setFiles((prevFiles) =>
99
+ prevFiles.filter((prevFile) => {
100
+ if (prevFile.name !== file.name) {
101
+ // Create a new preview url for the file
102
+ prevFile.preview = URL.createObjectURL(prevFile)
103
+ return prevFile
104
+ }
105
+ }),
106
+ )
107
+ }}
108
+ >
109
+ <MinusIcon className='text-white' />
110
+ </button>
111
+ <div style={thumbInner}>
112
+ <img
113
+ alt={file.name}
114
+ src={file.preview}
115
+ style={img}
116
+ // Revoke data uri after image is loaded, will prevent memory leaks
117
+ // (see: https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL)
118
+ // Notice: However, I can't use it because I'm re-rendering the thumbs when removing a file from the list
119
+ /*
120
+ onLoad={() => {
121
+ URL.revokeObjectURL(file.preview)
122
+ }}
123
+ */
124
+ />
125
+ </div>
126
+ </div>
127
+ ))
128
+
129
+ useEffect(() => {
130
+ // Make sure to revoke the data uris to avoid memory leaks, will run on unmount
131
+ return () => files.forEach((file: any) => URL.revokeObjectURL(file.preview))
132
+ }, [])
133
+
134
+ return (
135
+ <ContainerBox title={t('uploadPhotosToGallery')}>
136
+ <section className='container mt-4 rounded-2xl border-4 border-dashed border-gray-400 bg-accent py-8'>
137
+ <div {...getRootProps({ className: 'dropzone' })}>
138
+ <input {...getInputProps()} />
139
+ <div className='flex flex-col items-center justify-center'>
140
+ <div className='p-4 text-xl font-semibold text-card-foreground'>
141
+ {t('dropzoneText')}
142
+ </div>
143
+ <div className='text-muted-foreground'>
144
+ {t('accepts')}: {Object.values(acceptedFileTypes).flat().join(', ')}
145
+ </div>
146
+ </div>
147
+ </div>
148
+ {thumbs?.length > 0 && <aside className='mt-4 flex flex-row flex-wrap gap-2'>{thumbs}</aside>}
149
+ </section>
150
+ </ContainerBox>
151
+ )
152
+ }
153
+
154
+ export default forwardRef(Dropzone)
@@ -1,44 +1,44 @@
1
- import React, { useState } from 'react'
2
-
3
- const ProtectedDocument = ({
4
- section,
5
- fieldName,
6
- file,
7
- width,
8
- height,
9
- className = 'rounded-3xl object-cover',
10
- }: {
11
- section: string
12
- fieldName: string
13
- file: string
14
- width: number
15
- height: number
16
- className?: string
17
- }) => {
18
- const [loading, setLoading] = useState(true)
19
-
20
- const params = new URLSearchParams({
21
- name: file,
22
- sectionName: section,
23
- fieldName: fieldName,
24
- })
25
- const url = `/api/document?${params.toString()}`
26
-
27
- return (
28
- <div className={className}>
29
- {loading && (
30
- <div className='animate-pulse bg-gray-500' style={{ width, height }} />
31
- )}
32
- <embed
33
- src={url}
34
- className='max-w-full'
35
- width={width}
36
- height={height}
37
- onLoad={() => setLoading(false)}
38
- style={loading ? { width: 0, height: 0, position: 'absolute' } : undefined}
39
- />
40
- </div>
41
- )
42
- }
43
-
44
- export default ProtectedDocument
1
+ import React, { useState } from 'react'
2
+
3
+ const ProtectedDocument = ({
4
+ section,
5
+ fieldName,
6
+ file,
7
+ width,
8
+ height,
9
+ className = 'rounded-3xl object-cover',
10
+ }: {
11
+ section: string
12
+ fieldName: string
13
+ file: string
14
+ width: number
15
+ height: number
16
+ className?: string
17
+ }) => {
18
+ const [loading, setLoading] = useState(true)
19
+
20
+ const params = new URLSearchParams({
21
+ name: file,
22
+ sectionName: section,
23
+ fieldName: fieldName,
24
+ })
25
+ const url = `/api/document?${params.toString()}`
26
+
27
+ return (
28
+ <div className={className}>
29
+ {loading && (
30
+ <div className='animate-pulse bg-gray-500' style={{ width, height }} />
31
+ )}
32
+ <embed
33
+ src={url}
34
+ className='max-w-full'
35
+ width={width}
36
+ height={height}
37
+ onLoad={() => setLoading(false)}
38
+ style={loading ? { width: 0, height: 0, position: 'absolute' } : undefined}
39
+ />
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default ProtectedDocument