fix frontend
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "npm run build --workspace @rentaldrivego/types",
|
"predev": "npm run build --workspace @rentaldrivego/types",
|
||||||
"dev": "node ../../scripts/run-with-env-file.cjs ../../.env.local ../../node_modules/.bin/ts-node-dev --respawn --transpile-only src/index.ts",
|
"dev": "node ../../scripts/run-with-env-file.cjs ../../.env.local ../../node_modules/.bin/ts-node-dev --respawn --transpile-only --ignore-watch ../../packages/types/dist src/index.ts",
|
||||||
"prebuild": "npm run build --workspace @rentaldrivego/types",
|
"prebuild": "npm run build --workspace @rentaldrivego/types",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"prestart": "npm run build --workspace @rentaldrivego/types",
|
"prestart": "npm run build --workspace @rentaldrivego/types",
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { headers } from 'next/headers'
|
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
import SignInPageClient from '@/app/sign-in/[[...sign-in]]/SignInPageClient'
|
import SignInPageClient from '@/app/sign-in/[[...sign-in]]/SignInPageClient'
|
||||||
|
|
||||||
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
|
|
||||||
|
|
||||||
function isInternalHost(host: string | null): boolean {
|
|
||||||
if (!host) return false
|
|
||||||
const hostname = host.split(':')[0]?.toLowerCase()
|
|
||||||
return ['host.docker.internal', 'dashboard', 'admin', 'api', 'marketplace'].includes(hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SignInPage({
|
export default async function SignInPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -19,35 +9,6 @@ export default async function SignInPage({
|
|||||||
const params = await searchParams
|
const params = await searchParams
|
||||||
const embedded = params.embedded === '1'
|
const embedded = params.embedded === '1'
|
||||||
|
|
||||||
if (!embedded) {
|
|
||||||
const requestHeaders = headers()
|
|
||||||
const host = requestHeaders.get('host')
|
|
||||||
const forwardedHost = requestHeaders.get('x-forwarded-host')
|
|
||||||
const proto = requestHeaders.get('x-forwarded-proto') ?? 'http'
|
|
||||||
const query = new URLSearchParams()
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (value === undefined) continue
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const item of value) query.append(key, item)
|
|
||||||
} else {
|
|
||||||
query.set(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const marketplaceOrigin = new URL(MARKETPLACE_URL)
|
|
||||||
const publicHost = forwardedHost && !isInternalHost(forwardedHost)
|
|
||||||
? forwardedHost
|
|
||||||
: host && !isInternalHost(host)
|
|
||||||
? host
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (!publicHost || publicHost === marketplaceOrigin.host) {
|
|
||||||
const destination = `${MARKETPLACE_URL}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
|
||||||
redirect(destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<SignInPageClient embedded={embedded} />
|
<SignInPageClient embedded={embedded} />
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import WorkspaceFrame from '@/components/WorkspaceFrame'
|
|
||||||
|
|
||||||
export default function SignInPage() {
|
|
||||||
return <WorkspaceFrame target="sign-in" />
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
import MarketplaceShell from '@/components/MarketplaceShell'
|
import MarketplaceShell from '@/components/MarketplaceShell'
|
||||||
import { getMarketplaceLanguage } from '@/lib/i18n.server'
|
import { getMarketplaceLanguage } from '@/lib/i18n.server'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
@@ -15,6 +16,12 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const language = getMarketplaceLanguage()
|
const language = getMarketplaceLanguage()
|
||||||
|
const cookieStore = cookies()
|
||||||
|
const rawTheme =
|
||||||
|
cookieStore.get('rentaldrivego-theme')?.value ??
|
||||||
|
cookieStore.get('marketplace-theme')?.value
|
||||||
|
const theme = rawTheme === 'dark' ? 'dark' : 'light'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={language} dir={language === 'ar' ? 'rtl' : 'ltr'} suppressHydrationWarning>
|
<html lang={language} dir={language === 'ar' ? 'rtl' : 'ltr'} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
@@ -27,7 +34,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body suppressHydrationWarning>
|
<body suppressHydrationWarning>
|
||||||
<MarketplaceShell initialLanguage={language}>{children}</MarketplaceShell>
|
<MarketplaceShell initialLanguage={language} initialTheme={theme}>{children}</MarketplaceShell>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { headers } from 'next/headers'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { resolveServerAppUrl } from '@/lib/appUrls'
|
||||||
|
|
||||||
|
type SearchParams = Record<string, string | string[] | undefined>
|
||||||
|
|
||||||
|
function buildSearch(params: SearchParams): string {
|
||||||
|
const nextParams = new URLSearchParams()
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
nextParams.set(key, value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const entry of value) nextParams.append(key, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = nextParams.toString()
|
||||||
|
return search ? `?${search}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SignInPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: SearchParams
|
||||||
|
}) {
|
||||||
|
const requestHeaders = headers()
|
||||||
|
const dashboardUrl = resolveServerAppUrl(
|
||||||
|
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
|
requestHeaders.get('host'),
|
||||||
|
requestHeaders.get('x-forwarded-proto'),
|
||||||
|
)
|
||||||
|
const params = searchParams
|
||||||
|
|
||||||
|
redirect(`${dashboardUrl}/sign-in${buildSearch(params)}`)
|
||||||
|
}
|
||||||
@@ -81,6 +81,11 @@ export default function MarketplaceHeader({
|
|||||||
setThemeMenuOpen((open) => !open)
|
setThemeMenuOpen((open) => !open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signInParams = new URLSearchParams()
|
||||||
|
signInParams.set('lang', currentLanguage.value)
|
||||||
|
signInParams.set('theme', theme)
|
||||||
|
const signInHref = `${dashboardUrl}/sign-in?${signInParams.toString()}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-blue-900 dark:bg-[#07101e]/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
|
<header className="sticky top-0 z-50 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-blue-900 dark:bg-[#07101e]/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
|
||||||
<div className="shell flex min-h-14 flex-col gap-1.5 py-1.5 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:py-2">
|
<div className="shell flex min-h-14 flex-col gap-1.5 py-1.5 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:py-2">
|
||||||
@@ -113,7 +118,7 @@ export default function MarketplaceHeader({
|
|||||||
{dict.explore}
|
{dict.explore}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/sign-in"
|
href={signInHref}
|
||||||
className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-[#162038] dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm"
|
className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-[#162038] dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm"
|
||||||
>
|
>
|
||||||
{dict.signIn}
|
{dict.signIn}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
import { appPrivacyHref, footerPageHref } from '@/lib/footerContent'
|
import { appPrivacyHref, footerPageHref } from '@/lib/footerContent'
|
||||||
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE, isMarketplaceLanguage, type MarketplaceLanguage } from '@/lib/i18n'
|
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE, isMarketplaceLanguage, type MarketplaceLanguage } from '@/lib/i18n'
|
||||||
import { SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
|
import { SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
|
||||||
@@ -157,14 +157,17 @@ export function useMarketplacePreferences() {
|
|||||||
export default function MarketplaceShell({
|
export default function MarketplaceShell({
|
||||||
children,
|
children,
|
||||||
initialLanguage = 'en',
|
initialLanguage = 'en',
|
||||||
|
initialTheme = 'light',
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
initialLanguage?: MarketplaceLanguage
|
initialLanguage?: MarketplaceLanguage
|
||||||
|
initialTheme?: Theme
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
|
||||||
const [language, setLanguageState] = useState<MarketplaceLanguage>(initialLanguage)
|
const [language, setLanguageState] = useState<MarketplaceLanguage>(initialLanguage)
|
||||||
const [theme, setThemeState] = useState<Theme>('light')
|
const [theme, setThemeState] = useState<Theme>(initialTheme)
|
||||||
const [hydrated, setHydrated] = useState(false)
|
const [hydrated, setHydrated] = useState(false)
|
||||||
const previousLanguage = useRef<MarketplaceLanguage>(initialLanguage)
|
const previousLanguage = useRef<MarketplaceLanguage>(initialLanguage)
|
||||||
const [companyName, setCompanyName] = useState<string | null>(null)
|
const [companyName, setCompanyName] = useState<string | null>(null)
|
||||||
@@ -320,11 +323,11 @@ export default function MarketplaceShell({
|
|||||||
try { sessionStorage.setItem('marketplace-language', language) } catch {}
|
try { sessionStorage.setItem('marketplace-language', language) } catch {}
|
||||||
writeScopedPreference(SHARED_LANGUAGE_KEY, language, ['marketplace-language'])
|
writeScopedPreference(SHARED_LANGUAGE_KEY, language, ['marketplace-language'])
|
||||||
|
|
||||||
if (previousLanguage.current !== language) {
|
if (previousLanguage.current !== language && pathname !== '/sign-in') {
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}
|
||||||
previousLanguage.current = language
|
previousLanguage.current = language
|
||||||
}, [hydrated, language, router])
|
}, [hydrated, language, pathname, router])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.classList.toggle('dark', theme === 'dark')
|
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||||
|
|||||||
@@ -94,33 +94,39 @@ export default function WorkspaceFrame({ target }: { target: FrameId }) {
|
|||||||
}, [target])
|
}, [target])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (target === 'sign-in' && src) return
|
||||||
|
|
||||||
const nextUrl = new URL(buildFrameUrl(config.appUrl, framePath))
|
const nextUrl = new URL(buildFrameUrl(config.appUrl, framePath))
|
||||||
nextUrl.searchParams.set('theme', theme)
|
nextUrl.searchParams.set('theme', theme)
|
||||||
nextUrl.searchParams.set('lang', language)
|
nextUrl.searchParams.set('lang', language)
|
||||||
nextUrl.searchParams.set('embedded', '1')
|
nextUrl.searchParams.set('embedded', '1')
|
||||||
setSrc(nextUrl.toString())
|
setSrc(nextUrl.toString())
|
||||||
}, [config.appUrl, framePath, language, theme])
|
}, [config.appUrl, framePath, language, src, target, theme])
|
||||||
|
|
||||||
|
const content = src ? (
|
||||||
|
<iframe
|
||||||
|
title={config.label}
|
||||||
|
src={src}
|
||||||
|
className="block w-full border-0 bg-white dark:bg-[#07101e]"
|
||||||
|
style={{ height: FRAME_HEIGHT }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center text-sm text-stone-500 dark:text-stone-300"
|
||||||
|
style={{ height: FRAME_HEIGHT }}
|
||||||
|
>
|
||||||
|
{loadingLabel} {config.label}…
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (target === 'sign-in') {
|
||||||
|
return <main className="min-h-[calc(100vh-56px)]">{content}</main>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="site-page">
|
<main className="site-page">
|
||||||
<div className="site-section py-6 sm:py-8 lg:py-10">
|
<div className="site-section py-6 sm:py-8 lg:py-10">
|
||||||
<section className="site-panel overflow-hidden p-0">
|
<section className="site-panel overflow-hidden p-0">{content}</section>
|
||||||
{src ? (
|
|
||||||
<iframe
|
|
||||||
title={config.label}
|
|
||||||
src={src}
|
|
||||||
className="w-full border-0 bg-white dark:bg-[#07101e]"
|
|
||||||
style={{ height: FRAME_HEIGHT }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-center text-sm text-stone-500 dark:text-stone-300"
|
|
||||||
style={{ height: FRAME_HEIGHT }}
|
|
||||||
>
|
|
||||||
{loadingLabel} {config.label}…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-4
@@ -92,7 +92,7 @@ services:
|
|||||||
- .env.docker.dev
|
- .env.docker.dev
|
||||||
environment:
|
environment:
|
||||||
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
FILE_STORAGE_ROOT: /var/lib/rentaldrivego/storage
|
||||||
command: ["sh", "-c", "npm run build --workspace @rentaldrivego/types && cd /app/apps/api && exec /app/node_modules/.bin/ts-node-dev --respawn --transpile-only src/index.ts"]
|
command: ["sh", "-c", "npm run build --workspace @rentaldrivego/types && cd /app/apps/api && exec /app/node_modules/.bin/ts-node-dev --respawn --transpile-only --ignore-watch ../../packages/types/dist src/index.ts"]
|
||||||
ports:
|
ports:
|
||||||
- "4000:4000"
|
- "4000:4000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -114,13 +114,14 @@ services:
|
|||||||
[
|
[
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
"npm run build --workspace @rentaldrivego/types && cd /app/apps/marketplace && rm -rf .next && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3000",
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/marketplace && mkdir -p .next && find .next -mindepth 1 -exec rm -rf {} + && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3000",
|
||||||
]
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- app_node_modules:/app/node_modules
|
- app_node_modules:/app/node_modules
|
||||||
|
- marketplace_next:/app/apps/marketplace/.next
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["marketplace", "full"]
|
profiles: ["marketplace", "full"]
|
||||||
|
|
||||||
@@ -136,13 +137,14 @@ services:
|
|||||||
[
|
[
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
"npm run build --workspace @rentaldrivego/types && cd /app/apps/dashboard && rm -rf .next && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3001",
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/dashboard && mkdir -p .next && find .next -mindepth 1 -exec rm -rf {} + && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3001",
|
||||||
]
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- app_node_modules:/app/node_modules
|
- app_node_modules:/app/node_modules
|
||||||
|
- dashboard_next:/app/apps/dashboard/.next
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["dashboard", "full"]
|
profiles: ["dashboard", "full"]
|
||||||
|
|
||||||
@@ -158,13 +160,14 @@ services:
|
|||||||
[
|
[
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
"npm run build --workspace @rentaldrivego/types && cd /app/apps/admin && rm -rf .next && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3002",
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/admin && mkdir -p .next && find .next -mindepth 1 -exec rm -rf {} + && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3002",
|
||||||
]
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- app_node_modules:/app/node_modules
|
- app_node_modules:/app/node_modules
|
||||||
|
- admin_next:/app/apps/admin/.next
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["admin", "full"]
|
profiles: ["admin", "full"]
|
||||||
|
|
||||||
@@ -176,3 +179,6 @@ volumes:
|
|||||||
app_node_modules:
|
app_node_modules:
|
||||||
postgres_bootstrap_state:
|
postgres_bootstrap_state:
|
||||||
api_uploads_dev:
|
api_uploads_dev:
|
||||||
|
marketplace_next:
|
||||||
|
dashboard_next:
|
||||||
|
admin_next:
|
||||||
|
|||||||
Reference in New Issue
Block a user