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.
- package/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/helpers/utils.js +16 -16
- package/dist/lib/section-creators.js +166 -166
- package/package.json +2 -2
- package/templates/default/.eslintrc.json +5 -5
- package/templates/default/.prettierignore +7 -7
- package/templates/default/.prettierrc.json +27 -27
- package/templates/default/CHANGELOG.md +140 -140
- package/templates/default/_gitignore +57 -57
- package/templates/default/app/(auth)/auth/login/LoginPage.tsx +192 -192
- package/templates/default/app/(auth)/auth/login/page.tsx +11 -11
- package/templates/default/app/(auth)/auth-language-provider.tsx +34 -34
- package/templates/default/app/(auth)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/admins/page.tsx +10 -10
- package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +22 -22
- package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/dashboard/page.tsx +70 -70
- package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +20 -20
- package/templates/default/app/(rootLayout)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/loading.tsx +10 -10
- package/templates/default/app/(rootLayout)/log/page.tsx +7 -7
- package/templates/default/app/(rootLayout)/new/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/section/[section]/page.tsx +19 -19
- package/templates/default/app/(rootLayout)/settings/page.tsx +13 -13
- package/templates/default/app/api/auth/csrf/route.ts +25 -25
- package/templates/default/app/api/auth/refresh/route.ts +10 -10
- package/templates/default/app/api/auth/route.ts +49 -49
- package/templates/default/app/api/auth/session/route.ts +20 -20
- package/templates/default/app/api/document/route.ts +165 -165
- package/templates/default/app/api/editor/photo/route.ts +49 -49
- package/templates/default/app/api/photo/route.ts +27 -27
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +95 -95
- package/templates/default/app/api/submit/section/item/route.ts +56 -56
- package/templates/default/app/api/submit/section/simple/route.ts +86 -86
- package/templates/default/app/api/video/route.ts +174 -174
- package/templates/default/app/globals.css +236 -236
- package/templates/default/cms.config.ts +56 -56
- package/templates/default/components/admin/admin-card.tsx +165 -165
- package/templates/default/components/admin/admin-edit-page.tsx +124 -124
- package/templates/default/components/admin/admin-privilege-card.tsx +184 -184
- package/templates/default/components/admin/new-admin-form.tsx +172 -172
- package/templates/default/components/container-box.tsx +24 -24
- package/templates/default/components/dnd-kit/draggable.tsx +21 -21
- package/templates/default/components/dnd-kit/droppable.tsx +20 -20
- package/templates/default/components/dnd-kit/sortable-item.tsx +18 -18
- package/templates/default/components/feedback/error-component.tsx +16 -16
- package/templates/default/components/feedback/info-card.tsx +93 -93
- package/templates/default/components/feedback/loading-spinners.tsx +67 -67
- package/templates/default/components/feedback/modal.tsx +166 -166
- package/templates/default/components/feedback/progress-bar.tsx +48 -48
- package/templates/default/components/feedback/tooltip-component.tsx +27 -27
- package/templates/default/components/form/form-input-element.tsx +70 -70
- package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
- package/templates/default/components/form/helpers/util.ts +17 -17
- package/templates/default/components/form/inputs/checkbox-form-input.tsx +46 -46
- package/templates/default/components/form/inputs/color-form-input.tsx +44 -44
- package/templates/default/components/form/inputs/date-form-input.tsx +93 -93
- package/templates/default/components/form/inputs/map-form-input.tsx +141 -141
- package/templates/default/components/form/inputs/multiple-select-form-input.tsx +85 -85
- package/templates/default/components/form/inputs/number-form-input.tsx +43 -43
- package/templates/default/components/form/inputs/password-form-input.tsx +47 -47
- package/templates/default/components/form/inputs/photo-form-input.tsx +279 -279
- package/templates/default/components/form/inputs/rich-text-form-input.tsx +148 -148
- package/templates/default/components/form/inputs/select-form-input.tsx +159 -159
- package/templates/default/components/form/inputs/slug-form-input.tsx +131 -131
- package/templates/default/components/form/inputs/tags-form-input.tsx +255 -255
- package/templates/default/components/form/inputs/text-form-input.tsx +61 -61
- package/templates/default/components/form/inputs/textarea-form-input.tsx +61 -61
- package/templates/default/components/layout/default-nav-items.tsx +3 -3
- package/templates/default/components/layout/layout.tsx +84 -84
- package/templates/default/components/layout/navbar.tsx +258 -258
- package/templates/default/components/layout/sidebar-dropdown-item.tsx +83 -83
- package/templates/default/components/layout/sidebar-item.tsx +24 -24
- package/templates/default/components/layout/sidebar.tsx +229 -229
- package/templates/default/components/layout/theme-provider.tsx +8 -8
- package/templates/default/components/layout/theme-toggle.tsx +39 -39
- package/templates/default/components/locale/locale-switcher.tsx +98 -98
- package/templates/default/components/media/dropzone.tsx +154 -154
- package/templates/default/components/media/protected-document.tsx +44 -44
- package/templates/default/components/media/protected-image.tsx +143 -143
- package/templates/default/components/media/protected-video.tsx +76 -76
- package/templates/default/components/multi-select.tsx +1150 -1150
- package/templates/default/components/pages/admins-page.tsx +43 -43
- package/templates/default/components/pages/browse-page.tsx +106 -106
- package/templates/default/components/pages/categorized-section-page.tsx +31 -31
- package/templates/default/components/pages/dashboard-page-alt.tsx +45 -45
- package/templates/default/components/pages/item-edit-page.tsx +267 -267
- package/templates/default/components/pages/log-page.tsx +107 -107
- package/templates/default/components/pages/new-page.tsx +183 -183
- package/templates/default/components/pages/section-page.tsx +203 -203
- package/templates/default/components/pages/settings-page.tsx +232 -232
- package/templates/default/components/pagination/pagination-buttons.tsx +147 -147
- package/templates/default/components/pagination/pagination.tsx +36 -36
- package/templates/default/components/sections/category-delete-confirm-page.tsx +130 -130
- package/templates/default/components/sections/category-section-select-input.tsx +139 -139
- package/templates/default/components/sections/conditional-fields.tsx +49 -49
- package/templates/default/components/sections/section-icon.tsx +8 -8
- package/templates/default/components/sections/section-item-card.tsx +143 -143
- package/templates/default/components/sections/section-item-status-badge.tsx +17 -17
- package/templates/default/components/sections/select-input-buttons.tsx +125 -125
- package/templates/default/components/select-box.tsx +98 -98
- package/templates/default/components/ui/accordion.tsx +53 -53
- package/templates/default/components/ui/alert-dialog.tsx +113 -113
- package/templates/default/components/ui/alert.tsx +47 -47
- package/templates/default/components/ui/badge.tsx +38 -38
- package/templates/default/components/ui/card.tsx +43 -43
- package/templates/default/components/ui/command.tsx +137 -137
- package/templates/default/components/ui/custom-alert-dialog.tsx +113 -113
- package/templates/default/components/ui/custom-dialog.tsx +123 -123
- package/templates/default/components/ui/dialog.tsx +123 -123
- package/templates/default/components/ui/direction.tsx +22 -22
- package/templates/default/components/ui/dropdown-menu.tsx +182 -182
- package/templates/default/components/ui/input-group.tsx +54 -54
- package/templates/default/components/ui/input.tsx +22 -22
- package/templates/default/components/ui/label.tsx +19 -19
- package/templates/default/components/ui/popover.tsx +42 -42
- package/templates/default/components/ui/progress.tsx +31 -31
- package/templates/default/components/ui/scroll-area.tsx +42 -42
- package/templates/default/components/ui/select.tsx +165 -165
- package/templates/default/components/ui/separator.tsx +28 -28
- package/templates/default/components/ui/sheet.tsx +103 -103
- package/templates/default/components/ui/spinner.tsx +16 -16
- package/templates/default/components/ui/switch.tsx +29 -29
- package/templates/default/components/ui/table.tsx +83 -83
- package/templates/default/components/ui/tabs.tsx +55 -55
- package/templates/default/components/ui/toast.tsx +113 -113
- package/templates/default/components/ui/toaster.tsx +35 -35
- package/templates/default/components/ui/tooltip.tsx +30 -30
- package/templates/default/components/ui/use-toast.ts +187 -187
- package/templates/default/drizzle.config.ts +4 -4
- package/templates/default/dynamic-schemas/schema.ts +75 -225
- package/templates/default/env/env.ts +46 -46
- package/templates/default/envConfig.ts +4 -4
- package/templates/default/lib/postinstall.js +14 -14
- package/templates/default/lib/utils.ts +6 -6
- package/templates/default/next-env.d.ts +6 -6
- package/templates/default/next.config.ts +24 -24
- package/templates/default/package.json +1 -1
- package/templates/default/postcss.config.mjs +6 -6
- package/templates/default/proxy.ts +32 -32
- 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
|