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 +1 -1
- package/templates/default/app/(auth)/auth/login/LoginPage.tsx +13 -4
- package/templates/default/app/(auth)/auth/login/page.tsx +0 -1
- package/templates/default/app/(auth)/auth-locale-provider.tsx +34 -0
- package/templates/default/app/(auth)/layout.tsx +19 -3
- package/templates/default/app/(rootLayout)/layout.tsx +4 -2
- package/templates/default/app/api/auth/route.ts +2 -2
- package/templates/default/app/api/submit/section/item/[slug]/route.ts +15 -15
- package/templates/default/app/api/submit/section/item/route.ts +14 -14
- package/templates/default/app/api/submit/section/simple/route.ts +15 -15
- package/templates/default/cms.config.ts +6 -0
- package/templates/default/components/Modal.tsx +16 -6
- package/templates/default/components/Navbar.tsx +25 -10
- package/templates/default/components/SelectBox.tsx +15 -1
- package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
- package/templates/default/components/form/inputs/SelectFormInput.tsx +19 -10
- package/templates/default/components/locale-dropdown.tsx +74 -0
- package/templates/default/components/locale-picker.tsx +85 -0
- package/templates/default/components/login-locale-dropdown.tsx +45 -0
- package/templates/default/components/theme-toggle.tsx +1 -1
- package/templates/default/package.json +3 -2
- package/templates/default/dynamic-schemas/schema.ts +0 -381
package/package.json
CHANGED
|
@@ -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(
|
|
90
|
+
setTheme(theme === 'dark' ? 'light' : 'dark')
|
|
82
91
|
}}
|
|
83
92
|
type='button'
|
|
84
|
-
className='
|
|
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' />
|
|
@@ -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
|
|
33
|
-
const
|
|
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
|
-
<
|
|
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
|
|
35
|
-
const
|
|
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'>
|
|
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
|
-
<
|
|
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={
|
|
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
|
-
<
|
|
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={
|
|
204
|
-
width={
|
|
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
|
-
</
|
|
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}>
|
|
@@ -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
|
-
*
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|