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.
Files changed (36) hide show
  1. package/package.json +2 -2
  2. package/templates/default/app/(auth)/auth/login/LoginPage.tsx +9 -9
  3. package/templates/default/app/(auth)/auth-language-provider.tsx +34 -0
  4. package/templates/default/app/(auth)/layout.tsx +10 -10
  5. package/templates/default/app/(rootLayout)/edit/[section]/[itemId]/page.tsx +4 -1
  6. package/templates/default/app/(rootLayout)/layout.tsx +5 -5
  7. package/templates/default/app/(rootLayout)/section/[section]/page.tsx +4 -1
  8. package/templates/default/app/api/auth/route.ts +2 -2
  9. package/templates/default/app/api/submit/section/item/[slug]/route.ts +32 -3
  10. package/templates/default/app/api/submit/section/simple/route.ts +32 -3
  11. package/templates/default/app/globals.css +9 -0
  12. package/templates/default/cms.config.ts +4 -4
  13. package/templates/default/components/ItemEditPage.tsx +82 -2
  14. package/templates/default/components/LocaleSwitcher.tsx +89 -0
  15. package/templates/default/components/Navbar.tsx +5 -5
  16. package/templates/default/components/NewPage.tsx +1 -0
  17. package/templates/default/components/SectionPage.tsx +81 -1
  18. package/templates/default/components/form/ContentLocaleContext.tsx +11 -0
  19. package/templates/default/components/form/Form.tsx +48 -5
  20. package/templates/default/components/form/FormInputs.tsx +16 -7
  21. package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
  22. package/templates/default/components/form/inputs/NumberFormInput.tsx +2 -1
  23. package/templates/default/components/form/inputs/PhotoFormInput.tsx +168 -112
  24. package/templates/default/components/form/inputs/RichTextFormInput.tsx +3 -0
  25. package/templates/default/components/form/inputs/SelectFormInput.tsx +1 -1
  26. package/templates/default/components/form/inputs/TagsFormInput.tsx +6 -2
  27. package/templates/default/components/form/inputs/TextFormInput.tsx +3 -0
  28. package/templates/default/components/form/inputs/TextareaFormInput.tsx +3 -0
  29. package/templates/default/components/{locale-dropdown.tsx → language-dropdown.tsx} +74 -74
  30. package/templates/default/components/{locale-picker.tsx → language-picker.tsx} +85 -85
  31. package/templates/default/components/login-language-dropdown.tsx +46 -0
  32. package/templates/default/components/ui/alert.tsx +2 -1
  33. package/templates/default/dynamic-schemas/schema.ts +475 -448
  34. package/templates/default/package.json +1 -1
  35. package/templates/default/app/(auth)/auth-locale-provider.tsx +0 -34
  36. 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.8.10",
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 { wasLocaleChangedOnLogin } from 'nextjs-cms/translations'
15
- import LoginLocaleDropdown from '@/components/login-locale-dropdown'
16
- import { useAuthLocale } from '../../auth-locale-provider'
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, initialLocale } = useAuthLocale()
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 locale if user explicitly changed it on login page
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 localeChanged = wasLocaleChangedOnLogin()
44
+ const languageChanged = wasLanguageChangedOnLogin()
45
45
  await login({
46
46
  username: formRef.current?.username.value,
47
47
  password: formRef.current?.password.value,
48
- locale: localeChanged ? initialLocale : undefined,
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
- <LoginLocaleDropdown
86
+ <LoginLanguageDropdown
87
87
  supportedLanguages={supportedLanguages}
88
88
  fallbackLanguage={fallbackLanguage}
89
- initialLocale={initialLocale}
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 { resolveLocale, RTL_LOCALES, LOCALE_COOKIE_NAME } from 'nextjs-cms/translations'
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 { AuthLocaleProvider } from './auth-locale-provider'
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 cookieLocale = cookieStore.get(LOCALE_COOKIE_NAME)?.value
48
- const locale = resolveLocale(cookieLocale, supportedLanguages, fallbackLanguage)
49
- const isRTL = RTL_LOCALES.has(locale)
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={locale} dir={isRTL ? 'rtl' : 'ltr'} suppressHydrationWarning>
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={locale} dictionaries={dictionaries}>
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
- <AuthLocaleProvider
68
+ <AuthLanguageProvider
69
69
  supportedLanguages={supportedLanguages}
70
70
  fallbackLanguage={fallbackLanguage}
71
- initialLocale={locale}
71
+ initialLanguage={language}
72
72
  >
73
73
  <Providers session={undefined}>{children}</Providers>
74
- </AuthLocaleProvider>
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 { resolveLocale, RTL_LOCALES } from 'nextjs-cms/translations'
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 locale = resolveLocale(session.user.locale, supportedLanguages, fallbackLanguage)
48
- const isRTL = RTL_LOCALES.has(locale)
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={locale} dir={isRTL ? 'rtl' : 'ltr'} suppressHydrationWarning>
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={locale} dictionaries={dictionaries}>
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, locale } = await request.json()
7
+ const { username, password, language } = await request.json()
8
8
  try {
9
- const loginResult = await login({ username, password, locale })
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 submit = new EditSubmit({
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 submit = new SimpleSectionSubmit({
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 { CMSConfig } from 'nextjs-cms/core/config'
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
+ }