fix: remove medium theme, fix navbar re-render, fix Node.js env loading
- Remove 'medium' theme from dashboard and marketplace; keep only light/dark - Marketplace: move public pages into (public)/ route group so header/footer persist across navigations without re-rendering (eliminates usePathname) - MarketplaceShell is now a pure context provider; chrome lives in (public)/layout.tsx which Next.js holds stable across navigations - WorkspaceFrame: remove history.replaceState and standalone-route logic - Docker API: bypass npm run dev to avoid node --env-file in containers - Add scripts/run-with-env-file.cjs for Node.js < 20.6 compatibility; update apps/api/package.json dev script to use it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
|
||||
import {
|
||||
cloneMarketplaceHomepageContent,
|
||||
resolveMarketplaceHomepageSections,
|
||||
type MarketplaceHomepageConfig,
|
||||
type MarketplaceHomepageSectionType,
|
||||
} from '@rentaldrivego/types'
|
||||
import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
|
||||
|
||||
export default function HomeContent() {
|
||||
const { language } = useMarketplacePreferences()
|
||||
const [content, setContent] = useState<MarketplaceHomepageConfig>(cloneMarketplaceHomepageContent())
|
||||
const dict = content[language]
|
||||
const sections = resolveMarketplaceHomepageSections(dict.sections)
|
||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
||||
const starterSignupHref = `${dashboardUrl}/sign-up?plan=STARTER&billing=MONTHLY¤cy=MAD`
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function loadHomepageContent() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/site/platform/homepage`, { cache: 'no-store' })
|
||||
const json = await res.json().catch(() => null)
|
||||
if (!res.ok || !json?.data || cancelled) return
|
||||
setContent(json.data as MarketplaceHomepageConfig)
|
||||
} catch {
|
||||
// Fall back to the shared defaults when the API is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
loadHomepageContent()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
function hasSection(section: MarketplaceHomepageSectionType) {
|
||||
return sections.includes(section)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen overflow-hidden bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)]">
|
||||
<section className="relative">
|
||||
<div className="absolute inset-x-0 top-0 h-[38rem] bg-[radial-gradient(circle_at_top_left,rgba(217,119,6,0.24),transparent_34%),radial-gradient(circle_at_top_right,rgba(15,118,110,0.18),transparent_26%)] dark:bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.16),transparent_34%),radial-gradient(circle_at_top_right,rgba(45,212,191,0.14),transparent_26%)]" />
|
||||
<div className="shell relative py-10 sm:py-14 lg:py-20">
|
||||
<div className={`grid gap-8 ${hasSection('hero') && hasSection('surface') ? 'lg:grid-cols-[minmax(0,1.15fr)_24rem] lg:items-stretch xl:grid-cols-[minmax(0,1.2fr)_28rem]' : ''}`}>
|
||||
{hasSection('hero') ? (
|
||||
<div className="rounded-[2rem] border border-stone-200/80 bg-white/80 p-7 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur dark:border-stone-800 dark:bg-stone-900/70">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.32em] text-amber-700 dark:text-amber-300">{dict.heroKicker}</p>
|
||||
<h1 className="mt-5 max-w-4xl text-5xl font-black leading-none tracking-[-0.04em] text-stone-950 dark:text-stone-50 sm:text-6xl lg:text-7xl">
|
||||
{dict.heroTitle}
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-base leading-8 text-stone-600 dark:text-stone-300 sm:text-lg">
|
||||
{dict.heroBody}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<a
|
||||
href={starterSignupHref}
|
||||
className="rounded-full bg-stone-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-800 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||
>
|
||||
{dict.startTrial}
|
||||
</a>
|
||||
<a
|
||||
href="/explore"
|
||||
className="rounded-full border border-stone-300 bg-white/85 px-6 py-3 text-sm font-semibold text-stone-700 transition hover:border-stone-950 hover:text-stone-950 dark:border-stone-700 dark:bg-stone-950/30 dark:text-stone-200 dark:hover:border-stone-200 dark:hover:text-white"
|
||||
>
|
||||
{dict.exploreVehicles}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-3 sm:grid-cols-3">
|
||||
{dict.metrics.map(({ value, label }) => (
|
||||
<div key={value} className="rounded-[1.5rem] border border-stone-200/80 bg-stone-50/90 p-4 dark:border-stone-800 dark:bg-stone-950/50">
|
||||
<p className="text-xs font-bold tracking-[0.28em] text-stone-400 dark:text-stone-500">{value}</p>
|
||||
<p className="mt-3 text-sm font-semibold leading-6 text-stone-900 dark:text-stone-100">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasSection('surface') ? (
|
||||
<div className="relative overflow-hidden rounded-[2rem] border border-stone-200/80 bg-stone-950 p-6 text-white shadow-[0_30px_80px_rgba(28,25,23,0.18)] dark:border-stone-700">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(245,158,11,0.34),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(20,184,166,0.25),transparent_32%)]" />
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="rounded-full border border-white/15 bg-white/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200">
|
||||
{dict.surfaceLabel}
|
||||
</span>
|
||||
<span className="rounded-full bg-emerald-400/15 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-200">
|
||||
{dict.liveLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 className="mt-6 text-3xl font-black leading-tight tracking-[-0.03em]">{dict.surfaceTitle}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-stone-300">{dict.surfaceBody}</p>
|
||||
|
||||
<div className="mt-8 space-y-3">
|
||||
{[
|
||||
['01', dict.trustedFleets],
|
||||
['02', dict.brandedFlows],
|
||||
['03', dict.multiTenant],
|
||||
].map(([step, label]) => (
|
||||
<div key={step} className="flex items-center justify-between rounded-[1.25rem] border border-white/10 bg-white/5 px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-white text-xs font-bold text-stone-950">
|
||||
{step}
|
||||
</span>
|
||||
<p className="text-sm font-semibold text-white">{label}</p>
|
||||
</div>
|
||||
<span className="text-lg text-amber-300">+</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasSection('audiences') ? <div className="mt-8 grid gap-4 lg:grid-cols-[1.15fr_0.85fr]">
|
||||
<article className="rounded-[2rem] border border-stone-200/80 bg-white/80 p-7 backdrop-blur dark:border-stone-800 dark:bg-stone-900/60">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500 dark:text-stone-400">{dict.companyKicker}</p>
|
||||
<h2 className="mt-4 text-2xl font-black tracking-[-0.03em] text-stone-950 dark:text-stone-50 sm:text-3xl">{dict.companyTitle}</h2>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-stone-600 dark:text-stone-300">{dict.companyBody}</p>
|
||||
</article>
|
||||
<article className="rounded-[2rem] border border-stone-200/80 bg-[#f7efe2] p-7 dark:border-stone-800 dark:bg-[#1d1712]">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-300">{dict.renterKicker}</p>
|
||||
<h2 className="mt-4 text-2xl font-black tracking-[-0.03em] text-stone-950 dark:text-stone-50 sm:text-3xl">{dict.renterTitle}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-stone-700 dark:text-stone-300">{dict.renterBody}</p>
|
||||
</article>
|
||||
</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{hasSection('pillars') ? <section className="shell pb-10">
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
{dict.pillars.map(({ title, body }, index) => (
|
||||
<article
|
||||
key={title}
|
||||
className={`rounded-[2rem] border p-7 shadow-sm ${
|
||||
index === 1
|
||||
? 'border-stone-900 bg-stone-950 text-white dark:border-amber-300 dark:bg-amber-300 dark:text-stone-950'
|
||||
: 'border-stone-200 bg-white dark:border-stone-800 dark:bg-stone-900'
|
||||
}`}
|
||||
>
|
||||
<p
|
||||
className={`text-xs font-bold uppercase tracking-[0.26em] ${
|
||||
index === 1 ? 'text-stone-300 dark:text-stone-700' : 'text-stone-400 dark:text-stone-500'
|
||||
}`}
|
||||
>
|
||||
0{index + 1}
|
||||
</p>
|
||||
<h3 className="mt-4 text-2xl font-black tracking-[-0.03em]">{title}</h3>
|
||||
<p
|
||||
className={`mt-4 text-sm leading-7 ${
|
||||
index === 1 ? 'text-stone-200 dark:text-stone-800' : 'text-stone-600 dark:text-stone-300'
|
||||
}`}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section> : null}
|
||||
|
||||
{(hasSection('features') || hasSection('steps')) ? <section className="shell py-10">
|
||||
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
|
||||
{hasSection('features') ? (
|
||||
<div className="rounded-[2rem] border border-stone-200 bg-[linear-gradient(160deg,#fffdf8_0%,#f1ebe1_100%)] p-8 dark:border-stone-800 dark:bg-[linear-gradient(160deg,#17120d_0%,#221a14_100%)]">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500 dark:text-stone-400">{dict.featureLabel}</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
{dict.features.map((item, index) => (
|
||||
<div key={item} className="flex items-start gap-4 rounded-[1.25rem] border border-stone-200/80 bg-white/85 px-4 py-4 dark:border-stone-700 dark:bg-stone-950/40">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-stone-950 text-xs font-bold text-white dark:bg-amber-300 dark:text-stone-950">
|
||||
{index + 1}
|
||||
</span>
|
||||
<p className="text-sm font-semibold leading-6 text-stone-800 dark:text-stone-100">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : <div />}
|
||||
|
||||
{hasSection('steps') ? (
|
||||
<div className="rounded-[2rem] border border-stone-200 bg-white p-8 shadow-sm dark:border-stone-800 dark:bg-stone-900">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-300">{dict.readyKicker}</p>
|
||||
<h2 className="mt-4 text-3xl font-black tracking-[-0.03em] text-stone-950 dark:text-stone-50">{dict.stepsTitle}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
{dict.steps.map(({ step, title, body }) => (
|
||||
<article key={step} className="rounded-[1.5rem] border border-stone-200 bg-stone-50 p-5 dark:border-stone-800 dark:bg-stone-950">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500 dark:text-stone-400">
|
||||
{dict.stepLabel} {step}
|
||||
</p>
|
||||
<h3 className="mt-3 text-lg font-black text-stone-950 dark:text-stone-50">{title}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-stone-600 dark:text-stone-300">{body}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : <div />}
|
||||
</div>
|
||||
</section> : null}
|
||||
|
||||
{hasSection('closing') ? <section className="shell pb-16 pt-6 sm:pb-20">
|
||||
<div className="overflow-hidden rounded-[2rem] border border-stone-200 bg-stone-950 px-8 py-10 text-white dark:border-stone-700">
|
||||
<div className="grid gap-8 lg:grid-cols-[1fr_auto] lg:items-end">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.28em] text-amber-300">{dict.readyKicker}</p>
|
||||
<h2 className="mt-4 max-w-3xl text-4xl font-black leading-tight tracking-[-0.04em] sm:text-5xl">{dict.readyTitle}</h2>
|
||||
<p className="mt-5 max-w-2xl text-sm leading-8 text-stone-300 sm:text-base">{dict.readyBody}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 lg:justify-end">
|
||||
<a
|
||||
href="/pricing"
|
||||
className="rounded-full border border-white/20 px-6 py-3 text-sm font-semibold text-white transition hover:bg-white hover:text-stone-950"
|
||||
>
|
||||
{dict.viewPricing}
|
||||
</a>
|
||||
<a
|
||||
href={starterSignupHref}
|
||||
className="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-stone-950 transition hover:bg-amber-200"
|
||||
>
|
||||
{dict.createWorkspace}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section> : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user