|
|
|
@@ -3,15 +3,6 @@
|
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
|
import { useParams, useRouter } from 'next/navigation'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import {
|
|
|
|
|
clonePublicHomepageLayout,
|
|
|
|
|
clonePublicSitePageSections,
|
|
|
|
|
resolvePublicHomepageLayout,
|
|
|
|
|
type PublicHomepageLayout,
|
|
|
|
|
type PublicHomepageSectionType,
|
|
|
|
|
type PublicSitePageSections,
|
|
|
|
|
} from '@rentaldrivego/types'
|
|
|
|
|
import { HomepageLayoutEditor } from './HomepageLayoutEditor'
|
|
|
|
|
|
|
|
|
|
const API_BASE = '/api/v1'
|
|
|
|
|
|
|
|
|
@@ -51,42 +42,6 @@ interface CompanyDetail {
|
|
|
|
|
defaultLocale: string
|
|
|
|
|
defaultCurrency: string
|
|
|
|
|
isListedOnMarketplace: boolean
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
heroTitle: string | null
|
|
|
|
|
heroDescription: string | null
|
|
|
|
|
viewOffersLabel: string | null
|
|
|
|
|
viewPricingLabel: string | null
|
|
|
|
|
contactCompanyLabel: string | null
|
|
|
|
|
activeOffersTitle: string | null
|
|
|
|
|
seeAllOffersLabel: string | null
|
|
|
|
|
noActiveOffersLabel: string | null
|
|
|
|
|
publishedVehiclesTitle: string | null
|
|
|
|
|
viewVehicleLabel: string | null
|
|
|
|
|
pricingEyebrow: string | null
|
|
|
|
|
pricingTitle: string | null
|
|
|
|
|
pricingDescription: string | null
|
|
|
|
|
showOffers: boolean
|
|
|
|
|
showVehicles: boolean
|
|
|
|
|
showPricing: boolean
|
|
|
|
|
layout?: PublicHomepageLayout | null
|
|
|
|
|
} | null
|
|
|
|
|
menuConfig: {
|
|
|
|
|
aboutLabel: string | null
|
|
|
|
|
vehiclesLabel: string | null
|
|
|
|
|
offersLabel: string | null
|
|
|
|
|
pricingLabel: string | null
|
|
|
|
|
blogLabel: string | null
|
|
|
|
|
contactLabel: string | null
|
|
|
|
|
bookCarLabel: string | null
|
|
|
|
|
siteNavigationLabel: string | null
|
|
|
|
|
showAbout: boolean
|
|
|
|
|
showVehicles: boolean
|
|
|
|
|
showOffers: boolean
|
|
|
|
|
showPricing: boolean
|
|
|
|
|
showBlog: boolean
|
|
|
|
|
showContact: boolean
|
|
|
|
|
pageSections?: PublicSitePageSections | null
|
|
|
|
|
} | null
|
|
|
|
|
} | null
|
|
|
|
|
contractSettings: {
|
|
|
|
|
legalName: string | null
|
|
|
|
@@ -159,42 +114,6 @@ interface FormState {
|
|
|
|
|
defaultLocale: string
|
|
|
|
|
defaultCurrency: string
|
|
|
|
|
isListedOnMarketplace: boolean
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
heroTitle: string
|
|
|
|
|
heroDescription: string
|
|
|
|
|
viewOffersLabel: string
|
|
|
|
|
viewPricingLabel: string
|
|
|
|
|
contactCompanyLabel: string
|
|
|
|
|
activeOffersTitle: string
|
|
|
|
|
seeAllOffersLabel: string
|
|
|
|
|
noActiveOffersLabel: string
|
|
|
|
|
publishedVehiclesTitle: string
|
|
|
|
|
viewVehicleLabel: string
|
|
|
|
|
pricingEyebrow: string
|
|
|
|
|
pricingTitle: string
|
|
|
|
|
pricingDescription: string
|
|
|
|
|
showOffers: boolean
|
|
|
|
|
showVehicles: boolean
|
|
|
|
|
showPricing: boolean
|
|
|
|
|
layout: PublicHomepageLayout
|
|
|
|
|
}
|
|
|
|
|
menuConfig: {
|
|
|
|
|
aboutLabel: string
|
|
|
|
|
vehiclesLabel: string
|
|
|
|
|
offersLabel: string
|
|
|
|
|
pricingLabel: string
|
|
|
|
|
blogLabel: string
|
|
|
|
|
contactLabel: string
|
|
|
|
|
bookCarLabel: string
|
|
|
|
|
siteNavigationLabel: string
|
|
|
|
|
showAbout: boolean
|
|
|
|
|
showVehicles: boolean
|
|
|
|
|
showOffers: boolean
|
|
|
|
|
showPricing: boolean
|
|
|
|
|
showBlog: boolean
|
|
|
|
|
showContact: boolean
|
|
|
|
|
pageSections: PublicSitePageSections
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
contractSettings: {
|
|
|
|
|
legalName: string
|
|
|
|
@@ -218,8 +137,6 @@ interface FormState {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CompanyHomePageConfig = NonNullable<CompanyDetail['brand']>['homePageConfig']
|
|
|
|
|
|
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
|
|
|
ACTIVE: 'text-emerald-400',
|
|
|
|
|
TRIALING: 'text-sky-400',
|
|
|
|
@@ -229,31 +146,6 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
|
|
PAST_DUE: 'text-orange-400',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PREVIEW_PRICING_PLANS = [
|
|
|
|
|
{ name: 'Starter', price: 'MAD 499', cadence: '/month' },
|
|
|
|
|
{ name: 'Growth', price: 'MAD 999', cadence: '/month', highlighted: true },
|
|
|
|
|
{ name: 'Pro', price: 'Custom', cadence: '' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const HOMEPAGE_SECTION_ORDER: PublicHomepageSectionType[] = ['hero', 'offers', 'vehicles', 'pricing']
|
|
|
|
|
|
|
|
|
|
const PAGE_SECTION_GROUPS: Array<{
|
|
|
|
|
page: keyof PublicSitePageSections
|
|
|
|
|
label: string
|
|
|
|
|
sections: Array<{ key: string; label: string }>
|
|
|
|
|
}> = [
|
|
|
|
|
{ page: 'home', label: 'Homepage', sections: [{ key: 'hero', label: 'Hero' }, { key: 'offers', label: 'Offers' }, { key: 'vehicles', label: 'Vehicles' }, { key: 'pricing', label: 'Pricing' }] },
|
|
|
|
|
{ page: 'about', label: 'About', sections: [{ key: 'hero', label: 'Hero' }, { key: 'highlights', label: 'Highlights' }, { key: 'details', label: 'Details' }] },
|
|
|
|
|
{ page: 'offers', label: 'Offers', sections: [{ key: 'header', label: 'Header' }, { key: 'grid', label: 'Offers grid' }] },
|
|
|
|
|
{ page: 'pricing', label: 'Pricing', sections: [{ key: 'hero', label: 'Hero' }, { key: 'plans', label: 'Plans' }, { key: 'payments', label: 'Payments' }, { key: 'faq', label: 'FAQ' }] },
|
|
|
|
|
{ page: 'vehicles', label: 'Vehicles', sections: [{ key: 'header', label: 'Header' }, { key: 'filters', label: 'Filters' }, { key: 'grid', label: 'Vehicle grid' }] },
|
|
|
|
|
{ page: 'vehicleDetail', label: 'Vehicle detail', sections: [{ key: 'gallery', label: 'Gallery' }, { key: 'summary', label: 'Summary card' }] },
|
|
|
|
|
{ page: 'blog', label: 'Blog', sections: [{ key: 'hero', label: 'Hero' }, { key: 'posts', label: 'Posts' }] },
|
|
|
|
|
{ page: 'contact', label: 'Contact', sections: [{ key: 'content', label: 'Content card' }] },
|
|
|
|
|
{ page: 'booking', label: 'Booking', sections: [{ key: 'content', label: 'Booking form' }] },
|
|
|
|
|
{ page: 'bookingConfirmation', label: 'Booking confirmation', sections: [{ key: 'hero', label: 'Confirmation header' }, { key: 'summary', label: 'Booking summary' }, { key: 'actions', label: 'Action buttons' }] },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const INPUT_CLASS = 'mt-1 w-full rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-emerald-500'
|
|
|
|
|
const LABEL_CLASS = 'text-xs font-medium uppercase tracking-wide text-zinc-500'
|
|
|
|
|
|
|
|
|
@@ -270,52 +162,6 @@ function toNullable(value: string) {
|
|
|
|
|
return trimmed ? trimmed : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolvePreviewText(value: string, fallback: string) {
|
|
|
|
|
const trimmed = value.trim()
|
|
|
|
|
return trimmed ? trimmed : fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveLegacyHomepageVisibility(
|
|
|
|
|
config: CompanyHomePageConfig | null | undefined,
|
|
|
|
|
type: Exclude<PublicHomepageSectionType, 'hero'>,
|
|
|
|
|
) {
|
|
|
|
|
if (type === 'offers') return config?.showOffers ?? true
|
|
|
|
|
if (type === 'vehicles') return config?.showVehicles ?? true
|
|
|
|
|
return config?.showPricing ?? true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildHomepageLayout(company: CompanyDetail): PublicHomepageLayout {
|
|
|
|
|
const pageSections = mergePageSectionsConfig(company.brand?.menuConfig?.pageSections)
|
|
|
|
|
const visibleTypes = HOMEPAGE_SECTION_ORDER.filter((type) => {
|
|
|
|
|
if (!pageSections.home[type]) return false
|
|
|
|
|
if (type === 'hero') return true
|
|
|
|
|
return resolveLegacyHomepageVisibility(company.brand?.homePageConfig ?? null, type)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return resolvePublicHomepageLayout(company.brand?.homePageConfig?.layout, visibleTypes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergePageSectionsConfig(config?: PublicSitePageSections | null): PublicSitePageSections {
|
|
|
|
|
const defaults = clonePublicSitePageSections()
|
|
|
|
|
return {
|
|
|
|
|
...defaults,
|
|
|
|
|
...(config ?? {}),
|
|
|
|
|
home: { ...defaults.home, ...(config?.home ?? {}) },
|
|
|
|
|
about: { ...defaults.about, ...(config?.about ?? {}) },
|
|
|
|
|
offers: { ...defaults.offers, ...(config?.offers ?? {}) },
|
|
|
|
|
pricing: { ...defaults.pricing, ...(config?.pricing ?? {}) },
|
|
|
|
|
vehicles: { ...defaults.vehicles, ...(config?.vehicles ?? {}) },
|
|
|
|
|
vehicleDetail: { ...defaults.vehicleDetail, ...(config?.vehicleDetail ?? {}) },
|
|
|
|
|
blog: { ...defaults.blog, ...(config?.blog ?? {}) },
|
|
|
|
|
contact: { ...defaults.contact, ...(config?.contact ?? {}) },
|
|
|
|
|
booking: { ...defaults.booking, ...(config?.booking ?? {}) },
|
|
|
|
|
bookingConfirmation: {
|
|
|
|
|
...defaults.bookingConfirmation,
|
|
|
|
|
...(config?.bookingConfirmation ?? {}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createFormState(company: CompanyDetail): FormState {
|
|
|
|
|
return {
|
|
|
|
|
company: {
|
|
|
|
@@ -352,42 +198,6 @@ function createFormState(company: CompanyDetail): FormState {
|
|
|
|
|
defaultLocale: company.brand?.defaultLocale ?? 'en',
|
|
|
|
|
defaultCurrency: company.brand?.defaultCurrency ?? 'MAD',
|
|
|
|
|
isListedOnMarketplace: company.brand?.isListedOnMarketplace ?? true,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
heroTitle: company.brand?.homePageConfig?.heroTitle ?? '',
|
|
|
|
|
heroDescription: company.brand?.homePageConfig?.heroDescription ?? '',
|
|
|
|
|
viewOffersLabel: company.brand?.homePageConfig?.viewOffersLabel ?? '',
|
|
|
|
|
viewPricingLabel: company.brand?.homePageConfig?.viewPricingLabel ?? '',
|
|
|
|
|
contactCompanyLabel: company.brand?.homePageConfig?.contactCompanyLabel ?? '',
|
|
|
|
|
activeOffersTitle: company.brand?.homePageConfig?.activeOffersTitle ?? '',
|
|
|
|
|
seeAllOffersLabel: company.brand?.homePageConfig?.seeAllOffersLabel ?? '',
|
|
|
|
|
noActiveOffersLabel: company.brand?.homePageConfig?.noActiveOffersLabel ?? '',
|
|
|
|
|
publishedVehiclesTitle: company.brand?.homePageConfig?.publishedVehiclesTitle ?? '',
|
|
|
|
|
viewVehicleLabel: company.brand?.homePageConfig?.viewVehicleLabel ?? '',
|
|
|
|
|
pricingEyebrow: company.brand?.homePageConfig?.pricingEyebrow ?? '',
|
|
|
|
|
pricingTitle: company.brand?.homePageConfig?.pricingTitle ?? '',
|
|
|
|
|
pricingDescription: company.brand?.homePageConfig?.pricingDescription ?? '',
|
|
|
|
|
showOffers: company.brand?.homePageConfig?.showOffers ?? true,
|
|
|
|
|
showVehicles: company.brand?.homePageConfig?.showVehicles ?? true,
|
|
|
|
|
showPricing: company.brand?.homePageConfig?.showPricing ?? true,
|
|
|
|
|
layout: buildHomepageLayout(company),
|
|
|
|
|
},
|
|
|
|
|
menuConfig: {
|
|
|
|
|
aboutLabel: company.brand?.menuConfig?.aboutLabel ?? '',
|
|
|
|
|
vehiclesLabel: company.brand?.menuConfig?.vehiclesLabel ?? '',
|
|
|
|
|
offersLabel: company.brand?.menuConfig?.offersLabel ?? '',
|
|
|
|
|
pricingLabel: company.brand?.menuConfig?.pricingLabel ?? '',
|
|
|
|
|
blogLabel: company.brand?.menuConfig?.blogLabel ?? '',
|
|
|
|
|
contactLabel: company.brand?.menuConfig?.contactLabel ?? '',
|
|
|
|
|
bookCarLabel: company.brand?.menuConfig?.bookCarLabel ?? '',
|
|
|
|
|
siteNavigationLabel: company.brand?.menuConfig?.siteNavigationLabel ?? '',
|
|
|
|
|
showAbout: company.brand?.menuConfig?.showAbout ?? true,
|
|
|
|
|
showVehicles: company.brand?.menuConfig?.showVehicles ?? true,
|
|
|
|
|
showOffers: company.brand?.menuConfig?.showOffers ?? true,
|
|
|
|
|
showPricing: company.brand?.menuConfig?.showPricing ?? true,
|
|
|
|
|
showBlog: company.brand?.menuConfig?.showBlog ?? true,
|
|
|
|
|
showContact: company.brand?.menuConfig?.showContact ?? true,
|
|
|
|
|
pageSections: mergePageSectionsConfig(company.brand?.menuConfig?.pageSections),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
contractSettings: {
|
|
|
|
|
legalName: company.contractSettings?.legalName ?? '',
|
|
|
|
@@ -452,154 +262,6 @@ export default function AdminCompanyDetailPage() {
|
|
|
|
|
setForm((current) => current ? { ...current, [section]: { ...current[section], ...patch } } : current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateBrandConfig<K extends keyof FormState['brand'] & ('homePageConfig' | 'menuConfig')>(
|
|
|
|
|
section: K,
|
|
|
|
|
patch: Partial<FormState['brand'][K]>,
|
|
|
|
|
) {
|
|
|
|
|
setForm((current) => current ? {
|
|
|
|
|
...current,
|
|
|
|
|
brand: {
|
|
|
|
|
...current.brand,
|
|
|
|
|
[section]: {
|
|
|
|
|
...current.brand[section],
|
|
|
|
|
...patch,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} : current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateHomepageLayout(layout: PublicHomepageLayout) {
|
|
|
|
|
setForm((current) => current ? {
|
|
|
|
|
...current,
|
|
|
|
|
brand: {
|
|
|
|
|
...current.brand,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
...current.brand.homePageConfig,
|
|
|
|
|
layout,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} : current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addHomepageSection(type: PublicHomepageSectionType) {
|
|
|
|
|
setForm((current) => {
|
|
|
|
|
if (!current) return current
|
|
|
|
|
if (current.brand.homePageConfig.layout.items.some((item) => item.type === type)) return current
|
|
|
|
|
const fallback = clonePublicHomepageLayout().items.find((item) => item.type === type)
|
|
|
|
|
if (!fallback) return current
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
brand: {
|
|
|
|
|
...current.brand,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
...current.brand.homePageConfig,
|
|
|
|
|
showOffers: type === 'offers' ? true : current.brand.homePageConfig.showOffers,
|
|
|
|
|
showVehicles: type === 'vehicles' ? true : current.brand.homePageConfig.showVehicles,
|
|
|
|
|
showPricing: type === 'pricing' ? true : current.brand.homePageConfig.showPricing,
|
|
|
|
|
layout: {
|
|
|
|
|
items: [...current.brand.homePageConfig.layout.items, fallback],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
menuConfig: {
|
|
|
|
|
...current.brand.menuConfig,
|
|
|
|
|
pageSections: {
|
|
|
|
|
...current.brand.menuConfig.pageSections,
|
|
|
|
|
home: {
|
|
|
|
|
...current.brand.menuConfig.pageSections.home,
|
|
|
|
|
[type]: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeHomepageSection(type: PublicHomepageSectionType) {
|
|
|
|
|
setForm((current) => current ? {
|
|
|
|
|
...current,
|
|
|
|
|
brand: {
|
|
|
|
|
...current.brand,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
...current.brand.homePageConfig,
|
|
|
|
|
showOffers: type === 'offers' ? false : current.brand.homePageConfig.showOffers,
|
|
|
|
|
showVehicles: type === 'vehicles' ? false : current.brand.homePageConfig.showVehicles,
|
|
|
|
|
showPricing: type === 'pricing' ? false : current.brand.homePageConfig.showPricing,
|
|
|
|
|
layout: {
|
|
|
|
|
items: current.brand.homePageConfig.layout.items.filter((item) => item.type !== type),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
menuConfig: {
|
|
|
|
|
...current.brand.menuConfig,
|
|
|
|
|
pageSections: {
|
|
|
|
|
...current.brand.menuConfig.pageSections,
|
|
|
|
|
home: {
|
|
|
|
|
...current.brand.menuConfig.pageSections.home,
|
|
|
|
|
[type]: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} : current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePageSection<
|
|
|
|
|
P extends keyof PublicSitePageSections,
|
|
|
|
|
S extends keyof PublicSitePageSections[P]
|
|
|
|
|
>(page: P, section: S, value: boolean) {
|
|
|
|
|
setForm((current) => {
|
|
|
|
|
if (!current) return current
|
|
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
|
...current,
|
|
|
|
|
brand: {
|
|
|
|
|
...current.brand,
|
|
|
|
|
menuConfig: {
|
|
|
|
|
...current.brand.menuConfig,
|
|
|
|
|
pageSections: {
|
|
|
|
|
...current.brand.menuConfig.pageSections,
|
|
|
|
|
[page]: {
|
|
|
|
|
...current.brand.menuConfig.pageSections[page],
|
|
|
|
|
[section]: value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (page !== 'home') return next
|
|
|
|
|
|
|
|
|
|
const homepageSection = section as PublicHomepageSectionType
|
|
|
|
|
const nextItems = value
|
|
|
|
|
? next.brand.homePageConfig.layout.items.some((item) => item.type === homepageSection)
|
|
|
|
|
? next.brand.homePageConfig.layout.items
|
|
|
|
|
: [...next.brand.homePageConfig.layout.items, clonePublicHomepageLayout().items.find((item) => item.type === homepageSection)!]
|
|
|
|
|
: next.brand.homePageConfig.layout.items.filter((item) => item.type !== homepageSection)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...next,
|
|
|
|
|
brand: {
|
|
|
|
|
...next.brand,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
...next.brand.homePageConfig,
|
|
|
|
|
showOffers: homepageSection === 'offers' ? value : next.brand.homePageConfig.showOffers,
|
|
|
|
|
showVehicles: homepageSection === 'vehicles' ? value : next.brand.homePageConfig.showVehicles,
|
|
|
|
|
showPricing: homepageSection === 'pricing' ? value : next.brand.homePageConfig.showPricing,
|
|
|
|
|
layout: { items: nextItems },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPageSectionValue<
|
|
|
|
|
P extends keyof PublicSitePageSections,
|
|
|
|
|
S extends keyof PublicSitePageSections[P]
|
|
|
|
|
>(page: P, section: S) {
|
|
|
|
|
return form?.brand.menuConfig.pageSections[page][section] ?? false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveChanges() {
|
|
|
|
|
if (!form) return
|
|
|
|
|
setSaving(true)
|
|
|
|
@@ -644,42 +306,6 @@ export default function AdminCompanyDetailPage() {
|
|
|
|
|
defaultLocale: form.brand.defaultLocale,
|
|
|
|
|
defaultCurrency: form.brand.defaultCurrency,
|
|
|
|
|
isListedOnMarketplace: form.brand.isListedOnMarketplace,
|
|
|
|
|
homePageConfig: {
|
|
|
|
|
heroTitle: toNullable(form.brand.homePageConfig.heroTitle),
|
|
|
|
|
heroDescription: toNullable(form.brand.homePageConfig.heroDescription),
|
|
|
|
|
viewOffersLabel: toNullable(form.brand.homePageConfig.viewOffersLabel),
|
|
|
|
|
viewPricingLabel: toNullable(form.brand.homePageConfig.viewPricingLabel),
|
|
|
|
|
contactCompanyLabel: toNullable(form.brand.homePageConfig.contactCompanyLabel),
|
|
|
|
|
activeOffersTitle: toNullable(form.brand.homePageConfig.activeOffersTitle),
|
|
|
|
|
seeAllOffersLabel: toNullable(form.brand.homePageConfig.seeAllOffersLabel),
|
|
|
|
|
noActiveOffersLabel: toNullable(form.brand.homePageConfig.noActiveOffersLabel),
|
|
|
|
|
publishedVehiclesTitle: toNullable(form.brand.homePageConfig.publishedVehiclesTitle),
|
|
|
|
|
viewVehicleLabel: toNullable(form.brand.homePageConfig.viewVehicleLabel),
|
|
|
|
|
pricingEyebrow: toNullable(form.brand.homePageConfig.pricingEyebrow),
|
|
|
|
|
pricingTitle: toNullable(form.brand.homePageConfig.pricingTitle),
|
|
|
|
|
pricingDescription: toNullable(form.brand.homePageConfig.pricingDescription),
|
|
|
|
|
showOffers: form.brand.homePageConfig.showOffers,
|
|
|
|
|
showVehicles: form.brand.homePageConfig.showVehicles,
|
|
|
|
|
showPricing: form.brand.homePageConfig.showPricing,
|
|
|
|
|
layout: form.brand.homePageConfig.layout,
|
|
|
|
|
},
|
|
|
|
|
menuConfig: {
|
|
|
|
|
aboutLabel: toNullable(form.brand.menuConfig.aboutLabel),
|
|
|
|
|
vehiclesLabel: toNullable(form.brand.menuConfig.vehiclesLabel),
|
|
|
|
|
offersLabel: toNullable(form.brand.menuConfig.offersLabel),
|
|
|
|
|
pricingLabel: toNullable(form.brand.menuConfig.pricingLabel),
|
|
|
|
|
blogLabel: toNullable(form.brand.menuConfig.blogLabel),
|
|
|
|
|
contactLabel: toNullable(form.brand.menuConfig.contactLabel),
|
|
|
|
|
bookCarLabel: toNullable(form.brand.menuConfig.bookCarLabel),
|
|
|
|
|
siteNavigationLabel: toNullable(form.brand.menuConfig.siteNavigationLabel),
|
|
|
|
|
showAbout: form.brand.menuConfig.showAbout,
|
|
|
|
|
showVehicles: form.brand.menuConfig.showVehicles,
|
|
|
|
|
showOffers: form.brand.menuConfig.showOffers,
|
|
|
|
|
showPricing: form.brand.menuConfig.showPricing,
|
|
|
|
|
showBlog: form.brand.menuConfig.showBlog,
|
|
|
|
|
showContact: form.brand.menuConfig.showContact,
|
|
|
|
|
pageSections: form.brand.menuConfig.pageSections,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
contractSettings: {
|
|
|
|
|
legalName: toNullable(form.contractSettings.legalName),
|
|
|
|
@@ -761,158 +387,6 @@ export default function AdminCompanyDetailPage() {
|
|
|
|
|
customers: company._count?.customers ?? company.customers?.length ?? 0,
|
|
|
|
|
reservations: company._count?.reservations ?? company.reservations?.length ?? 0,
|
|
|
|
|
}
|
|
|
|
|
const previewCompanyName = form.brand.displayName.trim() || form.company.name.trim() || company.name
|
|
|
|
|
const previewTagline = resolvePreviewText(
|
|
|
|
|
form.brand.tagline,
|
|
|
|
|
'This company uses RentalDriveGo to publish vehicles, run bookings, and accept direct rental payments.',
|
|
|
|
|
)
|
|
|
|
|
const previewHeroTitle = resolvePreviewText(form.brand.homePageConfig.heroTitle, previewTagline)
|
|
|
|
|
const previewHeroDescription = resolvePreviewText(
|
|
|
|
|
form.brand.homePageConfig.heroDescription,
|
|
|
|
|
'Manage direct bookings, highlight active offers, and turn your fleet into a modern rental storefront.',
|
|
|
|
|
)
|
|
|
|
|
const previewOffersLabel = resolvePreviewText(form.brand.homePageConfig.viewOffersLabel, 'View offers')
|
|
|
|
|
const previewPricingLabel = resolvePreviewText(form.brand.homePageConfig.viewPricingLabel, 'View pricing')
|
|
|
|
|
const previewContactLabel = resolvePreviewText(form.brand.homePageConfig.contactCompanyLabel, 'Contact company')
|
|
|
|
|
const previewOffersTitle = resolvePreviewText(form.brand.homePageConfig.activeOffersTitle, 'Active offers')
|
|
|
|
|
const previewSeeAllOffersLabel = resolvePreviewText(form.brand.homePageConfig.seeAllOffersLabel, 'See all offers')
|
|
|
|
|
const previewNoOffersLabel = resolvePreviewText(form.brand.homePageConfig.noActiveOffersLabel, 'No active offers right now')
|
|
|
|
|
const previewVehiclesTitle = resolvePreviewText(form.brand.homePageConfig.publishedVehiclesTitle, 'Published vehicles')
|
|
|
|
|
const previewViewVehicleLabel = resolvePreviewText(form.brand.homePageConfig.viewVehicleLabel, 'View vehicle')
|
|
|
|
|
const previewPricingEyebrow = resolvePreviewText(form.brand.homePageConfig.pricingEyebrow, 'Pricing')
|
|
|
|
|
const previewPricingTitle = resolvePreviewText(form.brand.homePageConfig.pricingTitle, 'Plans for every fleet size')
|
|
|
|
|
const previewPricingDescription = resolvePreviewText(
|
|
|
|
|
form.brand.homePageConfig.pricingDescription,
|
|
|
|
|
'Showcase transparent pricing and make it easy for renters to understand the value of booking direct.',
|
|
|
|
|
)
|
|
|
|
|
const previewBookCarLabel = resolvePreviewText(form.brand.menuConfig.bookCarLabel, 'Book car')
|
|
|
|
|
const previewFooterLabel = resolvePreviewText(form.brand.menuConfig.siteNavigationLabel, 'Site navigation')
|
|
|
|
|
const homepageLayout = resolvePublicHomepageLayout(
|
|
|
|
|
form.brand.homePageConfig.layout,
|
|
|
|
|
form.brand.homePageConfig.layout.items.map((item) => item.type),
|
|
|
|
|
)
|
|
|
|
|
const availableHomepageSections = HOMEPAGE_SECTION_ORDER.filter(
|
|
|
|
|
(type) => !homepageLayout.items.some((item) => item.type === type),
|
|
|
|
|
)
|
|
|
|
|
const previewMenuItems = [
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.aboutLabel, 'About'), visible: form.brand.menuConfig.showAbout },
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.vehiclesLabel, 'Vehicles'), visible: form.brand.menuConfig.showVehicles },
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.offersLabel, 'Offers'), visible: form.brand.menuConfig.showOffers },
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.pricingLabel, 'Pricing'), visible: form.brand.menuConfig.showPricing },
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.blogLabel, 'Blog'), visible: form.brand.menuConfig.showBlog },
|
|
|
|
|
{ label: resolvePreviewText(form.brand.menuConfig.contactLabel, 'Contact'), visible: form.brand.menuConfig.showContact },
|
|
|
|
|
].filter((item) => item.visible)
|
|
|
|
|
|
|
|
|
|
function renderHomepagePreviewSection(type: PublicHomepageSectionType) {
|
|
|
|
|
if (type === 'hero') {
|
|
|
|
|
return (
|
|
|
|
|
<section className="grid h-full gap-6 lg:grid-cols-[1.15fr_0.85fr] lg:items-center">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-sky-700">{previewCompanyName}</p>
|
|
|
|
|
<h4 className="mt-5 text-4xl font-black tracking-tight text-slate-900">{previewHeroTitle}</h4>
|
|
|
|
|
<p className="mt-5 max-w-2xl text-base leading-8 text-slate-600">{previewHeroDescription}</p>
|
|
|
|
|
<div className="mt-8 flex flex-wrap gap-3">
|
|
|
|
|
<span className="rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white">{previewOffersLabel}</span>
|
|
|
|
|
<span className="rounded-full border border-sky-200 bg-sky-50 px-5 py-3 text-sm font-semibold text-sky-800">{previewPricingLabel}</span>
|
|
|
|
|
<span className="rounded-full border border-slate-300 px-5 py-3 text-sm font-semibold text-slate-700">{previewContactLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex min-h-72 items-center justify-center rounded-[1.5rem] bg-slate-100 p-8 text-center text-sm leading-7 text-slate-500">
|
|
|
|
|
Hero image preview area.
|
|
|
|
|
<br />
|
|
|
|
|
Published brand media will render here on the public site.
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'offers') {
|
|
|
|
|
return (
|
|
|
|
|
<section className="h-full">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<h4 className="text-2xl font-bold text-slate-900">{previewOffersTitle}</h4>
|
|
|
|
|
<span className="text-sm font-semibold text-sky-700">{previewSeeAllOffersLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
|
|
|
|
<div className="rounded-[1.5rem] border border-slate-200 bg-white p-5 shadow-sm">
|
|
|
|
|
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-sky-700">Sample offer</p>
|
|
|
|
|
<h5 className="mt-3 text-lg font-semibold text-slate-900">Weekend escape</h5>
|
|
|
|
|
<p className="mt-3 text-3xl font-black text-sky-700">15%</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-[1.5rem] border border-dashed border-slate-300 bg-white p-5 text-sm text-slate-500 shadow-sm">
|
|
|
|
|
{previewNoOffersLabel}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'vehicles') {
|
|
|
|
|
return (
|
|
|
|
|
<section className="h-full">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<h4 className="text-2xl font-bold text-slate-900">{previewVehiclesTitle}</h4>
|
|
|
|
|
<p className="text-sm text-slate-500">{counts.vehicles} published vehicles</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-4 grid gap-5 md:grid-cols-2">
|
|
|
|
|
{[1, 2].map((index) => (
|
|
|
|
|
<article key={index} className="overflow-hidden rounded-[1.5rem] border border-slate-200 bg-white shadow-sm">
|
|
|
|
|
<div className="aspect-[16/10] bg-[linear-gradient(135deg,#e2e8f0,#f8fafc)]" />
|
|
|
|
|
<div className="p-5">
|
|
|
|
|
<h5 className="text-xl font-bold text-slate-900">Vehicle {index}</h5>
|
|
|
|
|
<p className="mt-1 text-sm text-slate-500">Category</p>
|
|
|
|
|
<div className="mt-4 flex items-end justify-between gap-3">
|
|
|
|
|
<p className="text-xl font-black text-slate-900">MAD 850/day</p>
|
|
|
|
|
<span className="rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white">{previewViewVehicleLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="h-full space-y-6">
|
|
|
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-sky-700">{previewPricingEyebrow}</p>
|
|
|
|
|
<h4 className="mt-2 text-3xl font-black tracking-tight text-slate-900">{previewPricingTitle}</h4>
|
|
|
|
|
<p className="mt-3 max-w-3xl text-sm leading-7 text-slate-600">{previewPricingDescription}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="rounded-full border border-slate-300 px-5 py-2.5 text-sm font-semibold text-slate-700">
|
|
|
|
|
{previewPricingLabel}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
|
|
|
{PREVIEW_PRICING_PLANS.slice(0, 2).map((plan) => (
|
|
|
|
|
<article
|
|
|
|
|
key={plan.name}
|
|
|
|
|
className={`rounded-[1.5rem] border bg-white p-8 shadow-sm ${plan.highlighted ? 'border-sky-300 shadow-lg shadow-sky-100' : 'border-slate-200'}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h5 className="text-2xl font-black text-slate-900">{plan.name}</h5>
|
|
|
|
|
<p className="mt-3 text-sm leading-6 text-slate-600">Pricing copy preview for the public homepage.</p>
|
|
|
|
|
</div>
|
|
|
|
|
{plan.highlighted ? (
|
|
|
|
|
<span className="rounded-full bg-sky-100 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-sky-800">
|
|
|
|
|
Popular
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-8 flex items-end gap-2">
|
|
|
|
|
<span className="text-4xl font-black tracking-tight text-slate-900">{plan.price}</span>
|
|
|
|
|
{plan.cadence ? <span className="pb-1 text-sm text-slate-500">{plan.cadence}</span> : null}
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="shell space-y-6 py-8">
|
|
|
|
@@ -1108,253 +582,6 @@ export default function AdminCompanyDetailPage() {
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section id="site-config" className="panel p-6 xl:col-span-2">
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-base font-semibold">Public site homepage and menu</h2>
|
|
|
|
|
<p className="mt-1 text-sm text-zinc-500">Blank text fields fall back to the default localized copy on the public site.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-6 grid gap-6 xl:grid-cols-2">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-semibold text-zinc-200">Homepage content</h3>
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<label className="md:col-span-2">
|
|
|
|
|
<span className={LABEL_CLASS}>Hero title</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.heroTitle} onChange={(e) => updateBrandConfig('homePageConfig', { heroTitle: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="md:col-span-2">
|
|
|
|
|
<span className={LABEL_CLASS}>Hero description</span>
|
|
|
|
|
<textarea className={`${INPUT_CLASS} min-h-24`} value={form.brand.homePageConfig.heroDescription} onChange={(e) => updateBrandConfig('homePageConfig', { heroDescription: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Offers button label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.viewOffersLabel} onChange={(e) => updateBrandConfig('homePageConfig', { viewOffersLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Pricing button label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.viewPricingLabel} onChange={(e) => updateBrandConfig('homePageConfig', { viewPricingLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="md:col-span-2">
|
|
|
|
|
<span className={LABEL_CLASS}>Contact button label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.contactCompanyLabel} onChange={(e) => updateBrandConfig('homePageConfig', { contactCompanyLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Offers section title</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.activeOffersTitle} onChange={(e) => updateBrandConfig('homePageConfig', { activeOffersTitle: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>See all offers label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.seeAllOffersLabel} onChange={(e) => updateBrandConfig('homePageConfig', { seeAllOffersLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="md:col-span-2">
|
|
|
|
|
<span className={LABEL_CLASS}>No active offers text</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.noActiveOffersLabel} onChange={(e) => updateBrandConfig('homePageConfig', { noActiveOffersLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Vehicles section title</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.publishedVehiclesTitle} onChange={(e) => updateBrandConfig('homePageConfig', { publishedVehiclesTitle: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>View vehicle label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.viewVehicleLabel} onChange={(e) => updateBrandConfig('homePageConfig', { viewVehicleLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Pricing eyebrow</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.pricingEyebrow} onChange={(e) => updateBrandConfig('homePageConfig', { pricingEyebrow: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Pricing title</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.homePageConfig.pricingTitle} onChange={(e) => updateBrandConfig('homePageConfig', { pricingTitle: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="md:col-span-2">
|
|
|
|
|
<span className={LABEL_CLASS}>Pricing description</span>
|
|
|
|
|
<textarea className={`${INPUT_CLASS} min-h-24`} value={form.brand.homePageConfig.pricingDescription} onChange={(e) => updateBrandConfig('homePageConfig', { pricingDescription: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<HomepageLayoutEditor
|
|
|
|
|
layout={homepageLayout}
|
|
|
|
|
availableSections={availableHomepageSections}
|
|
|
|
|
onChange={updateHomepageLayout}
|
|
|
|
|
onAddSection={addHomepageSection}
|
|
|
|
|
onRemoveSection={removeHomepageSection}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-semibold text-zinc-200">Navigation menu</h3>
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>About label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.aboutLabel} onChange={(e) => updateBrandConfig('menuConfig', { aboutLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showAbout} onChange={(e) => updateBrandConfig('menuConfig', { showAbout: e.target.checked })} />
|
|
|
|
|
Show About
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Vehicles label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.vehiclesLabel} onChange={(e) => updateBrandConfig('menuConfig', { vehiclesLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showVehicles} onChange={(e) => updateBrandConfig('menuConfig', { showVehicles: e.target.checked })} />
|
|
|
|
|
Show Vehicles
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Offers label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.offersLabel} onChange={(e) => updateBrandConfig('menuConfig', { offersLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showOffers} onChange={(e) => updateBrandConfig('menuConfig', { showOffers: e.target.checked })} />
|
|
|
|
|
Show Offers
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Pricing label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.pricingLabel} onChange={(e) => updateBrandConfig('menuConfig', { pricingLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showPricing} onChange={(e) => updateBrandConfig('menuConfig', { showPricing: e.target.checked })} />
|
|
|
|
|
Show Pricing
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Blog label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.blogLabel} onChange={(e) => updateBrandConfig('menuConfig', { blogLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showBlog} onChange={(e) => updateBrandConfig('menuConfig', { showBlog: e.target.checked })} />
|
|
|
|
|
Show Blog
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Contact label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.contactLabel} onChange={(e) => updateBrandConfig('menuConfig', { contactLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
|
|
|
|
|
<input type="checkbox" checked={form.brand.menuConfig.showContact} onChange={(e) => updateBrandConfig('menuConfig', { showContact: e.target.checked })} />
|
|
|
|
|
Show Contact
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Book car button label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.bookCarLabel} onChange={(e) => updateBrandConfig('menuConfig', { bookCarLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span className={LABEL_CLASS}>Footer navigation label</span>
|
|
|
|
|
<input className={INPUT_CLASS} value={form.brand.menuConfig.siteNavigationLabel} onChange={(e) => updateBrandConfig('menuConfig', { siteNavigationLabel: e.target.value })} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/60 p-4">
|
|
|
|
|
<h4 className="text-sm font-semibold text-zinc-100">Page sections</h4>
|
|
|
|
|
<p className="mt-1 text-xs text-zinc-500">Turn sections on or off across every public page.</p>
|
|
|
|
|
<div className="mt-4 grid gap-4">
|
|
|
|
|
{PAGE_SECTION_GROUPS.map((group) => (
|
|
|
|
|
<div key={group.page} className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
|
|
|
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500">{group.label}</p>
|
|
|
|
|
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
|
|
|
|
{group.sections.map((section) => (
|
|
|
|
|
<label key={section.key} className="flex items-center gap-3 text-sm text-zinc-300">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={getPageSectionValue(group.page, section.key as keyof PublicSitePageSections[typeof group.page])}
|
|
|
|
|
onChange={(e) => updatePageSection(group.page, section.key as keyof PublicSitePageSections[typeof group.page], e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
{section.label}
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-8 rounded-[2rem] border border-zinc-800 bg-zinc-950 p-4">
|
|
|
|
|
<div className="flex items-center justify-between gap-4 border-b border-zinc-800 px-2 pb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-semibold text-zinc-100">Live homepage preview</h3>
|
|
|
|
|
<p className="mt-1 text-xs text-zinc-500">Unsaved changes appear here instantly before you publish them.</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">
|
|
|
|
|
Draft preview
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-4 overflow-hidden rounded-[1.75rem] border border-sky-100 bg-[linear-gradient(180deg,#eff6ff,white_28%)] shadow-2xl shadow-black/20">
|
|
|
|
|
<div className="border-b border-slate-200 bg-white/90 px-5 py-4 backdrop-blur-sm">
|
|
|
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-sky-600 text-sm font-black text-white">
|
|
|
|
|
{previewCompanyName.charAt(0).toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-black tracking-tight text-slate-900">{previewCompanyName}</p>
|
|
|
|
|
<p className="text-xs text-slate-500">{form.brand.subdomain || form.company.slug || company.slug}.rentaldrivego.com</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<nav className="flex flex-wrap items-center gap-2 text-sm text-slate-600">
|
|
|
|
|
{previewMenuItems.map((item) => (
|
|
|
|
|
<span key={item.label} className="rounded-lg px-3 py-2 transition-colors hover:bg-slate-100 hover:text-slate-900">
|
|
|
|
|
{item.label}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
<span className="inline-flex rounded-full bg-slate-900 px-5 py-2 text-sm font-semibold text-white">
|
|
|
|
|
{previewBookCarLabel}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="px-5 py-8 lg:px-8">
|
|
|
|
|
<div className="grid gap-6 lg:hidden">
|
|
|
|
|
{homepageLayout.items.map((item) => (
|
|
|
|
|
<div key={`mobile-${item.id}`} className="rounded-[1.5rem] border border-slate-200 bg-white p-5 shadow-sm">
|
|
|
|
|
{renderHomepagePreviewSection(item.type)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className="relative hidden lg:block"
|
|
|
|
|
style={{ height: `${homepageLayout.items.reduce((max, item) => Math.max(max, item.y + item.h - 1), 1) * 72}px` }}
|
|
|
|
|
>
|
|
|
|
|
{homepageLayout.items.map((item) => (
|
|
|
|
|
<div
|
|
|
|
|
key={item.id}
|
|
|
|
|
className="absolute rounded-[1.75rem] border border-slate-200/80 bg-white/92 p-6 shadow-lg shadow-slate-200/70"
|
|
|
|
|
style={{
|
|
|
|
|
left: `${((item.x - 1) / 12) * 100}%`,
|
|
|
|
|
top: `${(item.y - 1) * 72}px`,
|
|
|
|
|
width: `${(item.w / 12) * 100}%`,
|
|
|
|
|
height: `${item.h * 72}px`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{renderHomepagePreviewSection(item.type)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="border-t border-stone-200 bg-white/90 px-5 py-4 backdrop-blur-md">
|
|
|
|
|
<div className="flex flex-col items-center justify-between gap-3 lg:flex-row">
|
|
|
|
|
<p className="text-xs font-medium uppercase tracking-[0.16em] text-stone-400">{previewFooterLabel}</p>
|
|
|
|
|
<div className="flex flex-col items-center gap-3 sm:flex-row">
|
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-2 text-sm text-stone-500">
|
|
|
|
|
{previewMenuItems.map((item) => (
|
|
|
|
|
<span key={`${item.label}-footer`} className="rounded-full px-3 py-1.5 transition hover:bg-stone-100 hover:text-stone-900">
|
|
|
|
|
{item.label}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-stone-500">© {new Date().getFullYear()} {previewCompanyName}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="panel p-6">
|
|
|
|
|
<h2 className="text-base font-semibold">Operations and finance</h2>
|
|
|
|
|
<div className="mt-4 grid gap-6">
|
|
|
|
|