add homepage management

This commit is contained in:
root
2026-05-07 00:34:29 -04:00
parent 750ae56a29
commit c4a45c8b21
33 changed files with 4228 additions and 418 deletions
@@ -0,0 +1,801 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import {
cloneMarketplaceHomepageContent,
resolveMarketplaceHomepageSections,
type MarketplaceHomepageConfig,
type MarketplaceHomepageContent,
type MarketplaceHomepageSectionType,
type MarketplaceLanguage,
} from '@rentaldrivego/types'
import { useAdminI18n } from '@/components/I18nProvider'
const API_BASE = '/api/v1'
interface Company {
id: string
name: string
slug: string
status: string
email: string
brand: {
displayName: string | null
} | null
}
const STATUS_COLORS: Record<string, string> = {
ACTIVE: 'text-emerald-400 bg-emerald-900/30',
TRIALING: 'text-sky-400 bg-sky-900/30',
SUSPENDED: 'text-red-400 bg-red-900/30',
PENDING: 'text-amber-400 bg-amber-900/30',
CANCELLED: 'text-zinc-400 bg-zinc-800',
}
const INPUT_CLASS = 'w-full rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500'
const LABEL_CLASS = 'mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500'
function cloneHomepageContent(content: MarketplaceHomepageConfig) {
const cloned = JSON.parse(JSON.stringify(content)) as MarketplaceHomepageConfig
;(['en', 'fr', 'ar'] as MarketplaceLanguage[]).forEach((language) => {
cloned[language].sections = resolveMarketplaceHomepageSections(cloned[language].sections)
})
return cloned
}
const HOMEPAGE_SECTIONS: MarketplaceHomepageSectionType[] = [
'hero',
'surface',
'pillars',
'audiences',
'features',
'steps',
'closing',
]
export default function AdminSiteConfigPage() {
const { language, dict } = useAdminI18n()
const copy = {
en: {
title: 'Site configuration',
description: 'Edit the main marketplace homepage here, or jump into a company to manage its branded public homepage and menu.',
homepageTitle: 'Main website homepage',
homepageDescription: 'This controls the marketplace homepage shown on the main RentalDriveGo website.',
saveHomepage: 'Save homepage',
savingHomepage: 'Saving…',
homepageSaved: 'Marketplace homepage saved.',
search: 'Search by company or slug…',
loading: 'Loading…',
empty: 'No companies found',
company: 'Company',
slug: 'Slug',
status: 'Status',
actions: 'Actions',
configure: 'Configure site',
manageCompany: 'Open company',
companiesTitle: 'Company branded sites',
languageLabel: 'Language',
hero: 'Hero',
surface: 'Surface',
companySection: 'Operator card',
renterSection: 'Renter card',
valueSection: 'Value blocks',
featureSection: 'Feature list',
stepsSection: 'Launch steps',
closingSection: 'Closing call to action',
previewTitle: 'Live preview',
previewDescription: 'Draft changes appear here before you save them.',
homepageSections: 'Homepage sections',
homepageSectionsDescription: 'Add or remove blocks from the main marketplace homepage.',
addSection: 'Add',
removeSection: 'Remove',
expand: 'Expand',
collapse: 'Collapse',
},
fr: {
title: 'Configuration du site',
description: 'Modifiez ici la homepage principale de la marketplace, ou ouvrez une entreprise pour gérer sa homepage publique et son menu.',
homepageTitle: 'Homepage du site principal',
homepageDescription: 'Cette section contrôle la homepage marketplace affichée sur le site principal RentalDriveGo.',
saveHomepage: 'Enregistrer la homepage',
savingHomepage: 'Enregistrement…',
homepageSaved: 'Homepage marketplace enregistrée.',
search: 'Rechercher par entreprise ou slug…',
loading: 'Chargement…',
empty: 'Aucune entreprise trouvée',
company: 'Entreprise',
slug: 'Slug',
status: 'Statut',
actions: 'Actions',
configure: 'Configurer le site',
manageCompany: 'Ouvrir lentreprise',
companiesTitle: 'Sites de marque des entreprises',
languageLabel: 'Langue',
hero: 'Hero',
surface: 'Bloc marketplace',
companySection: 'Bloc opérateur',
renterSection: 'Bloc client',
valueSection: 'Blocs de valeur',
featureSection: 'Liste des fonctionnalités',
stepsSection: 'Étapes de lancement',
closingSection: 'Appel à laction final',
previewTitle: 'Aperçu en direct',
previewDescription: 'Les brouillons apparaissent ici avant lenregistrement.',
homepageSections: 'Sections de la homepage',
homepageSectionsDescription: 'Ajoutez ou retirez des blocs de la homepage marketplace principale.',
addSection: 'Ajouter',
removeSection: 'Retirer',
expand: 'Ouvrir',
collapse: 'Réduire',
},
ar: {
title: 'إعدادات الموقع',
description: 'عدّل الصفحة الرئيسية للموقع الرئيسي هنا، أو افتح شركة لإدارة صفحتها العامة وقائمة التنقل الخاصة بها.',
homepageTitle: 'الصفحة الرئيسية للموقع الرئيسي',
homepageDescription: 'هذا القسم يتحكم في الصفحة الرئيسية للمنصة على موقع RentalDriveGo الرئيسي.',
saveHomepage: 'حفظ الصفحة الرئيسية',
savingHomepage: 'جارٍ الحفظ…',
homepageSaved: 'تم حفظ الصفحة الرئيسية للمنصة.',
search: 'ابحث باسم الشركة أو slug…',
loading: 'جارٍ التحميل…',
empty: 'لم يتم العثور على شركات',
company: 'الشركة',
slug: 'Slug',
status: 'الحالة',
actions: 'الإجراءات',
configure: 'إعداد الموقع',
manageCompany: 'فتح الشركة',
companiesTitle: 'المواقع العامة للشركات',
languageLabel: 'اللغة',
hero: 'البطاقة الرئيسية',
surface: 'قسم المنصة',
companySection: 'بطاقة المشغل',
renterSection: 'بطاقة المستأجر',
valueSection: 'عناصر القيمة',
featureSection: 'قائمة الميزات',
stepsSection: 'خطوات الإطلاق',
closingSection: 'الدعوة الختامية',
previewTitle: 'معاينة مباشرة',
previewDescription: 'تظهر التغييرات هنا قبل الحفظ.',
homepageSections: 'أقسام الصفحة الرئيسية',
homepageSectionsDescription: 'أضف أو احذف كتل الصفحة الرئيسية للمنصة.',
addSection: 'إضافة',
removeSection: 'إزالة',
expand: 'توسيع',
collapse: 'طي',
},
}[language]
const [companies, setCompanies] = useState<Company[]>([])
const [filtered, setFiltered] = useState<Company[]>([])
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [homepage, setHomepage] = useState<MarketplaceHomepageConfig>(cloneMarketplaceHomepageContent())
const [homepageLanguage, setHomepageLanguage] = useState<MarketplaceLanguage>(language)
const [homepageSaving, setHomepageSaving] = useState(false)
const [homepageMessage, setHomepageMessage] = useState<string | null>(null)
const [homepageExpanded, setHomepageExpanded] = useState(false)
useEffect(() => {
setHomepageLanguage(language)
}, [language])
useEffect(() => {
async function fetchData() {
const token = localStorage.getItem('admin_token')
try {
const [companiesRes, homepageRes] = await Promise.all([
fetch(`${API_BASE}/admin/companies`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
cache: 'no-store',
}),
fetch(`${API_BASE}/admin/site-config/marketplace-homepage`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
cache: 'no-store',
}),
])
const companiesJson = await companiesRes.json()
if (!companiesRes.ok) throw new Error(companiesJson?.message ?? 'Failed to fetch companies')
setCompanies(companiesJson.data ?? [])
setFiltered(companiesJson.data ?? [])
const homepageJson = await homepageRes.json()
if (homepageRes.ok && homepageJson?.data) {
setHomepage(cloneHomepageContent(homepageJson.data as MarketplaceHomepageConfig))
}
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
useEffect(() => {
const q = search.toLowerCase()
setFiltered(
companies.filter((company) =>
company.name.toLowerCase().includes(q)
|| company.slug.toLowerCase().includes(q)
|| company.email.toLowerCase().includes(q),
),
)
}, [search, companies])
function updateHomepageContent(patch: Partial<MarketplaceHomepageContent>) {
setHomepage((current) => ({
...current,
[homepageLanguage]: {
...current[homepageLanguage],
...patch,
},
}))
}
function updateMetric(index: number, label: string) {
const metrics = homepage[homepageLanguage].metrics.map((metric, metricIndex) => (
metricIndex === index ? { ...metric, label } : metric
))
updateHomepageContent({ metrics })
}
function updatePillar(index: number, patch: { title?: string; body?: string }) {
const pillars = homepage[homepageLanguage].pillars.map((pillar, pillarIndex) => (
pillarIndex === index ? { ...pillar, ...patch } : pillar
))
updateHomepageContent({ pillars })
}
function updateStep(index: number, patch: { title?: string; body?: string }) {
const steps = homepage[homepageLanguage].steps.map((step, stepIndex) => (
stepIndex === index ? { ...step, ...patch } : step
))
updateHomepageContent({ steps })
}
function getActiveSections() {
return resolveMarketplaceHomepageSections(activeContent.sections)
}
function addHomepageSection(section: MarketplaceHomepageSectionType) {
const sections = getActiveSections()
if (sections.includes(section)) return
updateHomepageContent({ sections: [...sections, section] })
}
function removeHomepageSection(section: MarketplaceHomepageSectionType) {
const sections = getActiveSections().filter((item) => item !== section)
if (sections.length === 0) return
updateHomepageContent({ sections })
}
async function saveHomepage() {
setHomepageSaving(true)
setHomepageMessage(null)
const token = localStorage.getItem('admin_token')
try {
const res = await fetch(`${API_BASE}/admin/site-config/marketplace-homepage`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ homepage }),
})
const json = await res.json()
if (!res.ok) throw new Error(json?.message ?? 'Failed to save homepage')
setHomepage(cloneHomepageContent(json.data as MarketplaceHomepageConfig))
setHomepageMessage(copy.homepageSaved)
} catch (err: any) {
setError(err.message)
} finally {
setHomepageSaving(false)
}
}
const activeContent = homepage[homepageLanguage]
const activeSections = getActiveSections()
const availableSections = HOMEPAGE_SECTIONS.filter((section) => !activeSections.includes(section))
const sectionLabels: Record<MarketplaceHomepageSectionType, string> = {
hero: copy.hero,
surface: copy.surface,
pillars: copy.valueSection,
audiences: `${copy.companySection} / ${copy.renterSection}`,
features: copy.featureSection,
steps: copy.stepsSection,
closing: copy.closingSection,
}
return (
<div className="shell space-y-6 py-8">
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-emerald-400">{dict.platform}</p>
<h1 className="mt-1 text-3xl font-black">{copy.title}</h1>
<p className="mt-2 max-w-3xl text-sm text-zinc-500">{copy.description}</p>
</div>
<input
className="w-full rounded-xl border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500 xl:w-72"
placeholder={copy.search}
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</div>
{error ? <div className="panel p-4 text-sm text-red-400">{error}</div> : null}
<section className="panel p-6">
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div>
<h2 className="text-base font-semibold text-zinc-100">{copy.homepageTitle}</h2>
<p className="mt-1 text-sm text-zinc-500">{copy.homepageDescription}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500">{copy.languageLabel}</span>
{(['en', 'fr', 'ar'] as MarketplaceLanguage[]).map((value) => (
<button
key={value}
type="button"
onClick={() => setHomepageLanguage(value)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
homepageLanguage === value ? 'bg-emerald-500 text-white' : 'bg-zinc-800 text-zinc-300 hover:bg-zinc-700'
}`}
>
{value.toUpperCase()}
</button>
))}
</div>
<button
type="button"
onClick={() => setHomepageExpanded((current) => !current)}
className="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs font-semibold text-zinc-200 transition hover:border-zinc-600 hover:bg-zinc-800"
>
{homepageExpanded ? copy.collapse : copy.expand}
</button>
</div>
</div>
{homepageExpanded ? (
<>
{homepageMessage ? <div className="mt-4 rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-300">{homepageMessage}</div> : null}
<div className="mt-6 rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
<h3 className="text-sm font-semibold text-zinc-100">{copy.homepageSections}</h3>
<p className="mt-1 text-xs text-zinc-500">{copy.homepageSectionsDescription}</p>
<div className="mt-4 flex flex-wrap gap-2">
{activeSections.map((section) => (
<div key={section} className="inline-flex items-center gap-2 rounded-full border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs font-semibold text-zinc-200">
<span>{sectionLabels[section]}</span>
<button
type="button"
onClick={() => removeHomepageSection(section)}
className="rounded-full border border-rose-500/30 bg-rose-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-rose-300"
>
{copy.removeSection}
</button>
</div>
))}
</div>
{availableSections.length ? (
<div className="mt-4 flex flex-wrap gap-2">
{availableSections.map((section) => (
<button
key={section}
type="button"
onClick={() => addHomepageSection(section)}
className="rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1.5 text-xs font-semibold text-emerald-300 transition hover:bg-emerald-500/20"
>
{copy.addSection} {sectionLabels[section]}
</button>
))}
</div>
) : null}
</div>
<div className="mt-6 rounded-[1.75rem] border border-zinc-800 bg-zinc-950/60 p-4">
<div className="flex items-center justify-between gap-4 border-b border-zinc-800 pb-4">
<div>
<h3 className="text-sm font-semibold text-zinc-100">{copy.previewTitle}</h3>
<p className="mt-1 text-xs text-zinc-500">{copy.previewDescription}</p>
</div>
<span className="rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-300">
{homepageLanguage.toUpperCase()}
</span>
</div>
<div className="mt-4 overflow-hidden rounded-[1.5rem] border border-stone-200 bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] shadow-2xl shadow-black/20">
<div className="border-b border-stone-200 bg-white/90 px-5 py-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-bold uppercase tracking-[0.32em] text-amber-700">{activeContent.heroKicker}</p>
<p className="mt-2 text-sm text-stone-500">rentaldrivego.com</p>
</div>
<div className="flex flex-wrap gap-2 text-xs font-semibold text-stone-600">
{activeContent.metrics.map((metric) => (
<span key={metric.value} className="rounded-full border border-stone-300 bg-white px-3 py-1.5">
{metric.label}
</span>
))}
</div>
</div>
</div>
<div className="space-y-10 px-5 py-6 lg:px-8">
{activeSections.includes('hero') ? <section className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<div>
<h4 className="text-4xl font-black tracking-[-0.04em] text-stone-950">{activeContent.heroTitle}</h4>
<p className="mt-5 max-w-2xl text-base leading-8 text-stone-600">{activeContent.heroBody}</p>
<div className="mt-8 flex flex-wrap gap-3">
<span className="rounded-full bg-stone-950 px-5 py-3 text-sm font-semibold text-white">{activeContent.startTrial}</span>
<span className="rounded-full border border-stone-300 bg-white px-5 py-3 text-sm font-semibold text-stone-700">{activeContent.exploreVehicles}</span>
</div>
</div>
{activeSections.includes('surface') ? (
<div className="rounded-[1.5rem] bg-stone-950 p-6 text-white">
<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">
{activeContent.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">
{activeContent.liveLabel}
</span>
</div>
<h5 className="mt-5 text-2xl font-black tracking-[-0.03em]">{activeContent.surfaceTitle}</h5>
<p className="mt-4 text-sm leading-7 text-stone-300">{activeContent.surfaceBody}</p>
<div className="mt-6 space-y-3">
{[activeContent.trustedFleets, activeContent.brandedFlows, activeContent.multiTenant].map((item, index) => (
<div key={`${item}-${index}`} className="rounded-[1.25rem] border border-white/10 bg-white/5 px-4 py-3 text-sm font-semibold">
{item}
</div>
))}
</div>
</div>
) : null}
</section> : null}
{activeSections.includes('surface') && !activeSections.includes('hero') ? (
<section className="rounded-[1.5rem] bg-stone-950 p-6 text-white">
<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">
{activeContent.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">
{activeContent.liveLabel}
</span>
</div>
<h5 className="mt-5 text-2xl font-black tracking-[-0.03em]">{activeContent.surfaceTitle}</h5>
<p className="mt-4 text-sm leading-7 text-stone-300">{activeContent.surfaceBody}</p>
</section>
) : null}
{activeSections.includes('pillars') ? <section className="grid gap-4 lg:grid-cols-3">
{activeContent.pillars.map((pillar, index) => (
<article
key={`${pillar.title}-${index}`}
className={`rounded-[1.5rem] border p-5 ${index === 1 ? 'border-stone-900 bg-stone-950 text-white' : 'border-stone-200 bg-white text-stone-900'}`}
>
<p className={`text-xs font-bold uppercase tracking-[0.24em] ${index === 1 ? 'text-stone-300' : 'text-stone-400'}`}>0{index + 1}</p>
<h5 className="mt-3 text-xl font-black">{pillar.title}</h5>
<p className={`mt-3 text-sm leading-7 ${index === 1 ? 'text-stone-200' : 'text-stone-600'}`}>{pillar.body}</p>
</article>
))}
</section> : null}
{activeSections.includes('audiences') ? <section className="grid gap-4 lg:grid-cols-2">
<article className="rounded-[1.5rem] border border-stone-200 bg-white p-5">
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500">{activeContent.companyKicker}</p>
<h5 className="mt-3 text-2xl font-black text-stone-950">{activeContent.companyTitle}</h5>
<p className="mt-3 text-sm leading-7 text-stone-600">{activeContent.companyBody}</p>
</article>
<article className="rounded-[1.5rem] border border-stone-200 bg-[#f7efe2] p-5">
<p className="text-xs font-bold uppercase tracking-[0.24em] text-amber-700">{activeContent.renterKicker}</p>
<h5 className="mt-3 text-2xl font-black text-stone-950">{activeContent.renterTitle}</h5>
<p className="mt-3 text-sm leading-7 text-stone-700">{activeContent.renterBody}</p>
</article>
</section> : null}
{(activeSections.includes('features') || activeSections.includes('steps')) ? <section className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
{activeSections.includes('features') ? (
<article className="rounded-[1.5rem] border border-stone-200 bg-[#fffdf8] p-5">
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500">{activeContent.featureLabel}</p>
<div className="mt-5 space-y-3">
{activeContent.features.map((feature, index) => (
<div key={`${feature}-${index}`} className="flex items-start gap-3 rounded-[1rem] border border-stone-200 bg-white px-4 py-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-stone-950 text-xs font-bold text-white">
{index + 1}
</span>
<p className="text-sm font-semibold leading-6 text-stone-800">{feature}</p>
</div>
))}
</div>
</article>
) : <div />}
{activeSections.includes('steps') ? (
<article className="rounded-[1.5rem] border border-stone-200 bg-white p-5">
<p className="text-xs font-bold uppercase tracking-[0.24em] text-amber-700">{activeContent.readyKicker}</p>
<h5 className="mt-3 text-2xl font-black text-stone-950">{activeContent.stepsTitle}</h5>
<div className="mt-5 space-y-3">
{activeContent.steps.map((step) => (
<div key={step.step} className="rounded-[1rem] border border-stone-200 bg-stone-50 p-4">
<p className="text-xs font-bold uppercase tracking-[0.24em] text-stone-500">{activeContent.stepLabel} {step.step}</p>
<h6 className="mt-2 text-lg font-black text-stone-950">{step.title}</h6>
<p className="mt-2 text-sm leading-7 text-stone-600">{step.body}</p>
</div>
))}
</div>
</article>
) : <div />}
</section> : null}
{activeSections.includes('closing') ? <section className="rounded-[1.5rem] bg-stone-950 px-6 py-7 text-white">
<p className="text-xs font-bold uppercase tracking-[0.28em] text-amber-300">{activeContent.readyKicker}</p>
<h5 className="mt-4 max-w-3xl text-3xl font-black tracking-[-0.04em]">{activeContent.readyTitle}</h5>
<p className="mt-4 max-w-2xl text-sm leading-8 text-stone-300">{activeContent.readyBody}</p>
<div className="mt-6 flex flex-wrap gap-3">
<span className="rounded-full border border-white/20 px-5 py-3 text-sm font-semibold">{activeContent.viewPricing}</span>
<span className="rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-stone-950">{activeContent.createWorkspace}</span>
</div>
</section> : null}
</div>
</div>
</div>
<div className="mt-6 space-y-8">
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Brand kicker</span>
<input className={INPUT_CLASS} value={activeContent.heroKicker} onChange={(event) => updateHomepageContent({ heroKicker: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Primary CTA</span>
<input className={INPUT_CLASS} value={activeContent.startTrial} onChange={(event) => updateHomepageContent({ startTrial: event.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>{copy.hero} title</span>
<input className={INPUT_CLASS} value={activeContent.heroTitle} onChange={(event) => updateHomepageContent({ heroTitle: event.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>{copy.hero} body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={activeContent.heroBody} onChange={(event) => updateHomepageContent({ heroBody: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Secondary CTA</span>
<input className={INPUT_CLASS} value={activeContent.exploreVehicles} onChange={(event) => updateHomepageContent({ exploreVehicles: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.surface} badge</span>
<input className={INPUT_CLASS} value={activeContent.surfaceLabel} onChange={(event) => updateHomepageContent({ surfaceLabel: event.target.value })} />
</label>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="md:col-span-2">
<span className={LABEL_CLASS}>{copy.surface} title</span>
<input className={INPUT_CLASS} value={activeContent.surfaceTitle} onChange={(event) => updateHomepageContent({ surfaceTitle: event.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>{copy.surface} body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={activeContent.surfaceBody} onChange={(event) => updateHomepageContent({ surfaceBody: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Live label</span>
<input className={INPUT_CLASS} value={activeContent.liveLabel} onChange={(event) => updateHomepageContent({ liveLabel: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Trusted fleets</span>
<input className={INPUT_CLASS} value={activeContent.trustedFleets} onChange={(event) => updateHomepageContent({ trustedFleets: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Branded flows</span>
<input className={INPUT_CLASS} value={activeContent.brandedFlows} onChange={(event) => updateHomepageContent({ brandedFlows: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Multi-tenant</span>
<input className={INPUT_CLASS} value={activeContent.multiTenant} onChange={(event) => updateHomepageContent({ multiTenant: event.target.value })} />
</label>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>{copy.companySection} kicker</span>
<input className={INPUT_CLASS} value={activeContent.companyKicker} onChange={(event) => updateHomepageContent({ companyKicker: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.renterSection} kicker</span>
<input className={INPUT_CLASS} value={activeContent.renterKicker} onChange={(event) => updateHomepageContent({ renterKicker: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.companySection} title</span>
<input className={INPUT_CLASS} value={activeContent.companyTitle} onChange={(event) => updateHomepageContent({ companyTitle: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.renterSection} title</span>
<input className={INPUT_CLASS} value={activeContent.renterTitle} onChange={(event) => updateHomepageContent({ renterTitle: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.companySection} body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={activeContent.companyBody} onChange={(event) => updateHomepageContent({ companyBody: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>{copy.renterSection} body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={activeContent.renterBody} onChange={(event) => updateHomepageContent({ renterBody: event.target.value })} />
</label>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<div className="space-y-4">
<h3 className="text-sm font-semibold text-zinc-200">{copy.valueSection}</h3>
<div className="grid gap-4">
{activeContent.metrics.map((metric, index) => (
<label key={metric.value}>
<span className={LABEL_CLASS}>Metric {metric.value}</span>
<input className={INPUT_CLASS} value={metric.label} onChange={(event) => updateMetric(index, event.target.value)} />
</label>
))}
{activeContent.pillars.map((pillar, index) => (
<div key={`${pillar.title}-${index}`} className="grid gap-4 rounded-2xl border border-zinc-800 bg-zinc-950/50 p-4">
<label>
<span className={LABEL_CLASS}>Pillar {index + 1} title</span>
<input className={INPUT_CLASS} value={pillar.title} onChange={(event) => updatePillar(index, { title: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Pillar {index + 1} body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={pillar.body} onChange={(event) => updatePillar(index, { body: event.target.value })} />
</label>
</div>
))}
</div>
</div>
<div className="space-y-4">
<h3 className="text-sm font-semibold text-zinc-200">{copy.featureSection}</h3>
<label>
<span className={LABEL_CLASS}>Feature section label</span>
<input className={INPUT_CLASS} value={activeContent.featureLabel} onChange={(event) => updateHomepageContent({ featureLabel: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Features, one per line</span>
<textarea
className={`${INPUT_CLASS} min-h-40`}
value={activeContent.features.join('\n')}
onChange={(event) => updateHomepageContent({ features: event.target.value.split('\n').map((item) => item.trim()).filter(Boolean) })}
/>
</label>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<div className="space-y-4">
<h3 className="text-sm font-semibold text-zinc-200">{copy.stepsSection}</h3>
<label>
<span className={LABEL_CLASS}>Steps title</span>
<input className={INPUT_CLASS} value={activeContent.stepsTitle} onChange={(event) => updateHomepageContent({ stepsTitle: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Step label</span>
<input className={INPUT_CLASS} value={activeContent.stepLabel} onChange={(event) => updateHomepageContent({ stepLabel: event.target.value })} />
</label>
{activeContent.steps.map((step, index) => (
<div key={step.step} className="grid gap-4 rounded-2xl border border-zinc-800 bg-zinc-950/50 p-4">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500">Step {step.step}</p>
<label>
<span className={LABEL_CLASS}>Title</span>
<input className={INPUT_CLASS} value={step.title} onChange={(event) => updateStep(index, { title: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={step.body} onChange={(event) => updateStep(index, { body: event.target.value })} />
</label>
</div>
))}
</div>
<div className="space-y-4">
<h3 className="text-sm font-semibold text-zinc-200">{copy.closingSection}</h3>
<label>
<span className={LABEL_CLASS}>Closing kicker</span>
<input className={INPUT_CLASS} value={activeContent.readyKicker} onChange={(event) => updateHomepageContent({ readyKicker: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Closing title</span>
<input className={INPUT_CLASS} value={activeContent.readyTitle} onChange={(event) => updateHomepageContent({ readyTitle: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Closing body</span>
<textarea className={`${INPUT_CLASS} min-h-24`} value={activeContent.readyBody} onChange={(event) => updateHomepageContent({ readyBody: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Pricing CTA</span>
<input className={INPUT_CLASS} value={activeContent.viewPricing} onChange={(event) => updateHomepageContent({ viewPricing: event.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Workspace CTA</span>
<input className={INPUT_CLASS} value={activeContent.createWorkspace} onChange={(event) => updateHomepageContent({ createWorkspace: event.target.value })} />
</label>
</div>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
type="button"
onClick={saveHomepage}
disabled={homepageSaving}
className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-60"
>
{homepageSaving ? copy.savingHomepage : copy.saveHomepage}
</button>
</div>
</>
) : null}
</section>
<section className="panel overflow-hidden">
<div className="border-b border-zinc-800 px-6 py-4">
<h2 className="text-base font-semibold text-zinc-100">{copy.companiesTitle}</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800">
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">{copy.company}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">{copy.slug}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">{copy.status}</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-zinc-500">{copy.actions}</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/60">
{loading ? (
<tr>
<td colSpan={4} className="px-6 py-12 text-center text-zinc-500">{copy.loading}</td>
</tr>
) : filtered.length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-12 text-center text-zinc-500">{copy.empty}</td>
</tr>
) : filtered.map((company) => (
<tr key={company.id} className="transition-colors hover:bg-zinc-800/30">
<td className="px-6 py-4">
<p className="font-medium text-zinc-100">{company.brand?.displayName ?? company.name}</p>
<p className="text-xs text-zinc-500">{company.email}</p>
</td>
<td className="px-6 py-4 font-mono text-xs text-zinc-400">{company.slug}</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${STATUS_COLORS[company.status] ?? 'text-zinc-400 bg-zinc-800'}`}>
{company.status}
</span>
</td>
<td className="px-6 py-4">
<div className="flex justify-end gap-2">
<Link
href={`/dashboard/companies/${company.id}#site-config`}
className="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-emerald-500"
>
{copy.configure}
</Link>
<Link
href={`/dashboard/companies/${company.id}`}
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-xs font-medium text-zinc-200 transition-colors hover:bg-zinc-700"
>
{copy.manageCompany}
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
)
}