create-nextjs-cms 0.9.31 → 0.9.32
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 +2 -2
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/error.tsx +64 -0
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/page.tsx +59 -47
- package/templates/default/app/(rootLayout)/(plugins)/[...slug]/plugin-server-registry.ts +14 -20
- package/templates/default/app/(rootLayout)/dashboard/page.tsx +10 -12
- package/templates/default/app/(rootLayout)/log/page.tsx +0 -2
- package/templates/default/app/(rootLayout)/settings/page.tsx +0 -2
- package/templates/default/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nextjs-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,8 +28,8 @@
|
|
|
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",
|
|
32
|
+
"@lzcms/eslint-config": "0.3.0",
|
|
33
33
|
"@lzcms/tsconfig": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"prettier": "@lzcms/prettier-config",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { AlertTriangle, Bug, RefreshCw } from 'lucide-react'
|
|
6
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
7
|
+
import { Badge } from '@/components/ui/badge'
|
|
8
|
+
|
|
9
|
+
export default function PluginError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
10
|
+
const t = useI18n()
|
|
11
|
+
const errorMessage = error.message || t('unexpectedPluginError')
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Surface plugin failures in the console; error.digest correlates with server logs.
|
|
15
|
+
console.error('[plugin]', error)
|
|
16
|
+
}, [error])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className='flex min-h-[360px] items-center justify-center p-4 sm:p-6'>
|
|
20
|
+
<section className='bg-card text-card-foreground w-full max-w-3xl overflow-hidden rounded-lg border shadow-sm'>
|
|
21
|
+
<div className='from-destructive/10 via-background to-background border-b bg-linear-to-r px-5 py-5 sm:px-6'>
|
|
22
|
+
<div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between'>
|
|
23
|
+
<div className='flex items-start gap-4'>
|
|
24
|
+
<div className='bg-destructive/10 dark:bg-destructive/30 text-destructive ring-destructive/20 flex size-12 shrink-0 items-center justify-center rounded-lg ring-1'>
|
|
25
|
+
<AlertTriangle className='size-6' aria-hidden='true' />
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<Badge variant='destructive' className='mb-2'>
|
|
29
|
+
{t('pluginError')}
|
|
30
|
+
</Badge>
|
|
31
|
+
<h2 className='text-xl font-semibold tracking-tight'>{t('pluginFailedToLoad')}</h2>
|
|
32
|
+
<p className='text-muted-foreground mt-2 max-w-2xl text-sm'>
|
|
33
|
+
{t('pluginFailedToLoadDescription')}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<Button type='button' size='sm' onClick={() => reset()} className='self-start'>
|
|
39
|
+
<RefreshCw className='size-4' aria-hidden='true' />
|
|
40
|
+
{t('retry')}
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<dl className='divide-y text-sm'>
|
|
46
|
+
<div className='grid gap-2 px-5 py-4 sm:grid-cols-[10rem_1fr] sm:px-6'>
|
|
47
|
+
<dt className='text-foreground flex items-center gap-2 font-medium'>
|
|
48
|
+
<Bug className='text-muted-foreground size-4' aria-hidden='true' />
|
|
49
|
+
{t('details')}
|
|
50
|
+
</dt>
|
|
51
|
+
<dd className='text-muted-foreground wrap-break-word'>{errorMessage}</dd>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{error.digest && (
|
|
55
|
+
<div className='grid gap-2 px-5 py-4 sm:grid-cols-[10rem_1fr] sm:px-6'>
|
|
56
|
+
<dt className='text-foreground font-medium'>{t('errorDigest')}</dt>
|
|
57
|
+
<dd className='text-muted-foreground font-mono text-xs break-all'>{error.digest}</dd>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</dl>
|
|
61
|
+
</section>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -1,47 +1,59 @@
|
|
|
1
|
-
import { notFound } from 'next/navigation'
|
|
2
|
-
import auth from 'nextjs-cms/auth'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
import { notFound } from 'next/navigation'
|
|
2
|
+
import auth from 'nextjs-cms/auth'
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { getAdminPrivileges } from 'nextjs-cms/api/server/actions'
|
|
5
|
+
import { findPluginRouteByPath, isDashboardOverridePlugin, runRoutePrefetches } from 'nextjs-cms/plugins/server'
|
|
6
|
+
import { api, HydrateClient } from '@/app/_trpc/server'
|
|
7
|
+
import { getPluginServerComponent } from './plugin-server-registry'
|
|
8
|
+
import LoadingSpinners from '@/components/feedback/loading-spinners'
|
|
9
|
+
|
|
10
|
+
type Params = Promise<{ slug?: string[] }>
|
|
11
|
+
|
|
12
|
+
export default async function Page(props: { params: Params }) {
|
|
13
|
+
const params = await props.params
|
|
14
|
+
const slug = params.slug ?? []
|
|
15
|
+
|
|
16
|
+
const path = `/${slug.join('/')}`
|
|
17
|
+
const session = await auth()
|
|
18
|
+
if (!session?.user) {
|
|
19
|
+
notFound()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const plugin = await findPluginRouteByPath(path)
|
|
23
|
+
if (!plugin) {
|
|
24
|
+
notFound()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Bypass privilege check for dashboard override plugin
|
|
28
|
+
const isDashboardPlugin = await isDashboardOverridePlugin(plugin.pluginName)
|
|
29
|
+
if (!isDashboardPlugin) {
|
|
30
|
+
const privilegeSet = await getAdminPrivileges(session.user.id)
|
|
31
|
+
if (!privilegeSet.has(plugin.pluginName)) {
|
|
32
|
+
notFound()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fire-and-forget: HydrateClient streams resolved query data as it becomes available,
|
|
37
|
+
// so awaiting here would block the shell on slow upstream APIs (cPanel, GA, etc.)
|
|
38
|
+
void runRoutePrefetches(api, plugin)
|
|
39
|
+
|
|
40
|
+
const PluginComponent = await getPluginServerComponent(plugin.pluginName, plugin.component)
|
|
41
|
+
if (!PluginComponent) {
|
|
42
|
+
notFound()
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<HydrateClient>
|
|
46
|
+
{/* This way, or use useSuspenseQuery
|
|
47
|
+
in each plugin client component and remove the <Suspense /> here */}
|
|
48
|
+
<Suspense
|
|
49
|
+
fallback={
|
|
50
|
+
<div className='p-20'>
|
|
51
|
+
<LoadingSpinners />
|
|
52
|
+
</div>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<PluginComponent />
|
|
56
|
+
</Suspense>
|
|
57
|
+
</HydrateClient>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
|
|
4
3
|
export const pluginNamesMap: Record<string, string> = {
|
|
5
4
|
'cpanel-dashboard': '@nextjscms/plugin-cpanel-dashboard/server',
|
|
6
5
|
'cpanel-emails': '@nextjscms/plugin-cpanel-emails/server',
|
|
7
6
|
'google-analytics': '@nextjscms/plugin-google-analytics/server',
|
|
8
7
|
// A workaround to avoid importing error if no plugins are installed
|
|
9
8
|
blank: 'nextjs-cms/plugins/blank-component',
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const getPluginServerComponent = async (
|
|
13
|
-
pluginName: string,
|
|
14
|
-
componentName: string | undefined,
|
|
15
|
-
): Promise<ComponentType | null> => {
|
|
16
|
-
const modulePath = pluginNamesMap[pluginName]
|
|
17
|
-
if (!modulePath) return null
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
),
|
|
23
|
-
{ ssr: true },
|
|
24
|
-
)
|
|
25
|
-
return Component
|
|
26
|
-
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getPluginServerComponent = async (
|
|
12
|
+
pluginName: string,
|
|
13
|
+
componentName: string | undefined,
|
|
14
|
+
): Promise<ComponentType | null> => {
|
|
15
|
+
const modulePath = pluginNamesMap[pluginName]
|
|
16
|
+
if (!modulePath) return null
|
|
17
|
+
const mod = await import(modulePath)
|
|
18
|
+
const Component = componentName ? mod[componentName] : mod.default
|
|
19
|
+
return (Component as ComponentType) ?? null
|
|
20
|
+
}
|
|
@@ -2,42 +2,40 @@ import { getDashboardOverride, runRoutePrefetches } from 'nextjs-cms/plugins/ser
|
|
|
2
2
|
import { api, HydrateClient } from '@/app/_trpc/server'
|
|
3
3
|
import { getPluginServerComponent } from '../(plugins)/[...slug]/plugin-server-registry'
|
|
4
4
|
|
|
5
|
-
export const dynamic = 'force-dynamic'
|
|
6
|
-
|
|
7
5
|
function DefaultDashboard() {
|
|
8
6
|
return (
|
|
9
7
|
<div className='w-full'>
|
|
10
|
-
<div className='bg-linear-to-r from-amber-200 via-orange-200 to-rose-200 p-8
|
|
8
|
+
<div className='text-foreground bg-linear-to-r from-amber-200 via-orange-200 to-rose-200 p-8 dark:from-amber-900 dark:via-orange-900 dark:to-rose-900'>
|
|
11
9
|
<h1 className='text-3xl font-extrabold tracking-tight'>Welcome to Mission Control</h1>
|
|
12
|
-
<p className='mt-2 text-base
|
|
10
|
+
<p className='text-muted-foreground mt-2 text-base'>
|
|
13
11
|
Your CMS is ready. Chart a course, publish boldly, and keep the lights green.
|
|
14
12
|
</p>
|
|
15
13
|
</div>
|
|
16
14
|
|
|
17
15
|
<div className='space-y-6 p-6'>
|
|
18
|
-
<section className='rounded-lg border
|
|
16
|
+
<section className='bg-card text-card-foreground rounded-lg border p-6 shadow-sm'>
|
|
19
17
|
<h2 className='text-xl font-semibold'>Today's focus</h2>
|
|
20
|
-
<p className='mt-2 text-sm
|
|
18
|
+
<p className='text-muted-foreground mt-2 text-sm'>
|
|
21
19
|
Start with one small win: verify a section, ship a quick update, or polish a headline.
|
|
22
20
|
</p>
|
|
23
21
|
</section>
|
|
24
22
|
|
|
25
23
|
<section className='grid gap-4 md:grid-cols-3'>
|
|
26
|
-
<div className='rounded-lg border
|
|
24
|
+
<div className='bg-card text-card-foreground rounded-lg border p-4 shadow-sm'>
|
|
27
25
|
<div className='text-sm font-semibold'>Ship with confidence</div>
|
|
28
|
-
<div className='mt-2 text-sm
|
|
26
|
+
<div className='text-muted-foreground mt-2 text-sm'>
|
|
29
27
|
Keep changes tight and reversible. Small releases, fast feedback.
|
|
30
28
|
</div>
|
|
31
29
|
</div>
|
|
32
|
-
<div className='rounded-lg border
|
|
30
|
+
<div className='bg-card text-card-foreground rounded-lg border p-4 shadow-sm'>
|
|
33
31
|
<div className='text-sm font-semibold'>Stay organized</div>
|
|
34
|
-
<div className='mt-2 text-sm
|
|
32
|
+
<div className='text-muted-foreground mt-2 text-sm'>
|
|
35
33
|
Group your content by intent, not just by type.
|
|
36
34
|
</div>
|
|
37
35
|
</div>
|
|
38
|
-
<div className='rounded-lg border
|
|
36
|
+
<div className='bg-card text-card-foreground rounded-lg border p-4 shadow-sm'>
|
|
39
37
|
<div className='text-sm font-semibold'>Delight users</div>
|
|
40
|
-
<div className='mt-2 text-sm
|
|
38
|
+
<div className='text-muted-foreground mt-2 text-sm'>
|
|
41
39
|
A clean headline and a single strong image can do the heavy lifting.
|
|
42
40
|
</div>
|
|
43
41
|
</div>
|