create-nextjs-cms 0.8.10 → 0.9.1
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/(auth)/auth/login/LoginPage.tsx +9 -9
- package/templates/default/app/(auth)/auth-language-provider.tsx +34 -0
- package/templates/default/app/(auth)/layout.tsx +10 -10
- package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +4 -1
- package/templates/default/app/(rootLayout)/layout.tsx +5 -5
- package/templates/default/app/(rootLayout)/section/[section]/page.tsx +4 -1
- package/templates/default/app/api/auth/route.ts +2 -2
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +32 -3
- package/templates/default/app/api/submit/section/simple/route.ts +32 -3
- package/templates/default/app/globals.css +9 -0
- package/templates/default/cms.config.ts +4 -4
- package/templates/default/components/ItemEditPage.tsx +82 -2
- package/templates/default/components/LocaleSwitcher.tsx +89 -0
- package/templates/default/components/Navbar.tsx +5 -5
- package/templates/default/components/NewPage.tsx +1 -0
- package/templates/default/components/SectionPage.tsx +81 -1
- package/templates/default/components/form/ContentLocaleContext.tsx +11 -0
- package/templates/default/components/form/Form.tsx +48 -5
- package/templates/default/components/form/FormInputs.tsx +16 -7
- package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
- package/templates/default/components/form/inputs/NumberFormInput.tsx +2 -1
- package/templates/default/components/form/inputs/PhotoFormInput.tsx +168 -112
- package/templates/default/components/form/inputs/RichTextFormInput.tsx +3 -0
- package/templates/default/components/form/inputs/SelectFormInput.tsx +1 -1
- package/templates/default/components/form/inputs/TagsFormInput.tsx +6 -2
- package/templates/default/components/form/inputs/TextFormInput.tsx +3 -0
- package/templates/default/components/form/inputs/TextareaFormInput.tsx +3 -0
- package/templates/default/components/{locale-dropdown.tsx → language-dropdown.tsx} +74 -74
- package/templates/default/components/{locale-picker.tsx → language-picker.tsx} +85 -85
- package/templates/default/components/login-language-dropdown.tsx +46 -0
- package/templates/default/components/ui/alert.tsx +2 -1
- package/templates/default/dynamic-schemas/schema.ts +475 -448
- package/templates/default/package.json +1 -1
- package/templates/default/app/(auth)/auth-locale-provider.tsx +0 -34
- package/templates/default/components/login-locale-dropdown.tsx +0 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nextjs-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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/prettier-config": "0.1.0",
|
|
32
31
|
"@lzcms/eslint-config": "0.3.0",
|
|
32
|
+
"@lzcms/prettier-config": "0.1.0",
|
|
33
33
|
"@lzcms/tsconfig": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"prettier": "@lzcms/prettier-config",
|
|
@@ -11,9 +11,9 @@ import { Button } from '@/components/ui/button'
|
|
|
11
11
|
import { MoonIcon, SunIcon } from '@radix-ui/react-icons'
|
|
12
12
|
import { useTheme } from 'next-themes'
|
|
13
13
|
import { login, useSession } from 'nextjs-cms/auth/react'
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
14
|
+
import { wasLanguageChangedOnLogin } from 'nextjs-cms/translations'
|
|
15
|
+
import LoginLanguageDropdown from '@/components/login-language-dropdown'
|
|
16
|
+
import { useAuthLanguage } from '../../auth-language-provider'
|
|
17
17
|
|
|
18
18
|
type fromErrors = {
|
|
19
19
|
username?: string | null
|
|
@@ -23,7 +23,7 @@ type fromErrors = {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export default function LoginPage() {
|
|
26
|
-
const { supportedLanguages, fallbackLanguage,
|
|
26
|
+
const { supportedLanguages, fallbackLanguage, initialLanguage } = useAuthLanguage()
|
|
27
27
|
const t = useI18n()
|
|
28
28
|
const session = useSession()
|
|
29
29
|
const searchParams = useSearchParams()
|
|
@@ -39,13 +39,13 @@ export default function LoginPage() {
|
|
|
39
39
|
loginButtonRef.current.innerHTML = t('loading')
|
|
40
40
|
}
|
|
41
41
|
try {
|
|
42
|
-
// Only send
|
|
42
|
+
// Only send language if user explicitly changed it on login page
|
|
43
43
|
// Otherwise, server will use admin's stored DB preference
|
|
44
|
-
const
|
|
44
|
+
const languageChanged = wasLanguageChangedOnLogin()
|
|
45
45
|
await login({
|
|
46
46
|
username: formRef.current?.username.value,
|
|
47
47
|
password: formRef.current?.password.value,
|
|
48
|
-
|
|
48
|
+
language: languageChanged ? initialLanguage : undefined,
|
|
49
49
|
})
|
|
50
50
|
} catch (error: any) {
|
|
51
51
|
if (loginButtonRef.current) {
|
|
@@ -83,10 +83,10 @@ export default function LoginPage() {
|
|
|
83
83
|
<>
|
|
84
84
|
<div className='bg-background text-foreground fixed top-0 left-0 h-full w-full flex-1 flex-col justify-center overflow-auto px-6 py-12 lg:px-8'>
|
|
85
85
|
<div className='flex w-full items-center justify-end gap-2 absolute end-10 top-10'>
|
|
86
|
-
<
|
|
86
|
+
<LoginLanguageDropdown
|
|
87
87
|
supportedLanguages={supportedLanguages}
|
|
88
88
|
fallbackLanguage={fallbackLanguage}
|
|
89
|
-
|
|
89
|
+
initialLanguage={initialLanguage}
|
|
90
90
|
/>
|
|
91
91
|
<Button
|
|
92
92
|
variant='default'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface AuthLanguageValue {
|
|
6
|
+
supportedLanguages: readonly string[]
|
|
7
|
+
fallbackLanguage: string
|
|
8
|
+
initialLanguage: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const AuthLanguageContext = createContext<AuthLanguageValue | null>(null)
|
|
12
|
+
|
|
13
|
+
export function AuthLanguageProvider({
|
|
14
|
+
supportedLanguages,
|
|
15
|
+
fallbackLanguage,
|
|
16
|
+
initialLanguage,
|
|
17
|
+
children,
|
|
18
|
+
}: AuthLanguageValue & { children: ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<AuthLanguageContext.Provider
|
|
21
|
+
value={{ supportedLanguages, fallbackLanguage, initialLanguage }}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</AuthLanguageContext.Provider>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useAuthLanguage(): AuthLanguageValue {
|
|
29
|
+
const ctx = useContext(AuthLanguageContext)
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
throw new Error('useAuthLanguage must be used within AuthLanguageProvider')
|
|
32
|
+
}
|
|
33
|
+
return ctx
|
|
34
|
+
}
|
|
@@ -8,10 +8,10 @@ import Providers from '@/app/providers'
|
|
|
8
8
|
import auth from 'nextjs-cms/auth'
|
|
9
9
|
import { getCMSConfig } from 'nextjs-cms/core'
|
|
10
10
|
import { I18nProviderClient } from 'nextjs-cms/translations/client'
|
|
11
|
-
import {
|
|
11
|
+
import { resolveLanguage, RTL_LANGUAGES, LANGUAGE_COOKIE_NAME } from 'nextjs-cms/translations'
|
|
12
12
|
import { getClientDictionaries } from 'nextjs-cms/translations/server'
|
|
13
13
|
import { redirect } from 'next/navigation'
|
|
14
|
-
import {
|
|
14
|
+
import { AuthLanguageProvider } from './auth-language-provider'
|
|
15
15
|
import { DirectionProvider } from "@/components/ui/direction"
|
|
16
16
|
|
|
17
17
|
const inter = Inter({
|
|
@@ -44,13 +44,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|
|
44
44
|
|
|
45
45
|
const { supportedLanguages, fallbackLanguage } = cmsConfig.i18n
|
|
46
46
|
const cookieStore = await cookies()
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const isRTL =
|
|
47
|
+
const cookieLanguage = cookieStore.get(LANGUAGE_COOKIE_NAME)?.value
|
|
48
|
+
const language = resolveLanguage(cookieLanguage, supportedLanguages, fallbackLanguage)
|
|
49
|
+
const isRTL = RTL_LANGUAGES.has(language)
|
|
50
50
|
const dictionaries = getClientDictionaries(supportedLanguages)
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<html lang={
|
|
53
|
+
<html lang={language} dir={isRTL ? 'rtl' : 'ltr'} suppressHydrationWarning>
|
|
54
54
|
<body
|
|
55
55
|
className={cn(
|
|
56
56
|
'bg-background min-h-screen font-sans antialiased',
|
|
@@ -58,20 +58,20 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|
|
58
58
|
)}
|
|
59
59
|
>
|
|
60
60
|
<DirectionProvider dir={isRTL ? 'rtl' : 'ltr'} direction={isRTL ? 'rtl' : 'ltr'}>
|
|
61
|
-
<I18nProviderClient locale={
|
|
61
|
+
<I18nProviderClient locale={language} dictionaries={dictionaries}>
|
|
62
62
|
<ThemeProvider
|
|
63
63
|
attribute='class'
|
|
64
64
|
defaultTheme={cmsConfig.ui.defaultTheme}
|
|
65
65
|
enableSystem
|
|
66
66
|
disableTransitionOnChange
|
|
67
67
|
>
|
|
68
|
-
<
|
|
68
|
+
<AuthLanguageProvider
|
|
69
69
|
supportedLanguages={supportedLanguages}
|
|
70
70
|
fallbackLanguage={fallbackLanguage}
|
|
71
|
-
|
|
71
|
+
initialLanguage={language}
|
|
72
72
|
>
|
|
73
73
|
<Providers session={undefined}>{children}</Providers>
|
|
74
|
-
</
|
|
74
|
+
</AuthLanguageProvider>
|
|
75
75
|
</ThemeProvider>
|
|
76
76
|
</I18nProviderClient>
|
|
77
77
|
</DirectionProvider>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import ItemEditPage from '@/components/ItemEditPage'
|
|
2
2
|
import { api, HydrateClient } from 'nextjs-cms/api/trpc/server'
|
|
3
3
|
type Params = Promise<{ section: string; itemId: string }>
|
|
4
|
+
type SearchParams = Promise<{ locale?: string }>
|
|
4
5
|
|
|
5
|
-
export default async function Page(props: { params: Params }) {
|
|
6
|
+
export default async function Page(props: { params: Params; searchParams: SearchParams }) {
|
|
6
7
|
const params = await props.params
|
|
8
|
+
const searchParams = await props.searchParams
|
|
7
9
|
await api.hasItemsSections.editItem.prefetch({
|
|
8
10
|
sectionName: params.section,
|
|
9
11
|
sectionItemId: params.itemId,
|
|
12
|
+
locale: searchParams.locale,
|
|
10
13
|
})
|
|
11
14
|
|
|
12
15
|
return (
|
|
@@ -7,7 +7,7 @@ import Providers from '@/app/providers'
|
|
|
7
7
|
import auth from 'nextjs-cms/auth'
|
|
8
8
|
import { getCMSConfig } from 'nextjs-cms/core'
|
|
9
9
|
import { I18nProviderClient } from 'nextjs-cms/translations/client'
|
|
10
|
-
import {
|
|
10
|
+
import { resolveLanguage, RTL_LANGUAGES } from 'nextjs-cms/translations'
|
|
11
11
|
import { getClientDictionaries } from 'nextjs-cms/translations/server'
|
|
12
12
|
import { api, HydrateClient } from 'nextjs-cms/api/trpc/server'
|
|
13
13
|
import Layout from '@/components/Layout'
|
|
@@ -44,12 +44,12 @@ export default async function CMSLayout({ children }: { children: React.ReactNod
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const { supportedLanguages, fallbackLanguage } = cmsConfig.i18n
|
|
47
|
-
const
|
|
48
|
-
const isRTL =
|
|
47
|
+
const language = resolveLanguage(session.user.language, supportedLanguages, fallbackLanguage)
|
|
48
|
+
const isRTL = RTL_LANGUAGES.has(language)
|
|
49
49
|
const dictionaries = getClientDictionaries(supportedLanguages)
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
|
-
<html lang={
|
|
52
|
+
<html lang={language} dir={isRTL ? 'rtl' : 'ltr'} suppressHydrationWarning>
|
|
53
53
|
<body
|
|
54
54
|
className={cn(
|
|
55
55
|
'bg-background min-h-screen font-sans antialiased',
|
|
@@ -57,7 +57,7 @@ export default async function CMSLayout({ children }: { children: React.ReactNod
|
|
|
57
57
|
)}
|
|
58
58
|
>
|
|
59
59
|
<DirectionProvider dir={isRTL ? 'rtl' : 'ltr'} direction={isRTL ? 'rtl' : 'ltr'}>
|
|
60
|
-
<I18nProviderClient locale={
|
|
60
|
+
<I18nProviderClient locale={language} dictionaries={dictionaries}>
|
|
61
61
|
<ThemeProvider
|
|
62
62
|
attribute='class'
|
|
63
63
|
defaultTheme={cmsConfig.ui.defaultTheme}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import SectionPage from '@/components/SectionPage'
|
|
2
2
|
import { api, HydrateClient } from 'nextjs-cms/api/trpc/server'
|
|
3
3
|
type Params = Promise<{ section: string }>
|
|
4
|
+
type SearchParams = Promise<{ locale?: string }>
|
|
4
5
|
|
|
5
|
-
export default async function Page(props: { params: Params }) {
|
|
6
|
+
export default async function Page(props: { params: Params; searchParams: SearchParams }) {
|
|
6
7
|
const params = await props.params
|
|
8
|
+
const searchParams = await props.searchParams
|
|
7
9
|
await api.simpleSections.create.prefetch({
|
|
8
10
|
sectionName: params.section,
|
|
11
|
+
locale: searchParams.locale,
|
|
9
12
|
})
|
|
10
13
|
|
|
11
14
|
return (
|
|
@@ -4,9 +4,9 @@ import { deleteSession, login } from 'nextjs-cms/auth/actions'
|
|
|
4
4
|
import { getRequestMetadataFromHeaders, recordLog } from 'nextjs-cms/logging'
|
|
5
5
|
|
|
6
6
|
export async function POST(request: NextRequest) {
|
|
7
|
-
const { username, password,
|
|
7
|
+
const { username, password, language } = await request.json()
|
|
8
8
|
try {
|
|
9
|
-
const loginResult = await login({ username, password,
|
|
9
|
+
const loginResult = await login({ username, password, language })
|
|
10
10
|
const requestMetadata = getRequestMetadataFromHeaders(request.headers)
|
|
11
11
|
|
|
12
12
|
await recordLog({
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { EditSubmit } from 'nextjs-cms/core/submit'
|
|
2
|
+
import { EditSubmit, LocaleSubmit } from 'nextjs-cms/core/submit'
|
|
3
3
|
import auth from 'nextjs-cms/auth'
|
|
4
4
|
import { getRequestMetadataFromHeaders } from 'nextjs-cms/logging'
|
|
5
|
+
import { getCMSConfig } from 'nextjs-cms/core/config'
|
|
6
|
+
import { resolveLocale } from 'nextjs-cms/core/localization'
|
|
7
|
+
import getString from 'nextjs-cms/translations'
|
|
5
8
|
|
|
6
9
|
export async function PUT(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
|
|
7
10
|
const session = await auth()
|
|
@@ -39,13 +42,39 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const
|
|
45
|
+
const localeCode = formData.get('locale')
|
|
46
|
+
const localeValue = typeof localeCode === 'string' ? localeCode : undefined
|
|
47
|
+
const cmsConfig = await getCMSConfig()
|
|
48
|
+
const localeResult = resolveLocale({
|
|
49
|
+
localization: cmsConfig.localization,
|
|
50
|
+
locale: localeValue,
|
|
51
|
+
})
|
|
52
|
+
const isLocaleSubmit = localeValue && localeResult.localizationEnabled && localeResult.isDefault === false
|
|
53
|
+
const localeForSubmit = localeResult.resolvedLocale?.code
|
|
54
|
+
|
|
55
|
+
if (isLocaleSubmit && !localeForSubmit) {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{
|
|
58
|
+
error: getString('invalidLocale', user.language, {
|
|
59
|
+
locale: localeValue,
|
|
60
|
+
locales: localeResult.availableLocales.map((l) => l.code).join(', '),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
{ status: 400 },
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const config = {
|
|
43
68
|
itemId: itemId as string,
|
|
44
69
|
sectionName,
|
|
45
70
|
user,
|
|
46
71
|
postData: formData,
|
|
47
72
|
requestMetadata,
|
|
48
|
-
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const submit = isLocaleSubmit
|
|
76
|
+
? new LocaleSubmit({ ...config, locale: localeForSubmit as string })
|
|
77
|
+
: new EditSubmit(config)
|
|
49
78
|
|
|
50
79
|
await submit.initialize()
|
|
51
80
|
await submit.submit()
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { SimpleSectionSubmit } from 'nextjs-cms/core/submit'
|
|
2
|
+
import { SimpleSectionSubmit, LocaleSubmit } from 'nextjs-cms/core/submit'
|
|
3
3
|
import auth from 'nextjs-cms/auth'
|
|
4
4
|
import { getRequestMetadataFromHeaders } from 'nextjs-cms/logging'
|
|
5
|
+
import { getCMSConfig } from 'nextjs-cms/core/config'
|
|
6
|
+
import { resolveLocale } from 'nextjs-cms/core/localization'
|
|
7
|
+
import getString from 'nextjs-cms/translations'
|
|
5
8
|
|
|
6
9
|
export async function PUT(request: NextRequest) {
|
|
7
10
|
const session = await auth()
|
|
@@ -29,13 +32,39 @@ export async function PUT(request: NextRequest) {
|
|
|
29
32
|
)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
const
|
|
35
|
+
const localeCode = formData.get('locale')
|
|
36
|
+
const localeValue = typeof localeCode === 'string' ? localeCode : undefined
|
|
37
|
+
const cmsConfig = await getCMSConfig()
|
|
38
|
+
const localeResult = resolveLocale({
|
|
39
|
+
localization: cmsConfig.localization,
|
|
40
|
+
locale: localeValue,
|
|
41
|
+
})
|
|
42
|
+
const isLocaleSubmit = localeValue && localeResult.localizationEnabled && localeResult.isDefault === false
|
|
43
|
+
const localeForSubmit = localeResult.resolvedLocale?.code
|
|
44
|
+
|
|
45
|
+
if (isLocaleSubmit && !localeForSubmit) {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{
|
|
48
|
+
error: getString('invalidLocale', user.language, {
|
|
49
|
+
locale: localeValue,
|
|
50
|
+
locales: localeResult.availableLocales.map((l) => l.code).join(', '),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
{ status: 400 },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const config = {
|
|
33
58
|
itemId: '1',
|
|
34
59
|
sectionName,
|
|
35
60
|
user,
|
|
36
61
|
postData: formData,
|
|
37
62
|
requestMetadata,
|
|
38
|
-
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const submit = isLocaleSubmit
|
|
66
|
+
? new LocaleSubmit({ ...config, locale: localeForSubmit as string })
|
|
67
|
+
: new SimpleSectionSubmit(config)
|
|
39
68
|
|
|
40
69
|
await submit.initialize()
|
|
41
70
|
await submit.submit()
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
--color-warning: hsl(var(--warning));
|
|
49
49
|
--color-warning-foreground: hsl(var(--warning-foreground));
|
|
50
50
|
|
|
51
|
+
--color-info: hsl(var(--info));
|
|
52
|
+
--color-info-foreground: hsl(var(--info-foreground));
|
|
53
|
+
|
|
51
54
|
--color-destructive: hsl(var(--destructive));
|
|
52
55
|
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
53
56
|
|
|
@@ -129,6 +132,9 @@
|
|
|
129
132
|
--warning: 45 100% 50%;
|
|
130
133
|
--warning-foreground: 210 40% 98%;
|
|
131
134
|
|
|
135
|
+
--info: 213 80% 52%;
|
|
136
|
+
--info-foreground: 222 80% 52%;
|
|
137
|
+
|
|
132
138
|
--secondary: 210 40% 96.1%;
|
|
133
139
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
134
140
|
|
|
@@ -170,6 +176,9 @@
|
|
|
170
176
|
--warning: 45 100% 50%;
|
|
171
177
|
--warning-foreground: 210 40% 98%;
|
|
172
178
|
|
|
179
|
+
--info: 213 60% 52%;
|
|
180
|
+
--info-foreground: 200 80% 52%;
|
|
181
|
+
|
|
173
182
|
--muted: 217.2 32.6% 17.5%;
|
|
174
183
|
--muted-foreground: 215 20.2% 65.1%;
|
|
175
184
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { defineConfig, type CMSConfig } from 'nextjs-cms/core/config'
|
|
2
2
|
import { resolve } from 'path'
|
|
3
3
|
|
|
4
|
-
export const config: CMSConfig = {
|
|
4
|
+
export const config: CMSConfig = defineConfig({
|
|
5
|
+
ui: {
|
|
5
6
|
title: 'NEXTJS CMS',
|
|
6
7
|
defaultTheme: 'dark',
|
|
7
8
|
},
|
|
@@ -52,6 +53,5 @@ export const config: CMSConfig = {
|
|
|
52
53
|
columns: 'camelCase',
|
|
53
54
|
},
|
|
54
55
|
},
|
|
55
|
-
},
|
|
56
56
|
},
|
|
57
|
-
}
|
|
57
|
+
})
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useAxiosPrivate } from 'nextjs-cms/auth/hooks'
|
|
4
|
-
import { RefObject, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
|
5
5
|
import { SectionType } from 'nextjs-cms/core/types'
|
|
6
6
|
import { useI18n } from 'nextjs-cms/translations/client'
|
|
7
7
|
import useModal from '@/hooks/useModal'
|
|
8
8
|
import { AxiosError } from 'axios'
|
|
9
9
|
import InfoCard from '@/components/InfoCard'
|
|
10
|
-
import { useRouter } from 'next/navigation'
|
|
10
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
11
11
|
import { useToast } from '@/components/ui/use-toast'
|
|
12
12
|
import { DropzoneHandles } from '@/components/Dropzone'
|
|
13
13
|
import { VariantHandles } from '@/components/NewVariantComponent'
|
|
14
14
|
import { trpc } from '@/app/_trpc/client'
|
|
15
15
|
import Form from '@/components/form/Form'
|
|
16
16
|
import ErrorComponent from '@/components/ErrorComponent'
|
|
17
|
+
import LocaleSwitcher from '@/components/LocaleSwitcher'
|
|
18
|
+
import { Trash2 } from 'lucide-react'
|
|
19
|
+
import { Alert, AlertDescription } from './ui/alert'
|
|
20
|
+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
|
|
17
21
|
|
|
18
22
|
export default function ItemEditPage({
|
|
19
23
|
section,
|
|
@@ -36,17 +40,47 @@ export default function ItemEditPage({
|
|
|
36
40
|
const [progress, setProgress] = useState(0)
|
|
37
41
|
const [progressVariant, setProgressVariant] = useState<'determinate' | 'query'>('determinate')
|
|
38
42
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
43
|
+
const [submitSuccessCount, setSubmitSuccessCount] = useState(0)
|
|
39
44
|
const { setModal } = useModal()
|
|
40
45
|
const axiosPrivate = useAxiosPrivate()
|
|
41
46
|
const controller = new AbortController()
|
|
42
47
|
const { toast } = useToast()
|
|
43
48
|
const router = useRouter()
|
|
49
|
+
const searchParams = useSearchParams()
|
|
50
|
+
const locale = searchParams.get('locale') ?? undefined
|
|
51
|
+
const [formDirty, setFormDirty] = useState(false)
|
|
52
|
+
const handleDirtyChange = useCallback((isDirty: boolean) => setFormDirty(isDirty), [])
|
|
44
53
|
const dropzoneRef: RefObject<DropzoneHandles | null> = useRef(null)
|
|
45
54
|
const variantRef: RefObject<VariantHandles[]> = useRef([])
|
|
46
55
|
const [data, {refetch}] = trpc.hasItemsSections.editItem.useSuspenseQuery({
|
|
47
56
|
sectionName: section,
|
|
48
57
|
sectionItemId: itemId,
|
|
58
|
+
locale,
|
|
49
59
|
})
|
|
60
|
+
const deleteLocaleMutation = trpc.hasItemsSections.deleteLocaleTranslation.useMutation()
|
|
61
|
+
|
|
62
|
+
const handleDeleteLocale = async () => {
|
|
63
|
+
if (!locale) return
|
|
64
|
+
if (!window.confirm(t('deleteLocaleTranslationText'))) return
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await deleteLocaleMutation.mutateAsync({
|
|
68
|
+
sectionName: section,
|
|
69
|
+
sectionItemId: itemId,
|
|
70
|
+
locale,
|
|
71
|
+
})
|
|
72
|
+
toast({
|
|
73
|
+
variant: 'success',
|
|
74
|
+
description: t('localeTranslationDeleted'),
|
|
75
|
+
})
|
|
76
|
+
router.push(`/edit/${section}/${itemId}`)
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
toast({
|
|
79
|
+
variant: 'destructive',
|
|
80
|
+
description: error?.message ?? t('somethingWentWrong'),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
50
84
|
|
|
51
85
|
const handleSubmit = async (formData: FormData) => {
|
|
52
86
|
setIsSubmitting(true)
|
|
@@ -57,6 +91,10 @@ export default function ItemEditPage({
|
|
|
57
91
|
formData.append('formType', 'edit')
|
|
58
92
|
formData.append('sectionType', sectionType)
|
|
59
93
|
|
|
94
|
+
if (locale) {
|
|
95
|
+
formData.append('locale', locale)
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
// Retrieve the files from the Dropzone's state
|
|
61
99
|
if (dropzoneRef.current) {
|
|
62
100
|
const files: File[] = dropzoneRef.current.getFiles()
|
|
@@ -128,6 +166,7 @@ export default function ItemEditPage({
|
|
|
128
166
|
})
|
|
129
167
|
}
|
|
130
168
|
await refetch()
|
|
169
|
+
setSubmitSuccessCount((currentCount) => currentCount + 1)
|
|
131
170
|
switch (sectionType) {
|
|
132
171
|
case 'categorized':
|
|
133
172
|
// Just close the modal and refetch the page
|
|
@@ -197,7 +236,44 @@ export default function ItemEditPage({
|
|
|
197
236
|
/{t('edit')} {data.section?.title.singular}
|
|
198
237
|
</span>
|
|
199
238
|
</div>
|
|
239
|
+
{data.localization && (
|
|
240
|
+
<div className="px-4 pt-4">
|
|
241
|
+
<LocaleSwitcher
|
|
242
|
+
section={section}
|
|
243
|
+
itemId={itemId}
|
|
244
|
+
currentLocale={data.localization.currentLocale}
|
|
245
|
+
defaultLocale={data.localization.defaultLocale}
|
|
246
|
+
existingTranslations={data.localization.existingTranslations}
|
|
247
|
+
locales={data.localization.locales}
|
|
248
|
+
hasUnsavedChanges={formDirty}
|
|
249
|
+
/>
|
|
250
|
+
{locale && (
|
|
251
|
+
<Alert variant='info' className='mt-2 w-full'>
|
|
252
|
+
<AlertDescription className='text-md font-bold'>
|
|
253
|
+
<div className='flex items-center justify-between'>
|
|
254
|
+
<span>
|
|
255
|
+
{t('editingContentTranslation', { locale })}
|
|
256
|
+
</span>
|
|
257
|
+
{data.localization.existingTranslations.includes(locale) && (
|
|
258
|
+
<button
|
|
259
|
+
type='button'
|
|
260
|
+
onClick={handleDeleteLocale}
|
|
261
|
+
disabled={deleteLocaleMutation.isPending}
|
|
262
|
+
className='inline-flex items-center gap-1.5 rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-700 disabled:opacity-50'
|
|
263
|
+
>
|
|
264
|
+
<Trash2 className='h-3.5 w-3.5' />
|
|
265
|
+
{deleteLocaleMutation.isPending ? t('loading') : t('deleteLocaleTranslation')}
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
</AlertDescription>
|
|
271
|
+
</Alert>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
200
275
|
<Form
|
|
276
|
+
key={locale ?? 'default'}
|
|
201
277
|
formType='edit'
|
|
202
278
|
progressVariant={progressVariant}
|
|
203
279
|
response={response}
|
|
@@ -207,6 +283,10 @@ export default function ItemEditPage({
|
|
|
207
283
|
variantRef={variantRef}
|
|
208
284
|
handleSubmit={handleSubmit}
|
|
209
285
|
isSubmitting={isSubmitting}
|
|
286
|
+
submitSuccessCount={submitSuccessCount}
|
|
287
|
+
contentLocale={data.localization?.currentLocale}
|
|
288
|
+
defaultLocale={data.localization?.defaultLocale}
|
|
289
|
+
onDirtyChange={handleDirtyChange}
|
|
210
290
|
/>
|
|
211
291
|
</div>
|
|
212
292
|
</div>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { Check } from 'lucide-react'
|
|
6
|
+
import { useI18n } from 'nextjs-cms/translations/client'
|
|
7
|
+
import { useSession } from 'nextjs-cms/auth/react'
|
|
8
|
+
import ContainerBox from './ContainerBox'
|
|
9
|
+
|
|
10
|
+
type LocaleConfig = {
|
|
11
|
+
code: string
|
|
12
|
+
label: string
|
|
13
|
+
rtl?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
section: string
|
|
18
|
+
itemId?: string
|
|
19
|
+
currentLocale: LocaleConfig
|
|
20
|
+
defaultLocale: LocaleConfig
|
|
21
|
+
existingTranslations: string[]
|
|
22
|
+
locales: LocaleConfig[]
|
|
23
|
+
hasUnsavedChanges?: boolean
|
|
24
|
+
/** Base path for locale links. Defaults to `/edit/${section}/${itemId}` for hasItems sections. */
|
|
25
|
+
basePath?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function LocaleSwitcher({
|
|
29
|
+
section,
|
|
30
|
+
itemId,
|
|
31
|
+
currentLocale,
|
|
32
|
+
defaultLocale,
|
|
33
|
+
existingTranslations,
|
|
34
|
+
locales,
|
|
35
|
+
hasUnsavedChanges,
|
|
36
|
+
basePath,
|
|
37
|
+
}: Props) {
|
|
38
|
+
const t = useI18n()
|
|
39
|
+
const resolvedBasePath = basePath ?? `/edit/${section}/${itemId}`
|
|
40
|
+
|
|
41
|
+
const getHref = (localeCode: string) => {
|
|
42
|
+
if (localeCode === defaultLocale.code) {
|
|
43
|
+
return resolvedBasePath
|
|
44
|
+
}
|
|
45
|
+
return `${resolvedBasePath}?locale=${localeCode}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleClick = (e: React.MouseEvent, localeCode: string) => {
|
|
49
|
+
if (localeCode === currentLocale.code) {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (hasUnsavedChanges) {
|
|
54
|
+
if (!window.confirm(t('unsavedChangesConfirm'))) {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ContainerBox title={t('localesHeading')}>
|
|
62
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
63
|
+
{locales.map((locale) => {
|
|
64
|
+
const isActive = locale.code === currentLocale.code
|
|
65
|
+
const hasTranslation = locale.code === defaultLocale.code || existingTranslations.includes(locale.code)
|
|
66
|
+
const isDefault = locale.code === defaultLocale.code
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Link
|
|
70
|
+
key={locale.code}
|
|
71
|
+
href={getHref(locale.code)}
|
|
72
|
+
onClick={(e) => handleClick(e, locale.code)}
|
|
73
|
+
className={cn(
|
|
74
|
+
'inline-flex border border-primary/20 items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
|
|
75
|
+
isActive
|
|
76
|
+
? 'bg-success text-success-foreground'
|
|
77
|
+
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{locale.label}
|
|
81
|
+
{isDefault && <span className="text-[10px] opacity-70">{t('baseLocaleBadge')}</span>}
|
|
82
|
+
{hasTranslation && !isDefault && <Check className="h-3 w-3" />}
|
|
83
|
+
</Link>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
</ContainerBox>
|
|
88
|
+
)
|
|
89
|
+
}
|