fix the errors in dashboard

This commit is contained in:
root
2026-05-23 19:03:15 -04:00
parent c6cebdd125
commit f5292f8b6c
29 changed files with 1085 additions and 126 deletions
@@ -324,7 +324,10 @@ export default function OffersPage() {
function load() {
apiFetch<Offer[]>('/offers')
.then((data) => setOffers(data ?? []))
.then((data) => {
setOffers(Array.isArray(data) ? data : [])
setError(null)
})
.catch((err) => setError(err.message))
}
@@ -334,7 +337,7 @@ export default function OffersPage() {
setLoadingFleet(true)
try {
const data = await apiFetch<FleetVehicle[]>('/vehicles?pageSize=200')
setFleet(data ?? [])
setFleet(Array.isArray(data) ? data : [])
} catch {
// non-fatal; vehicle picker just stays empty
} finally {
@@ -363,7 +366,7 @@ export default function OffersPage() {
const detail = await apiFetch<Offer>(`/offers/${offer.id}`)
setForm((prev) => ({
...prev,
vehicleIds: detail.vehicles?.map((v) => v.vehicleId) ?? [],
vehicleIds: Array.isArray(detail?.vehicles) ? detail.vehicles.map((v) => v.vehicleId) : [],
}))
} catch {
// leave vehicleIds as empty if fetch fails
@@ -117,11 +117,19 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
changed = true
}
// useSearchParams() can return empty params during hydration without a Suspense boundary.
// Always preserve embedded=1 when the component was server-rendered as embedded so
// the router.replace doesn't strip it and trigger an unintended redirect.
if (embedded && params.get('embedded') !== '1') {
params.set('embedded', '1')
changed = true
}
if (!changed) return
const nextQuery = params.toString()
router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false })
}, [language, pathname, router, searchParams, theme])
}, [embedded, language, pathname, router, searchParams, theme])
useEffect(() => {
const currentPath = window.location.pathname + (window.location.search || '')
@@ -1,7 +1,16 @@
import { Suspense } from 'react'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import SignInPageClient from './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({
searchParams,
}: {
@@ -13,6 +22,7 @@ export default async function SignInPage({
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()
@@ -25,12 +35,22 @@ export default async function SignInPage({
}
}
const destination = host
? `${proto}://${host}/sign-in${query.size ? `?${query.toString()}` : ''}`
: `${process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'}/sign-in${query.size ? `?${query.toString()}` : ''}`
const marketplaceOrigin = new URL(MARKETPLACE_URL)
const publicHost = forwardedHost && !isInternalHost(forwardedHost)
? forwardedHost
: host && !isInternalHost(host)
? host
: null
redirect(destination)
if (!publicHost || publicHost === marketplaceOrigin.host) {
const destination = `${MARKETPLACE_URL}/sign-in${query.size ? `?${query.toString()}` : ''}`
redirect(destination)
}
}
return <SignInPageClient embedded={embedded} />
return (
<Suspense fallback={null}>
<SignInPageClient embedded={embedded} />
</Suspense>
)
}
@@ -14,6 +14,12 @@ type FooterItem = {
href?: string
}
const appPrivacyHref = {
en: `${marketplaceUrl}/app-privacy-en`,
fr: `${marketplaceUrl}/app-privacy-fr`,
ar: `${marketplaceUrl}/app-privacy-ar`,
} as const
const localeOptions: Array<{ value: DashboardLanguage; label: string; flag: string }> = [
{ value: 'en', label: 'Global (English)', flag: '🇺🇸' },
{ value: 'ar', label: 'North Africa (Arabic)', flag: '🇲🇦' },
@@ -34,13 +40,13 @@ function getFooterContent(language: DashboardLanguage): {
{ label: 'Conditions dutilisation', href: `${marketplaceUrl}/footer/terms-of-service` },
{ label: 'Sécurité', href: `${marketplaceUrl}/footer/security` },
{ label: 'Conformité', href: `${marketplaceUrl}/footer/compliance` },
{ label: 'Politique de confidentialité', href: `${marketplaceUrl}/footer/privacy-policy` },
{ label: 'Politique de confidentialité', href: appPrivacyHref.fr },
{ label: 'Politique relative aux cookies', href: `${marketplaceUrl}/footer/cookie-policy` },
],
secondary: [
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
{ label: 'Contacter les ventes', href: `${marketplaceUrl}/footer/contact-sales` },
{ label: 'Nos bureaux', href: `${marketplaceUrl}/footer/our-offices` },
{ label: 'Conditions générales', href: `${marketplaceUrl}/footer/general-conditions` },
],
localeLabel: 'Europe (French)',
rightsLabel: 'Tous droits réservés.',
@@ -52,13 +58,13 @@ function getFooterContent(language: DashboardLanguage): {
{ label: 'شروط الاستخدام', href: `${marketplaceUrl}/footer/terms-of-service` },
{ label: 'الأمان', href: `${marketplaceUrl}/footer/security` },
{ label: 'الامتثال', href: `${marketplaceUrl}/footer/compliance` },
{ label: 'سياسة الخصوصية', href: `${marketplaceUrl}/footer/privacy-policy` },
{ label: 'سياسة الخصوصية', href: appPrivacyHref.ar },
{ label: 'سياسة ملفات تعريف الارتباط', href: `${marketplaceUrl}/footer/cookie-policy` },
],
secondary: [
{ label: 'النشرة الإخبارية', href: `${marketplaceUrl}/footer/newsletter` },
{ label: 'تواصل مع المبيعات', href: `${marketplaceUrl}/footer/contact-sales` },
{ label: 'مكاتبنا', href: `${marketplaceUrl}/footer/our-offices` },
{ label: 'الشروط العامة', href: `${marketplaceUrl}/footer/general-conditions` },
],
localeLabel: 'North Africa (Arabic)',
rightsLabel: 'جميع الحقوق محفوظة.',
@@ -71,13 +77,13 @@ function getFooterContent(language: DashboardLanguage): {
{ label: 'Terms of Service', href: `${marketplaceUrl}/footer/terms-of-service` },
{ label: 'Security', href: `${marketplaceUrl}/footer/security` },
{ label: 'Compliance', href: `${marketplaceUrl}/footer/compliance` },
{ label: 'Privacy Policy', href: `${marketplaceUrl}/footer/privacy-policy` },
{ label: 'Privacy Policy', href: appPrivacyHref.en },
{ label: 'Cookie Policy', href: `${marketplaceUrl}/footer/cookie-policy` },
],
secondary: [
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
{ label: 'Contact Sales', href: `${marketplaceUrl}/footer/contact-sales` },
{ label: 'Our Offices', href: `${marketplaceUrl}/footer/our-offices` },
{ label: 'General Conditions', href: `${marketplaceUrl}/footer/general-conditions` },
],
localeLabel: 'Global (English)',
rightsLabel: 'All rights reserved.',
+26 -3
View File
@@ -1,18 +1,41 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PUBLIC_DASHBOARD_PREFIXES = ['/dashboard/sign-in', '/dashboard/forgot-password']
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)
}
function isProtectedRoute(req: NextRequest) {
return req.nextUrl.pathname === '/dashboard' || req.nextUrl.pathname.startsWith('/dashboard/')
const { pathname } = req.nextUrl
if (PUBLIC_DASHBOARD_PREFIXES.some((p) => pathname === p || pathname.startsWith(p + '/'))) return false
return pathname === '/dashboard' || pathname.startsWith('/dashboard/')
}
function localJwtMiddleware(req: NextRequest): NextResponse {
if (!isProtectedRoute(req)) return NextResponse.next()
// Check for employee_token cookie (set on login) OR allow if coming from sign-in
const token = req.cookies.get('employee_token')?.value
if (!token) {
const forwardedHost = req.headers.get('x-forwarded-host')
const forwardedProto = req.headers.get('x-forwarded-proto')
const marketplaceOrigin = new URL(MARKETPLACE_URL)
const signInUrl = req.nextUrl.clone()
signInUrl.pathname = '/sign-in'
if (forwardedHost && !isInternalHost(forwardedHost)) {
signInUrl.host = forwardedHost
signInUrl.protocol = (forwardedProto ?? 'http') + ':'
} else if (isInternalHost(signInUrl.host)) {
signInUrl.host = marketplaceOrigin.host
signInUrl.protocol = marketplaceOrigin.protocol
}
const isMarketplaceHost = signInUrl.host === marketplaceOrigin.host
signInUrl.pathname = isMarketplaceHost ? '/sign-in' : '/dashboard/sign-in'
signInUrl.searchParams.set('redirect', req.nextUrl.pathname)
return NextResponse.redirect(signInUrl)
}