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,143 @@
1
+ import Image from 'next/image'
2
+ import React, { useEffect } from 'react'
3
+ import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
4
+ import { AxiosInstance } from 'axios'
5
+ import { base64ToBlob } from 'nextjs-cms/utils'
6
+
7
+ const defaultDataPlaceholder: string =
8
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mPkY2Q8AgABCwDWK6njxAAAAABJRU5ErkJggg=='
9
+
10
+ async function getProtectedPhoto({
11
+ src,
12
+ axiosPrivate,
13
+ }: {
14
+ src: string
15
+ axiosPrivate: AxiosInstance
16
+ }): Promise<string | null> {
17
+ try {
18
+ const res = await axiosPrivate.get(src)
19
+ return res.data
20
+ } catch (error) {
21
+ return null
22
+ }
23
+ }
24
+
25
+ const ProtectedImage = ({
26
+ section,
27
+ photo,
28
+ isThumb = false,
29
+ alt,
30
+ width,
31
+ height,
32
+ fill = false,
33
+ className = 'rounded-3xl object-cover',
34
+ displayAs = 'blob',
35
+ }: {
36
+ section: string
37
+ photo: string
38
+ isThumb?: boolean
39
+ alt: string
40
+ width: number
41
+ height: number
42
+ fill?: boolean
43
+ className?: string
44
+ displayAs?: 'blob' | 'base64'
45
+ }) => {
46
+ const axiosPrivate = useAxiosPrivate()
47
+ const [srcUrl, setSrcUrl] = React.useState<string | null>(null)
48
+
49
+ // Use trpc?
50
+ /*
51
+ const { data, isLoading } = trpc.files.getPhoto.useQuery({
52
+ name: photo,
53
+ folder: section,
54
+ isThumb: isThumb,
55
+ })
56
+
57
+ useEffect(() => {
58
+ if (!data) return
59
+ switch (displayAs) {
60
+ case 'blob':
61
+ setSrcUrl(
62
+ URL.createObjectURL(
63
+ base64ToBlob({
64
+ base64: data,
65
+ contentType: 'image/webp',
66
+ stripHeader: true,
67
+ }),
68
+ ),
69
+ )
70
+ break
71
+ case 'base64':
72
+ setSrcUrl(data)
73
+ break
74
+ }
75
+
76
+ return () => {
77
+ setSrcUrl(null)
78
+ if (displayAs === 'blob' && srcUrl) {
79
+ URL.revokeObjectURL(srcUrl)
80
+ }
81
+ }
82
+ }, [data])
83
+ */
84
+ useEffect(() => {
85
+ if (!photo || !section) return
86
+ getProtectedPhoto({
87
+ src: `/photo?name=${photo}&folder=${section}&isThumb=${isThumb}`,
88
+ axiosPrivate: axiosPrivate,
89
+ }).then((res) => {
90
+ if (!res) return
91
+ switch (displayAs) {
92
+ case 'blob':
93
+ setSrcUrl(
94
+ URL.createObjectURL(
95
+ base64ToBlob({
96
+ base64: res,
97
+ contentType: 'image/webp',
98
+ stripHeader: true,
99
+ }),
100
+ ),
101
+ )
102
+ break
103
+ case 'base64':
104
+ setSrcUrl(res)
105
+ break
106
+ }
107
+ })
108
+
109
+ return () => {
110
+ setSrcUrl(null)
111
+ if (displayAs === 'blob' && srcUrl) {
112
+ URL.revokeObjectURL(srcUrl)
113
+ }
114
+ }
115
+ }, [])
116
+
117
+ return (
118
+ <Image
119
+ alt={alt}
120
+ className={className}
121
+ src={srcUrl ? srcUrl : defaultDataPlaceholder}
122
+ width={fill ? undefined : width}
123
+ height={fill ? undefined : height}
124
+ style={
125
+ !fill
126
+ ? {
127
+ width: `${width}px`,
128
+ height: 'auto',
129
+ }
130
+ : undefined
131
+ }
132
+ fill={fill ? true : undefined}
133
+ onLoad={(e) => {
134
+ // Revoke the blob url after the image is loaded
135
+ if (displayAs === 'blob' && srcUrl) {
136
+ URL.revokeObjectURL(srcUrl)
137
+ }
138
+ }}
139
+ />
140
+ )
141
+ }
142
+
143
+ export default ProtectedImage
@@ -0,0 +1,76 @@
1
+ import React, { useEffect } from 'react'
2
+ import { useRefreshToken } from 'nextjs-cms/auth/hooks'
3
+ import { useSession } from 'nextjs-cms/auth/react'
4
+
5
+ const ProtectedVideo = ({
6
+ section,
7
+ fieldName,
8
+ file,
9
+ width,
10
+ height,
11
+ className = 'rounded-3xl object-cover',
12
+ }: {
13
+ section: string
14
+ fieldName: string
15
+ file: string
16
+ width: number
17
+ height: number
18
+ className?: string
19
+ }) => {
20
+ const refresh = useRefreshToken()
21
+ const ref = React.useRef<HTMLVideoElement>(null)
22
+ const [currentTime, setCurrentTime] = React.useState(0)
23
+ const [isSeeking, setIsSeeking] = React.useState(false)
24
+
25
+ const session = useSession()
26
+ useEffect(() => {
27
+ if (ref.current && currentTime) {
28
+ ref.current.currentTime = currentTime
29
+ ref.current.play().then(() => {})
30
+ }
31
+ }, [session])
32
+
33
+ return (
34
+ <div className={className}>
35
+ {/**
36
+ * The video will stream as long as the access token is valid
37
+ * If the access token expires, the video will pause
38
+ * A refresh token request will be sent to `/api/auth/refresh`
39
+ * If the refresh token is valid, the video will resume playing
40
+ * If the refresh token is invalid, the user will be logged out
41
+ */}
42
+ <video
43
+ ref={ref}
44
+ src={`/api/video?sectionName=${section}&fieldName=${fieldName}&name=${file}`}
45
+ className='max-w-full'
46
+ width={width}
47
+ controls={true}
48
+ height={height}
49
+ onErrorCapture={async (e) => {
50
+ if (isSeeking) return
51
+ setIsSeeking(true)
52
+ // console.log('seeked')
53
+ /**
54
+ * Pause the video
55
+ */
56
+ e.currentTarget.pause()
57
+ /**
58
+ * Get the current time of the video and save it in the state
59
+ */
60
+ setCurrentTime(e.currentTarget.currentTime)
61
+ /**
62
+ * Refresh the access token
63
+ * This will trigger the `useEffect` hook
64
+ * and set the current time of the video
65
+ * to the time it was paused at
66
+ * and play the video
67
+ */
68
+ await refresh()
69
+ setIsSeeking(false)
70
+ }}
71
+ />
72
+ </div>
73
+ )
74
+ }
75
+
76
+ export default ProtectedVideo
@@ -0,0 +1,143 @@
1
+ import React from 'react'
2
+ import getString from 'nextjs-cms/translations'
3
+ import useModal from '@/hooks/useModal'
4
+ import ProtectedImage from '@/components/ProtectedImage'
5
+ import SectionItemStatusBadge from '@/components/SectionItemStatusBadge'
6
+ import { displayDateFromString } from 'nextjs-cms/utils'
7
+ import Link from 'next/link'
8
+ import { CardTitle, CardHeader, CardFooter, Card, CardContent } from '@/components/ui/card'
9
+ import { Button } from '@/components/ui/button'
10
+ import { PersonIcon } from '@radix-ui/react-icons'
11
+ import { trpc } from '@/app/_trpc/client'
12
+ import { RouterOutputs } from 'nextjs-cms/api'
13
+
14
+ // Used to get elements of array types as types
15
+ type ArrElement<ArrType> = ArrType extends readonly (infer ElementType)[] ? ElementType : never
16
+
17
+ export default function SectionItemCard({
18
+ sectionName,
19
+ item,
20
+ action,
21
+ }: {
22
+ sectionName: string
23
+ item: ArrElement<RouterOutputs['hasItemsSections']['listItems']['items']>
24
+ action: any
25
+ }) {
26
+ const { setModal, modal, modalResponse, setModalResponse } = useModal()
27
+ const utils = trpc.useUtils()
28
+ const deleteMutation = trpc.hasItemsSections.deleteItem.useMutation({
29
+ onError: (error) => {
30
+ setModal({
31
+ title: getString('delete'),
32
+ body: (
33
+ <div className='rounded border-2 border-dashed border-red-500 bg-red-100 p-2 text-red-800'>
34
+ {error.message}
35
+ </div>
36
+ ),
37
+ headerColor: 'bg-red-700',
38
+ titleColor: 'text-white',
39
+ lang: 'en',
40
+ })
41
+ },
42
+
43
+ onSuccess: async (data) => {
44
+ await utils.hasItemsSections.listItems.invalidate({
45
+ sectionName: sectionName,
46
+ })
47
+ action()
48
+ setModal({
49
+ title: getString('delete'),
50
+ body: (
51
+ <div className='rounded border-2 border-dashed border-green-500 bg-green-100 p-2 text-green-800'>
52
+ {getString('itemDeletedSuccessfully')}
53
+ </div>
54
+ ),
55
+ headerColor: 'bg-green-700',
56
+ titleColor: 'text-white',
57
+ lang: 'en',
58
+ })
59
+ },
60
+ })
61
+ const handleDelete = async () => {
62
+ deleteMutation.mutate({
63
+ sectionName: sectionName,
64
+ sectionItemId: item.id,
65
+ })
66
+ }
67
+
68
+ return (
69
+ <>
70
+ <Card className='relative mx-auto w-full max-w-md overflow-hidden rounded-lg border-secondary-foreground pb-[85px] shadow'>
71
+ {item.coverPhoto ? (
72
+ <div className='relative h-[200px]'>
73
+ {[0, 1].includes(item.permission) ? <SectionItemStatusBadge status={item.permission} /> : null}
74
+ <ProtectedImage
75
+ className='h-64 w-full object-cover'
76
+ photo={item.coverPhoto}
77
+ section={sectionName}
78
+ isThumb={true}
79
+ alt={'item'}
80
+ width={100}
81
+ height={50}
82
+ fill={true}
83
+ />
84
+ </div>
85
+ ) : null}
86
+ <CardHeader className='p-6'>
87
+ <CardTitle className='text-xl font-bold'>{item.headingTitle}</CardTitle>
88
+ </CardHeader>
89
+ <CardContent className='mt-2'>
90
+ <div className='flex flex-row flex-wrap gap-1'>
91
+ <PersonIcon fontSize='small' /> {item.createdBy},{' '}
92
+ {displayDateFromString(item.createdAt, 'en', false, ',')}
93
+ </div>
94
+ </CardContent>
95
+ <CardFooter className='absolute bottom-0 start-0 flex h-[85px] w-full justify-between p-6'>
96
+ <Link
97
+ href={`/edit/${sectionName}/${item.id}`}
98
+ className='rounded bg-primary px-3 py-1 font-semibold text-primary-foreground'
99
+ >
100
+ {getString('edit')}
101
+ </Link>
102
+ <Button
103
+ onClick={() => {
104
+ setModal({
105
+ title: getString('delete'),
106
+ body: (
107
+ <div className='p-4'>
108
+ <div className='flex flex-col gap-4'>
109
+ <div>{getString('deleteItemText')}</div>
110
+ <div className='flex gap-2'>
111
+ <button
112
+ className='rounded bg-success px-2 py-1 text-success-foreground'
113
+ onClick={handleDelete}
114
+ >
115
+ Yes
116
+ </button>
117
+ <button
118
+ className='rounded bg-destructive px-2 py-1 text-destructive-foreground'
119
+ onClick={() => {
120
+ setModal(null)
121
+ }}
122
+ >
123
+ No
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ ),
129
+ headerColor: 'bg-red-700',
130
+ titleColor: 'text-white',
131
+ lang: 'en',
132
+ })
133
+ }}
134
+ variant='destructive'
135
+ className='rounded px-3 py-1 font-semibold'
136
+ >
137
+ {getString('delete')}
138
+ </Button>
139
+ </CardFooter>
140
+ </Card>
141
+ </>
142
+ )
143
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react'
2
+ import getString from 'nextjs-cms/translations'
3
+ import { Badge } from '@/components/ui/badge'
4
+
5
+ const SectionItemStatusBadge = ({ status }: { status: number }) => {
6
+ return (
7
+ <Badge
8
+ variant={status === 1 ? 'success' : 'destructive'}
9
+ className='absolute top-3 shadow left-3 z-30 font-bold'
10
+ >
11
+ {status === 1 ? getString('approved') : getString('pending_approval')}
12
+ </Badge>
13
+ )
14
+ }
15
+
16
+ export default SectionItemStatusBadge
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
4
+ import React, { RefObject, useEffect, useRef, useState } from 'react'
5
+ import getString from 'nextjs-cms/translations'
6
+ import { AxiosError } from 'axios'
7
+ import InfoCard from '@/components/InfoCard'
8
+ import LoadingSpinners from '@/components/LoadingSpinners'
9
+ import { DropzoneHandles } from '@/components/Dropzone'
10
+ import { useToast } from '@/components/ui/use-toast'
11
+ import { trpc } from '@/app/_trpc/client'
12
+ import Form from '@/components/form/Form'
13
+
14
+ export default function SectionPage({ section }: { section: string }) {
15
+ const [progress, setProgress] = useState(0)
16
+ const [progressVariant, setProgressVariant] = useState<'determinate' | 'query'>('determinate')
17
+ const [isSubmitting, setIsSubmitting] = useState(false)
18
+ const [response, setResponse] = useState<any>(null)
19
+ const axiosPrivate = useAxiosPrivate()
20
+ const controller = new AbortController()
21
+ const { toast } = useToast()
22
+ const dropzoneRef: RefObject<DropzoneHandles | null> = useRef(null)
23
+
24
+ const { isLoading, isError, data, error, refetch } = trpc.simpleSections.create.useQuery({
25
+ sectionName: section,
26
+ })
27
+
28
+ const handleSubmit = async (formData: FormData) => {
29
+ setIsSubmitting(true)
30
+ if (isSubmitting) return
31
+ setResponse(null)
32
+ formData.append('sectionName', section)
33
+ formData.append('formType', 'edit')
34
+ formData.append('sectionType', 'has_items')
35
+ try {
36
+ const res = await axiosPrivate.put(`/submit/section/simple`, formData, {
37
+ signal: controller.signal,
38
+ onUploadProgress: (progressEvent) => {
39
+ if (!progressEvent.total) return
40
+ const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
41
+ // Update progress bar value here
42
+ setProgress(progress)
43
+
44
+ if (progress === 100) {
45
+ setProgressVariant('query')
46
+ }
47
+ },
48
+ headers: {
49
+ 'Content-Type': 'multipart/form-data',
50
+ },
51
+ })
52
+
53
+ if (res) {
54
+ setIsSubmitting(false)
55
+ setProgress(0)
56
+ setProgressVariant('determinate')
57
+ if (res.status === 200) {
58
+ toast({
59
+ variant: res.data?.errors?.length ? 'warning' : 'success',
60
+ title: getString('sectionUpdatedSuccessfully'),
61
+ description: res.data?.errors?.length
62
+ ? getString('errorsInSubmit')
63
+ : getString('sectionUpdatedSuccessfully'),
64
+ })
65
+
66
+ // Refetch the page
67
+ await refetch()
68
+ }
69
+ }
70
+ } catch (error: AxiosError | any) {
71
+ setIsSubmitting(false)
72
+ setProgress(0)
73
+ setProgressVariant('determinate')
74
+ if (error?.response?.data) {
75
+ setResponse(<InfoCard result={{ key: 'danger', title: error.response.data.error, status: false }} />)
76
+ }
77
+ }
78
+ }
79
+
80
+ function cancelSubmit() {
81
+ controller.abort()
82
+ setIsSubmitting(false)
83
+ setProgress(0)
84
+ setProgressVariant('determinate')
85
+ }
86
+
87
+ useEffect(() => {
88
+ return () => {
89
+ cancelSubmit()
90
+ }
91
+ }, [])
92
+
93
+ return (
94
+ <div className='flex w-full flex-col overflow-hidden'>
95
+ {isLoading ? (
96
+ <div>
97
+ <LoadingSpinners />
98
+ </div>
99
+ ) : null}
100
+ {isError ? <div>{error?.message}</div> : null}
101
+ {data ? (
102
+ <div className='flex w-full flex-col'>
103
+ <div className='relative z-[1] border-b-2 p-8 pt-12 font-extrabold text-foreground'>
104
+ <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>
105
+ <h1 className='pb-4 text-4xl'>{data.section?.title}</h1>
106
+ <span>
107
+ /{getString('edit')} {data.section?.title}
108
+ </span>
109
+ </div>
110
+ <Form
111
+ formType='edit'
112
+ progressVariant={progressVariant}
113
+ response={response}
114
+ progress={progress}
115
+ data={data}
116
+ dropzoneRef={dropzoneRef}
117
+ handleSubmit={handleSubmit}
118
+ isSubmitting={isSubmitting}
119
+ />
120
+ </div>
121
+ ) : null}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import { Fragment, useState } from 'react'
4
+ import { Listbox, Transition, ListboxOptions, ListboxOption, ListboxButton } from '@headlessui/react'
5
+ import classNames from 'classnames'
6
+ import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
7
+ import { SelectOption } from 'nextjs-cms/core/fields'
8
+
9
+ export default function SelectBox({
10
+ items,
11
+ defaultValue,
12
+ onChange = () => {},
13
+ classname,
14
+ }: {
15
+ items: SelectOption[]
16
+ defaultValue?: string | number | undefined
17
+ onChange: any
18
+ classname: string
19
+ }) {
20
+ // Set the selected item,
21
+ // check for defaultValue and set it if it exists,
22
+ const [selected, setSelected] = useState(
23
+ defaultValue ? items.filter((item) => item.value?.toString() === defaultValue)[0] : items[0],
24
+ )
25
+
26
+ /*useEffect(() => {
27
+ onChange(selected)
28
+ }, [selected])*/
29
+
30
+ /*useEffect(() => {
31
+ setSelected(defaultValue ? items.filter((item) => item.value?.toString() === defaultValue)[0] : items[0])
32
+ }, [defaultValue])*/
33
+
34
+ return (
35
+ <Listbox
36
+ as='div'
37
+ value={selected}
38
+ onChange={(value) => {
39
+ setSelected(value)
40
+ onChange(value)
41
+ }}
42
+ >
43
+ {({ open }) => (
44
+ <>
45
+ {/*<Listbox.Label className='block text-sm font-medium leading-6 text-gray-900'>Fabrics</Listbox.Label>*/}
46
+ <div className={`relative ${classname}`}>
47
+ <ListboxButton className='relative w-full cursor-default rounded bg-input p-3 pe-1 ps-1 text-start text-foreground shadow-sm outline-0 ring-2 ring-gray-300 hover:ring-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6'>
48
+ <span className='ml-3 block truncate'>{selected?.label ? selected.label : ''}</span>
49
+ <span className='pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2'>
50
+ <ChevronDownIcon className='h-5 w-5 text-gray-400' aria-hidden='true' />
51
+ </span>
52
+ </ListboxButton>
53
+
54
+ <Transition
55
+ show={open}
56
+ as={Fragment}
57
+ leave='transition ease-in duration-100'
58
+ leaveFrom='opacity-100'
59
+ leaveTo='opacity-0'
60
+ >
61
+ <ListboxOptions className='absolute z-40 mt-1 max-h-56 w-full overflow-auto rounded-md border bg-background py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'>
62
+ {items.map((item: SelectOption, index: number) => (
63
+ <ListboxOption
64
+ key={`${item.label}-${index}`}
65
+ className={({ focus }) =>
66
+ classNames(
67
+ focus ? 'bg-indigo-600 text-white' : 'text-foreground',
68
+ 'relative cursor-default select-none py-2 pe-1 ps-1',
69
+ )
70
+ }
71
+ value={item}
72
+ >
73
+ {({ selected, focus }) => (
74
+ <div className='flex flex-wrap items-center gap-1 px-3'>
75
+ {selected && item.value !== undefined ? (
76
+ <div
77
+ className={classNames(focus ? 'text-white' : 'text-foreground')}
78
+ >
79
+ <CheckIcon />
80
+ </div>
81
+ ) : null}
82
+
83
+ <div
84
+ className={classNames(selected ? 'font-extrabold' : 'font-normal')}
85
+ >
86
+ {item.label}
87
+ </div>
88
+ </div>
89
+ )}
90
+ </ListboxOption>
91
+ ))}
92
+ </ListboxOptions>
93
+ </Transition>
94
+ </div>
95
+ </>
96
+ )}
97
+ </Listbox>
98
+ )
99
+ }