From e7505eab4d2867aac220c0cd15d41c8f9c25f5dd Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 13:38:02 +0800 Subject: [PATCH] Update homepage positions and form notifications --- .../dealers/dealer-partnership-forms.tsx | 79 ++++++--- .../[locale]/products/[slug]/page.tsx | 157 ++++++++++-------- .../[slug]/product-template-mount.tsx | 56 ++++++- .../support/contact/support-contact-form.tsx | 46 +++-- src/app/api/sales/distributor/route.ts | 12 +- src/app/api/sales/oem/route.ts | 12 +- src/app/api/support/contact/route.ts | 12 +- .../home/home-featured-products.tsx | 61 ++++++- src/components/home/home-hero.tsx | 131 +++++++++++---- src/components/site/form-success-dialog.tsx | 74 +++++++++ src/lib/smtp-notifications.ts | 40 ++++- src/payload-types.ts | 28 +++- src/payload/globals/Homepage.ts | 34 +++- 13 files changed, 577 insertions(+), 165 deletions(-) create mode 100644 src/components/site/form-success-dialog.tsx diff --git a/src/app/(frontend)/[locale]/dealers/dealer-partnership-forms.tsx b/src/app/(frontend)/[locale]/dealers/dealer-partnership-forms.tsx index 2c104f6..908bf26 100644 --- a/src/app/(frontend)/[locale]/dealers/dealer-partnership-forms.tsx +++ b/src/app/(frontend)/[locale]/dealers/dealer-partnership-forms.tsx @@ -2,6 +2,7 @@ import { startTransition, useState } from 'react' import type { Locale } from '@/i18n/config' +import { FormSuccessDialog } from '@/components/site/form-success-dialog' type DistributorFormState = { companyName: string @@ -55,6 +56,35 @@ type PartnershipFormIntro = { title?: string | null } +type SuccessDialogState = { + message: string + title: string +} + +function getPartnershipSuccessDialog(type: 'distributor' | 'oem', locale: Locale): SuccessDialogState { + if (type === 'oem') { + return locale === 'zh' + ? { + message: 'OEM 合作申请已提交,我们会尽快查看并联系您。', + title: 'OEM 合作申请已提交', + } + : { + message: 'Your OEM inquiry has been submitted successfully. We will review it and contact you soon.', + title: 'OEM inquiry submitted', + } + } + + return locale === 'zh' + ? { + message: '经销商申请已提交,我们会尽快查看并联系您。', + title: '经销商申请已提交', + } + : { + message: 'Your distributor request has been submitted successfully. We will review it and contact you soon.', + title: 'Distributor request submitted', + } +} + export function DealerPartnershipForms({ intro, locale, @@ -68,17 +98,17 @@ export function DealerPartnershipForms({ const [oemForm, setOemForm] = useState(INITIAL_OEM_FORM) const [error, setError] = useState(null) const [isPending, setIsPending] = useState(false) - const [success, setSuccess] = useState(null) + const [successDialog, setSuccessDialog] = useState(null) const [startedAt] = useState(() => Date.now()) - async function submitForm(endpoint: string, payload: object) { + async function submitForm(endpoint: string, payload: object, type: 'distributor' | 'oem') { setError(null) - setSuccess(null) + setSuccessDialog(null) setIsPending(true) try { const response = await fetch(endpoint, { - body: JSON.stringify({ ...payload, startedAt }), + body: JSON.stringify({ ...payload, locale, startedAt }), headers: { 'Content-Type': 'application/json' }, method: 'POST', }) @@ -93,13 +123,10 @@ export function DealerPartnershipForms({ } startTransition(() => { - setSuccess( - data.message || - (locale === 'zh' - ? '申请已提交,我们会尽快联系您。' - : 'Your request has been submitted successfully.'), - ) + setSuccessDialog(getPartnershipSuccessDialog(type, locale)) }) + + return true } catch (submissionError) { setError( submissionError instanceof Error @@ -108,6 +135,7 @@ export function DealerPartnershipForms({ ? '提交失败,请稍后再试。' : 'Submission failed. Please try again.', ) + return false } finally { setIsPending(false) } @@ -115,18 +143,29 @@ export function DealerPartnershipForms({ async function handleDistributorSubmit(event: React.FormEvent) { event.preventDefault() - await submitForm('/api/sales/distributor', distributorForm) - setDistributorForm(INITIAL_DISTRIBUTOR_FORM) + if (await submitForm('/api/sales/distributor', distributorForm, 'distributor')) { + setDistributorForm(INITIAL_DISTRIBUTOR_FORM) + } } async function handleOemSubmit(event: React.FormEvent) { event.preventDefault() - await submitForm('/api/sales/oem', oemForm) - setOemForm(INITIAL_OEM_FORM) + if (await submitForm('/api/sales/oem', oemForm, 'oem')) { + setOemForm(INITIAL_OEM_FORM) + } } return (
+ {successDialog ? ( + setSuccessDialog(null)} + title={successDialog.title} + /> + ) : null} +
@@ -249,7 +288,7 @@ export function DealerPartnershipForms({ setDistributorForm((current) => ({ ...current, message: value })) } /> - + ) : (
@@ -307,7 +346,7 @@ export function DealerPartnershipForms({ value={oemForm.message} onChange={(value) => setOemForm((current) => ({ ...current, message: value }))} /> - + )}
@@ -320,12 +359,10 @@ function SubmitState({ error, isPending, locale, - success, }: { error: string | null isPending: boolean locale: Locale - success: string | null }) { return ( <> @@ -335,12 +372,6 @@ function SubmitState({
) : null} - {success ? ( -
- {success} -
- ) : null} - + +
+ ✓ +
+

+ {title} +

+

{message}

+
+ +
+
+ + ) +} diff --git a/src/lib/smtp-notifications.ts b/src/lib/smtp-notifications.ts index bdefa71..97fd84b 100644 --- a/src/lib/smtp-notifications.ts +++ b/src/lib/smtp-notifications.ts @@ -16,6 +16,8 @@ type SendSubmissionNotificationInput = { to: string } +export type NotificationLocale = 'en' | 'zh' + const DEFAULT_FROM = 'Eversolo Website ' const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/+$/, '') || '' @@ -77,12 +79,42 @@ function isLikelyEmail(value: string | undefined) { return Boolean(value && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) } -export function getAfterServiceNotificationRecipient() { - return process.env.AFTER_SERVICE_NOTIFY_EMAIL?.trim() || 'service@eversolo.com' +function normalizeRecipient(value: string | undefined) { + return value?.trim() || undefined } -export function getSalesNotificationRecipient() { - return process.env.SALES_NOTIFY_EMAIL?.trim() || 'sales@eversolo.com' +export function normalizeNotificationLocale( + value: unknown, + referer?: string | null, +): NotificationLocale { + if (value === 'zh' || value === 'en') return value + + if (referer) { + try { + const pathname = new URL(referer).pathname + if (pathname === '/zh' || pathname.startsWith('/zh/')) return 'zh' + } catch { + // Ignore malformed referer headers and fall back to English. + } + } + + return 'en' +} + +export function getAfterServiceNotificationRecipient(locale: NotificationLocale = 'en') { + return ( + normalizeRecipient(locale === 'zh' ? process.env.AFTER_SERVICE_NOTIFY_ZH_EMAIL : undefined) || + normalizeRecipient(process.env.AFTER_SERVICE_NOTIFY_EMAIL) || + 'service@eversolo.com' + ) +} + +export function getSalesNotificationRecipient(locale: NotificationLocale = 'en') { + return ( + normalizeRecipient(locale === 'zh' ? process.env.SALES_NOTIFY_ZH_EMAIL : undefined) || + normalizeRecipient(process.env.SALES_NOTIFY_EMAIL) || + 'sales@eversolo.com' + ) } export async function sendSubmissionNotification({ diff --git a/src/payload-types.ts b/src/payload-types.ts index 5c4efc9..cda4b06 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1838,7 +1838,19 @@ export interface Homepage { mobileImage?: (number | null) | Media; video?: (number | null) | Video; externalVideoUrl?: string | null; - textPosition?: ('left' | 'center' | 'right') | null; + textPosition?: + | ( + | 'leftTop' + | 'centerTop' + | 'rightTop' + | 'left' + | 'center' + | 'right' + | 'leftBottom' + | 'centerBottom' + | 'rightBottom' + ) + | null; textFont?: ('default' | 'serif' | 'mono') | null; /** * Use any CSS color value, for example #ffffff, #141617, rgb(20 22 23), or white. @@ -1900,7 +1912,19 @@ export interface Homepage { * Used on mobile first. If empty, mobile falls back to the PC image. */ mobileImage?: (number | null) | Media; - textPosition?: ('left' | 'center' | 'right') | null; + textPosition?: + | ( + | 'leftTop' + | 'centerTop' + | 'rightTop' + | 'left' + | 'center' + | 'right' + | 'leftBottom' + | 'centerBottom' + | 'rightBottom' + ) + | null; textFont?: ('default' | 'serif' | 'mono') | null; /** * Use any CSS color value, for example #141617, #ffffff, rgb(20 22 23), or white. diff --git a/src/payload/globals/Homepage.ts b/src/payload/globals/Homepage.ts index dd6b38e..a590e09 100644 --- a/src/payload/globals/Homepage.ts +++ b/src/payload/globals/Homepage.ts @@ -11,6 +11,32 @@ const seoFields: Field[] = [ { name: 'noIndex', label: { en: 'No Index', zh: '禁止索引' }, type: 'checkbox' as const, defaultValue: false }, ] +const homepageTextPositionOptions = [ + { label: 'Left Top / 左上', value: 'leftTop' }, + { label: 'Center Top / 中上', value: 'centerTop' }, + { label: 'Right Top / 右上', value: 'rightTop' }, + { label: 'Left Center / 左中', value: 'left' }, + { label: 'Center / 居中', value: 'center' }, + { label: 'Right Center / 右中', value: 'right' }, + { label: 'Left Bottom / 左下', value: 'leftBottom' }, + { label: 'Center Bottom / 中下', value: 'centerBottom' }, + { label: 'Right Bottom / 右下', value: 'rightBottom' }, +] + +const homepageTextPositionField: Field = { + name: 'textPosition', + label: { en: 'Text Position', zh: '文字位置' }, + type: 'select', + defaultValue: 'left', + localized: true, + options: homepageTextPositionOptions, +} + +const homepageHeroTextStyleFields: Field[] = [ + homepageTextPositionField, + ...heroTextStyleFields.filter((field) => !('name' in field) || field.name !== 'textPosition'), +] + function localeSortOrderField(): Field { return { name: 'localeOrder', @@ -81,7 +107,7 @@ const heroFields: Field[] = [ label: { en: 'Third-Party Video URL', zh: '第三方视频地址' }, type: 'text' as const, }, - ...heroTextStyleFields, + ...homepageHeroTextStyleFields, { name: 'eyebrow', label: { en: 'Eyebrow', zh: '眉题' }, type: 'text' as const, localized: true }, { name: 'headline', label: { en: 'Headline', zh: '主标题' }, type: 'text' as const, localized: true, required: true }, { name: 'supportingLine', label: { en: 'Supporting Line', zh: '辅助文案' }, type: 'text' as const, localized: true }, @@ -251,11 +277,7 @@ export const Homepage: GlobalConfig = { type: 'select', defaultValue: 'center', localized: true, - options: [ - { label: 'Left / 左侧', value: 'left' }, - { label: 'Center / 居中', value: 'center' }, - { label: 'Right / 右侧', value: 'right' }, - ], + options: homepageTextPositionOptions, }, { name: 'textFont',