Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions apps/landing-page/app/_components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ const FEISHU_GROUP_QR = '/community/feishu-group-qr.png';
const FEISHU_MARK = '/launch-week/feishu-mark.png';
const X_PROFILE = 'https://x.com/OpenDesignHQ';

// Pricing can opt into the existing Cloud account menu without restoring it
// across the marketing site. The enhancer reads these values from data-*
// because its inline script cannot access import.meta.env at runtime.
const env = (import.meta.env ?? {}) as Record<string, string | undefined>;
const CLOUD_API_BASE =
env.PUBLIC_CLOUD_API_BASE ?? env.PUBLIC_AMR_API_BASE ?? 'https://amr-api.open-design.ai';
const CLOUD_CONSOLE_URL =
env.PUBLIC_CLOUD_CONSOLE_URL ??
env.PUBLIC_AMR_CONSOLE_URL ??
'https://open-design.ai/cloud/dashboard?source=open_design';

// Solution → Use cases / Roles. Hrefs mirror upstream main's header 1:1 and
// pair positionally with the localized `useCaseItems` / `roleItems` tuples.
const USE_CASE_HREFS = [
Expand Down Expand Up @@ -155,6 +166,8 @@ export interface HeaderProps {
copy?: HeaderCopy;
/** Brand link target — `#top` on the homepage, `/` on sub-pages. */
brandHref?: string;
/** Render the signed-in Cloud avatar/menu. Disabled on marketing pages by default. */
showAccount?: boolean;
}

export function Header({
Expand All @@ -164,6 +177,7 @@ export function Header({
locale = DEFAULT_LOCALE,
copy,
brandHref = '#top',
showAccount = false,
}: HeaderProps) {
const headerCopy = copy ?? getCommonCopy(locale).header;
const href = (path: string) => localizedHref(path, locale);
Expand Down Expand Up @@ -623,6 +637,53 @@ export function Header({
>
{headerCopy.download}
</a>
{showAccount ? (
<div
className='nav-account'
data-amr-account
data-amr-api={CLOUD_API_BASE}
data-amr-console={CLOUD_CONSOLE_URL}
>
<details className='nav-account-menu' data-amr-menu hidden>
<summary
className='nav-account-trigger'
aria-label={headerCopy.accountAria}
title={headerCopy.accountAria}
>
<img className='nav-avatar' alt='' data-amr-avatar />
<span
className='nav-avatar-fallback'
data-amr-avatar-fallback
aria-hidden='true'
/>
</summary>
<div className='nav-account-dropdown' role='menu'>
<div className='nav-account-id'>
<span className='nav-account-name' data-amr-name />
<span className='nav-account-email' data-amr-email />
</div>
<a
className='nav-account-item'
role='menuitem'
href={CLOUD_CONSOLE_URL}
target='_blank'
rel='noreferrer noopener'
data-amr-console-link
>
{headerCopy.menuConsole}
</a>
<button
type='button'
className='nav-account-item nav-account-signout'
role='menuitem'
data-amr-signout
>
{headerCopy.menuSignOut}
</button>
</div>
</details>
</div>
) : null}
</div>
</div>
{/*
Expand Down
16 changes: 12 additions & 4 deletions apps/landing-page/app/_components/pricing-individual-plans.astro
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,13 @@ const cardData = [
tier: 'go' as const,
logo: null,
view: goView,
regularMonthlyPrice: GO_PLAN.monthly.priceUsd,
},
...tiers.map((tier) => ({
tier: tier.tier as PlanTierId,
logo: `/pricing/plan-${tier.tier}.svg`,
view: individualView(tier),
regularMonthlyPrice: tier.monthly.priceUsd,
})),
];

Expand Down Expand Up @@ -321,7 +323,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
>
<div class="benefits-summary-stage">
<div class="four-tier-grid decision-grid">
{cardData.map(({ tier, logo, view }) => {
{cardData.map(({ tier, logo, view, regularMonthlyPrice }) => {
const popular = orderedPopularModels();
const flagship = tier === 'plus' ? plusFlagshipModels : flagshipModels;
const imageResolution = tier === 'plus' ? '1K' : tier === 'pro' ? '2K' : '4K';
Expand All @@ -345,17 +347,22 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
)}
</div>
<div class="plan-badges">
<span class="discount-corner-badge" data-when="monthly">{fillTemplate(L.firstMonthTag, { pct: String(view.monthly.discount) })}</span>
<span class="discount-corner-badge" data-when="monthly" data-intro-only>{fillTemplate(L.firstMonthTag, { pct: String(view.monthly.discount) })}</span>
<span class="discount-corner-badge" data-when="yearly">{fillTemplate(L.yearlyDiscountTag, { pct: String(view.yearly.discount) })}</span>
</div>
</div>
<p>{tierTaglines[tier]}</p>
</header>

<div class="price" data-when="monthly">
<span>$</span><strong class="rolling-price-number">{view.monthly.amount}</strong><small>{L.perMonth}</small><del>{view.monthly.strike}</del>
<span>$</span><strong
class="rolling-price-number"
data-monthly-price
data-intro-price={view.monthly.amount}
data-regular-price={formatUsd(regularMonthlyPrice).replace('$', '')}
>{view.monthly.amount}</strong><small>{L.perMonth}</small><del data-intro-only>{view.monthly.strike}</del>
</div>
<div class="compact-price-detail" data-when="monthly">
<div class="compact-price-detail" data-when="monthly" data-intro-only>
<span class="price-savings-note">{fillTemplate(P.saveAmount, { amount: view.monthly.saving })}</span>
<small>{view.monthly.billed}</small>
</div>
Expand All @@ -372,6 +379,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
data-pricing-cta
data-tier={tier}
data-plan-name={tier.charAt(0).toUpperCase() + tier.slice(1)}
data-default-label={tierCopy[tier].ctaLabel}
>
<span>{tierCopy[tier].ctaLabel}</span>
</a>
Expand Down
4 changes: 4 additions & 0 deletions apps/landing-page/app/_components/sub-page-layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface Props {
* max-width + side padding.
*/
mainClassName?: string;
/** Opt into the signed-in Cloud account menu for this route only. */
showHeaderAccount?: boolean;
}

const {
Expand All @@ -82,6 +84,7 @@ const {
localeCanonicalOnly = false,
suppressLocaleAutoRedirect = false,
mainClassName = 'sub-main container',
showHeaderAccount = false,
} = Astro.props;
// Clamp the meta/OG/Twitter description to a search-friendly length (the full
// string still flows into JSON-LD untouched).
Expand Down Expand Up @@ -119,6 +122,7 @@ const headerHtml = renderToStaticMarkup(
localeSwitcher: getHeaderLocaleSwitcher(locale, Astro.url.pathname, {
canonicalOnly: localeCanonicalOnly,
}),
showAccount: showHeaderAccount,
}) as ReturnType<typeof createElement>,
);
const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
Expand Down
60 changes: 48 additions & 12 deletions apps/landing-page/app/_lib/pricing-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,65 @@ import type { LandingLocaleCode } from '../i18n';

export interface PricingPlanActionLabels {
current: string;
currentCanceling: string;
downgrade: string;
upgrade: string;
intervalUpgrade: string;
switchBackToInterval: string;
intervalDowngradeUnavailable: string;
checkoutUnavailable: string;
scheduled: string;
}

const PLAN_ACTION_LABELS: Partial<Record<LandingLocaleCode, PricingPlanActionLabels>> = {
en: { current: 'Current plan', downgrade: 'Downgrade to {plan}', upgrade: 'Upgrade to {plan}' },
zh: { current: '当前套餐', downgrade: '降级至 {plan}', upgrade: '升级至 {plan}' },
'zh-tw': { current: '目前方案', downgrade: '降級至 {plan}', upgrade: '升級至 {plan}' },
ja: { current: '現在のプラン', downgrade: '{plan} にダウングレード', upgrade: '{plan} にアップグレード' },
ko: { current: '현재 요금제', downgrade: '{plan}(으)로 다운그레이드', upgrade: '{plan}(으)로 업그레이드' },
de: { current: 'Aktueller Tarif', downgrade: 'Auf {plan} downgraden', upgrade: 'Auf {plan} upgraden' },
fr: { current: 'Offre actuelle', downgrade: 'Rétrograder vers {plan}', upgrade: 'Passer à {plan}' },
ru: { current: 'Текущий тариф', downgrade: 'Понизить до {plan}', upgrade: 'Повысить до {plan}' },
es: { current: 'Plan actual', downgrade: 'Bajar a {plan}', upgrade: 'Subir a {plan}' },
'pt-br': { current: 'Plano atual', downgrade: 'Fazer downgrade para {plan}', upgrade: 'Fazer upgrade para {plan}' },
const EN_PLAN_ACTION_LABELS: PricingPlanActionLabels = {
current: 'Current plan',
currentCanceling: 'Current plan · Cancels at period end',
downgrade: 'Downgrade to {plan}',
upgrade: 'Upgrade to {plan}',
intervalUpgrade: 'Switch {plan} to {interval}',
switchBackToInterval: 'Switch back to {interval} before upgrading',
intervalDowngradeUnavailable: 'Cancel your subscription to change plans',
checkoutUnavailable: 'Personal checkout unavailable for this account',
scheduled: '{plan} · Scheduled',
};

const PLAN_ACTION_LABELS: Partial<Record<LandingLocaleCode, Partial<PricingPlanActionLabels>>> = {
en: EN_PLAN_ACTION_LABELS,
zh: {
current: '当前套餐', currentCanceling: '当前套餐 · 将在周期结束时取消', downgrade: '降级至 {plan}', upgrade: '升级至 {plan}', intervalUpgrade: '将 {plan} 改为{interval}', switchBackToInterval: '请先切回{interval}再升级', intervalDowngradeUnavailable: '取消订阅后可变更套餐', checkoutUnavailable: '此账户暂不能购买个人套餐', scheduled: '{plan} · 已安排',
},
'zh-tw': {
current: '目前方案', currentCanceling: '目前方案 · 將於週期結束時取消', downgrade: '降級至 {plan}', upgrade: '升級至 {plan}', intervalUpgrade: '將 {plan} 改為{interval}', switchBackToInterval: '請先切回{interval}再升級', intervalDowngradeUnavailable: '取消訂閱後可變更方案', checkoutUnavailable: '此帳戶暫不能購買個人方案', scheduled: '{plan} · 已排程',
},
ja: {
current: '現在のプラン', currentCanceling: '現在のプラン · 期間終了時に解約', downgrade: '{plan} にダウングレード', upgrade: '{plan} にアップグレード', intervalUpgrade: '{plan} を{interval}に変更', switchBackToInterval: 'アップグレードする前に{interval}へ戻してください', intervalDowngradeUnavailable: 'プランを変更するにはサブスクリプションを解約してください', checkoutUnavailable: 'このアカウントでは個人プランを購入できません', scheduled: '{plan} · 予約済み',
},
ko: {
current: '현재 요금제', currentCanceling: '현재 요금제 · 기간 종료 시 해지', downgrade: '{plan}(으)로 다운그레이드', upgrade: '{plan}(으)로 업그레이드', intervalUpgrade: '{plan}을(를) {interval}(으)로 변경', switchBackToInterval: '업그레이드 전에 {interval}(으)로 돌아가세요', intervalDowngradeUnavailable: '요금제를 변경하려면 구독을 취소하세요', checkoutUnavailable: '이 계정에서는 개인 요금제를 구매할 수 없습니다', scheduled: '{plan} · 예약됨',
},
de: {
current: 'Aktueller Tarif', currentCanceling: 'Aktueller Tarif · Endet zum Periodenende', downgrade: 'Auf {plan} downgraden', upgrade: 'Auf {plan} upgraden', intervalUpgrade: '{plan} auf {interval} umstellen', switchBackToInterval: 'Vor dem Upgrade zu {interval} zurückwechseln', intervalDowngradeUnavailable: 'Kündige dein Abonnement, um den Tarif zu wechseln', checkoutUnavailable: 'Persönliche Tarife sind für dieses Konto nicht verfügbar', scheduled: '{plan} · Geplant',
},
fr: {
current: 'Offre actuelle', currentCanceling: 'Offre actuelle · Résiliation en fin de période', downgrade: 'Rétrograder vers {plan}', upgrade: 'Passer à {plan}', intervalUpgrade: 'Passer {plan} en {interval}', switchBackToInterval: 'Revenez à {interval} avant la mise à niveau', intervalDowngradeUnavailable: 'Résiliez votre abonnement pour changer d’offre', checkoutUnavailable: 'Les offres personnelles sont indisponibles pour ce compte', scheduled: '{plan} · Planifié',
},
ru: {
current: 'Текущий тариф', currentCanceling: 'Текущий тариф · Отмена в конце периода', downgrade: 'Понизить до {plan}', upgrade: 'Повысить до {plan}', intervalUpgrade: 'Перевести {plan} на {interval}', switchBackToInterval: 'Перед повышением вернитесь на {interval}', intervalDowngradeUnavailable: 'Отмените подписку, чтобы изменить тариф', checkoutUnavailable: 'Личные тарифы недоступны для этого аккаунта', scheduled: '{plan} · Запланировано',
},
es: {
current: 'Plan actual', currentCanceling: 'Plan actual · Se cancela al final del periodo', downgrade: 'Bajar a {plan}', upgrade: 'Subir a {plan}', intervalUpgrade: 'Cambiar {plan} a {interval}', switchBackToInterval: 'Vuelve a {interval} antes de subir de plan', intervalDowngradeUnavailable: 'Cancela tu suscripción para cambiar de plan', checkoutUnavailable: 'Los planes personales no están disponibles para esta cuenta', scheduled: '{plan} · Programado',
},
'pt-br': {
current: 'Plano atual', currentCanceling: 'Plano atual · Cancela ao fim do período', downgrade: 'Fazer downgrade para {plan}', upgrade: 'Fazer upgrade para {plan}', intervalUpgrade: 'Mudar {plan} para {interval}', switchBackToInterval: 'Volte para {interval} antes de fazer upgrade', intervalDowngradeUnavailable: 'Cancele a assinatura para mudar de plano', checkoutUnavailable: 'Planos pessoais não estão disponíveis para esta conta', scheduled: '{plan} · Agendado',
},
};

export function getCurrentPlanLabel(locale: LandingLocaleCode): string {
return getPricingPlanActionLabels(locale).current;
}

export function getPricingPlanActionLabels(locale: LandingLocaleCode): PricingPlanActionLabels {
return PLAN_ACTION_LABELS[locale] ?? PLAN_ACTION_LABELS.en!;
return { ...EN_PLAN_ACTION_LABELS, ...(PLAN_ACTION_LABELS[locale] ?? {}) };
}

export type PlanTierId = 'plus' | 'pro' | 'max';
Expand Down
Loading
Loading