feat: update eversolo site cms and media workflows

This commit is contained in:
Codex
2026-05-31 17:29:08 +08:00
parent 8529276a6f
commit 895d28b88d
144 changed files with 8614 additions and 2814 deletions
+82 -187
View File
@@ -2,12 +2,12 @@ import type { Metadata } from 'next'
import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { getPageHeroOverlay, PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
import type { Dealer, Media } from '@/payload-types'
import type { Dealer, DealersPage as DealersPageSettings, Media } from '@/payload-types'
export const revalidate = 300
@@ -29,141 +29,6 @@ type UploadLike = {
url?: string | null
}
const DEALERS_PAGE_DEFAULTS = {
en: {
businessContact: {
ctaHref: '/en/contact',
ctaLabel: 'Open Contact',
eyebrow: 'Business Contact',
title: 'Distributor and OEM requests now live on the contact page',
},
emptyState: {
message: 'Authorized dealer information is being updated.',
storesMessage: 'Experience store information is being prepared.',
},
map: {
show: true,
},
hero: {
buttonHref: '/en/contact',
buttonLabel: 'Contact Sales',
description:
'Shop online or connect with authorized Eversolo dealers and experience spaces around the world.',
eyebrow: 'Where to Buy',
imageUrl: null,
textColor: null,
textFont: null,
textPosition: 'left',
title: 'Where to Buy',
},
storeLinks: [
{
description: 'Browse Eversolo products and buying options.',
href: '/en/products',
label: 'Products',
linkLabel: 'Learn More',
openInNewTab: false,
},
{
description: 'Contact Eversolo for regional buying support.',
href: '/en/contact',
label: 'Contact Sales',
linkLabel: 'Contact Us',
openInNewTab: false,
},
{
description: 'Find support resources before and after purchase.',
href: '/en/support',
label: 'Support',
linkLabel: 'View Support',
openInNewTab: false,
},
{
description: 'Explore reviews and videos from the Eversolo community.',
href: '/en/reviews',
label: 'Videos & Reviews',
linkLabel: 'Explore',
openInNewTab: false,
},
],
},
zh: {
businessContact: {
ctaHref: '/zh/contact',
ctaLabel: '前往联系我们',
eyebrow: '商务合作',
title: '经销申请与 OEM 需求已移至联系我们页面',
},
emptyState: {
message: '授权经销商信息正在同步中。',
storesMessage: '体验店信息正在整理中。',
},
map: {
show: true,
},
hero: {
buttonHref: '/zh/contact',
buttonLabel: '联系销售',
description: '线上了解购买渠道,或查找 Eversolo 授权经销商与线下体验空间。',
eyebrow: '购买渠道',
imageUrl: null,
textColor: null,
textFont: null,
textPosition: 'left',
title: 'Where to Buy',
},
storeLinks: [
{
description: '浏览 Eversolo 产品与购买信息。',
href: '/zh/products',
label: '产品中心',
linkLabel: '了解更多',
openInNewTab: false,
},
{
description: '提交购买、经销或区域合作咨询。',
href: '/zh/contact',
label: '联系销售',
linkLabel: '联系我们',
openInNewTab: false,
},
{
description: '查看购买前后所需的服务与支持。',
href: '/zh/support',
label: '服务支持',
linkLabel: '查看支持',
openInNewTab: false,
},
{
description: '查看 Eversolo 视频与媒体评价。',
href: '/zh/reviews',
label: '视频与评测',
linkLabel: '查看内容',
openInNewTab: false,
},
],
},
} satisfies Record<
Locale,
{
businessContact: { ctaHref: string; ctaLabel: string; eyebrow: string; title: string }
emptyState: { message: string; storesMessage: string }
hero: {
buttonHref: string
buttonLabel: string
description: string
eyebrow: string
imageUrl?: string | null
textColor: string | null
textFont: 'default' | 'serif' | 'mono' | null
textPosition: 'left' | 'center' | 'right'
title: string
}
map: { show: boolean }
storeLinks: StoreLink[]
}
>
const COUNTRY_CODES_BY_NAME: Record<string, string> = {
Australia: 'AU',
Austria: 'AT',
@@ -215,10 +80,6 @@ const WORLD_MAP_BLOBS = [
'M83 17 C93 10 109 12 116 22 C111 29 96 31 87 26 C82 23 80 20 83 17 Z',
]
function cmsValue<T>(value: T | null | undefined, fallback: T): T {
return value ?? fallback
}
function resolveCountryCode(dealer: Dealer) {
const maybeCode = 'countryCode' in dealer ? dealer.countryCode : null
const code = typeof maybeCode === 'string' ? maybeCode.trim().toUpperCase() : ''
@@ -242,14 +103,14 @@ function getDealerImage(dealer: Dealer) {
if (!media || typeof media === 'number') return null
const upload = media as UploadLike
return normalizeUploadUrl(upload.sizes?.card?.url ?? upload.url ?? upload.thumbnailURL ?? null)
return normalizeUploadUrl(upload.url ?? upload.thumbnailURL ?? null)
}
function getUploadImageUrl(media: number | Media | null | undefined) {
if (!media || typeof media === 'number') return null
const upload = media as UploadLike
return normalizeUploadUrl(upload.sizes?.card?.url ?? upload.url ?? upload.thumbnailURL ?? null)
return normalizeUploadUrl(upload.url ?? upload.thumbnailURL ?? null)
}
function groupDealers(dealers: Dealer[]) {
@@ -301,35 +162,38 @@ async function getDealersPageSettingsUncached(locale: Locale) {
depth: 1,
locale,
slug: 'dealersPage',
})
const defaults = DEALERS_PAGE_DEFAULTS[locale]
}) as DealersPageSettings
const emptyState = settings.emptyState
const hero = settings.hero
return {
businessContact: {
ctaHref: cmsValue(settings.businessContact?.ctaHref, defaults.businessContact.ctaHref),
ctaLabel: cmsValue(settings.businessContact?.ctaLabel, defaults.businessContact.ctaLabel),
eyebrow: cmsValue(settings.businessContact?.eyebrow, defaults.businessContact.eyebrow),
title: cmsValue(settings.businessContact?.title, defaults.businessContact.title),
ctaHref: settings.businessContact?.ctaHref || null,
ctaLabel: settings.businessContact?.ctaLabel || null,
eyebrow: settings.businessContact?.eyebrow || null,
title: settings.businessContact?.title || null,
},
emptyState: {
message: cmsValue(emptyState?.message, defaults.emptyState.message),
storesMessage: cmsValue(emptyState?.storesMessage, defaults.emptyState.storesMessage),
message: emptyState?.message || null,
storesMessage: emptyState?.storesMessage || null,
},
hero: {
buttonHref: cmsValue(settings.hero?.buttonHref, defaults.hero.buttonHref),
buttonLabel: cmsValue(settings.hero?.buttonLabel, defaults.hero.buttonLabel),
description: cmsValue(settings.hero?.description, defaults.hero.description),
eyebrow: cmsValue(settings.hero?.eyebrow, defaults.hero.eyebrow),
imageUrl: getUploadImageUrl(settings.hero?.image),
textColor: settings.hero?.textColor || null,
textFont: settings.hero?.textFont || null,
textPosition: settings.hero?.textPosition || defaults.hero.textPosition,
title: cmsValue(settings.hero?.title, defaults.hero.title),
buttonHref: hero?.buttonHref || null,
buttonLabel: hero?.buttonLabel || null,
description: hero?.description || null,
eyebrow: hero?.eyebrow || null,
imageUrl: getUploadImageUrl(hero?.image),
mobileImageUrl: getUploadImageUrl(hero?.mobileImage),
showOverlay: getPageHeroOverlay(hero),
textColor: hero?.textColor || null,
textFont: hero?.textFont || null,
textPosition: hero?.textPosition || null,
title: hero?.title || null,
},
map: {
show: (settings as typeof settings & { map?: { show?: boolean | null } }).map?.show ?? defaults.map.show,
show: (settings as typeof settings & { map?: { show?: boolean | null } }).map?.show ?? false,
},
showDealers: (settings as typeof settings & { showDealers?: boolean | null }).showDealers ?? false,
storeLinks:
settings.storeLinks
?.map((item) => ({
@@ -342,10 +206,29 @@ async function getDealersPageSettingsUncached(locale: Locale) {
openInNewTab: item.openInNewTab ?? false,
}))
.filter((item) => item.label || item.href)
.slice(0, 4) || defaults.storeLinks,
.slice(0, 4) || [],
}
} catch {
return DEALERS_PAGE_DEFAULTS[locale]
return {
businessContact: { ctaHref: null, ctaLabel: null, eyebrow: null, title: null },
emptyState: { message: null, storesMessage: null },
hero: {
buttonHref: null,
buttonLabel: null,
description: null,
eyebrow: null,
imageUrl: null,
mobileImageUrl: null,
showOverlay: true,
textColor: null,
textFont: null,
textPosition: null,
title: null,
},
map: { show: false },
showDealers: false,
storeLinks: [],
}
}
}
@@ -692,15 +575,15 @@ export default async function WhereToBuyPage({
getDealersPageData(locale),
getDealersPageSettings(locale),
])
const hasDealers = dealerDocs.length > 0
const showDealers = settings.showDealers
const hasStores = storeDocs.length > 0
const shouldShowTabs = hasDealers && hasStores
const shouldShowTabs = showDealers && hasStores
const requestedTab: DealerTab = query?.tab === 'stores' ? 'stores' : 'dealers'
const activeTab: DealerTab = shouldShowTabs
? requestedTab
: hasStores
? 'stores'
: 'dealers'
: showDealers
? 'dealers'
: 'stores'
const dealerItems = dealerDocs.map((dealer) => ({
'@type': 'Organization',
address: dealer.address || undefined,
@@ -713,7 +596,7 @@ export default async function WhereToBuyPage({
return (
<div className="mx-auto max-w-page px-6 pb-12 pt-4 md:px-12 md:pb-16 md:pt-5">
{dealerItems.length > 0 ? (
{showDealers && dealerItems.length > 0 ? (
<script
type="application/ld+json"
suppressHydrationWarning
@@ -736,6 +619,8 @@ export default async function WhereToBuyPage({
<PageHero
eyebrow={settings.hero.eyebrow}
imageUrl={settings.hero.imageUrl}
mobileImageUrl={settings.hero.mobileImageUrl}
overlay={settings.hero.showOverlay}
textColor={settings.hero.textColor}
textFont={settings.hero.textFont}
textPosition={settings.hero.textPosition}
@@ -757,7 +642,7 @@ export default async function WhereToBuyPage({
{shouldShowTabs ? <DealerTabs activeTab={activeTab} locale={locale} /> : null}
{activeTab === 'dealers' ? (
{showDealers && activeTab === 'dealers' ? (
<div className="mt-8 space-y-8">
{settings.map.show ? <DealerWorldMap dealers={dealerDocs} locale={locale} /> : null}
@@ -804,25 +689,35 @@ export default async function WhereToBuyPage({
</div>
)}
<section className="mt-10 rounded-[14px] bg-white px-6 py-6 shadow-[0_16px_45px_rgba(15,23,42,0.05)] ring-1 ring-black/7 md:px-8">
<div className="flex flex-col gap-5 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm uppercase tracking-[0.18em] text-text-muted">
{settings.businessContact.eyebrow}
{settings.businessContact.eyebrow ||
settings.businessContact.title ||
(settings.businessContact.ctaHref && settings.businessContact.ctaLabel) ? (
<section className="mt-10 rounded-[14px] bg-white px-6 py-6 shadow-[0_16px_45px_rgba(15,23,42,0.05)] ring-1 ring-black/7 md:px-8">
<div className="flex flex-col gap-5 md:flex-row md:items-center md:justify-between">
<div>
{settings.businessContact.eyebrow ? (
<div className="text-sm uppercase tracking-[0.18em] text-text-muted">
{settings.businessContact.eyebrow}
</div>
) : null}
{settings.businessContact.title ? (
<h2 className="mt-2 text-2xl font-medium tracking-tight">
{settings.businessContact.title}
</h2>
) : null}
</div>
<h2 className="mt-2 text-2xl font-medium tracking-tight">
{settings.businessContact.title}
</h2>
{settings.businessContact.ctaHref && settings.businessContact.ctaLabel ? (
<Link
href={settings.businessContact.ctaHref}
className="inline-flex items-center rounded-full bg-black px-5 py-3 text-sm font-medium text-white"
>
{settings.businessContact.ctaLabel}
<span aria-hidden className="ml-2"></span>
</Link>
) : null}
</div>
<Link
href={settings.businessContact.ctaHref}
className="inline-flex items-center rounded-full bg-black px-5 py-3 text-sm font-medium text-white"
>
{settings.businessContact.ctaLabel}
<span aria-hidden className="ml-2"></span>
</Link>
</div>
</section>
</section>
) : null}
</div>
)
}