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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +395 -0
- package/dist/lib/utils.d.ts +11 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +48 -0
- package/package.json +44 -0
- package/templates/default/.env +24 -0
- package/templates/default/.env.development +8 -0
- package/templates/default/.eslintrc.json +5 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc.json +19 -0
- package/templates/default/CHANGELOG.md +77 -0
- package/templates/default/README.md +45 -0
- package/templates/default/app/(auth)/auth/login/LoginPage.tsx +175 -0
- package/templates/default/app/(auth)/auth/login/page.tsx +12 -0
- package/templates/default/app/(rootLayout)/admins/page.tsx +5 -0
- package/templates/default/app/(rootLayout)/advanced/page.tsx +5 -0
- package/templates/default/app/(rootLayout)/analytics/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/dashboard/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/emails/page.tsx +6 -0
- package/templates/default/app/(rootLayout)/layout.tsx +5 -0
- package/templates/default/app/(rootLayout)/loading.tsx +10 -0
- package/templates/default/app/(rootLayout)/log/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/new/[section]/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/page.tsx +9 -0
- package/templates/default/app/(rootLayout)/section/[section]/page.tsx +7 -0
- package/templates/default/app/(rootLayout)/settings/page.tsx +7 -0
- package/templates/default/app/_trpc/client.ts +4 -0
- package/templates/default/app/api/auth/csrf/route.ts +25 -0
- package/templates/default/app/api/auth/refresh/route.ts +10 -0
- package/templates/default/app/api/auth/route.ts +23 -0
- package/templates/default/app/api/auth/session/route.ts +20 -0
- package/templates/default/app/api/editor/photo/route.ts +42 -0
- package/templates/default/app/api/photo/route.ts +27 -0
- package/templates/default/app/api/placeholder/route.ts +7 -0
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +63 -0
- package/templates/default/app/api/submit/section/item/route.ts +53 -0
- package/templates/default/app/api/submit/section/simple/route.ts +54 -0
- package/templates/default/app/api/trpc/[trpc]/route.ts +33 -0
- package/templates/default/app/api/video/route.ts +174 -0
- package/templates/default/app/dictionaries.ts +14 -0
- package/templates/default/app/layout.tsx +28 -0
- package/templates/default/app/providers.tsx +151 -0
- package/templates/default/cli.ts +4 -0
- package/templates/default/components/AdminCard.tsx +163 -0
- package/templates/default/components/AdminEditPage.tsx +123 -0
- package/templates/default/components/AdminPrivilegeCard.tsx +184 -0
- package/templates/default/components/AdminsPage.tsx +43 -0
- package/templates/default/components/AdvancedSettingsPage.tsx +167 -0
- package/templates/default/components/AnalyticsPage.tsx +127 -0
- package/templates/default/components/BarChartBox.tsx +43 -0
- package/templates/default/components/BrowsePage.tsx +119 -0
- package/templates/default/components/CategorizedSectionPage.tsx +36 -0
- package/templates/default/components/CategoryDeleteConfirmPage.tsx +129 -0
- package/templates/default/components/CategorySectionSelectInput.tsx +139 -0
- package/templates/default/components/ConditionalFields.tsx +49 -0
- package/templates/default/components/ContainerBox.tsx +24 -0
- package/templates/default/components/DashboardPage.tsx +187 -0
- package/templates/default/components/DashboardPageAlt.tsx +43 -0
- package/templates/default/components/DefaultNavItems.tsx +3 -0
- package/templates/default/components/Dropzone.tsx +153 -0
- package/templates/default/components/EmailCard.tsx +137 -0
- package/templates/default/components/EmailPasswordForm.tsx +84 -0
- package/templates/default/components/EmailQuotaForm.tsx +72 -0
- package/templates/default/components/EmailsPage.tsx +48 -0
- package/templates/default/components/GalleryPhoto.tsx +93 -0
- package/templates/default/components/InfoCard.tsx +94 -0
- package/templates/default/components/ItemEditPage.tsx +217 -0
- package/templates/default/components/Layout.tsx +70 -0
- package/templates/default/components/LoadingSpinners.tsx +67 -0
- package/templates/default/components/LogPage.tsx +17 -0
- package/templates/default/components/Modal.tsx +99 -0
- package/templates/default/components/Navbar.tsx +29 -0
- package/templates/default/components/NavbarAlt.tsx +182 -0
- package/templates/default/components/NewAdminForm.tsx +172 -0
- package/templates/default/components/NewEmailForm.tsx +131 -0
- package/templates/default/components/NewPage.tsx +206 -0
- package/templates/default/components/NewVariantComponent.tsx +228 -0
- package/templates/default/components/PhotoGallery.tsx +35 -0
- package/templates/default/components/PieChartBox.tsx +101 -0
- package/templates/default/components/ProgressBar.tsx +24 -0
- package/templates/default/components/ProtectedDocument.tsx +78 -0
- package/templates/default/components/ProtectedImage.tsx +143 -0
- package/templates/default/components/ProtectedVideo.tsx +76 -0
- package/templates/default/components/SectionItemCard.tsx +143 -0
- package/templates/default/components/SectionItemStatusBadge.tsx +16 -0
- package/templates/default/components/SectionPage.tsx +124 -0
- package/templates/default/components/SelectBox.tsx +99 -0
- package/templates/default/components/SelectInputButtons.tsx +124 -0
- package/templates/default/components/SettingsPage.tsx +238 -0
- package/templates/default/components/Sidebar.tsx +209 -0
- package/templates/default/components/SidebarDropdownItem.tsx +74 -0
- package/templates/default/components/SidebarItem.tsx +19 -0
- package/templates/default/components/TempPage.tsx +12 -0
- package/templates/default/components/ThemeProvider.tsx +8 -0
- package/templates/default/components/TooltipComponent.tsx +27 -0
- package/templates/default/components/VariantCard.tsx +123 -0
- package/templates/default/components/VariantEditPage.tsx +229 -0
- package/templates/default/components/analytics/BounceRate.tsx +69 -0
- package/templates/default/components/analytics/LivePageViews.tsx +54 -0
- package/templates/default/components/analytics/LiveUsersCount.tsx +32 -0
- package/templates/default/components/analytics/MonthlyPageViews.tsx +41 -0
- package/templates/default/components/analytics/TopCountries.tsx +51 -0
- package/templates/default/components/analytics/TopDevices.tsx +45 -0
- package/templates/default/components/analytics/TopMediums.tsx +57 -0
- package/templates/default/components/analytics/TopSources.tsx +44 -0
- package/templates/default/components/analytics/TotalPageViews.tsx +40 -0
- package/templates/default/components/analytics/TotalSessions.tsx +40 -0
- package/templates/default/components/analytics/TotalUniqueUsers.tsx +40 -0
- package/templates/default/components/custom/RightHomeRoomVariantCard.tsx +137 -0
- package/templates/default/components/dndKit/Draggable.tsx +21 -0
- package/templates/default/components/dndKit/Droppable.tsx +20 -0
- package/templates/default/components/dndKit/SortableItem.tsx +18 -0
- package/templates/default/components/form/DateRangeFormInput.tsx +55 -0
- package/templates/default/components/form/Form.tsx +298 -0
- package/templates/default/components/form/FormInputElement.tsx +68 -0
- package/templates/default/components/form/FormInputs.tsx +108 -0
- package/templates/default/components/form/helpers/util.ts +20 -0
- package/templates/default/components/form/inputs/CheckboxFormInput.tsx +33 -0
- package/templates/default/components/form/inputs/ColorFormInput.tsx +44 -0
- package/templates/default/components/form/inputs/DateFormInput.tsx +107 -0
- package/templates/default/components/form/inputs/DocumentFormInput.tsx +124 -0
- package/templates/default/components/form/inputs/MapFormInput.tsx +139 -0
- package/templates/default/components/form/inputs/MultipleSelectFormInput.tsx +150 -0
- package/templates/default/components/form/inputs/NumberFormInput.tsx +42 -0
- package/templates/default/components/form/inputs/PasswordFormInput.tsx +47 -0
- package/templates/default/components/form/inputs/PhotoFormInput.tsx +218 -0
- package/templates/default/components/form/inputs/RichTextFormInput.tsx +133 -0
- package/templates/default/components/form/inputs/SelectFormInput.tsx +164 -0
- package/templates/default/components/form/inputs/TagsFormInput.tsx +63 -0
- package/templates/default/components/form/inputs/TextFormInput.tsx +48 -0
- package/templates/default/components/form/inputs/TextareaFormInput.tsx +47 -0
- package/templates/default/components/form/inputs/VideoFormInput.tsx +117 -0
- package/templates/default/components/pagination/Pagination.tsx +36 -0
- package/templates/default/components/pagination/PaginationButtons.tsx +145 -0
- package/templates/default/components/ui/accordion.tsx +57 -0
- package/templates/default/components/ui/alert.tsx +46 -0
- package/templates/default/components/ui/badge.tsx +33 -0
- package/templates/default/components/ui/button.tsx +57 -0
- package/templates/default/components/ui/calendar.tsx +68 -0
- package/templates/default/components/ui/card.tsx +76 -0
- package/templates/default/components/ui/checkbox.tsx +29 -0
- package/templates/default/components/ui/dropdown-menu.tsx +205 -0
- package/templates/default/components/ui/input.tsx +25 -0
- package/templates/default/components/ui/label.tsx +26 -0
- package/templates/default/components/ui/popover.tsx +31 -0
- package/templates/default/components/ui/scroll-area.tsx +42 -0
- package/templates/default/components/ui/select.tsx +164 -0
- package/templates/default/components/ui/sheet.tsx +107 -0
- package/templates/default/components/ui/switch.tsx +29 -0
- package/templates/default/components/ui/table.tsx +120 -0
- package/templates/default/components/ui/tabs.tsx +55 -0
- package/templates/default/components/ui/toast.tsx +113 -0
- package/templates/default/components/ui/toaster.tsx +35 -0
- package/templates/default/components/ui/tooltip.tsx +30 -0
- package/templates/default/components/ui/use-toast.ts +188 -0
- package/templates/default/components.json +16 -0
- package/templates/default/context/ModalProvider.tsx +53 -0
- package/templates/default/drizzle.config.ts +4 -0
- package/templates/default/dynamic-schemas/schema.ts +373 -0
- package/templates/default/env/env.js +130 -0
- package/templates/default/envConfig.ts +4 -0
- package/templates/default/hooks/useModal.ts +8 -0
- package/templates/default/lib/apiHelpers.ts +106 -0
- package/templates/default/lz.config.ts +40 -0
- package/templates/default/middleware.ts +33 -0
- package/templates/default/next.config.ts +46 -0
- package/templates/default/package.json +134 -0
- package/templates/default/postcss.config.js +6 -0
- package/templates/default/postinstall.js +14 -0
- package/templates/default/public/blank_avatar.png +0 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/public/img/placeholder.svg +1 -0
- package/templates/default/public/lazemni_logo.png +0 -0
- package/templates/default/public/next.svg +1 -0
- package/templates/default/public/vercel.svg +1 -0
- package/templates/default/section-tests.ts +92 -0
- package/templates/default/styles/globals.css +88 -0
- package/templates/default/tailwind.config.js +95 -0
- package/templates/default/test.ts +77 -0
- 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
|
+
}
|