create-nextjs-cms 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +395 -0
  6. package/dist/lib/utils.d.ts +11 -0
  7. package/dist/lib/utils.d.ts.map +1 -0
  8. package/dist/lib/utils.js +48 -0
  9. package/package.json +44 -0
  10. package/templates/default/.env +24 -0
  11. package/templates/default/.env.development +8 -0
  12. package/templates/default/.eslintrc.json +5 -0
  13. package/templates/default/.prettierignore +7 -0
  14. package/templates/default/.prettierrc.json +19 -0
  15. package/templates/default/CHANGELOG.md +77 -0
  16. package/templates/default/README.md +45 -0
  17. package/templates/default/app/(auth)/auth/login/LoginPage.tsx +175 -0
  18. package/templates/default/app/(auth)/auth/login/page.tsx +12 -0
  19. package/templates/default/app/(rootLayout)/admins/page.tsx +5 -0
  20. package/templates/default/app/(rootLayout)/advanced/page.tsx +5 -0
  21. package/templates/default/app/(rootLayout)/analytics/page.tsx +7 -0
  22. package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +7 -0
  23. package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +7 -0
  24. package/templates/default/app/(rootLayout)/dashboard/page.tsx +7 -0
  25. package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +7 -0
  26. package/templates/default/app/(rootLayout)/emails/page.tsx +6 -0
  27. package/templates/default/app/(rootLayout)/layout.tsx +5 -0
  28. package/templates/default/app/(rootLayout)/loading.tsx +10 -0
  29. package/templates/default/app/(rootLayout)/log/page.tsx +7 -0
  30. package/templates/default/app/(rootLayout)/new/[section]/page.tsx +7 -0
  31. package/templates/default/app/(rootLayout)/page.tsx +9 -0
  32. package/templates/default/app/(rootLayout)/section/[section]/page.tsx +7 -0
  33. package/templates/default/app/(rootLayout)/settings/page.tsx +7 -0
  34. package/templates/default/app/_trpc/client.ts +4 -0
  35. package/templates/default/app/api/auth/csrf/route.ts +25 -0
  36. package/templates/default/app/api/auth/refresh/route.ts +10 -0
  37. package/templates/default/app/api/auth/route.ts +23 -0
  38. package/templates/default/app/api/auth/session/route.ts +20 -0
  39. package/templates/default/app/api/editor/photo/route.ts +42 -0
  40. package/templates/default/app/api/photo/route.ts +27 -0
  41. package/templates/default/app/api/placeholder/route.ts +7 -0
  42. package/templates/default/app/api/submit/section/item/[slug]/route.ts +63 -0
  43. package/templates/default/app/api/submit/section/item/route.ts +53 -0
  44. package/templates/default/app/api/submit/section/simple/route.ts +54 -0
  45. package/templates/default/app/api/trpc/[trpc]/route.ts +33 -0
  46. package/templates/default/app/api/video/route.ts +174 -0
  47. package/templates/default/app/dictionaries.ts +14 -0
  48. package/templates/default/app/layout.tsx +28 -0
  49. package/templates/default/app/providers.tsx +151 -0
  50. package/templates/default/cli.ts +4 -0
  51. package/templates/default/components/AdminCard.tsx +163 -0
  52. package/templates/default/components/AdminEditPage.tsx +123 -0
  53. package/templates/default/components/AdminPrivilegeCard.tsx +184 -0
  54. package/templates/default/components/AdminsPage.tsx +43 -0
  55. package/templates/default/components/AdvancedSettingsPage.tsx +167 -0
  56. package/templates/default/components/AnalyticsPage.tsx +127 -0
  57. package/templates/default/components/BarChartBox.tsx +43 -0
  58. package/templates/default/components/BrowsePage.tsx +119 -0
  59. package/templates/default/components/CategorizedSectionPage.tsx +36 -0
  60. package/templates/default/components/CategoryDeleteConfirmPage.tsx +129 -0
  61. package/templates/default/components/CategorySectionSelectInput.tsx +139 -0
  62. package/templates/default/components/ConditionalFields.tsx +49 -0
  63. package/templates/default/components/ContainerBox.tsx +24 -0
  64. package/templates/default/components/DashboardPage.tsx +187 -0
  65. package/templates/default/components/DashboardPageAlt.tsx +43 -0
  66. package/templates/default/components/DefaultNavItems.tsx +3 -0
  67. package/templates/default/components/Dropzone.tsx +153 -0
  68. package/templates/default/components/EmailCard.tsx +137 -0
  69. package/templates/default/components/EmailPasswordForm.tsx +84 -0
  70. package/templates/default/components/EmailQuotaForm.tsx +72 -0
  71. package/templates/default/components/EmailsPage.tsx +48 -0
  72. package/templates/default/components/GalleryPhoto.tsx +93 -0
  73. package/templates/default/components/InfoCard.tsx +94 -0
  74. package/templates/default/components/ItemEditPage.tsx +217 -0
  75. package/templates/default/components/Layout.tsx +70 -0
  76. package/templates/default/components/LoadingSpinners.tsx +67 -0
  77. package/templates/default/components/LogPage.tsx +17 -0
  78. package/templates/default/components/Modal.tsx +99 -0
  79. package/templates/default/components/Navbar.tsx +29 -0
  80. package/templates/default/components/NavbarAlt.tsx +182 -0
  81. package/templates/default/components/NewAdminForm.tsx +172 -0
  82. package/templates/default/components/NewEmailForm.tsx +131 -0
  83. package/templates/default/components/NewPage.tsx +206 -0
  84. package/templates/default/components/NewVariantComponent.tsx +228 -0
  85. package/templates/default/components/PhotoGallery.tsx +35 -0
  86. package/templates/default/components/PieChartBox.tsx +101 -0
  87. package/templates/default/components/ProgressBar.tsx +24 -0
  88. package/templates/default/components/ProtectedDocument.tsx +78 -0
  89. package/templates/default/components/ProtectedImage.tsx +143 -0
  90. package/templates/default/components/ProtectedVideo.tsx +76 -0
  91. package/templates/default/components/SectionItemCard.tsx +143 -0
  92. package/templates/default/components/SectionItemStatusBadge.tsx +16 -0
  93. package/templates/default/components/SectionPage.tsx +124 -0
  94. package/templates/default/components/SelectBox.tsx +99 -0
  95. package/templates/default/components/SelectInputButtons.tsx +124 -0
  96. package/templates/default/components/SettingsPage.tsx +238 -0
  97. package/templates/default/components/Sidebar.tsx +209 -0
  98. package/templates/default/components/SidebarDropdownItem.tsx +74 -0
  99. package/templates/default/components/SidebarItem.tsx +19 -0
  100. package/templates/default/components/TempPage.tsx +12 -0
  101. package/templates/default/components/ThemeProvider.tsx +8 -0
  102. package/templates/default/components/TooltipComponent.tsx +27 -0
  103. package/templates/default/components/VariantCard.tsx +123 -0
  104. package/templates/default/components/VariantEditPage.tsx +229 -0
  105. package/templates/default/components/analytics/BounceRate.tsx +69 -0
  106. package/templates/default/components/analytics/LivePageViews.tsx +54 -0
  107. package/templates/default/components/analytics/LiveUsersCount.tsx +32 -0
  108. package/templates/default/components/analytics/MonthlyPageViews.tsx +41 -0
  109. package/templates/default/components/analytics/TopCountries.tsx +51 -0
  110. package/templates/default/components/analytics/TopDevices.tsx +45 -0
  111. package/templates/default/components/analytics/TopMediums.tsx +57 -0
  112. package/templates/default/components/analytics/TopSources.tsx +44 -0
  113. package/templates/default/components/analytics/TotalPageViews.tsx +40 -0
  114. package/templates/default/components/analytics/TotalSessions.tsx +40 -0
  115. package/templates/default/components/analytics/TotalUniqueUsers.tsx +40 -0
  116. package/templates/default/components/custom/RightHomeRoomVariantCard.tsx +137 -0
  117. package/templates/default/components/dndKit/Draggable.tsx +21 -0
  118. package/templates/default/components/dndKit/Droppable.tsx +20 -0
  119. package/templates/default/components/dndKit/SortableItem.tsx +18 -0
  120. package/templates/default/components/form/DateRangeFormInput.tsx +55 -0
  121. package/templates/default/components/form/Form.tsx +298 -0
  122. package/templates/default/components/form/FormInputElement.tsx +68 -0
  123. package/templates/default/components/form/FormInputs.tsx +108 -0
  124. package/templates/default/components/form/helpers/util.ts +20 -0
  125. package/templates/default/components/form/inputs/CheckboxFormInput.tsx +33 -0
  126. package/templates/default/components/form/inputs/ColorFormInput.tsx +44 -0
  127. package/templates/default/components/form/inputs/DateFormInput.tsx +107 -0
  128. package/templates/default/components/form/inputs/DocumentFormInput.tsx +124 -0
  129. package/templates/default/components/form/inputs/MapFormInput.tsx +139 -0
  130. package/templates/default/components/form/inputs/MultipleSelectFormInput.tsx +150 -0
  131. package/templates/default/components/form/inputs/NumberFormInput.tsx +42 -0
  132. package/templates/default/components/form/inputs/PasswordFormInput.tsx +47 -0
  133. package/templates/default/components/form/inputs/PhotoFormInput.tsx +218 -0
  134. package/templates/default/components/form/inputs/RichTextFormInput.tsx +133 -0
  135. package/templates/default/components/form/inputs/SelectFormInput.tsx +164 -0
  136. package/templates/default/components/form/inputs/TagsFormInput.tsx +63 -0
  137. package/templates/default/components/form/inputs/TextFormInput.tsx +48 -0
  138. package/templates/default/components/form/inputs/TextareaFormInput.tsx +47 -0
  139. package/templates/default/components/form/inputs/VideoFormInput.tsx +117 -0
  140. package/templates/default/components/pagination/Pagination.tsx +36 -0
  141. package/templates/default/components/pagination/PaginationButtons.tsx +145 -0
  142. package/templates/default/components/ui/accordion.tsx +57 -0
  143. package/templates/default/components/ui/alert.tsx +46 -0
  144. package/templates/default/components/ui/badge.tsx +33 -0
  145. package/templates/default/components/ui/button.tsx +57 -0
  146. package/templates/default/components/ui/calendar.tsx +68 -0
  147. package/templates/default/components/ui/card.tsx +76 -0
  148. package/templates/default/components/ui/checkbox.tsx +29 -0
  149. package/templates/default/components/ui/dropdown-menu.tsx +205 -0
  150. package/templates/default/components/ui/input.tsx +25 -0
  151. package/templates/default/components/ui/label.tsx +26 -0
  152. package/templates/default/components/ui/popover.tsx +31 -0
  153. package/templates/default/components/ui/scroll-area.tsx +42 -0
  154. package/templates/default/components/ui/select.tsx +164 -0
  155. package/templates/default/components/ui/sheet.tsx +107 -0
  156. package/templates/default/components/ui/switch.tsx +29 -0
  157. package/templates/default/components/ui/table.tsx +120 -0
  158. package/templates/default/components/ui/tabs.tsx +55 -0
  159. package/templates/default/components/ui/toast.tsx +113 -0
  160. package/templates/default/components/ui/toaster.tsx +35 -0
  161. package/templates/default/components/ui/tooltip.tsx +30 -0
  162. package/templates/default/components/ui/use-toast.ts +188 -0
  163. package/templates/default/components.json +16 -0
  164. package/templates/default/context/ModalProvider.tsx +53 -0
  165. package/templates/default/drizzle.config.ts +4 -0
  166. package/templates/default/dynamic-schemas/schema.ts +373 -0
  167. package/templates/default/env/env.js +130 -0
  168. package/templates/default/envConfig.ts +4 -0
  169. package/templates/default/hooks/useModal.ts +8 -0
  170. package/templates/default/lib/apiHelpers.ts +106 -0
  171. package/templates/default/lz.config.ts +40 -0
  172. package/templates/default/middleware.ts +33 -0
  173. package/templates/default/next.config.ts +46 -0
  174. package/templates/default/package.json +134 -0
  175. package/templates/default/postcss.config.js +6 -0
  176. package/templates/default/postinstall.js +14 -0
  177. package/templates/default/public/blank_avatar.png +0 -0
  178. package/templates/default/public/favicon.ico +0 -0
  179. package/templates/default/public/img/placeholder.svg +1 -0
  180. package/templates/default/public/lazemni_logo.png +0 -0
  181. package/templates/default/public/next.svg +1 -0
  182. package/templates/default/public/vercel.svg +1 -0
  183. package/templates/default/section-tests.ts +92 -0
  184. package/templates/default/styles/globals.css +88 -0
  185. package/templates/default/tailwind.config.js +95 -0
  186. package/templates/default/test.ts +77 -0
  187. package/templates/default/tsconfig.json +44 -0
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
4
+ import React, { RefObject, useEffect, useRef, useState } from 'react'
5
+ import { SectionType } from 'nextjs-cms/core/types'
6
+ import getString from 'nextjs-cms/translations'
7
+ import useModal from '@/hooks/useModal'
8
+ import { AxiosError } from 'axios'
9
+ import InfoCard from '@/components/InfoCard'
10
+ import LoadingSpinners from '@/components/LoadingSpinners'
11
+ import { useToast } from '@/components/ui/use-toast'
12
+ import { useRouter } from 'next/navigation'
13
+ import { DropzoneHandles } from '@/components/Dropzone'
14
+ import { VariantHandles } from '@/components/NewVariantComponent'
15
+ import { trpc } from '@/app/_trpc/client'
16
+ import Form from '@/components/form/Form'
17
+
18
+ export default function NewPage({
19
+ section,
20
+ sectionType,
21
+ hiddenInputs,
22
+ action,
23
+ }: {
24
+ section: string
25
+ sectionType: SectionType
26
+ hiddenInputs?: {
27
+ name: string
28
+ value: any
29
+ }[]
30
+ action?: any
31
+ }) {
32
+ const [progress, setProgress] = useState(0)
33
+ const [progressVariant, setProgressVariant] = useState<'determinate' | 'query'>('determinate')
34
+ const [isSubmitting, setIsSubmitting] = useState(false)
35
+ const [response, setResponse] = useState<any>(null)
36
+ const { setModal } = useModal()
37
+ const axiosPrivate = useAxiosPrivate()
38
+ const controller = new AbortController()
39
+ const { toast } = useToast()
40
+ const router = useRouter()
41
+ const dropzoneRef: RefObject<DropzoneHandles | null> = useRef(null)
42
+ const variantRef: RefObject<VariantHandles[]> = useRef([])
43
+ const utils = trpc.useUtils()
44
+
45
+ const { isLoading, isError, data, error } = trpc.hasItemsSections.newItem.useQuery({
46
+ sectionName: section,
47
+ })
48
+
49
+ const handleSubmit = async (formData: FormData) => {
50
+ setIsSubmitting(true)
51
+ if (isSubmitting) return
52
+ setResponse(null)
53
+ formData.append('sectionName', section)
54
+ formData.append('formType', 'new')
55
+ formData.append('sectionType', sectionType)
56
+
57
+ // Retrieve the files from the Dropzone's state
58
+ if (dropzoneRef.current) {
59
+ const files: File[] = dropzoneRef.current.getFiles()
60
+ // Add files to the body
61
+ files.forEach((file, index) => {
62
+ formData.append(`dropzoneFiles`, file)
63
+ })
64
+ }
65
+
66
+ // Retrieve the variants from the variants component
67
+ // For now, only one variant is allowed
68
+ const variantsObject: any[] = []
69
+ if (variantRef.current && data?.section?.variants?.length && data?.section?.variants[0]) {
70
+ // Add variants to the body
71
+ variantRef.current.map((ref, index) => {
72
+ variantsObject.push({
73
+ // @ts-ignore
74
+ variant: data.section.variants[index],
75
+ items: ref.getVariants(),
76
+ })
77
+ })
78
+
79
+ formData.append(`variantItems`, JSON.stringify(variantsObject))
80
+ }
81
+
82
+ // Check if there are any hidden inputs passed to the component
83
+ if (hiddenInputs) {
84
+ // Add hidden inputs to the body
85
+ hiddenInputs.forEach((input) => {
86
+ if (!input.value) return
87
+ formData.append(input.name, input.value)
88
+ })
89
+ }
90
+
91
+ // const body = Object.fromEntries(formData.entries())
92
+ try {
93
+ const res = await axiosPrivate.post(`/submit/section/item`, formData, {
94
+ signal: controller.signal,
95
+ onUploadProgress: (progressEvent) => {
96
+ if (!progressEvent.total) return
97
+ const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
98
+ // Update progress bar value here
99
+ setProgress(progress)
100
+
101
+ if (progress === 100) {
102
+ setProgressVariant('query')
103
+ }
104
+ },
105
+ headers: {
106
+ 'Content-Type': 'multipart/form-data',
107
+ },
108
+ })
109
+
110
+ if (res) {
111
+ setIsSubmitting(false)
112
+ setProgress(0)
113
+ setProgressVariant('determinate')
114
+ if (res.status === 200) {
115
+ toast({
116
+ variant: res.data?.errors?.length ? 'warning' : 'success',
117
+ title: getString('itemCreatedSuccessfully'),
118
+ description: res.data?.errors?.length
119
+ ? getString('errorsInSubmit')
120
+ : getString('itemCreatedSuccessfully'),
121
+ })
122
+
123
+ switch (sectionType) {
124
+ case 'categorized':
125
+ // Just close the modal and refetch the page
126
+ setModal(null)
127
+
128
+ if (action) {
129
+ await action().then(() => {
130
+ setModal(null)
131
+ router.refresh()
132
+ })
133
+ }
134
+
135
+ break
136
+
137
+ case 'has_items':
138
+ // Just display a success toast and refetch the page
139
+ setResponse(null)
140
+ // Redirect to the edit page
141
+ // router.push(`/edit/${section}/${res.data.identifier}`)
142
+ // Or redirect to the browse page
143
+ await utils.hasItemsSections.listItems.invalidate({
144
+ sectionName: section,
145
+ })
146
+ router.push(`/browse/${section}`)
147
+ break
148
+ }
149
+ }
150
+ }
151
+ } catch (error: AxiosError | any) {
152
+ setIsSubmitting(false)
153
+ setProgress(0)
154
+ setProgressVariant('determinate')
155
+ if (error?.response?.data) {
156
+ setResponse(<InfoCard result={{ key: 'danger', title: error.response.data.error, status: false }} />)
157
+ }
158
+ }
159
+ }
160
+
161
+ function cancelSubmit() {
162
+ controller.abort()
163
+ setIsSubmitting(false)
164
+ setProgress(0)
165
+ setProgressVariant('determinate')
166
+ }
167
+
168
+ useEffect(() => {
169
+ return () => {
170
+ cancelSubmit()
171
+ }
172
+ }, [])
173
+
174
+ return (
175
+ <div className='flex w-full flex-col overflow-hidden'>
176
+ {isLoading ? (
177
+ <div>
178
+ <LoadingSpinners />
179
+ </div>
180
+ ) : null}
181
+ {isError ? <div>{error?.message}</div> : null}
182
+ {data ? (
183
+ <div className='flex w-full flex-col'>
184
+ <div className='relative z-[1] border-b-2 p-8 pt-12 font-extrabold text-foreground'>
185
+ <div className='absolute left-0 top-0 z-[2] h-4 w-full bg-gradient-to-r from-emerald-800 via-emerald-400 to-sky-600'></div>
186
+ <h1 className='pb-4 text-4xl'>{data.section?.title.section}</h1>
187
+ <span>
188
+ /{getString('new')} {data.section?.title.singular}
189
+ </span>
190
+ </div>
191
+ <Form
192
+ formType='new'
193
+ progressVariant={progressVariant}
194
+ response={response}
195
+ progress={progress}
196
+ data={data}
197
+ dropzoneRef={dropzoneRef}
198
+ variantRef={variantRef}
199
+ handleSubmit={handleSubmit}
200
+ isSubmitting={isSubmitting}
201
+ />
202
+ </div>
203
+ ) : null}
204
+ </div>
205
+ )
206
+ }
@@ -0,0 +1,228 @@
1
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
2
+ import React, { forwardRef, Ref, useEffect, useImperativeHandle, useState } from 'react'
3
+ import ContainerBox from '@/components/ContainerBox'
4
+ import getString from 'nextjs-cms/translations'
5
+ import { Button } from '@/components/ui/button'
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { getNewVariantPage } from '@/lib/apiHelpers'
8
+ import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
9
+ import LoadingSpinners from '@/components/LoadingSpinners'
10
+ import { InputGroup, Variant, VariantItem } from 'nextjs-cms/core/types'
11
+ import FormInputs from '@/components/form/FormInputs'
12
+ import classNames from 'classnames'
13
+ import ProgressBar from '@/components/ProgressBar'
14
+ import { AxiosError } from 'axios'
15
+ import InfoCard from '@/components/InfoCard'
16
+ import { useToast } from '@/components/ui/use-toast'
17
+ import { nanoid } from 'nanoid'
18
+ import { MinusIcon } from '@radix-ui/react-icons'
19
+
20
+ export interface VariantHandles {
21
+ getVariants: () => VariantItem[]
22
+ removeVariants: () => void
23
+ }
24
+
25
+ function NewVariantComponent(
26
+ {
27
+ section,
28
+ variantInfo,
29
+ xsrfToken,
30
+ }: {
31
+ section: string
32
+ variantInfo: Variant
33
+ xsrfToken: string
34
+ },
35
+ ref: Ref<VariantHandles>,
36
+ ) {
37
+ const [open, setOpen] = React.useState(false)
38
+ const axiosPrivate = useAxiosPrivate()
39
+ const controller = new AbortController()
40
+ const { isLoading, isError, data, error } = useQuery({
41
+ queryKey: ['newVariant', variantInfo.variant_name],
42
+ queryFn: () => getNewVariantPage(section, variantInfo.variant_name, axiosPrivate, controller),
43
+ })
44
+
45
+ const [variants, setVariants] = useState<VariantItem[]>([])
46
+ const [progress, setProgress] = useState(0)
47
+ const [progressVariant, setProgressVariant] = useState<'determinate' | 'query'>('determinate')
48
+ const [isSubmitting, setIsSubmitting] = useState(false)
49
+ const [response, setResponse] = useState<any>(null)
50
+ const { toast } = useToast()
51
+
52
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
53
+ setIsSubmitting(true)
54
+ e.preventDefault()
55
+ e.stopPropagation()
56
+ setResponse(null)
57
+ const formData = new FormData(e.currentTarget)
58
+
59
+ formData.append('section', section)
60
+ formData.append('variant', variantInfo.variant_name)
61
+ formData.append('preSubmit', 'true')
62
+ formData.append('form_type', 'new')
63
+
64
+ const body = Object.fromEntries(formData.entries())
65
+ try {
66
+ const res = await axiosPrivate.post(`/api-submit`, body, {
67
+ signal: controller.signal,
68
+ onUploadProgress: (progressEvent) => {
69
+ if (!progressEvent.total) return
70
+ const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
71
+ // Update progress bar value here
72
+ setProgress(progress)
73
+
74
+ if (progress === 100) {
75
+ setProgressVariant('query')
76
+ }
77
+ },
78
+ headers: {
79
+ 'Content-Type': 'multipart/form-data',
80
+ },
81
+ })
82
+
83
+ if (res) {
84
+ setIsSubmitting(false)
85
+ setProgress(0)
86
+ setProgressVariant('determinate')
87
+ if (res.data.code === 200) {
88
+ setOpen(false)
89
+ // Handle closure
90
+ setResponse(null)
91
+
92
+ // Add the new variant to the list
93
+ // But first, let's remove the 'preSubmit' key
94
+ delete body.preSubmit
95
+ setVariants([
96
+ ...variants,
97
+ {
98
+ id: nanoid(),
99
+ // @ts-ignore
100
+ title: formData.get(data?.variant.heading_input_name),
101
+ data: body,
102
+ } as VariantItem,
103
+ ])
104
+ }
105
+ }
106
+ } catch (error: AxiosError | any) {
107
+ setIsSubmitting(false)
108
+ setProgress(0)
109
+ setProgressVariant('determinate')
110
+ if (error?.response?.data) {
111
+ setResponse(
112
+ <InfoCard result={{ key: 'danger', title: error.response.data.error.message, status: false }} />,
113
+ )
114
+ }
115
+ }
116
+ }
117
+
118
+ function cancelSubmit() {
119
+ controller.abort()
120
+ setIsSubmitting(false)
121
+ setProgress(0)
122
+ setProgressVariant('determinate')
123
+ }
124
+
125
+ useEffect(() => {
126
+ // console.log('variants', variants)
127
+ return () => {
128
+ cancelSubmit()
129
+ }
130
+ }, [variants])
131
+
132
+ useImperativeHandle(ref, () => ({
133
+ getVariants: () => variants,
134
+ removeVariants: () => {
135
+ setVariants([])
136
+ },
137
+ }))
138
+
139
+ return (
140
+ <ContainerBox title={`${getString('add')} ${variantInfo.variant_html_name_en}`}>
141
+ <div className='mb-4 grid grid-cols-1 gap-4 rounded-2xl border-4 border-dashed border-gray-400 bg-accent p-2 md:grid-cols-2 lg:grid-cols-4'>
142
+ {variants.map((v) => (
143
+ <ContainerBox title={v.title} key={v.id}>
144
+ {/* Delete Button */}
145
+ <button
146
+ type='button'
147
+ className='absolute end-1 top-1 rounded-lg bg-red-500 p-1'
148
+ onClick={() => {
149
+ setVariants((prev) =>
150
+ prev.filter((prevVariant) => {
151
+ if (prevVariant.id !== v.id) {
152
+ return prevVariant
153
+ }
154
+ }),
155
+ )
156
+ }}
157
+ >
158
+ <MinusIcon className='text-white' />
159
+ </button>
160
+ <div className='truncate'>{JSON.stringify(v.data)}</div>
161
+ </ContainerBox>
162
+ ))}
163
+ </div>
164
+ <Sheet open={open} onOpenChange={setOpen}>
165
+ <SheetTrigger>
166
+ <Button type='button'>{getString('add')} +</Button>
167
+ </SheetTrigger>
168
+ <SheetContent className='w-1/2 max-w-[2/3] sm:w-[940px] sm:max-w-full'>
169
+ <SheetHeader>
170
+ <SheetTitle>{getString('newVariant')}</SheetTitle>
171
+ <SheetDescription>
172
+ <form onSubmit={handleSubmit}>
173
+ <div className=''>
174
+ {isLoading && (
175
+ <div>
176
+ <LoadingSpinners />
177
+ </div>
178
+ )}
179
+
180
+ {data && data.inputGroups && (
181
+ <div className='flex flex-col gap-4'>
182
+ {data?.inputGroups?.length > 0 &&
183
+ data?.inputGroups?.map((inputGroup: InputGroup, index: number) => {
184
+ return (
185
+ <ContainerBox title={inputGroup.groupTitle} key={index}>
186
+ <FormInputs
187
+ inputs={[]}
188
+ sectionName={variantInfo.variant_name}
189
+ />
190
+ </ContainerBox>
191
+ )
192
+ })}
193
+ </div>
194
+ )}
195
+ <div className='my-4 flex flex-col gap-3 p-4'>
196
+ <div className='mt-5'>
197
+ <button
198
+ className={classNames({
199
+ 'w-full rounded bg-gradient-to-r p-2 font-bold text-white drop-shadow':
200
+ true,
201
+ 'from-emerald-700 via-green-700 to-green-500 dark:from-blue-800 dark:via-sky-800 dark:to-slate-500':
202
+ !isSubmitting,
203
+ 'from-gray-600 via-gray-500 to-gray-400': isSubmitting,
204
+ })}
205
+ type='submit'
206
+ disabled={isSubmitting}
207
+ >
208
+ {isSubmitting ? getString('loading') : getString('create')}
209
+ </button>
210
+ {isSubmitting && (
211
+ <div className='mt-0.5'>
212
+ <ProgressBar variant={progressVariant} value={progress} />
213
+ </div>
214
+ )}
215
+ </div>
216
+ {response && <div className='w-full'>{response}</div>}
217
+ </div>
218
+ </div>
219
+ </form>
220
+ </SheetDescription>
221
+ </SheetHeader>
222
+ </SheetContent>
223
+ </Sheet>
224
+ </ContainerBox>
225
+ )
226
+ }
227
+
228
+ export default forwardRef(NewVariantComponent)
@@ -0,0 +1,35 @@
1
+ import getString from 'nextjs-cms/translations'
2
+ import { PhotoGalleryItem } from 'nextjs-cms/core/types'
3
+ import GalleryPhoto from '@/components/GalleryPhoto'
4
+ import { Alert, AlertDescription } from '@/components/ui/alert'
5
+ import ContainerBox from '@/components/ContainerBox'
6
+ import React from 'react'
7
+
8
+ const PhotoGallery = ({ sectionName, gallery }: { sectionName: string; gallery: PhotoGalleryItem[] }) => {
9
+ return (
10
+ <ContainerBox title={getString('gallery')}>
11
+ {gallery && gallery.length > 0 ? (
12
+ <div className='flex flex-wrap gap-4'>
13
+ {gallery.map((photo: PhotoGalleryItem, index: number) => (
14
+ <GalleryPhoto
15
+ item={photo}
16
+ sectionName={sectionName}
17
+ key={photo.photo}
18
+ action={() => {
19
+ // This is the action that will be executed when the user removes a photo from the gallery
20
+ // Remove the removed photo from the gallery
21
+ gallery.splice(index, 1)
22
+ }}
23
+ />
24
+ ))}
25
+ </div>
26
+ ) : (
27
+ <Alert variant='light' className='mt-4'>
28
+ <AlertDescription className='font-bold'>{getString('noItems')}</AlertDescription>
29
+ </Alert>
30
+ )}
31
+ </ContainerBox>
32
+ )
33
+ }
34
+
35
+ export default PhotoGallery
@@ -0,0 +1,101 @@
1
+ import React from 'react'
2
+ import { PieChartDataItem, PieChartTitles } from 'nextjs-cms/core/types'
3
+ import ContainerBox from '@/components/ContainerBox'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import { ResponsiveContainer, PieChart, Pie, Legend } from 'recharts'
6
+ import LoadingSpinners from '@/components/LoadingSpinners'
7
+
8
+ const RADIAN = Math.PI / 180
9
+ const renderCustomizedLabel = ({
10
+ cx,
11
+ cy,
12
+ midAngle,
13
+ innerRadius,
14
+ outerRadius,
15
+ percent,
16
+ index,
17
+ name,
18
+ }: {
19
+ cx: number
20
+ cy: number
21
+ midAngle: number
22
+ innerRadius: number
23
+ outerRadius: number
24
+ percent: number
25
+ index: number
26
+ name: string
27
+ }) => {
28
+ const radius = innerRadius + (outerRadius - innerRadius) * 0.5
29
+ const x = cx + radius * Math.cos(-midAngle * RADIAN)
30
+ const y = cy + radius * Math.sin(-midAngle * RADIAN)
31
+
32
+ return (
33
+ <text
34
+ x={x}
35
+ y={y}
36
+ fill='white'
37
+ className='font-semibold'
38
+ textAnchor={x > cx ? 'start' : 'end'}
39
+ dominantBaseline='central'
40
+ >
41
+ {`${(percent * 100).toFixed(0)}%`}
42
+ </text>
43
+ )
44
+ }
45
+
46
+ export default function PieChartBox({
47
+ chartData,
48
+ chartBoxTitles,
49
+ height = 300,
50
+ legend = true,
51
+ isLoading,
52
+ }: {
53
+ chartData: PieChartDataItem[]
54
+ chartBoxTitles: PieChartTitles
55
+ height?: number
56
+ legend?: boolean
57
+ isLoading?: boolean
58
+ }) {
59
+ return (
60
+ <ContainerBox title={chartBoxTitles.mainTitle}>
61
+ {isLoading ? (
62
+ <LoadingSpinners single={true} />
63
+ ) : (
64
+ <>
65
+ <div className='text-muted-foreground'>
66
+ {chartBoxTitles.totalUnitSubtitle && (
67
+ <div>
68
+ {`${chartBoxTitles.totalUnitSubtitle.key}: `}{' '}
69
+ <Badge variant='secondary'>{chartBoxTitles.totalUnitSubtitle.value}</Badge>
70
+ </div>
71
+ )}
72
+
73
+ {chartBoxTitles.usedUnitSubtitle && (
74
+ <div>
75
+ {`${chartBoxTitles.usedUnitSubtitle.key}: `}
76
+ <Badge variant='secondary'>{chartBoxTitles.usedUnitSubtitle.value}</Badge>
77
+ </div>
78
+ )}
79
+ </div>
80
+ <div>
81
+ <div style={{ width: '100%', height: height }}>
82
+ <ResponsiveContainer>
83
+ <PieChart>
84
+ {legend && <Legend />}
85
+ <Pie
86
+ dataKey='value'
87
+ data={chartData}
88
+ fill='#8884d8'
89
+ label={renderCustomizedLabel}
90
+ labelLine={false}
91
+ startAngle={-180}
92
+ />
93
+ </PieChart>
94
+ </ResponsiveContainer>
95
+ </div>
96
+ </div>
97
+ </>
98
+ )}
99
+ </ContainerBox>
100
+ )
101
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react'
2
+ import { styled } from '@mui/material/styles'
3
+ import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'
4
+
5
+ const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
6
+ height: 10,
7
+ borderRadius: 5,
8
+ [`&.${linearProgressClasses.colorPrimary}`]: {
9
+ backgroundColor: theme.palette.grey[theme.palette.mode === 'light' ? 200 : 800],
10
+ },
11
+ [`& .${linearProgressClasses.bar}`]: {
12
+ borderRadius: 5,
13
+ backgroundColor: theme.palette.mode === 'light' ? '#1a90ff' : '#308fe8',
14
+ },
15
+ }))
16
+ export default function ProgressBar({
17
+ value,
18
+ variant,
19
+ }: {
20
+ value: number
21
+ variant?: 'determinate' | 'indeterminate' | 'buffer' | 'query'
22
+ }) {
23
+ return <BorderLinearProgress variant={variant} value={value} />
24
+ }
@@ -0,0 +1,78 @@
1
+ import React, { useEffect } from 'react'
2
+ import { trpc } from '@/app/_trpc/client'
3
+ import { base64ToBlob } from 'nextjs-cms/utils'
4
+
5
+ const ProtectedDocument = ({
6
+ section,
7
+ fieldName,
8
+ file,
9
+ width,
10
+ height,
11
+ className = 'rounded-3xl object-cover',
12
+ displayAs = 'base64',
13
+ }: {
14
+ section: string
15
+ fieldName: string
16
+ file: string
17
+ width: number
18
+ height: number
19
+ className?: string
20
+ displayAs?: 'blob' | 'base64'
21
+ }) => {
22
+ const [srcUrl, setSrcUrl] = React.useState<string | null>(null)
23
+ const { data, error, isLoading } = trpc.files.getDocument.useQuery({
24
+ name: file,
25
+ sectionName: section,
26
+ fieldName: fieldName,
27
+ })
28
+
29
+ useEffect(() => {
30
+ if (!data) return
31
+ if (error) return
32
+ switch (displayAs) {
33
+ case 'blob':
34
+ setSrcUrl(
35
+ URL.createObjectURL(
36
+ base64ToBlob({
37
+ base64: data.base64,
38
+ contentType: data.mimeType,
39
+ stripHeader: true,
40
+ }),
41
+ ),
42
+ )
43
+ break
44
+ case 'base64':
45
+ setSrcUrl(data.base64)
46
+ break
47
+ }
48
+
49
+ return () => {
50
+ setSrcUrl(null)
51
+ if (displayAs === 'blob' && srcUrl) {
52
+ URL.revokeObjectURL(srcUrl)
53
+ }
54
+ }
55
+ }, [data])
56
+ return (
57
+ <div className={className}>
58
+ {srcUrl ? (
59
+ <embed
60
+ src={srcUrl}
61
+ className='max-w-full'
62
+ width={width}
63
+ height={height}
64
+ onLoad={(e) => {
65
+ // Revoke the blob url after the image is loaded
66
+ if (displayAs === 'blob' && srcUrl) {
67
+ URL.revokeObjectURL(srcUrl)
68
+ }
69
+ }}
70
+ />
71
+ ) : (
72
+ <div className='animate-pulse bg-gray-500' style={{ width, height }} />
73
+ )}
74
+ </div>
75
+ )
76
+ }
77
+
78
+ export default ProtectedDocument