create-nextjs-cms 0.5.99 → 0.5.101

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextjs-cms",
3
- "version": "0.5.99",
3
+ "version": "0.5.101",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,8 @@ 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 LoginLocaleDropdown from '@/components/login-locale-dropdown'
15
+ import { useAuthLocale } from '../../auth-locale-provider'
14
16
 
15
17
  type fromErrors = {
16
18
  username?: string | null
@@ -20,13 +22,14 @@ type fromErrors = {
20
22
  }
21
23
 
22
24
  export default function LoginPage() {
25
+ const { supportedLanguages, fallbackLanguage, initialLocale } = useAuthLocale()
23
26
  const t = useI18n()
24
27
  const session = useSession()
25
28
  const searchParams = useSearchParams()
26
29
  const router = useRouter()
27
30
  const loginButtonRef = useRef<HTMLButtonElement>(null)
28
31
  const formRef = useRef<HTMLFormElement>(null)
29
- const { setTheme } = useTheme()
32
+ const { theme, setTheme } = useTheme()
30
33
 
31
34
  const handleSubmit = async (event: React.FormEvent<HTMLButtonElement>) => {
32
35
  event.preventDefault()
@@ -38,6 +41,7 @@ export default function LoginPage() {
38
41
  await login({
39
42
  username: formRef.current?.username.value,
40
43
  password: formRef.current?.password.value,
44
+ locale: initialLocale,
41
45
  })
42
46
  } catch (error: any) {
43
47
  if (loginButtonRef.current) {
@@ -74,14 +78,19 @@ export default function LoginPage() {
74
78
  return (
75
79
  <>
76
80
  <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'>
77
- <div className='flex w-full'>
81
+ <div className='flex w-full items-center justify-end gap-2 absolute end-10 top-10'>
82
+ <LoginLocaleDropdown
83
+ supportedLanguages={supportedLanguages}
84
+ fallbackLanguage={fallbackLanguage}
85
+ initialLocale={initialLocale}
86
+ />
78
87
  <Button
79
88
  variant='default'
80
89
  onClick={() => {
81
- setTheme((theme) => (theme === 'dark' ? 'light' : 'dark'))
90
+ setTheme(theme === 'dark' ? 'light' : 'dark')
82
91
  }}
83
92
  type='button'
84
- className='absolute end-10 top-10 rounded-full p-3 focus:outline-hidden'
93
+ className='rounded-full p-3 focus:outline-hidden'
85
94
  >
86
95
  <span className='sr-only'>Theme</span>
87
96
  <SunIcon className='hidden transition-all dark:block' aria-hidden='true' />
@@ -2,7 +2,6 @@ import LoginPage from './LoginPage'
2
2
  import auth from 'nextjs-cms/auth'
3
3
  import { redirect } from 'next/navigation'
4
4
 
5
- // export const dynamic = 'force-dynamic'
6
5
  export default async function Page() {
7
6
  const session = await auth()
8
7
  if (session) {
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react'
4
+
5
+ export interface AuthLocaleValue {
6
+ supportedLanguages: readonly string[]
7
+ fallbackLanguage: string
8
+ initialLocale: string
9
+ }
10
+
11
+ const AuthLocaleContext = createContext<AuthLocaleValue | null>(null)
12
+
13
+ export function AuthLocaleProvider({
14
+ supportedLanguages,
15
+ fallbackLanguage,
16
+ initialLocale,
17
+ children,
18
+ }: AuthLocaleValue & { children: ReactNode }) {
19
+ return (
20
+ <AuthLocaleContext.Provider
21
+ value={{ supportedLanguages, fallbackLanguage, initialLocale }}
22
+ >
23
+ {children}
24
+ </AuthLocaleContext.Provider>
25
+ )
26
+ }
27
+
28
+ export function useAuthLocale(): AuthLocaleValue {
29
+ const ctx = useContext(AuthLocaleContext)
30
+ if (!ctx) {
31
+ throw new Error('useAuthLocale must be used within AuthLocaleProvider')
32
+ }
33
+ return ctx
34
+ }
@@ -1,4 +1,5 @@
1
1
  import { Inter, Cairo } from 'next/font/google'
2
+ import { cookies } from 'next/headers'
2
3
  import '../globals.css'
3
4
  import type { Metadata } from 'next'
4
5
  import { cn } from '@/lib/utils'
@@ -7,6 +8,9 @@ import Providers from '@/app/providers'
7
8
  import auth from 'nextjs-cms/auth'
8
9
  import { getCMSConfig } from 'nextjs-cms/core'
9
10
  import { I18nProviderClient } from 'nextjs-cms/translations/client'
11
+ import { resolveLocale, RTL_LOCALES, LOCALE_COOKIE_NAME } from 'nextjs-cms/translations'
12
+ import { redirect } from 'next/navigation'
13
+ import { AuthLocaleProvider } from './auth-locale-provider'
10
14
 
11
15
  const inter = Inter({
12
16
  subsets: ['latin'],
@@ -28,9 +32,15 @@ export async function generateMetadata(): Promise<Metadata> {
28
32
 
29
33
  export default async function RootLayout({ children }: { children: React.ReactNode }) {
30
34
  const session = await auth()
35
+ if (session) {
36
+ redirect('/')
37
+ }
31
38
  const cmsConfig = await getCMSConfig()
32
- const locale = session?.user?.locale ?? 'en'
33
- const isRTL = locale === 'ar'
39
+ const { supportedLanguages, fallbackLanguage } = cmsConfig.i18n
40
+ const cookieStore = await cookies()
41
+ const cookieLocale = cookieStore.get(LOCALE_COOKIE_NAME)?.value
42
+ const locale = resolveLocale(cookieLocale, supportedLanguages, fallbackLanguage)
43
+ const isRTL = RTL_LOCALES.has(locale)
34
44
 
35
45
  return (
36
46
  <html lang={locale} dir={isRTL ? 'rtl' : 'ltr'} suppressHydrationWarning>
@@ -47,7 +57,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
47
57
  enableSystem
48
58
  disableTransitionOnChange
49
59
  >
50
- <Providers session={session ?? undefined}>{children}</Providers>
60
+ <AuthLocaleProvider
61
+ supportedLanguages={supportedLanguages}
62
+ fallbackLanguage={fallbackLanguage}
63
+ initialLocale={locale}
64
+ >
65
+ <Providers session={undefined}>{children}</Providers>
66
+ </AuthLocaleProvider>
51
67
  </ThemeProvider>
52
68
  </I18nProviderClient>
53
69
  </body>
@@ -7,6 +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
11
  import { api, HydrateClient } from 'nextjs-cms/api/trpc/server'
11
12
  import Layout from '@/components/Layout'
12
13
 
@@ -31,8 +32,9 @@ export async function generateMetadata(): Promise<Metadata> {
31
32
  export default async function CMSLayout({ children }: { children: React.ReactNode }) {
32
33
  const session = await auth()
33
34
  const cmsConfig = await getCMSConfig()
34
- const locale = session?.user?.locale ?? 'en'
35
- const isRTL = locale === 'ar'
35
+ const { supportedLanguages, fallbackLanguage } = cmsConfig.i18n
36
+ const locale = resolveLocale(session?.user?.locale, supportedLanguages, fallbackLanguage)
37
+ const isRTL = RTL_LOCALES.has(locale)
36
38
  await api.navigation.getSidebar.prefetch()
37
39
 
38
40
  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 } = await request.json()
7
+ const { username, password, locale } = await request.json()
8
8
  try {
9
- const loginResult = await login({ username, password })
9
+ const loginResult = await login({ username, password, locale })
10
10
  const requestMetadata = getRequestMetadataFromHeaders(request.headers)
11
11
 
12
12
  await recordLog({
@@ -1,7 +1,7 @@
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'
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'
5
5
 
6
6
  export async function PUT(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
7
7
  const session = await auth()
@@ -25,10 +25,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
25
25
  )
26
26
  }
27
27
 
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)
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)
32
32
 
33
33
  if (!sectionName) {
34
34
  return NextResponse.json(
@@ -39,13 +39,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
39
39
  )
40
40
  }
41
41
 
42
- const submit = new EditSubmit({
43
- itemId: itemId as string,
44
- sectionName,
45
- user,
46
- postData: formData,
47
- requestMetadata,
48
- })
42
+ const submit = new EditSubmit({
43
+ itemId: itemId as string,
44
+ sectionName,
45
+ user,
46
+ postData: formData,
47
+ requestMetadata,
48
+ })
49
49
 
50
50
  await submit.initialize()
51
51
  await submit.submit()
@@ -1,7 +1,7 @@
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'
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'
5
5
 
6
6
  export async function POST(request: NextRequest) {
7
7
  const session = await auth()
@@ -15,10 +15,10 @@ export async function POST(request: NextRequest) {
15
15
  )
16
16
  }
17
17
 
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)
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)
22
22
 
23
23
  if (!sectionName) {
24
24
  return NextResponse.json(
@@ -29,12 +29,12 @@ export async function POST(request: NextRequest) {
29
29
  )
30
30
  }
31
31
 
32
- const submit = new NewSubmit({
33
- sectionName,
34
- user,
35
- postData: formData,
36
- requestMetadata,
37
- })
32
+ const submit = new NewSubmit({
33
+ sectionName,
34
+ user,
35
+ postData: formData,
36
+ requestMetadata,
37
+ })
38
38
 
39
39
  await submit.initialize()
40
40
  await submit.submit()
@@ -1,7 +1,7 @@
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'
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'
5
5
 
6
6
  export async function PUT(request: NextRequest) {
7
7
  const session = await auth()
@@ -15,10 +15,10 @@ export async function PUT(request: NextRequest) {
15
15
  )
16
16
  }
17
17
 
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)
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)
22
22
 
23
23
  if (!sectionName) {
24
24
  return NextResponse.json(
@@ -29,13 +29,13 @@ export async function PUT(request: NextRequest) {
29
29
  )
30
30
  }
31
31
 
32
- const submit = new SimpleSectionSubmit({
33
- itemId: '1',
34
- sectionName,
35
- user,
36
- postData: formData,
37
- requestMetadata,
38
- })
32
+ const submit = new SimpleSectionSubmit({
33
+ itemId: '1',
34
+ sectionName,
35
+ user,
36
+ postData: formData,
37
+ requestMetadata,
38
+ })
39
39
 
40
40
  await submit.initialize()
41
41
  await submit.submit()
@@ -1,4 +1,6 @@
1
1
  import type { CMSConfig } from 'nextjs-cms/core/config'
2
+ import en from 'nextjs-cms/translations/dictionaries/en'
3
+ import ar from 'nextjs-cms/translations/dictionaries/ar'
2
4
  import process from 'process'
3
5
  import { resolve } from 'path'
4
6
 
@@ -43,4 +45,8 @@ export const config: CMSConfig = {
43
45
  },
44
46
  },
45
47
  },
48
+ i18n: {
49
+ supportedLanguages: { en, ar },
50
+ fallbackLanguage: 'en',
51
+ },
46
52
  }
@@ -1,12 +1,20 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useRef, useState } from 'react'
3
+ import { Suspense, useEffect, useRef, useState } from 'react'
4
4
  import useModal from '@/hooks/useModal'
5
5
  import classNames from 'classnames'
6
- import { X } from 'lucide-react'
6
+ import { X, Loader2 } from 'lucide-react'
7
7
 
8
8
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/custom-dialog'
9
9
 
10
+ function ModalLoadingFallback() {
11
+ return (
12
+ <div className='flex min-h-[200px] items-center justify-center p-8'>
13
+ <Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
14
+ </div>
15
+ )
16
+ }
17
+
10
18
  function Modal() {
11
19
  const { setModal, modal, modalResponse, setModalResponse } = useModal()
12
20
  const [open, setOpen] = useState(false)
@@ -52,9 +60,7 @@ function Modal() {
52
60
  >
53
61
  <div>{modal?.title || ''}</div>
54
62
  <X
55
- className={`absolute ${
56
- modal?.lang === 'ar' ? 'left-5' : 'right-5'
57
- } size-4 cursor-pointer rounded bg-white text-gray-500 hover:opacity-90 md:block lg:size-6`}
63
+ className={`absolute end-5 size-6 cursor-pointer rounded bg-white text-gray-500 hover:opacity-90 md:block`}
58
64
  onClick={handleClose}
59
65
  ref={cancelButtonRef}
60
66
  />
@@ -64,7 +70,11 @@ function Modal() {
64
70
  <div className='flex flex-col gap-1'>
65
71
  <div className='w-full overflow-hidden'>
66
72
  <div className='flex w-full flex-col font-semibold'>
67
- <div className='text-foreground w-full'>{modal?.body}</div>
73
+ <div className='text-foreground w-full'>
74
+ <Suspense fallback={<ModalLoadingFallback />}>
75
+ {modal?.body}
76
+ </Suspense>
77
+ </div>
68
78
  <div className='w-full'>
69
79
  {modalResponse && <div className={`w-full p-3`}>{modalResponse.message}</div>}
70
80
  </div>
@@ -4,6 +4,7 @@ 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 { useI18n } from 'nextjs-cms/translations/client'
7
+ import { RTL_LOCALES } from 'nextjs-cms/translations'
7
8
  import { trpc } from '@/app/_trpc/client'
8
9
  import ProtectedImage from '@/components/ProtectedImage'
9
10
  import { Spinner } from '@/components/ui/spinner'
@@ -23,6 +24,7 @@ import {
23
24
  import { useToast } from '@/components/ui/use-toast'
24
25
  import { logout, useSession } from 'nextjs-cms/auth/react'
25
26
  import ThemeToggle from './theme-toggle'
27
+ import LocaleDropdown from './locale-dropdown'
26
28
  type Props = {
27
29
  /**
28
30
  * Allows the parent component to modify the state when the
@@ -43,6 +45,8 @@ export default function Navbar(props: Props) {
43
45
  const { theme, setTheme } = useTheme()
44
46
  const session = useSession()
45
47
  const { toast } = useToast()
48
+ const locale = session?.data?.user?.locale ?? 'en'
49
+ const isRTL = RTL_LOCALES.has(locale)
46
50
  const logsQuery = trpc.logs.list.useQuery(
47
51
  {
48
52
  limit: 20,
@@ -117,13 +121,20 @@ export default function Navbar(props: Props) {
117
121
  asChild
118
122
  className='text-foreground hover:text-foreground/90 cursor-pointer'
119
123
  >
120
- <BellIcon className='h-6 w-6' />
124
+ <button
125
+ type='button'
126
+ className='relative flex h-10 items-center justify-center rounded-full focus:outline-hidden'
127
+ aria-label='Notifications'
128
+ >
129
+ <span className='absolute -inset-1.5' />
130
+ <BellIcon className='h-6 w-6' />
131
+ </button>
121
132
  </DropdownMenuTrigger>
122
133
  <DropdownMenuContent
123
- sideOffset={20}
124
- align='end'
134
+ sideOffset={12}
135
+ align={isRTL ? 'start' : 'end'}
125
136
  side='bottom'
126
- alignOffset={-20}
137
+ alignOffset={isRTL ? 20 : -20}
127
138
  className='w-[400px] max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
128
139
  >
129
140
  <DropdownMenuLabel>{t('notifications')}</DropdownMenuLabel>
@@ -176,6 +187,7 @@ export default function Navbar(props: Props) {
176
187
  </Link>
177
188
  </DropdownMenuContent>
178
189
  </DropdownMenu>
190
+ <LocaleDropdown />
179
191
  <ThemeToggle />
180
192
  {/* Profile dropdown */}
181
193
  <DropdownMenu>
@@ -183,7 +195,10 @@ export default function Navbar(props: Props) {
183
195
  asChild
184
196
  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'
185
197
  >
186
- <div>
198
+ <button
199
+ type='button'
200
+ className='relative flex h-10 items-center justify-center rounded-full'
201
+ >
187
202
  <span className='absolute -inset-1.5' />
188
203
  <span className='sr-only'>Open user menu</span>
189
204
  {session?.data?.user.image ? (
@@ -200,19 +215,19 @@ export default function Navbar(props: Props) {
200
215
  ) : (
201
216
  <Image
202
217
  src='/blank_avatar.png'
203
- height={36}
204
- width={36}
218
+ height={40}
219
+ width={40}
205
220
  alt='profile image'
206
221
  className='rounded-full ring-2 ring-amber-300 ring-inset'
207
222
  />
208
223
  )}
209
- </div>
224
+ </button>
210
225
  </DropdownMenuTrigger>
211
226
  <DropdownMenuContent
212
227
  sideOffset={12}
213
- align='end'
228
+ align={isRTL ? 'start' : 'end'}
214
229
  side='bottom'
215
- alignOffset={-10}
230
+ alignOffset={isRTL ? 10 : -10}
216
231
  className='w-56 max-w-full ring-1 ring-sky-400/80 dark:ring-amber-900'
217
232
  >
218
233
  <DropdownMenuLabel>{session.data?.user.name}</DropdownMenuLabel>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useState, useEffect } from 'react'
4
4
  import { Check, ChevronsUpDown } from 'lucide-react'
5
5
  import { cn } from '@/lib/utils'
6
6
  import { Button } from '@/components/ui/button'
@@ -28,6 +28,20 @@ export default function SelectBox({
28
28
  )
29
29
  const [open, setOpen] = useState(false)
30
30
 
31
+ // Update selected item when items or defaultValue changes (e.g., on language change)
32
+ useEffect(() => {
33
+ if (defaultValue !== undefined && defaultValue !== null) {
34
+ const foundItem = items.find((item) => item.value?.toString() === defaultValue.toString())
35
+ if (foundItem) {
36
+ setSelected(foundItem)
37
+ }
38
+ } else {
39
+ // If no value is selected, update to the first item (placeholder)
40
+ // This ensures the placeholder label is updated when language changes
41
+ setSelected(items[0])
42
+ }
43
+ }, [items, defaultValue])
44
+
31
45
  return (
32
46
  <div className={cn('relative', classname)}>
33
47
  <Popover open={open} onOpenChange={setOpen}>
@@ -8,4 +8,4 @@ export const revalidate = 0
8
8
 
9
9
  // @refresh reset
10
10
 
11
- export const configLastUpdated = 1768909879581
11
+ export const configLastUpdated = 1769510501212
@@ -1,9 +1,10 @@
1
- import React, { useEffect, useState } from 'react'
1
+ import React, { useEffect, useState, useMemo } from 'react'
2
2
  import FormInputElement from '@/components/form/FormInputElement'
3
3
  import SelectBox from '@/components/SelectBox'
4
4
  import LoadingSpinners from '@/components/LoadingSpinners'
5
5
  import { useAutoAnimate } from '@formkit/auto-animate/react'
6
6
  import { useI18n } from 'nextjs-cms/translations/client'
7
+ import { useSession } from 'nextjs-cms/auth/react'
7
8
  import { trpc } from '@/app/_trpc/client'
8
9
  import { SelectFieldClientConfig, SelectOption } from 'nextjs-cms/core/fields'
9
10
  import { ConditionalFields } from '@/components/ConditionalFields'
@@ -21,16 +22,23 @@ export default function SelectFormInput({
21
22
  level?: number
22
23
  }) {
23
24
  const t = useI18n()
25
+ const session = useSession()
26
+ const locale = session?.data?.user?.locale
27
+
24
28
  /**
25
- * Check and add the select option if it does not exist, the select option has a value of undefined
29
+ * Create options array with translated placeholder option.
30
+ * This ensures the placeholder is always up-to-date when the language changes.
26
31
  */
27
- const exists = input.options?.filter((option) => option.value === undefined)
28
- if (!exists || exists.length === 0) {
29
- if (input.options) {
30
- // @ts-ignore - This is a type-hack to add the placeholder `select` option to the options array
31
- input.options.unshift({ value: undefined, label: t('select') })
32
- }
33
- }
32
+ const optionsWithPlaceholder = useMemo(() => {
33
+ if (!input.options) return []
34
+
35
+ // Filter out any existing placeholder options (value === undefined)
36
+ const optionsWithoutPlaceholder = input.options.filter((option) => option.value !== undefined)
37
+
38
+ // Add the translated placeholder option at the beginning
39
+ // @ts-ignore - This is a type-hack to add the placeholder `select` option with undefined value
40
+ return [{ value: undefined, label: t('select') as string }, ...optionsWithoutPlaceholder]
41
+ }, [input.options, t, locale])
34
42
 
35
43
  const depth = input.section ? input.section.depth : 1
36
44
  const [parent] = useAutoAnimate(/* optional config */)
@@ -127,7 +135,8 @@ export default function SelectFormInput({
127
135
  />
128
136
  <SelectBox
129
137
  defaultValue={value?.value ? value?.value.toString() : undefined}
130
- items={input.options ? input.options : []}
138
+ // @ts-ignore - optionsWithPlaceholder includes placeholder with undefined value
139
+ items={optionsWithPlaceholder}
131
140
  onChange={(value: SelectOption) => {
132
141
  field.onChange(value.value)
133
142
  if (value) setValue(value)
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { trpc } from '@/app/_trpc/client'
6
+ import { useSession, refreshSession } from 'nextjs-cms/auth/react'
7
+ import { useI18n } from 'nextjs-cms/translations/client'
8
+ import { setLoginPageLocaleCookie } from 'nextjs-cms/translations'
9
+ import { useToast } from '@/components/ui/use-toast'
10
+ import LocalePicker from './locale-picker'
11
+
12
+ /**
13
+ * Locale dropdown for the app Navbar (authenticated).
14
+ * Persists via accountSettings.updateLocale → admins table → auth refresh.
15
+ * Also writes nextjs-cms-locale cookie so the login page shows the same locale after logout.
16
+ */
17
+ export default function LocaleDropdown() {
18
+ const t = useI18n()
19
+ const router = useRouter()
20
+ const { toast } = useToast()
21
+ const { data: session } = useSession()
22
+ const i18nQuery = trpc.config.getI18n.useQuery()
23
+ const updateLocale = trpc.accountSettings.updateLocale.useMutation({
24
+ onSuccess: async (_data, variables) => {
25
+ setLoginPageLocaleCookie(variables.locale)
26
+ try {
27
+ const res = await fetch('/api/auth/refresh', { credentials: 'include' })
28
+ const data = await res.json()
29
+ if (res.ok && data.session) {
30
+ await refreshSession(data.session)
31
+ router.refresh()
32
+ } else {
33
+ router.refresh()
34
+ }
35
+ } catch {
36
+ router.refresh()
37
+ } finally {
38
+ setPendingLocale(null)
39
+ }
40
+ },
41
+ onError: () => {
42
+ setPendingLocale(null)
43
+ toast({
44
+ variant: 'destructive',
45
+ title: t('error'),
46
+ description: t('somethingWentWrong'),
47
+ })
48
+ },
49
+ })
50
+ const [pendingLocale, setPendingLocale] = useState<string | null>(null)
51
+
52
+ const supported = i18nQuery.data?.supportedLanguages ?? []
53
+ const currentLocale = session?.user?.locale ?? i18nQuery.data?.fallbackLanguage ?? 'en'
54
+
55
+ const handleSelect = useCallback(
56
+ (locale: string) => {
57
+ if (locale === currentLocale) return
58
+ setPendingLocale(locale)
59
+ updateLocale.mutate({ locale })
60
+ },
61
+ [currentLocale, updateLocale],
62
+ )
63
+
64
+ return (
65
+ <LocalePicker
66
+ supportedLanguages={supported}
67
+ currentLocale={currentLocale}
68
+ onLocaleChange={handleSelect}
69
+ disabled={updateLocale.isPending}
70
+ loading={updateLocale.isPending && pendingLocale != null}
71
+ ariaLabel={t('language')}
72
+ />
73
+ )
74
+ }
@@ -0,0 +1,85 @@
1
+ 'use client'
2
+
3
+ import { Languages } from 'lucide-react'
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu'
10
+ import { Spinner } from '@/components/ui/spinner'
11
+ import { RTL_LOCALES } from 'nextjs-cms/translations'
12
+
13
+ const LOCALE_DISPLAY_NAMES: Record<string, string> = {
14
+ en: 'English',
15
+ ar: 'العربية',
16
+ }
17
+
18
+ function displayName(code: string): string {
19
+ return LOCALE_DISPLAY_NAMES[code] ?? code
20
+ }
21
+
22
+ export interface LocalePickerProps {
23
+ supportedLanguages: readonly string[]
24
+ currentLocale: string
25
+ onLocaleChange: (locale: string) => void
26
+ disabled?: boolean
27
+ loading?: boolean
28
+ ariaLabel: string
29
+ }
30
+
31
+ /**
32
+ * Shared locale dropdown UI. Use in Navbar (app) or Login page.
33
+ * Parent controls data source and onSelect behavior (tRPC + refresh vs cookie + refresh).
34
+ */
35
+ export default function LocalePicker({
36
+ supportedLanguages,
37
+ currentLocale,
38
+ onLocaleChange,
39
+ disabled = false,
40
+ loading = false,
41
+ ariaLabel,
42
+ }: LocalePickerProps) {
43
+ if (supportedLanguages.length < 2) return null
44
+
45
+ const isRTL = RTL_LOCALES.has(currentLocale)
46
+
47
+ return (
48
+ <DropdownMenu>
49
+ <DropdownMenuTrigger
50
+ asChild
51
+ className='text-foreground hover:text-foreground/90 relative rounded-full focus:outline-hidden cursor-pointer'
52
+ >
53
+ <button
54
+ type='button'
55
+ className='flex h-10 items-center justify-center'
56
+ aria-label={ariaLabel}
57
+ disabled={disabled}
58
+ >
59
+ {loading ? (
60
+ <Spinner className='size-5' />
61
+ ) : (
62
+ <Languages className='h-5 w-5' aria-hidden='true' />
63
+ )}
64
+ </button>
65
+ </DropdownMenuTrigger>
66
+ <DropdownMenuContent
67
+ align={isRTL ? 'start' : 'end'}
68
+ sideOffset={12}
69
+ className='ring-1 ring-sky-400/80 dark:ring-amber-900'
70
+ >
71
+ {supportedLanguages.map((code) => (
72
+ <DropdownMenuItem
73
+ key={code}
74
+ onClick={() => onLocaleChange(code)}
75
+ disabled={disabled}
76
+ className='cursor-pointer'
77
+ >
78
+ {displayName(code)}
79
+ {code === currentLocale ? ' ✓' : ''}
80
+ </DropdownMenuItem>
81
+ ))}
82
+ </DropdownMenuContent>
83
+ </DropdownMenu>
84
+ )
85
+ }
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useI18n } from 'nextjs-cms/translations/client'
6
+ import { setLoginPageLocaleCookie } from 'nextjs-cms/translations'
7
+ import LocalePicker from './locale-picker'
8
+
9
+ export interface LoginLocaleDropdownProps {
10
+ supportedLanguages: readonly string[]
11
+ fallbackLanguage: string
12
+ initialLocale: string
13
+ }
14
+
15
+ /**
16
+ * Locale dropdown for the login page (unauthenticated).
17
+ * Persists via nextjs-cms-locale cookie; auth layout reads it when no session.
18
+ */
19
+ export default function LoginLocaleDropdown({
20
+ supportedLanguages,
21
+ fallbackLanguage,
22
+ initialLocale,
23
+ }: LoginLocaleDropdownProps) {
24
+ const t = useI18n()
25
+ const router = useRouter()
26
+ const currentLocale = initialLocale || fallbackLanguage
27
+
28
+ const handleSelect = useCallback(
29
+ (locale: string) => {
30
+ if (locale === currentLocale) return
31
+ setLoginPageLocaleCookie(locale)
32
+ router.refresh()
33
+ },
34
+ [currentLocale, router],
35
+ )
36
+
37
+ return (
38
+ <LocalePicker
39
+ supportedLanguages={supportedLanguages}
40
+ currentLocale={currentLocale}
41
+ onLocaleChange={handleSelect}
42
+ ariaLabel={t('language')}
43
+ />
44
+ )
45
+ }
@@ -21,7 +21,7 @@ const ThemeToggle = () => {
21
21
  setTheme(theme === 'dark' ? 'light' : 'dark')
22
22
  }}
23
23
  type='button'
24
- className='text-foreground hover:text-foreground/90 relative rounded-full p-1 focus:outline-hidden'
24
+ className='text-foreground hover:text-foreground/90 relative flex h-10 items-center justify-center rounded-full focus:outline-hidden cursor-pointer'
25
25
  >
26
26
  <span className='absolute -inset-1.5' />
27
27
  <span className='sr-only'>Theme</span>
@@ -24,6 +24,7 @@
24
24
  "@radix-ui/react-aspect-ratio": "^1.1.2",
25
25
  "@radix-ui/react-checkbox": "^1.1.4",
26
26
  "@radix-ui/react-dialog": "^1.1.15",
27
+ "@radix-ui/react-direction": "^1.1.1",
27
28
  "@radix-ui/react-dropdown-menu": "^2.1.6",
28
29
  "@radix-ui/react-icons": "^1.3.2",
29
30
  "@radix-ui/react-label": "^2.1.2",
@@ -64,7 +65,7 @@
64
65
  "nanoid": "^5.1.2",
65
66
  "next": "16.1.1",
66
67
  "next-themes": "^0.4.6",
67
- "nextjs-cms": "0.5.99",
68
+ "nextjs-cms": "0.5.101",
68
69
  "plaiceholder": "^3.0.0",
69
70
  "prettier-plugin-tailwindcss": "^0.7.2",
70
71
  "qrcode": "^1.5.4",
@@ -97,7 +98,7 @@
97
98
  "eslint-config-prettier": "^10.0.1",
98
99
  "eslint-plugin-prettier": "^5.2.3",
99
100
  "fs-extra": "^11.3.3",
100
- "nextjs-cms-kit": "0.5.99",
101
+ "nextjs-cms-kit": "0.5.101",
101
102
  "postcss": "^8.5.1",
102
103
  "prettier": "3.5.0",
103
104
  "raw-loader": "^4.0.2",
@@ -1,381 +0,0 @@
1
- import {mysqlTable,int,longtext,mysqlEnum,varchar,boolean,double,timestamp} from 'drizzle-orm/mysql-core'
2
-
3
- export const AppInfoTable = mysqlTable('app_info', {
4
- id: int('id').autoincrement().notNull().primaryKey(),
5
- aboutEn: longtext('about_en').notNull(),
6
- aboutAr: longtext('about_ar').notNull(),
7
- aboutTr: longtext('about_tr').notNull(),
8
- privacyEn: longtext('privacy_en').notNull(),
9
- privacyAr: longtext('privacy_ar').notNull(),
10
- privacyTr: longtext('privacy_tr').notNull()
11
- });
12
-
13
-
14
- export const UserReportsTable = mysqlTable('user_reports', {
15
- id: int('id').autoincrement().notNull().primaryKey(),
16
- contentType: mysqlEnum('content_type', ['ad', 'user']).notNull(),
17
- reportType: mysqlEnum('report_type', ['explicit_content', 'wrong_information', 'no_longer_available', 'user_not_responsive', 'other']).notNull(),
18
- contentId: int('content_id').notNull(),
19
- catId: int('cat_id').notNull(),
20
- userId: int('user_id'),
21
- appId: varchar('app_id', { length: 36 }).notNull()
22
- });
23
-
24
-
25
- export const FeaturedSliderTable = mysqlTable('featured_slider', {
26
- id: int('id').autoincrement().notNull().primaryKey(),
27
- image: varchar('image', { length: 255 }).notNull(),
28
- titleEn: varchar('title_en', { length: 255 }).notNull(),
29
- titleAr: varchar('title_ar', { length: 255 }).notNull(),
30
- titleTr: varchar('title_tr', { length: 255 }).notNull(),
31
- descEn: longtext('desc_en').notNull(),
32
- descAr: longtext('desc_ar').notNull(),
33
- descTr: longtext('desc_tr').notNull()
34
- });
35
-
36
-
37
- export const MenuSettingsTable = mysqlTable('menu_settings', {
38
- id: int('id').autoincrement().notNull().primaryKey(),
39
- taxRate: varchar('tax_rate', { length: 255 }),
40
- hideSlider: boolean('hide_slider')
41
- });
42
-
43
-
44
- export const ServicesTable = mysqlTable('services', {
45
- id: int('id').autoincrement().notNull().primaryKey(),
46
- title: varchar('title', { length: 255 }).notNull(),
47
- catId: int('cat_id').notNull(),
48
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
49
- price: int('price'),
50
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']),
51
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
52
- desc: longtext('desc'),
53
- latitude: double('latitude'),
54
- longitude: double('longitude'),
55
- viewCount: int('view_count'),
56
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
57
- govId: int('gov_id').notNull(),
58
- districtId: int('district_id').notNull(),
59
- subDistrictId: int('sub_district_id'),
60
- townId: int('town_id')
61
- });
62
-
63
-
64
- export const RealestateTable = mysqlTable('realestate', {
65
- id: int('id').autoincrement().notNull().primaryKey(),
66
- catId: int('cat_id').notNull(),
67
- title: varchar('title', { length: 255 }).notNull(),
68
- price: int('price').notNull(),
69
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
70
- spaceGross: int('space_gross').notNull(),
71
- spaceNet: int('space_net').notNull(),
72
- latitude: double('latitude').notNull(),
73
- longitude: double('longitude').notNull(),
74
- roomCount: varchar('room_count', { length: 255 }),
75
- buildingAge: varchar('building_age', { length: 255 }),
76
- floorCount: varchar('floor_count', { length: 255 }),
77
- bathroomCount: varchar('bathroom_count', { length: 255 }),
78
- floorLocation: varchar('floor_location', { length: 255 }),
79
- heatingType: varchar('heating_type', { length: 255 }),
80
- kitchenType: varchar('kitchen_type', { length: 255 }),
81
- balcony: boolean('balcony'),
82
- lift: boolean('lift'),
83
- parkingType: varchar('parking_type', { length: 255 }),
84
- furnished: boolean('furnished'),
85
- belongsToSite: boolean('belongs_to_site'),
86
- siteName: varchar('site_name', { length: 255 }),
87
- installments: boolean('installments'),
88
- exchangeable: boolean('exchangeable'),
89
- fromWhom: varchar('from_whom', { length: 255 }),
90
- buildingMembershipFees: int('building_membership_fees'),
91
- deposit: int('deposit'),
92
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
93
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
94
- desc: longtext('desc'),
95
- viewCount: int('view_count'),
96
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
97
- govId: int('gov_id').notNull(),
98
- districtId: int('district_id').notNull(),
99
- subDistrictId: int('sub_district_id'),
100
- townId: int('town_id')
101
- });
102
-
103
-
104
- export const HomepageSliderTable = mysqlTable('homepage_slider', {
105
- id: int('id').autoincrement().notNull().primaryKey(),
106
- titleEn: varchar('title_en', { length: 255 }).notNull(),
107
- titleAr: varchar('title_ar', { length: 255 }).notNull(),
108
- titleTr: varchar('title_tr', { length: 255 }).notNull(),
109
- subtitleEn: varchar('subtitle_en', { length: 255 }).notNull(),
110
- subtitleAr: varchar('subtitle_ar', { length: 255 }).notNull(),
111
- subtitleTr: varchar('subtitle_tr', { length: 255 }).notNull(),
112
- photo: varchar('photo', { length: 255 }).notNull(),
113
- buttonUrl: varchar('button_url', { length: 255 }),
114
- buttonTextEn: varchar('button_text_en', { length: 255 }),
115
- buttonTextAr: varchar('button_text_ar', { length: 255 }),
116
- buttonTextTr: varchar('button_text_tr', { length: 255 }),
117
- buttonUrlTarget: mysqlEnum('button_url_target', ['_blank', '_self'])
118
- });
119
-
120
-
121
- export const ContestSubscribersTable = mysqlTable('contest_subscribers', {
122
- id: int('id').autoincrement().notNull().primaryKey(),
123
- userId: int('user_id').notNull(),
124
- contestId: varchar('contest_id', { length: 255 }).notNull()
125
- });
126
-
127
-
128
- export const ContestsTable = mysqlTable('contests', {
129
- id: int('id').autoincrement().notNull().primaryKey(),
130
- date: timestamp('date').notNull(),
131
- prize: varchar('prize', { length: 255 }).notNull(),
132
- winnerId: int('winner_id'),
133
- tags: varchar('tags', { length: 255 })
134
- });
135
-
136
-
137
- export const NotificationsTable = mysqlTable('notifications', {
138
- id: int('id').autoincrement().notNull().primaryKey(),
139
- type: mysqlEnum('type', ['ad_price_updated', 'ad_updated', 'ad_activated', 'ad_pending_review', 'ad_expired', 'ad_rejected', 'ad_viewed', 'ad_favorited', 'ad_shared', 'ad_reported', 'ad_deleted', 'ad_created', 'contest_winner', 'contest_reminder', 'contest_created', 'custom']).notNull(),
140
- contentId: int('content_id'),
141
- contentCatId: int('content_cat_id'),
142
- userId: int('user_id'),
143
- message: varchar('message', { length: 255 })
144
- });
145
-
146
-
147
- export const ModerationTable = mysqlTable('moderation', {
148
- id: int('id').autoincrement().notNull().primaryKey(),
149
- contentType: mysqlEnum('content_type', ['ad', 'user']).notNull(),
150
- contentId: int('content_id').notNull(),
151
- catId: int('cat_id'),
152
- userId: int('user_id').notNull(),
153
- flagged: int('flagged').notNull(),
154
- result: varchar('result', { length: 255 }).notNull()
155
- });
156
-
157
-
158
- export const JobsTable = mysqlTable('jobs', {
159
- id: int('id').autoincrement().notNull().primaryKey(),
160
- title: varchar('title', { length: 255 }).notNull(),
161
- catId: int('cat_id').notNull(),
162
- salary: int('salary'),
163
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
164
- workMethod: varchar('work_method', { length: 255 }).notNull(),
165
- minimumEducation: varchar('minimum_education', { length: 255 }).notNull(),
166
- experienceLevel: varchar('experience_level', { length: 255 }).notNull(),
167
- remote: boolean('remote'),
168
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
169
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
170
- desc: longtext('desc'),
171
- latitude: double('latitude'),
172
- longitude: double('longitude'),
173
- viewCount: int('view_count'),
174
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
175
- govId: int('gov_id').notNull(),
176
- districtId: int('district_id').notNull(),
177
- subDistrictId: int('sub_district_id'),
178
- townId: int('town_id')
179
- });
180
-
181
-
182
- export const FurnitureTable = mysqlTable('furniture', {
183
- id: int('id').autoincrement().notNull().primaryKey(),
184
- title: varchar('title', { length: 255 }).notNull(),
185
- catId: int('cat_id').notNull(),
186
- condition: varchar('condition', { length: 255 }).notNull(),
187
- price: int('price').notNull(),
188
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
189
- exchangeable: boolean('exchangeable'),
190
- fromWhom: varchar('from_whom', { length: 255 }).notNull(),
191
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
192
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
193
- desc: longtext('desc'),
194
- latitude: double('latitude'),
195
- longitude: double('longitude'),
196
- viewCount: int('view_count'),
197
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
198
- govId: int('gov_id').notNull(),
199
- districtId: int('district_id').notNull(),
200
- subDistrictId: int('sub_district_id'),
201
- townId: int('town_id')
202
- });
203
-
204
-
205
- export const FiltersTable = mysqlTable('filters', {
206
- id: int('id').autoincrement().notNull().primaryKey(),
207
- title: varchar('title', { length: 255 }).notNull(),
208
- Key: varchar('_key', { length: 255 }).notNull(),
209
- fieldName: varchar('field_name', { length: 255 }).notNull(),
210
- type: mysqlEnum('type', ['checkbox', 'select', 'text', 'finance', 'number', 'point']).notNull(),
211
- tableName: varchar('table_name', { length: 255 }),
212
- isMandatory: boolean('is_mandatory')
213
- });
214
-
215
-
216
- export const FaqTable = mysqlTable('faq', {
217
- id: int('id').autoincrement().notNull().primaryKey(),
218
- qEn: varchar('q_en', { length: 255 }).notNull(),
219
- qAr: varchar('q_ar', { length: 255 }).notNull(),
220
- qTr: varchar('q_tr', { length: 255 }),
221
- aEn: longtext('a_en').notNull(),
222
- aAr: longtext('a_ar'),
223
- aTr: longtext('a_tr')
224
- });
225
-
226
-
227
- export const ErrorsTable = mysqlTable('errors', {
228
- id: int('id').autoincrement().notNull().primaryKey(),
229
- title: varchar('title', { length: 255 }).notNull(),
230
- caseId: varchar('case_id', { length: 255 }).notNull(),
231
- userId: int('user_id'),
232
- itemSlug: varchar('item_slug', { length: 255 }),
233
- desc: longtext('desc'),
234
- isResolved: boolean('is_resolved')
235
- });
236
-
237
-
238
- export const ElectronicsTable = mysqlTable('electronics', {
239
- id: int('id').autoincrement().notNull().primaryKey(),
240
- title: varchar('title', { length: 255 }).notNull(),
241
- catId: int('cat_id').notNull(),
242
- condition: varchar('condition', { length: 255 }).notNull(),
243
- price: int('price').notNull(),
244
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
245
- exchangeable: boolean('exchangeable'),
246
- installments: boolean('installments'),
247
- fromWhom: varchar('from_whom', { length: 255 }).notNull(),
248
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
249
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
250
- desc: longtext('desc'),
251
- latitude: double('latitude'),
252
- longitude: double('longitude'),
253
- viewCount: int('view_count'),
254
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
255
- govId: int('gov_id').notNull(),
256
- districtId: int('district_id').notNull(),
257
- subDistrictId: int('sub_district_id'),
258
- townId: int('town_id')
259
- });
260
-
261
-
262
- export const ClothesTable = mysqlTable('clothes', {
263
- id: int('id').autoincrement().notNull().primaryKey(),
264
- title: varchar('title', { length: 255 }).notNull(),
265
- catId: int('cat_id').notNull(),
266
- condition: varchar('condition', { length: 255 }).notNull(),
267
- price: int('price').notNull(),
268
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
269
- exchangeable: boolean('exchangeable'),
270
- fromWhom: varchar('from_whom', { length: 255 }).notNull(),
271
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
272
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
273
- desc: longtext('desc'),
274
- latitude: double('latitude'),
275
- longitude: double('longitude'),
276
- viewCount: int('view_count'),
277
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
278
- govId: int('gov_id').notNull(),
279
- districtId: int('district_id').notNull(),
280
- subDistrictId: int('sub_district_id'),
281
- townId: int('town_id')
282
- });
283
-
284
-
285
- export const CatsTable = mysqlTable('cats', {
286
- id: int('id').autoincrement().notNull().primaryKey(),
287
- catOrder: int('cat_order').notNull(),
288
- slug: varchar('slug', { length: 255 }).notNull(),
289
- titleEn: varchar('title_en', { length: 255 }).notNull(),
290
- titleAr: varchar('title_ar', { length: 255 }).notNull(),
291
- titleTr: varchar('title_tr', { length: 255 }).notNull(),
292
- fullTitleEn: varchar('full_title_en', { length: 255 }),
293
- fullTitleAr: varchar('full_title_ar', { length: 255 }),
294
- fullTitleTr: varchar('full_title_tr', { length: 255 }),
295
- image: varchar('image', { length: 255 }),
296
- metaDescEn: varchar('meta_desc_en', { length: 255 }),
297
- metaDescAr: varchar('meta_desc_ar', { length: 255 }),
298
- metaDescTr: varchar('meta_desc_tr', { length: 255 }),
299
- filters: varchar('filters', { length: 255 }),
300
- tableName: varchar('table_name', { length: 255 }).notNull(),
301
- adCount: int('ad_count'),
302
- parentId: int('parent_id'),
303
- level: int('level')
304
- });
305
-
306
-
307
- export const CarsTable = mysqlTable('cars', {
308
- id: int('id').autoincrement().notNull().primaryKey(),
309
- modelYear: int('model_year').notNull(),
310
- model: varchar('model', { length: 255 }).notNull(),
311
- title: varchar('title', { length: 255 }).notNull(),
312
- catId: int('cat_id').notNull(),
313
- govId: int('gov_id').notNull(),
314
- districtId: int('district_id').notNull(),
315
- subDistrictId: int('sub_district_id'),
316
- townId: int('town_id'),
317
- price: int('price').notNull(),
318
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
319
- condition: varchar('condition', { length: 255 }).notNull(),
320
- fromWhom: varchar('from_whom', { length: 255 }).notNull(),
321
- engineCapacity: varchar('engine_capacity', { length: 255 }).notNull(),
322
- enginePower: varchar('engine_power', { length: 255 }).notNull(),
323
- tractionType: varchar('traction_type', { length: 255 }).notNull(),
324
- bodyType: varchar('body_type', { length: 255 }).notNull(),
325
- gearType: varchar('gear_type', { length: 255 }).notNull(),
326
- fuelType: varchar('fuel_type', { length: 255 }).notNull(),
327
- exchangeable: boolean('exchangeable'),
328
- installments: boolean('installments'),
329
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
330
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
331
- desc: longtext('desc'),
332
- latitude: double('latitude'),
333
- longitude: double('longitude'),
334
- viewCount: int('view_count'),
335
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired'])
336
- });
337
-
338
-
339
- export const CarsDbTable = mysqlTable('cars_db', {
340
- id: int('id').autoincrement().notNull().primaryKey(),
341
- name: varchar('name', { length: 255 }).notNull(),
342
- parentId: int('parent_id'),
343
- level: int('level')
344
- });
345
-
346
-
347
- export const BooksTable = mysqlTable('books', {
348
- id: int('id').autoincrement().notNull().primaryKey(),
349
- title: varchar('title', { length: 255 }).notNull(),
350
- catId: int('cat_id').notNull(),
351
- condition: varchar('condition', { length: 255 }).notNull(),
352
- price: int('price').notNull(),
353
- currency: mysqlEnum('currency', ['USD', 'TRY', 'SYP']).notNull(),
354
- exchangeable: boolean('exchangeable'),
355
- fromWhom: varchar('from_whom', { length: 255 }).notNull(),
356
- coverphoto: varchar('coverphoto', { length: 255 }).notNull(),
357
- coverphotoPlaceholder: varchar('coverphoto_placeholder', { length: 255 }),
358
- desc: longtext('desc'),
359
- latitude: double('latitude'),
360
- longitude: double('longitude'),
361
- viewCount: int('view_count'),
362
- status: mysqlEnum('status', ['pending_creation', 'active', 'pending_review', 'rejected', 'expired']),
363
- govId: int('gov_id').notNull(),
364
- districtId: int('district_id').notNull(),
365
- subDistrictId: int('sub_district_id'),
366
- townId: int('town_id')
367
- });
368
-
369
-
370
- export const AddressTable = mysqlTable('address', {
371
- id: int('id').autoincrement().notNull().primaryKey(),
372
- catOrder: int('cat_order').notNull(),
373
- titleEn: varchar('title_en', { length: 255 }).notNull(),
374
- titleAr: varchar('title_ar', { length: 255 }).notNull(),
375
- titleTr: varchar('title_tr', { length: 255 }).notNull(),
376
- image: varchar('image', { length: 255 }),
377
- path: varchar('path', { length: 255 }),
378
- parentId: int('parent_id'),
379
- level: int('level')
380
- });
381
-