create-nextjs-cms 0.9.28 → 0.9.30

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 (142) 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)/admins/page.tsx +10 -10
  16. package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +22 -22
  17. package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +15 -15
  18. package/templates/default/app/(rootLayout)/dashboard/page.tsx +70 -70
  19. package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +20 -20
  20. package/templates/default/app/(rootLayout)/layout.tsx +81 -81
  21. package/templates/default/app/(rootLayout)/loading.tsx +10 -10
  22. package/templates/default/app/(rootLayout)/log/page.tsx +7 -7
  23. package/templates/default/app/(rootLayout)/new/[section]/page.tsx +15 -15
  24. package/templates/default/app/(rootLayout)/section/[section]/page.tsx +19 -19
  25. package/templates/default/app/(rootLayout)/settings/page.tsx +13 -13
  26. package/templates/default/app/api/auth/csrf/route.ts +25 -25
  27. package/templates/default/app/api/auth/refresh/route.ts +10 -10
  28. package/templates/default/app/api/auth/route.ts +49 -49
  29. package/templates/default/app/api/auth/session/route.ts +20 -20
  30. package/templates/default/app/api/document/route.ts +165 -165
  31. package/templates/default/app/api/editor/photo/route.ts +49 -49
  32. package/templates/default/app/api/photo/route.ts +27 -27
  33. package/templates/default/app/api/submit/section/item/[slug]/route.ts +95 -95
  34. package/templates/default/app/api/submit/section/item/route.ts +56 -56
  35. package/templates/default/app/api/submit/section/simple/route.ts +86 -86
  36. package/templates/default/app/api/video/route.ts +174 -174
  37. package/templates/default/app/globals.css +236 -236
  38. package/templates/default/cms.config.ts +56 -56
  39. package/templates/default/components/admin/admin-card.tsx +165 -165
  40. package/templates/default/components/admin/admin-edit-page.tsx +124 -124
  41. package/templates/default/components/admin/admin-privilege-card.tsx +184 -184
  42. package/templates/default/components/admin/new-admin-form.tsx +172 -172
  43. package/templates/default/components/container-box.tsx +24 -24
  44. package/templates/default/components/dnd-kit/draggable.tsx +21 -21
  45. package/templates/default/components/dnd-kit/droppable.tsx +20 -20
  46. package/templates/default/components/dnd-kit/sortable-item.tsx +18 -18
  47. package/templates/default/components/feedback/error-component.tsx +16 -16
  48. package/templates/default/components/feedback/info-card.tsx +93 -93
  49. package/templates/default/components/feedback/loading-spinners.tsx +67 -67
  50. package/templates/default/components/feedback/modal.tsx +166 -166
  51. package/templates/default/components/feedback/progress-bar.tsx +48 -48
  52. package/templates/default/components/feedback/tooltip-component.tsx +27 -27
  53. package/templates/default/components/form/form-input-element.tsx +70 -70
  54. package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
  55. package/templates/default/components/form/helpers/util.ts +17 -17
  56. package/templates/default/components/form/inputs/checkbox-form-input.tsx +46 -46
  57. package/templates/default/components/form/inputs/color-form-input.tsx +44 -44
  58. package/templates/default/components/form/inputs/date-form-input.tsx +93 -93
  59. package/templates/default/components/form/inputs/map-form-input.tsx +141 -141
  60. package/templates/default/components/form/inputs/multiple-select-form-input.tsx +85 -85
  61. package/templates/default/components/form/inputs/number-form-input.tsx +43 -43
  62. package/templates/default/components/form/inputs/password-form-input.tsx +47 -47
  63. package/templates/default/components/form/inputs/photo-form-input.tsx +279 -279
  64. package/templates/default/components/form/inputs/rich-text-form-input.tsx +148 -148
  65. package/templates/default/components/form/inputs/select-form-input.tsx +159 -159
  66. package/templates/default/components/form/inputs/slug-form-input.tsx +131 -131
  67. package/templates/default/components/form/inputs/tags-form-input.tsx +255 -255
  68. package/templates/default/components/form/inputs/text-form-input.tsx +61 -61
  69. package/templates/default/components/form/inputs/textarea-form-input.tsx +61 -61
  70. package/templates/default/components/layout/default-nav-items.tsx +3 -3
  71. package/templates/default/components/layout/layout.tsx +84 -84
  72. package/templates/default/components/layout/navbar.tsx +258 -258
  73. package/templates/default/components/layout/sidebar-dropdown-item.tsx +83 -83
  74. package/templates/default/components/layout/sidebar-item.tsx +24 -24
  75. package/templates/default/components/layout/sidebar.tsx +229 -229
  76. package/templates/default/components/layout/theme-provider.tsx +8 -8
  77. package/templates/default/components/layout/theme-toggle.tsx +39 -39
  78. package/templates/default/components/locale/locale-switcher.tsx +98 -98
  79. package/templates/default/components/media/dropzone.tsx +154 -154
  80. package/templates/default/components/media/protected-document.tsx +44 -44
  81. package/templates/default/components/media/protected-image.tsx +143 -143
  82. package/templates/default/components/media/protected-video.tsx +76 -76
  83. package/templates/default/components/multi-select.tsx +1150 -1150
  84. package/templates/default/components/pages/admins-page.tsx +43 -43
  85. package/templates/default/components/pages/browse-page.tsx +106 -106
  86. package/templates/default/components/pages/categorized-section-page.tsx +31 -31
  87. package/templates/default/components/pages/dashboard-page-alt.tsx +45 -45
  88. package/templates/default/components/pages/item-edit-page.tsx +267 -267
  89. package/templates/default/components/pages/log-page.tsx +107 -107
  90. package/templates/default/components/pages/new-page.tsx +183 -183
  91. package/templates/default/components/pages/section-page.tsx +203 -203
  92. package/templates/default/components/pages/settings-page.tsx +232 -232
  93. package/templates/default/components/pagination/pagination-buttons.tsx +147 -147
  94. package/templates/default/components/pagination/pagination.tsx +36 -36
  95. package/templates/default/components/sections/category-delete-confirm-page.tsx +130 -130
  96. package/templates/default/components/sections/category-section-select-input.tsx +139 -139
  97. package/templates/default/components/sections/conditional-fields.tsx +49 -49
  98. package/templates/default/components/sections/section-icon.tsx +8 -8
  99. package/templates/default/components/sections/section-item-card.tsx +143 -143
  100. package/templates/default/components/sections/section-item-status-badge.tsx +17 -17
  101. package/templates/default/components/sections/select-input-buttons.tsx +125 -125
  102. package/templates/default/components/select-box.tsx +98 -98
  103. package/templates/default/components/ui/accordion.tsx +53 -53
  104. package/templates/default/components/ui/alert-dialog.tsx +113 -113
  105. package/templates/default/components/ui/alert.tsx +47 -47
  106. package/templates/default/components/ui/badge.tsx +38 -38
  107. package/templates/default/components/ui/card.tsx +43 -43
  108. package/templates/default/components/ui/command.tsx +137 -137
  109. package/templates/default/components/ui/custom-alert-dialog.tsx +113 -113
  110. package/templates/default/components/ui/custom-dialog.tsx +123 -123
  111. package/templates/default/components/ui/dialog.tsx +123 -123
  112. package/templates/default/components/ui/direction.tsx +22 -22
  113. package/templates/default/components/ui/dropdown-menu.tsx +182 -182
  114. package/templates/default/components/ui/input-group.tsx +54 -54
  115. package/templates/default/components/ui/input.tsx +22 -22
  116. package/templates/default/components/ui/label.tsx +19 -19
  117. package/templates/default/components/ui/popover.tsx +42 -42
  118. package/templates/default/components/ui/progress.tsx +31 -31
  119. package/templates/default/components/ui/scroll-area.tsx +42 -42
  120. package/templates/default/components/ui/select.tsx +165 -165
  121. package/templates/default/components/ui/separator.tsx +28 -28
  122. package/templates/default/components/ui/sheet.tsx +103 -103
  123. package/templates/default/components/ui/spinner.tsx +16 -16
  124. package/templates/default/components/ui/switch.tsx +29 -29
  125. package/templates/default/components/ui/table.tsx +83 -83
  126. package/templates/default/components/ui/tabs.tsx +55 -55
  127. package/templates/default/components/ui/toast.tsx +113 -113
  128. package/templates/default/components/ui/toaster.tsx +35 -35
  129. package/templates/default/components/ui/tooltip.tsx +30 -30
  130. package/templates/default/components/ui/use-toast.ts +187 -187
  131. package/templates/default/drizzle.config.ts +4 -4
  132. package/templates/default/dynamic-schemas/schema.ts +75 -225
  133. package/templates/default/env/env.ts +46 -46
  134. package/templates/default/envConfig.ts +4 -4
  135. package/templates/default/lib/postinstall.js +14 -14
  136. package/templates/default/lib/utils.ts +6 -6
  137. package/templates/default/next-env.d.ts +6 -6
  138. package/templates/default/next.config.ts +24 -24
  139. package/templates/default/package.json +1 -1
  140. package/templates/default/postcss.config.mjs +6 -6
  141. package/templates/default/proxy.ts +32 -32
  142. 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