fix frontend

This commit is contained in:
root
2026-05-25 00:47:22 -04:00
parent 9bd0938951
commit 5ef9efb8a9
9 changed files with 95 additions and 73 deletions
+1 -1
View File
@@ -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" />
}
+8 -1
View File
@@ -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>
) )
+39
View File
@@ -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
View File
@@ -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: