create-nextjs-cms 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/helpers/utils.js +16 -16
- package/dist/lib/section-creators.js +166 -166
- package/package.json +3 -3
- package/templates/default/.eslintrc.json +5 -5
- package/templates/default/.prettierignore +7 -7
- package/templates/default/.prettierrc.json +27 -27
- package/templates/default/CHANGELOG.md +140 -140
- package/templates/default/_gitignore +57 -57
- package/templates/default/app/(auth)/auth/login/LoginPage.tsx +192 -192
- package/templates/default/app/(auth)/auth/login/page.tsx +11 -11
- package/templates/default/app/(auth)/auth-locale-provider.tsx +34 -34
- package/templates/default/app/(auth)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/page.tsx +40 -40
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/plugin-server-registry.ts +22 -22
- package/templates/default/app/(rootLayout)/admins/page.tsx +10 -10
- package/templates/default/app/(rootLayout)/browse/[section]/[page]/page.tsx +22 -22
- package/templates/default/app/(rootLayout)/categorized/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/dashboard/page.tsx +63 -63
- package/templates/default/app/(rootLayout)/dashboard-new/page.tsx +7 -7
- package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +17 -17
- package/templates/default/app/(rootLayout)/layout.tsx +81 -81
- package/templates/default/app/(rootLayout)/loading.tsx +10 -10
- package/templates/default/app/(rootLayout)/log/page.tsx +7 -7
- package/templates/default/app/(rootLayout)/new/[section]/page.tsx +15 -15
- package/templates/default/app/(rootLayout)/section/[section]/page.tsx +16 -16
- package/templates/default/app/(rootLayout)/settings/page.tsx +13 -13
- package/templates/default/app/_trpc/client.ts +3 -3
- package/templates/default/app/api/auth/csrf/route.ts +25 -25
- package/templates/default/app/api/auth/refresh/route.ts +10 -10
- package/templates/default/app/api/auth/session/route.ts +20 -20
- package/templates/default/app/api/editor/photo/route.ts +49 -49
- package/templates/default/app/api/photo/route.ts +27 -27
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +66 -66
- package/templates/default/app/api/submit/section/item/route.ts +56 -56
- package/templates/default/app/api/submit/section/simple/route.ts +57 -57
- package/templates/default/app/api/trpc/[trpc]/route.ts +33 -33
- package/templates/default/app/api/video/route.ts +174 -174
- package/templates/default/app/globals.css +219 -219
- package/templates/default/app/providers.tsx +152 -152
- package/templates/default/cms.config.ts +49 -52
- package/templates/default/components/AdminCard.tsx +166 -166
- package/templates/default/components/AdminEditPage.tsx +124 -124
- package/templates/default/components/AdminPrivilegeCard.tsx +185 -185
- package/templates/default/components/AdminsPage.tsx +43 -43
- package/templates/default/components/AnalyticsPage.tsx +128 -128
- package/templates/default/components/BarChartBox.tsx +42 -42
- package/templates/default/components/BrowsePage.tsx +106 -106
- package/templates/default/components/CategorizedSectionPage.tsx +31 -31
- package/templates/default/components/CategoryDeleteConfirmPage.tsx +130 -130
- package/templates/default/components/CategorySectionSelectInput.tsx +140 -140
- package/templates/default/components/ConditionalFields.tsx +49 -49
- package/templates/default/components/ContainerBox.tsx +24 -24
- package/templates/default/components/DashboardNewPage.tsx +253 -253
- package/templates/default/components/DashboardPage.tsx +188 -188
- package/templates/default/components/DashboardPageAlt.tsx +45 -45
- package/templates/default/components/DefaultNavItems.tsx +3 -3
- package/templates/default/components/Dropzone.tsx +154 -154
- package/templates/default/components/EmailCard.tsx +138 -138
- package/templates/default/components/EmailPasswordForm.tsx +85 -85
- package/templates/default/components/EmailQuotaForm.tsx +73 -73
- package/templates/default/components/EmailsPage.tsx +49 -49
- package/templates/default/components/ErrorComponent.tsx +16 -16
- package/templates/default/components/GalleryPhoto.tsx +93 -93
- package/templates/default/components/InfoCard.tsx +93 -93
- package/templates/default/components/ItemEditPage.tsx +214 -214
- package/templates/default/components/Layout.tsx +84 -84
- package/templates/default/components/LoadingSpinners.tsx +67 -67
- package/templates/default/components/LogPage.tsx +107 -107
- package/templates/default/components/Modal.tsx +166 -166
- package/templates/default/components/Navbar.tsx +258 -258
- package/templates/default/components/NewAdminForm.tsx +173 -173
- package/templates/default/components/NewEmailForm.tsx +132 -132
- package/templates/default/components/NewPage.tsx +205 -205
- package/templates/default/components/NewVariantComponent.tsx +229 -229
- package/templates/default/components/PhotoGallery.tsx +35 -35
- package/templates/default/components/PieChartBox.tsx +101 -101
- package/templates/default/components/ProgressBar.tsx +48 -48
- package/templates/default/components/ProtectedDocument.tsx +78 -78
- package/templates/default/components/ProtectedImage.tsx +143 -143
- package/templates/default/components/ProtectedVideo.tsx +76 -76
- package/templates/default/components/SectionItemCard.tsx +144 -144
- package/templates/default/components/SectionItemStatusBadge.tsx +17 -17
- package/templates/default/components/SectionPage.tsx +125 -125
- package/templates/default/components/SelectBox.tsx +98 -98
- package/templates/default/components/SelectInputButtons.tsx +125 -125
- package/templates/default/components/SettingsPage.tsx +232 -232
- package/templates/default/components/Sidebar.tsx +201 -201
- package/templates/default/components/SidebarDropdownItem.tsx +80 -80
- package/templates/default/components/SidebarItem.tsx +20 -20
- package/templates/default/components/ThemeProvider.tsx +8 -8
- package/templates/default/components/TooltipComponent.tsx +27 -27
- package/templates/default/components/VariantCard.tsx +124 -124
- package/templates/default/components/VariantEditPage.tsx +230 -230
- package/templates/default/components/analytics/BounceRate.tsx +70 -70
- package/templates/default/components/analytics/LivePageViews.tsx +55 -55
- package/templates/default/components/analytics/LiveUsersCount.tsx +33 -33
- package/templates/default/components/analytics/MonthlyPageViews.tsx +42 -42
- package/templates/default/components/analytics/TopCountries.tsx +52 -52
- package/templates/default/components/analytics/TopDevices.tsx +46 -46
- package/templates/default/components/analytics/TopMediums.tsx +58 -58
- package/templates/default/components/analytics/TopSources.tsx +45 -45
- package/templates/default/components/analytics/TotalPageViews.tsx +41 -41
- package/templates/default/components/analytics/TotalSessions.tsx +41 -41
- package/templates/default/components/analytics/TotalUniqueUsers.tsx +41 -41
- package/templates/default/components/custom/RightHomeRoomVariantCard.tsx +138 -138
- package/templates/default/components/dndKit/Draggable.tsx +21 -21
- package/templates/default/components/dndKit/Droppable.tsx +20 -20
- package/templates/default/components/dndKit/SortableItem.tsx +18 -18
- package/templates/default/components/form/DateRangeFormInput.tsx +57 -57
- package/templates/default/components/form/Form.tsx +317 -317
- package/templates/default/components/form/FormInputElement.tsx +70 -70
- package/templates/default/components/form/FormInputs.tsx +112 -112
- package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
- package/templates/default/components/form/helpers/util.ts +17 -17
- package/templates/default/components/form/inputs/CheckboxFormInput.tsx +33 -33
- package/templates/default/components/form/inputs/ColorFormInput.tsx +44 -44
- package/templates/default/components/form/inputs/DateFormInput.tsx +156 -156
- package/templates/default/components/form/inputs/DocumentFormInput.tsx +222 -222
- package/templates/default/components/form/inputs/MapFormInput.tsx +140 -140
- package/templates/default/components/form/inputs/MultipleSelectFormInput.tsx +83 -83
- package/templates/default/components/form/inputs/NumberFormInput.tsx +42 -42
- package/templates/default/components/form/inputs/PasswordFormInput.tsx +47 -47
- package/templates/default/components/form/inputs/PhotoFormInput.tsx +219 -219
- package/templates/default/components/form/inputs/RichTextFormInput.tsx +135 -135
- package/templates/default/components/form/inputs/SelectFormInput.tsx +175 -175
- package/templates/default/components/form/inputs/SlugFormInput.tsx +129 -129
- package/templates/default/components/form/inputs/TagsFormInput.tsx +154 -154
- package/templates/default/components/form/inputs/TextFormInput.tsx +48 -48
- package/templates/default/components/form/inputs/TextareaFormInput.tsx +47 -47
- package/templates/default/components/form/inputs/VideoFormInput.tsx +118 -118
- package/templates/default/components/locale-dropdown.tsx +74 -74
- package/templates/default/components/locale-picker.tsx +85 -85
- package/templates/default/components/login-locale-dropdown.tsx +46 -46
- package/templates/default/components/multi-select.tsx +1144 -1144
- package/templates/default/components/pagination/Pagination.tsx +36 -36
- package/templates/default/components/pagination/PaginationButtons.tsx +147 -147
- package/templates/default/components/theme-toggle.tsx +37 -37
- package/templates/default/components/ui/accordion.tsx +53 -53
- package/templates/default/components/ui/alert-dialog.tsx +157 -157
- package/templates/default/components/ui/alert.tsx +46 -46
- package/templates/default/components/ui/badge.tsx +38 -38
- package/templates/default/components/ui/button.tsx +62 -62
- package/templates/default/components/ui/calendar.tsx +166 -166
- package/templates/default/components/ui/card.tsx +43 -43
- package/templates/default/components/ui/checkbox.tsx +29 -29
- package/templates/default/components/ui/command.tsx +137 -137
- package/templates/default/components/ui/custom-alert-dialog.tsx +113 -113
- package/templates/default/components/ui/custom-dialog.tsx +123 -123
- package/templates/default/components/ui/dialog.tsx +123 -123
- package/templates/default/components/ui/dropdown-menu.tsx +182 -182
- package/templates/default/components/ui/input-group.tsx +54 -54
- package/templates/default/components/ui/input.tsx +22 -22
- package/templates/default/components/ui/label.tsx +19 -19
- package/templates/default/components/ui/popover.tsx +42 -42
- package/templates/default/components/ui/progress.tsx +31 -31
- package/templates/default/components/ui/scroll-area.tsx +42 -42
- package/templates/default/components/ui/select.tsx +165 -165
- package/templates/default/components/ui/separator.tsx +28 -28
- package/templates/default/components/ui/sheet.tsx +103 -103
- package/templates/default/components/ui/switch.tsx +29 -29
- package/templates/default/components/ui/table.tsx +83 -83
- package/templates/default/components/ui/tabs.tsx +55 -55
- package/templates/default/components/ui/toast.tsx +113 -113
- package/templates/default/components/ui/toaster.tsx +35 -35
- package/templates/default/components/ui/tooltip.tsx +30 -30
- package/templates/default/components/ui/use-toast.ts +188 -188
- package/templates/default/components.json +21 -21
- package/templates/default/context/ModalProvider.tsx +53 -53
- package/templates/default/drizzle.config.ts +4 -4
- package/templates/default/dynamic-schemas/schema.ts +10 -0
- package/templates/default/env/env.js +130 -130
- package/templates/default/envConfig.ts +4 -4
- package/templates/default/hooks/useModal.ts +8 -8
- package/templates/default/lib/apiHelpers.ts +92 -92
- package/templates/default/lib/postinstall.js +14 -14
- package/templates/default/lib/utils.ts +6 -6
- package/templates/default/next-env.d.ts +6 -6
- package/templates/default/next.config.ts +23 -23
- package/templates/default/package.json +2 -4
- package/templates/default/postcss.config.mjs +6 -6
- package/templates/default/proxy.ts +32 -32
- package/templates/default/tsconfig.json +48 -48
|
@@ -1,258 +1,258 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import classNames from 'classnames'
|
|
3
|
-
import { MoonIcon, SunIcon, BellIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
|
4
|
-
import { useTheme } from 'next-themes'
|
|
5
|
-
import Link from 'next/link'
|
|
6
|
-
import { useI18n } from 'nextjs-cms/translations/client'
|
|
7
|
-
import { RTL_LOCALES } from 'nextjs-cms/translations'
|
|
8
|
-
import { trpc } from '@/app/_trpc/client'
|
|
9
|
-
import ProtectedImage from '@/components/ProtectedImage'
|
|
10
|
-
import { Spinner } from '@/components/ui/spinner'
|
|
11
|
-
import Image from 'next/image'
|
|
12
|
-
import { LogOut, Settings } from 'lucide-react'
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
DropdownMenu,
|
|
16
|
-
DropdownMenuContent,
|
|
17
|
-
DropdownMenuGroup,
|
|
18
|
-
DropdownMenuItem,
|
|
19
|
-
DropdownMenuLabel,
|
|
20
|
-
DropdownMenuSeparator,
|
|
21
|
-
DropdownMenuShortcut,
|
|
22
|
-
DropdownMenuTrigger,
|
|
23
|
-
} from '@/components/ui/dropdown-menu'
|
|
24
|
-
import { useToast } from '@/components/ui/use-toast'
|
|
25
|
-
import { logout, useSession } from 'nextjs-cms/auth/react'
|
|
26
|
-
import ThemeToggle from './theme-toggle'
|
|
27
|
-
import LocaleDropdown from './locale-dropdown'
|
|
28
|
-
type Props = {
|
|
29
|
-
/**
|
|
30
|
-
* Allows the parent component to modify the state when the
|
|
31
|
-
* menu button is clicked.
|
|
32
|
-
*/
|
|
33
|
-
onMenuButtonClick(): void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const formatTimestamp = (value?: Date | string | null) => {
|
|
37
|
-
if (!value) return ''
|
|
38
|
-
const date = value instanceof Date ? value : new Date(value)
|
|
39
|
-
if (Number.isNaN(date.getTime())) return ''
|
|
40
|
-
return date.toLocaleString()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export default function Navbar(props: Props) {
|
|
44
|
-
const t = useI18n()
|
|
45
|
-
const session = useSession()
|
|
46
|
-
const { toast } = useToast()
|
|
47
|
-
const locale = session?.data?.user?.locale ?? 'en'
|
|
48
|
-
const isRTL = RTL_LOCALES.has(locale)
|
|
49
|
-
const logsQuery = trpc.logs.list.useQuery(
|
|
50
|
-
{
|
|
51
|
-
limit: 20,
|
|
52
|
-
offset: 0,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
enabled: false,
|
|
56
|
-
},
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const handleNotificationsOpenChange = (open: boolean) => {
|
|
60
|
-
if (open) {
|
|
61
|
-
void logsQuery.refetch()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const logs = logsQuery.data?.items ?? []
|
|
66
|
-
const handleLogout = async (e: React.MouseEvent<HTMLDivElement>) => {
|
|
67
|
-
e.preventDefault()
|
|
68
|
-
e.stopPropagation()
|
|
69
|
-
try {
|
|
70
|
-
await logout()
|
|
71
|
-
} catch (error: any) {
|
|
72
|
-
toast({
|
|
73
|
-
variant: 'destructive',
|
|
74
|
-
title: t('logoutError'),
|
|
75
|
-
description: error.message,
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<>
|
|
82
|
-
<nav
|
|
83
|
-
className={classNames({
|
|
84
|
-
'sticky top-0 h-[65px] bg-white text-zinc-500 drop-shadow-sm dark:bg-slate-800': true, // colors
|
|
85
|
-
'top-0 z-10 w-full px-4 shadow-xs md:w-full': true, //positioning & styling
|
|
86
|
-
})}
|
|
87
|
-
>
|
|
88
|
-
<div className='mx-auto px-0 lg:px-2'>
|
|
89
|
-
<div className='flex h-16 items-center justify-between'>
|
|
90
|
-
<div className='flex items-center'>
|
|
91
|
-
<div className='shrink-0'>
|
|
92
|
-
<div className='flex md:hidden'>
|
|
93
|
-
{/* Mobile menu button */}
|
|
94
|
-
<button
|
|
95
|
-
onClick={props.onMenuButtonClick}
|
|
96
|
-
className='border-foreground text-foreground hover:text-foreground/90 relative inline-flex items-center justify-center rounded-md border p-2 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800 focus:outline-hidden'
|
|
97
|
-
>
|
|
98
|
-
<span className='absolute -inset-0.5' />
|
|
99
|
-
<span className='sr-only'>Open main menu</span>
|
|
100
|
-
<HamburgerMenuIcon className='block h-6 w-6' aria-hidden='true' />
|
|
101
|
-
</button>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
{/*<div className=''>
|
|
106
|
-
<div className='flex items-baseline space-x-4'>
|
|
107
|
-
<input
|
|
108
|
-
type='text'
|
|
109
|
-
className='h-full w-full border-0 bg-transparent text-amber-500 outline-0 ring-0'
|
|
110
|
-
placeholder='Search...'
|
|
111
|
-
/>
|
|
112
|
-
</div>
|
|
113
|
-
</div>*/}
|
|
114
|
-
</div>
|
|
115
|
-
<div className=''>
|
|
116
|
-
<div className='ms-4 flex items-center md:ms-6'>
|
|
117
|
-
<div className='flex flex-row items-center gap-3'>
|
|
118
|
-
<DropdownMenu onOpenChange={handleNotificationsOpenChange}>
|
|
119
|
-
<DropdownMenuTrigger
|
|
120
|
-
asChild
|
|
121
|
-
className='text-foreground hover:text-foreground/90 cursor-pointer'
|
|
122
|
-
>
|
|
123
|
-
<button
|
|
124
|
-
type='button'
|
|
125
|
-
className='relative flex h-10 items-center justify-center rounded-full focus:outline-hidden'
|
|
126
|
-
aria-label='Notifications'
|
|
127
|
-
>
|
|
128
|
-
<span className='absolute -inset-1.5' />
|
|
129
|
-
<BellIcon className='h-6 w-6' />
|
|
130
|
-
</button>
|
|
131
|
-
</DropdownMenuTrigger>
|
|
132
|
-
<DropdownMenuContent
|
|
133
|
-
sideOffset={12}
|
|
134
|
-
align={'end'}
|
|
135
|
-
side='bottom'
|
|
136
|
-
className='w-[400px] max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
137
|
-
>
|
|
138
|
-
<DropdownMenuLabel>{t('notifications')}</DropdownMenuLabel>
|
|
139
|
-
<DropdownMenuSeparator />
|
|
140
|
-
<DropdownMenuGroup className='max-h-[320px] overflow-y-auto'>
|
|
141
|
-
{logsQuery.isFetching && logs.length === 0 ? (
|
|
142
|
-
<DropdownMenuItem disabled className='flex items-center gap-2'>
|
|
143
|
-
<Spinner className='size-3' />
|
|
144
|
-
<span>{t('loading')}</span>
|
|
145
|
-
</DropdownMenuItem>
|
|
146
|
-
) : logsQuery.isError ? (
|
|
147
|
-
<DropdownMenuItem disabled>
|
|
148
|
-
{logsQuery.error?.message || t('noAccessToSection')}
|
|
149
|
-
</DropdownMenuItem>
|
|
150
|
-
) : logs.length === 0 ? (
|
|
151
|
-
<DropdownMenuItem disabled>{t('noData')}</DropdownMenuItem>
|
|
152
|
-
) : (
|
|
153
|
-
logs.map((log) => {
|
|
154
|
-
const actorLabel = log.actorUsername || log.actorId || ''
|
|
155
|
-
const contextLabel = log.entityLabel || log.sectionName || ''
|
|
156
|
-
const timestamp = formatTimestamp(log.createdAt)
|
|
157
|
-
const detailParts: string[] = []
|
|
158
|
-
if (actorLabel) detailParts.push(actorLabel)
|
|
159
|
-
if (contextLabel) detailParts.push(contextLabel)
|
|
160
|
-
if (timestamp) detailParts.push(timestamp)
|
|
161
|
-
const details = detailParts.join(' | ') || '-'
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<DropdownMenuItem
|
|
165
|
-
key={log.id}
|
|
166
|
-
className='flex flex-col items-start gap-1 py-2'
|
|
167
|
-
>
|
|
168
|
-
<span className='w-full truncate text-sm font-medium'>
|
|
169
|
-
{log.eventType}
|
|
170
|
-
</span>
|
|
171
|
-
<span className='text-muted-foreground w-full truncate text-xs'>
|
|
172
|
-
{details}
|
|
173
|
-
</span>
|
|
174
|
-
</DropdownMenuItem>
|
|
175
|
-
)
|
|
176
|
-
})
|
|
177
|
-
)}
|
|
178
|
-
</DropdownMenuGroup>
|
|
179
|
-
<DropdownMenuSeparator />
|
|
180
|
-
|
|
181
|
-
<Link href='/log'>
|
|
182
|
-
<DropdownMenuItem className='cursor-pointer'>
|
|
183
|
-
<span>{t('seeAll')}</span>
|
|
184
|
-
</DropdownMenuItem>
|
|
185
|
-
</Link>
|
|
186
|
-
</DropdownMenuContent>
|
|
187
|
-
</DropdownMenu>
|
|
188
|
-
<LocaleDropdown />
|
|
189
|
-
<ThemeToggle />
|
|
190
|
-
{/* Profile dropdown */}
|
|
191
|
-
<DropdownMenu>
|
|
192
|
-
<DropdownMenuTrigger
|
|
193
|
-
asChild
|
|
194
|
-
className='relative ms-2 flex max-w-xs cursor-pointer items-center rounded-full bg-gray-800 text-sm hover:ring-3 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800 focus:outline-hidden'
|
|
195
|
-
>
|
|
196
|
-
<button
|
|
197
|
-
type='button'
|
|
198
|
-
className='relative flex h-10 items-center justify-center rounded-full'
|
|
199
|
-
>
|
|
200
|
-
<span className='absolute -inset-1.5' />
|
|
201
|
-
<span className='sr-only'>Open user menu</span>
|
|
202
|
-
{session?.data?.user.image ? (
|
|
203
|
-
<ProtectedImage
|
|
204
|
-
section={'admins'}
|
|
205
|
-
photo={session.data.user.image}
|
|
206
|
-
isThumb={true}
|
|
207
|
-
alt={session.data.user.name}
|
|
208
|
-
width={40}
|
|
209
|
-
height={40}
|
|
210
|
-
// fill={true}
|
|
211
|
-
className='rounded-full'
|
|
212
|
-
/>
|
|
213
|
-
) : (
|
|
214
|
-
<Image
|
|
215
|
-
src='/blank_avatar.png'
|
|
216
|
-
height={40}
|
|
217
|
-
width={40}
|
|
218
|
-
alt='profile image'
|
|
219
|
-
className='rounded-full ring-2 ring-amber-300 ring-inset'
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
</button>
|
|
223
|
-
</DropdownMenuTrigger>
|
|
224
|
-
<DropdownMenuContent
|
|
225
|
-
sideOffset={12}
|
|
226
|
-
align={'end'}
|
|
227
|
-
side='bottom'
|
|
228
|
-
className='w-56 max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
229
|
-
>
|
|
230
|
-
<DropdownMenuLabel>{session.data?.user.name}</DropdownMenuLabel>
|
|
231
|
-
<DropdownMenuSeparator />
|
|
232
|
-
<DropdownMenuGroup>
|
|
233
|
-
<Link href='/settings'>
|
|
234
|
-
<DropdownMenuItem className='cursor-pointer'>
|
|
235
|
-
<Settings className='me-2 h-4 w-4' />
|
|
236
|
-
<span>{t('accountSettings')}</span>
|
|
237
|
-
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
238
|
-
</DropdownMenuItem>
|
|
239
|
-
</Link>
|
|
240
|
-
</DropdownMenuGroup>
|
|
241
|
-
<DropdownMenuSeparator />
|
|
242
|
-
|
|
243
|
-
<DropdownMenuItem className='cursor-pointer' onClick={handleLogout}>
|
|
244
|
-
<LogOut className='me-2 h-4 w-4' />
|
|
245
|
-
<span>{t('logout')}</span>
|
|
246
|
-
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
247
|
-
</DropdownMenuItem>
|
|
248
|
-
</DropdownMenuContent>
|
|
249
|
-
</DropdownMenu>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</nav>
|
|
256
|
-
</>
|
|
257
|
-
)
|
|
258
|
-
}
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import { MoonIcon, SunIcon, BellIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
|
4
|
+
import { useTheme } from 'next-themes'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
7
|
+
import { RTL_LOCALES } from 'nextjs-cms/translations'
|
|
8
|
+
import { trpc } from '@/app/_trpc/client'
|
|
9
|
+
import ProtectedImage from '@/components/ProtectedImage'
|
|
10
|
+
import { Spinner } from '@/components/ui/spinner'
|
|
11
|
+
import Image from 'next/image'
|
|
12
|
+
import { LogOut, Settings } from 'lucide-react'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuGroup,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuLabel,
|
|
20
|
+
DropdownMenuSeparator,
|
|
21
|
+
DropdownMenuShortcut,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@/components/ui/dropdown-menu'
|
|
24
|
+
import { useToast } from '@/components/ui/use-toast'
|
|
25
|
+
import { logout, useSession } from 'nextjs-cms/auth/react'
|
|
26
|
+
import ThemeToggle from './theme-toggle'
|
|
27
|
+
import LocaleDropdown from './locale-dropdown'
|
|
28
|
+
type Props = {
|
|
29
|
+
/**
|
|
30
|
+
* Allows the parent component to modify the state when the
|
|
31
|
+
* menu button is clicked.
|
|
32
|
+
*/
|
|
33
|
+
onMenuButtonClick(): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const formatTimestamp = (value?: Date | string | null) => {
|
|
37
|
+
if (!value) return ''
|
|
38
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
39
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
40
|
+
return date.toLocaleString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function Navbar(props: Props) {
|
|
44
|
+
const t = useI18n()
|
|
45
|
+
const session = useSession()
|
|
46
|
+
const { toast } = useToast()
|
|
47
|
+
const locale = session?.data?.user?.locale ?? 'en'
|
|
48
|
+
const isRTL = RTL_LOCALES.has(locale)
|
|
49
|
+
const logsQuery = trpc.logs.list.useQuery(
|
|
50
|
+
{
|
|
51
|
+
limit: 20,
|
|
52
|
+
offset: 0,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
enabled: false,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const handleNotificationsOpenChange = (open: boolean) => {
|
|
60
|
+
if (open) {
|
|
61
|
+
void logsQuery.refetch()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const logs = logsQuery.data?.items ?? []
|
|
66
|
+
const handleLogout = async (e: React.MouseEvent<HTMLDivElement>) => {
|
|
67
|
+
e.preventDefault()
|
|
68
|
+
e.stopPropagation()
|
|
69
|
+
try {
|
|
70
|
+
await logout()
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
toast({
|
|
73
|
+
variant: 'destructive',
|
|
74
|
+
title: t('logoutError'),
|
|
75
|
+
description: error.message,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<nav
|
|
83
|
+
className={classNames({
|
|
84
|
+
'sticky top-0 h-[65px] bg-white text-zinc-500 drop-shadow-sm dark:bg-slate-800': true, // colors
|
|
85
|
+
'top-0 z-10 w-full px-4 shadow-xs md:w-full': true, //positioning & styling
|
|
86
|
+
})}
|
|
87
|
+
>
|
|
88
|
+
<div className='mx-auto px-0 lg:px-2'>
|
|
89
|
+
<div className='flex h-16 items-center justify-between'>
|
|
90
|
+
<div className='flex items-center'>
|
|
91
|
+
<div className='shrink-0'>
|
|
92
|
+
<div className='flex md:hidden'>
|
|
93
|
+
{/* Mobile menu button */}
|
|
94
|
+
<button
|
|
95
|
+
onClick={props.onMenuButtonClick}
|
|
96
|
+
className='border-foreground text-foreground hover:text-foreground/90 relative inline-flex items-center justify-center rounded-md border p-2 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800 focus:outline-hidden'
|
|
97
|
+
>
|
|
98
|
+
<span className='absolute -inset-0.5' />
|
|
99
|
+
<span className='sr-only'>Open main menu</span>
|
|
100
|
+
<HamburgerMenuIcon className='block h-6 w-6' aria-hidden='true' />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/*<div className=''>
|
|
106
|
+
<div className='flex items-baseline space-x-4'>
|
|
107
|
+
<input
|
|
108
|
+
type='text'
|
|
109
|
+
className='h-full w-full border-0 bg-transparent text-amber-500 outline-0 ring-0'
|
|
110
|
+
placeholder='Search...'
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>*/}
|
|
114
|
+
</div>
|
|
115
|
+
<div className=''>
|
|
116
|
+
<div className='ms-4 flex items-center md:ms-6'>
|
|
117
|
+
<div className='flex flex-row items-center gap-3'>
|
|
118
|
+
<DropdownMenu onOpenChange={handleNotificationsOpenChange}>
|
|
119
|
+
<DropdownMenuTrigger
|
|
120
|
+
asChild
|
|
121
|
+
className='text-foreground hover:text-foreground/90 cursor-pointer'
|
|
122
|
+
>
|
|
123
|
+
<button
|
|
124
|
+
type='button'
|
|
125
|
+
className='relative flex h-10 items-center justify-center rounded-full focus:outline-hidden'
|
|
126
|
+
aria-label='Notifications'
|
|
127
|
+
>
|
|
128
|
+
<span className='absolute -inset-1.5' />
|
|
129
|
+
<BellIcon className='h-6 w-6' />
|
|
130
|
+
</button>
|
|
131
|
+
</DropdownMenuTrigger>
|
|
132
|
+
<DropdownMenuContent
|
|
133
|
+
sideOffset={12}
|
|
134
|
+
align={'end'}
|
|
135
|
+
side='bottom'
|
|
136
|
+
className='w-[400px] max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
137
|
+
>
|
|
138
|
+
<DropdownMenuLabel>{t('notifications')}</DropdownMenuLabel>
|
|
139
|
+
<DropdownMenuSeparator />
|
|
140
|
+
<DropdownMenuGroup className='max-h-[320px] overflow-y-auto'>
|
|
141
|
+
{logsQuery.isFetching && logs.length === 0 ? (
|
|
142
|
+
<DropdownMenuItem disabled className='flex items-center gap-2'>
|
|
143
|
+
<Spinner className='size-3' />
|
|
144
|
+
<span>{t('loading')}</span>
|
|
145
|
+
</DropdownMenuItem>
|
|
146
|
+
) : logsQuery.isError ? (
|
|
147
|
+
<DropdownMenuItem disabled>
|
|
148
|
+
{logsQuery.error?.message || t('noAccessToSection')}
|
|
149
|
+
</DropdownMenuItem>
|
|
150
|
+
) : logs.length === 0 ? (
|
|
151
|
+
<DropdownMenuItem disabled>{t('noData')}</DropdownMenuItem>
|
|
152
|
+
) : (
|
|
153
|
+
logs.map((log) => {
|
|
154
|
+
const actorLabel = log.actorUsername || log.actorId || ''
|
|
155
|
+
const contextLabel = log.entityLabel || log.sectionName || ''
|
|
156
|
+
const timestamp = formatTimestamp(log.createdAt)
|
|
157
|
+
const detailParts: string[] = []
|
|
158
|
+
if (actorLabel) detailParts.push(actorLabel)
|
|
159
|
+
if (contextLabel) detailParts.push(contextLabel)
|
|
160
|
+
if (timestamp) detailParts.push(timestamp)
|
|
161
|
+
const details = detailParts.join(' | ') || '-'
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<DropdownMenuItem
|
|
165
|
+
key={log.id}
|
|
166
|
+
className='flex flex-col items-start gap-1 py-2'
|
|
167
|
+
>
|
|
168
|
+
<span className='w-full truncate text-sm font-medium'>
|
|
169
|
+
{log.eventType}
|
|
170
|
+
</span>
|
|
171
|
+
<span className='text-muted-foreground w-full truncate text-xs'>
|
|
172
|
+
{details}
|
|
173
|
+
</span>
|
|
174
|
+
</DropdownMenuItem>
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
)}
|
|
178
|
+
</DropdownMenuGroup>
|
|
179
|
+
<DropdownMenuSeparator />
|
|
180
|
+
|
|
181
|
+
<Link href='/log'>
|
|
182
|
+
<DropdownMenuItem className='cursor-pointer'>
|
|
183
|
+
<span>{t('seeAll')}</span>
|
|
184
|
+
</DropdownMenuItem>
|
|
185
|
+
</Link>
|
|
186
|
+
</DropdownMenuContent>
|
|
187
|
+
</DropdownMenu>
|
|
188
|
+
<LocaleDropdown />
|
|
189
|
+
<ThemeToggle />
|
|
190
|
+
{/* Profile dropdown */}
|
|
191
|
+
<DropdownMenu>
|
|
192
|
+
<DropdownMenuTrigger
|
|
193
|
+
asChild
|
|
194
|
+
className='relative ms-2 flex max-w-xs cursor-pointer items-center rounded-full bg-gray-800 text-sm hover:ring-3 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800 focus:outline-hidden'
|
|
195
|
+
>
|
|
196
|
+
<button
|
|
197
|
+
type='button'
|
|
198
|
+
className='relative flex h-10 items-center justify-center rounded-full'
|
|
199
|
+
>
|
|
200
|
+
<span className='absolute -inset-1.5' />
|
|
201
|
+
<span className='sr-only'>Open user menu</span>
|
|
202
|
+
{session?.data?.user.image ? (
|
|
203
|
+
<ProtectedImage
|
|
204
|
+
section={'admins'}
|
|
205
|
+
photo={session.data.user.image}
|
|
206
|
+
isThumb={true}
|
|
207
|
+
alt={session.data.user.name}
|
|
208
|
+
width={40}
|
|
209
|
+
height={40}
|
|
210
|
+
// fill={true}
|
|
211
|
+
className='rounded-full'
|
|
212
|
+
/>
|
|
213
|
+
) : (
|
|
214
|
+
<Image
|
|
215
|
+
src='/blank_avatar.png'
|
|
216
|
+
height={40}
|
|
217
|
+
width={40}
|
|
218
|
+
alt='profile image'
|
|
219
|
+
className='rounded-full ring-2 ring-amber-300 ring-inset'
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</button>
|
|
223
|
+
</DropdownMenuTrigger>
|
|
224
|
+
<DropdownMenuContent
|
|
225
|
+
sideOffset={12}
|
|
226
|
+
align={'end'}
|
|
227
|
+
side='bottom'
|
|
228
|
+
className='w-56 max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
229
|
+
>
|
|
230
|
+
<DropdownMenuLabel>{session.data?.user.name}</DropdownMenuLabel>
|
|
231
|
+
<DropdownMenuSeparator />
|
|
232
|
+
<DropdownMenuGroup>
|
|
233
|
+
<Link href='/settings'>
|
|
234
|
+
<DropdownMenuItem className='cursor-pointer'>
|
|
235
|
+
<Settings className='me-2 h-4 w-4' />
|
|
236
|
+
<span>{t('accountSettings')}</span>
|
|
237
|
+
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
</Link>
|
|
240
|
+
</DropdownMenuGroup>
|
|
241
|
+
<DropdownMenuSeparator />
|
|
242
|
+
|
|
243
|
+
<DropdownMenuItem className='cursor-pointer' onClick={handleLogout}>
|
|
244
|
+
<LogOut className='me-2 h-4 w-4' />
|
|
245
|
+
<span>{t('logout')}</span>
|
|
246
|
+
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
247
|
+
</DropdownMenuItem>
|
|
248
|
+
</DropdownMenuContent>
|
|
249
|
+
</DropdownMenu>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</nav>
|
|
256
|
+
</>
|
|
257
|
+
)
|
|
258
|
+
}
|