create-nextjs-cms 0.5.90 → 0.5.92
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/package.json +3 -3
- package/templates/default/app/api/auth/route.ts +49 -23
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +15 -12
- package/templates/default/app/api/submit/section/item/route.ts +14 -11
- package/templates/default/app/api/submit/section/simple/route.ts +15 -12
- package/templates/default/components/LogPage.tsx +92 -15
- package/templates/default/components/Navbar.tsx +80 -4
- package/templates/default/components/theme-toggle.tsx +1 -1
- package/templates/default/components/ui/spinner.tsx +16 -0
- package/templates/default/dynamic-schemas/schema.ts +381 -381
- package/templates/default/lib/apiHelpers.ts +0 -14
- package/templates/default/package.json +2 -2
- package/templates/default/components/spinner.tsx +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nextjs-cms",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.92",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"prettier": "^3.3.3",
|
|
29
29
|
"tsx": "^4.20.6",
|
|
30
30
|
"typescript": "^5.9.2",
|
|
31
|
-
"@lzcms/eslint-config": "0.3.0",
|
|
32
31
|
"@lzcms/prettier-config": "0.1.0",
|
|
33
|
-
"@lzcms/tsconfig": "0.1.0"
|
|
32
|
+
"@lzcms/tsconfig": "0.1.0",
|
|
33
|
+
"@lzcms/eslint-config": "0.3.0"
|
|
34
34
|
},
|
|
35
35
|
"prettier": "@lzcms/prettier-config",
|
|
36
36
|
"scripts": {
|
|
@@ -1,23 +1,49 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server'
|
|
2
|
-
import auth from 'nextjs-cms/auth'
|
|
3
|
-
import { deleteSession, login } from 'nextjs-cms/auth/actions'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import auth from 'nextjs-cms/auth'
|
|
3
|
+
import { deleteSession, login } from 'nextjs-cms/auth/actions'
|
|
4
|
+
import { getRequestMetadataFromHeaders, recordLog } from 'nextjs-cms/logging'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
const { username, password } = await request.json()
|
|
8
|
+
try {
|
|
9
|
+
const loginResult = await login({ username, password })
|
|
10
|
+
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
11
|
+
|
|
12
|
+
await recordLog({
|
|
13
|
+
eventType: 'auth.login',
|
|
14
|
+
actorId: loginResult.user?.id ?? null,
|
|
15
|
+
actorUsername: loginResult.user?.username ?? null,
|
|
16
|
+
entityType: 'admin',
|
|
17
|
+
entityId: loginResult.user?.id ?? null,
|
|
18
|
+
entityLabel: loginResult.user?.username ?? null,
|
|
19
|
+
sectionName: 'auth',
|
|
20
|
+
requestMetadata,
|
|
21
|
+
})
|
|
22
|
+
return Response.json(loginResult, { status: 200 })
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
return Response.json({ error: error.message }, { status: 400 })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function DELETE(request: NextRequest) {
|
|
29
|
+
const session = await auth()
|
|
30
|
+
try {
|
|
31
|
+
const loginResult = await deleteSession(session)
|
|
32
|
+
if (session?.user?.id) {
|
|
33
|
+
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
34
|
+
await recordLog({
|
|
35
|
+
eventType: 'auth.logout',
|
|
36
|
+
actorId: session.user.id,
|
|
37
|
+
actorUsername: session.user.name ?? null,
|
|
38
|
+
entityType: 'admin',
|
|
39
|
+
entityId: session.user.id,
|
|
40
|
+
entityLabel: session.user.name ?? null,
|
|
41
|
+
sectionName: 'auth',
|
|
42
|
+
requestMetadata,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return Response.json(loginResult, { status: 200 })
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
return Response.json({ error: error.message }, { status: 400 })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { EditSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
-
import auth from 'nextjs-cms/auth'
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { EditSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
+
import auth from 'nextjs-cms/auth'
|
|
4
|
+
import { getRequestMetadataFromHeaders } from 'nextjs-cms/logging'
|
|
4
5
|
|
|
5
6
|
export async function PUT(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
|
|
6
7
|
const session = await auth()
|
|
@@ -24,9 +25,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
24
25
|
)
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const user = session.user
|
|
28
|
-
const formData = await request.formData()
|
|
29
|
-
const sectionName = formData.get('sectionName') as string | null
|
|
28
|
+
const user = session.user
|
|
29
|
+
const formData = await request.formData()
|
|
30
|
+
const sectionName = formData.get('sectionName') as string | null
|
|
31
|
+
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
30
32
|
|
|
31
33
|
if (!sectionName) {
|
|
32
34
|
return NextResponse.json(
|
|
@@ -37,12 +39,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
37
39
|
)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const submit = new EditSubmit({
|
|
41
|
-
itemId: itemId as string,
|
|
42
|
-
sectionName,
|
|
43
|
-
user,
|
|
44
|
-
postData: formData,
|
|
45
|
-
|
|
42
|
+
const submit = new EditSubmit({
|
|
43
|
+
itemId: itemId as string,
|
|
44
|
+
sectionName,
|
|
45
|
+
user,
|
|
46
|
+
postData: formData,
|
|
47
|
+
requestMetadata,
|
|
48
|
+
})
|
|
46
49
|
|
|
47
50
|
await submit.initialize()
|
|
48
51
|
await submit.submit()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { NewSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
-
import auth from 'nextjs-cms/auth'
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { NewSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
+
import auth from 'nextjs-cms/auth'
|
|
4
|
+
import { getRequestMetadataFromHeaders } from 'nextjs-cms/logging'
|
|
4
5
|
|
|
5
6
|
export async function POST(request: NextRequest) {
|
|
6
7
|
const session = await auth()
|
|
@@ -14,9 +15,10 @@ export async function POST(request: NextRequest) {
|
|
|
14
15
|
)
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const user = session.user
|
|
18
|
-
const formData = await request.formData()
|
|
19
|
-
const sectionName = formData.get('sectionName') as string | null
|
|
18
|
+
const user = session.user
|
|
19
|
+
const formData = await request.formData()
|
|
20
|
+
const sectionName = formData.get('sectionName') as string | null
|
|
21
|
+
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
20
22
|
|
|
21
23
|
if (!sectionName) {
|
|
22
24
|
return NextResponse.json(
|
|
@@ -27,11 +29,12 @@ export async function POST(request: NextRequest) {
|
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const submit = new NewSubmit({
|
|
31
|
-
sectionName,
|
|
32
|
-
user,
|
|
33
|
-
postData: formData,
|
|
34
|
-
|
|
32
|
+
const submit = new NewSubmit({
|
|
33
|
+
sectionName,
|
|
34
|
+
user,
|
|
35
|
+
postData: formData,
|
|
36
|
+
requestMetadata,
|
|
37
|
+
})
|
|
35
38
|
|
|
36
39
|
await submit.initialize()
|
|
37
40
|
await submit.submit()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { SimpleSectionSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
-
import auth from 'nextjs-cms/auth'
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { SimpleSectionSubmit } from 'nextjs-cms/core/submit'
|
|
3
|
+
import auth from 'nextjs-cms/auth'
|
|
4
|
+
import { getRequestMetadataFromHeaders } from 'nextjs-cms/logging'
|
|
4
5
|
|
|
5
6
|
export async function PUT(request: NextRequest) {
|
|
6
7
|
const session = await auth()
|
|
@@ -14,9 +15,10 @@ export async function PUT(request: NextRequest) {
|
|
|
14
15
|
)
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const user = session.user
|
|
18
|
-
const formData = await request.formData()
|
|
19
|
-
const sectionName = formData.get('sectionName') as string | null
|
|
18
|
+
const user = session.user
|
|
19
|
+
const formData = await request.formData()
|
|
20
|
+
const sectionName = formData.get('sectionName') as string | null
|
|
21
|
+
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
20
22
|
|
|
21
23
|
if (!sectionName) {
|
|
22
24
|
return NextResponse.json(
|
|
@@ -27,12 +29,13 @@ export async function PUT(request: NextRequest) {
|
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const submit = new SimpleSectionSubmit({
|
|
31
|
-
itemId: '1',
|
|
32
|
-
sectionName,
|
|
33
|
-
user,
|
|
34
|
-
postData: formData,
|
|
35
|
-
|
|
32
|
+
const submit = new SimpleSectionSubmit({
|
|
33
|
+
itemId: '1',
|
|
34
|
+
sectionName,
|
|
35
|
+
user,
|
|
36
|
+
postData: formData,
|
|
37
|
+
requestMetadata,
|
|
38
|
+
})
|
|
36
39
|
|
|
37
40
|
await submit.initialize()
|
|
38
41
|
await submit.submit()
|
|
@@ -1,15 +1,92 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import getString from 'nextjs-cms/translations'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import getString from 'nextjs-cms/translations'
|
|
4
|
+
import { trpc } from '@/app/_trpc/client'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
7
|
+
|
|
8
|
+
type LogMetadata = {
|
|
9
|
+
fields?: string[]
|
|
10
|
+
previousUsername?: string
|
|
11
|
+
newUsername?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formatTimestamp = (value: Date | string | null) => {
|
|
15
|
+
if (!value) return ''
|
|
16
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
17
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
18
|
+
return date.toLocaleString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parseMetadata = (metadata?: string | null): LogMetadata | null => {
|
|
22
|
+
if (!metadata) return null
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(metadata) as LogMetadata
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function LogPage() {
|
|
31
|
+
const [data] = trpc.logs.list.useSuspenseQuery({
|
|
32
|
+
limit: 50,
|
|
33
|
+
offset: 0,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const logs = data?.items ?? []
|
|
37
|
+
return (
|
|
38
|
+
<div className='w-full'>
|
|
39
|
+
<div className='text-foreground bg-linear-to-r from-sky-200 via-emerald-300 to-blue-600 p-8 font-extrabold dark:from-blue-800 dark:via-amber-700 dark:to-rose-900'>
|
|
40
|
+
<h1 className='text-3xl'>{getString('logs')}</h1>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className='flex flex-col gap-4 p-4'>
|
|
44
|
+
<Table>
|
|
45
|
+
<TableHeader>
|
|
46
|
+
<TableRow>
|
|
47
|
+
<TableHead>{getString('date')}</TableHead>
|
|
48
|
+
<TableHead>{getString('action')}</TableHead>
|
|
49
|
+
<TableHead>{getString('admin')}</TableHead>
|
|
50
|
+
<TableHead>{getString('section')}</TableHead>
|
|
51
|
+
<TableHead>{getString('details')}</TableHead>
|
|
52
|
+
</TableRow>
|
|
53
|
+
</TableHeader>
|
|
54
|
+
<TableBody>
|
|
55
|
+
{logs.length === 0 ? (
|
|
56
|
+
<TableRow>
|
|
57
|
+
<TableCell colSpan={5} className='text-muted-foreground text-center'>
|
|
58
|
+
{getString('no_data')}
|
|
59
|
+
</TableCell>
|
|
60
|
+
</TableRow>
|
|
61
|
+
) : (
|
|
62
|
+
logs.map((log) => {
|
|
63
|
+
const metadata = parseMetadata(log.metadata)
|
|
64
|
+
const fields = metadata?.fields?.length ? metadata.fields.join(', ') : null
|
|
65
|
+
const usernameChange =
|
|
66
|
+
metadata?.previousUsername && metadata?.newUsername
|
|
67
|
+
? `${metadata.previousUsername} -> ${metadata.newUsername}`
|
|
68
|
+
: null
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<TableRow key={log.id}>
|
|
72
|
+
<TableCell>{formatTimestamp(log.createdAt)}</TableCell>
|
|
73
|
+
<TableCell>
|
|
74
|
+
<Badge variant='outline'>{log.eventType}</Badge>
|
|
75
|
+
</TableCell>
|
|
76
|
+
<TableCell>{log.actorUsername || log.actorId || '-'}</TableCell>
|
|
77
|
+
<TableCell>{log.sectionName || '-'}</TableCell>
|
|
78
|
+
<TableCell className='text-muted-foreground'>
|
|
79
|
+
{log.entityLabel || log.entityId || '-'}
|
|
80
|
+
{fields ? ` | ${fields}` : ''}
|
|
81
|
+
{usernameChange ? ` | ${usernameChange}` : ''}
|
|
82
|
+
</TableCell>
|
|
83
|
+
</TableRow>
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
)}
|
|
87
|
+
</TableBody>
|
|
88
|
+
</Table>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -4,7 +4,9 @@ import { MoonIcon, SunIcon, BellIcon, HamburgerMenuIcon } from '@radix-ui/react-
|
|
|
4
4
|
import { useTheme } from 'next-themes'
|
|
5
5
|
import Link from 'next/link'
|
|
6
6
|
import getString from 'nextjs-cms/translations'
|
|
7
|
+
import { trpc } from '@/app/_trpc/client'
|
|
7
8
|
import ProtectedImage from '@/components/ProtectedImage'
|
|
9
|
+
import { Spinner } from '@/components/ui/spinner'
|
|
8
10
|
import Image from 'next/image'
|
|
9
11
|
import { LogOut, Settings } from 'lucide-react'
|
|
10
12
|
|
|
@@ -29,10 +31,34 @@ type Props = {
|
|
|
29
31
|
onMenuButtonClick(): void
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
const formatTimestamp = (value?: Date | string | null) => {
|
|
35
|
+
if (!value) return ''
|
|
36
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
37
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
38
|
+
return date.toLocaleString()
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export default function Navbar(props: Props) {
|
|
33
42
|
const { theme, setTheme } = useTheme()
|
|
34
43
|
const session = useSession()
|
|
35
44
|
const { toast } = useToast()
|
|
45
|
+
const logsQuery = trpc.logs.list.useQuery(
|
|
46
|
+
{
|
|
47
|
+
limit: 20,
|
|
48
|
+
offset: 0,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
enabled: false,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const handleNotificationsOpenChange = (open: boolean) => {
|
|
56
|
+
if (open) {
|
|
57
|
+
void logsQuery.refetch()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const logs = logsQuery.data?.items ?? []
|
|
36
62
|
const handleLogout = async (e: React.MouseEvent<HTMLDivElement>) => {
|
|
37
63
|
e.preventDefault()
|
|
38
64
|
e.stopPropagation()
|
|
@@ -85,17 +111,61 @@ export default function Navbar(props: Props) {
|
|
|
85
111
|
<div className=''>
|
|
86
112
|
<div className='ml-4 flex items-center md:ml-6'>
|
|
87
113
|
<div className='flex flex-row items-center gap-3'>
|
|
88
|
-
<DropdownMenu>
|
|
114
|
+
<DropdownMenu onOpenChange={handleNotificationsOpenChange}>
|
|
89
115
|
<DropdownMenuTrigger
|
|
90
116
|
asChild
|
|
91
117
|
className='text-foreground hover:text-foreground/90 cursor-pointer'
|
|
92
118
|
>
|
|
93
119
|
<BellIcon className='h-6 w-6' />
|
|
94
120
|
</DropdownMenuTrigger>
|
|
95
|
-
<DropdownMenuContent
|
|
121
|
+
<DropdownMenuContent
|
|
122
|
+
sideOffset={20}
|
|
123
|
+
align='end'
|
|
124
|
+
side='bottom'
|
|
125
|
+
alignOffset={-20}
|
|
126
|
+
className='w-[400px] max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
127
|
+
>
|
|
96
128
|
<DropdownMenuLabel>{getString('notifications')}</DropdownMenuLabel>
|
|
97
129
|
<DropdownMenuSeparator />
|
|
98
|
-
<DropdownMenuGroup
|
|
130
|
+
<DropdownMenuGroup className='max-h-[320px] overflow-y-auto'>
|
|
131
|
+
{logsQuery.isFetching && logs.length === 0 ? (
|
|
132
|
+
<DropdownMenuItem disabled className='flex items-center gap-2'>
|
|
133
|
+
<Spinner className='size-3' />
|
|
134
|
+
<span>{getString('loading')}</span>
|
|
135
|
+
</DropdownMenuItem>
|
|
136
|
+
) : logsQuery.isError ? (
|
|
137
|
+
<DropdownMenuItem disabled>
|
|
138
|
+
{getString('server_error')}
|
|
139
|
+
</DropdownMenuItem>
|
|
140
|
+
) : logs.length === 0 ? (
|
|
141
|
+
<DropdownMenuItem disabled>{getString('no_data')}</DropdownMenuItem>
|
|
142
|
+
) : (
|
|
143
|
+
logs.map((log) => {
|
|
144
|
+
const actorLabel = log.actorUsername || log.actorId || ''
|
|
145
|
+
const contextLabel = log.entityLabel || log.sectionName || ''
|
|
146
|
+
const timestamp = formatTimestamp(log.createdAt)
|
|
147
|
+
const detailParts: string[] = []
|
|
148
|
+
if (actorLabel) detailParts.push(actorLabel)
|
|
149
|
+
if (contextLabel) detailParts.push(contextLabel)
|
|
150
|
+
if (timestamp) detailParts.push(timestamp)
|
|
151
|
+
const details = detailParts.join(' | ') || '-'
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<DropdownMenuItem
|
|
155
|
+
key={log.id}
|
|
156
|
+
className='flex flex-col items-start gap-1 py-2'
|
|
157
|
+
>
|
|
158
|
+
<span className='w-full truncate text-sm font-medium'>
|
|
159
|
+
{log.eventType}
|
|
160
|
+
</span>
|
|
161
|
+
<span className='text-muted-foreground w-full truncate text-xs'>
|
|
162
|
+
{details}
|
|
163
|
+
</span>
|
|
164
|
+
</DropdownMenuItem>
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
)}
|
|
168
|
+
</DropdownMenuGroup>
|
|
99
169
|
<DropdownMenuSeparator />
|
|
100
170
|
|
|
101
171
|
<Link href='/log'>
|
|
@@ -137,7 +207,13 @@ export default function Navbar(props: Props) {
|
|
|
137
207
|
)}
|
|
138
208
|
</div>
|
|
139
209
|
</DropdownMenuTrigger>
|
|
140
|
-
<DropdownMenuContent
|
|
210
|
+
<DropdownMenuContent
|
|
211
|
+
sideOffset={12}
|
|
212
|
+
align='end'
|
|
213
|
+
side='bottom'
|
|
214
|
+
alignOffset={-10}
|
|
215
|
+
className='w-56 max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
|
|
216
|
+
>
|
|
141
217
|
<DropdownMenuLabel>{session.data?.user.name}</DropdownMenuLabel>
|
|
142
218
|
<DropdownMenuSeparator />
|
|
143
219
|
<DropdownMenuGroup>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useTheme } from 'next-themes'
|
|
3
3
|
import { MoonIcon, SunIcon } from '@radix-ui/react-icons'
|
|
4
|
-
import { Spinner } from '
|
|
4
|
+
import { Spinner } from '@/components/ui/spinner'
|
|
5
5
|
|
|
6
6
|
const ThemeToggle = () => {
|
|
7
7
|
const [mounted, setMounted] = useState(false)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|
6
|
+
return (
|
|
7
|
+
<Loader2Icon
|
|
8
|
+
role="status"
|
|
9
|
+
aria-label="Loading"
|
|
10
|
+
className={cn("size-4 animate-spin", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { Spinner }
|