From d8a9645e831bddb558ecece6cfc9ad8b56f92759 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 14 Aug 2025 15:01:38 +0800 Subject: [PATCH] feat: Implement billing plan selection UI with plan switcher and range options --- web/app/components/base/button/index.css | 2 + web/app/components/billing/pricing/header.tsx | 42 +++++++++++ web/app/components/billing/pricing/index.tsx | 72 +++++++++++-------- .../billing/pricing/plan-switcher/index.tsx | 64 +++++++++++++++++ .../plan-switcher/plan-range-switcher.tsx | 38 ++++++++++ .../billing/pricing/plan-switcher/tab.tsx | 37 ++++++++++ web/app/layout.tsx | 21 ++++-- web/i18n/en-US/billing.ts | 7 +- web/i18n/zh-Hans/billing.ts | 7 +- web/tailwind-common-config.ts | 4 ++ web/themes/manual-dark.css | 1 + web/themes/manual-light.css | 1 + 12 files changed, 256 insertions(+), 40 deletions(-) create mode 100644 web/app/components/billing/pricing/header.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/index.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/plan-range-switcher.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/tab.tsx diff --git a/web/app/components/base/button/index.css b/web/app/components/base/button/index.css index 47e59142cc..5899c027d3 100644 --- a/web/app/components/base/button/index.css +++ b/web/app/components/base/button/index.css @@ -60,6 +60,7 @@ @apply border-[0.5px] shadow-xs + backdrop-blur-[5px] bg-components-button-secondary-bg border-components-button-secondary-border hover:bg-components-button-secondary-bg-hover @@ -69,6 +70,7 @@ .btn-secondary.btn-disabled { @apply + backdrop-blur-sm bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled; diff --git a/web/app/components/billing/pricing/header.tsx b/web/app/components/billing/pricing/header.tsx new file mode 100644 index 0000000000..60f6d3d248 --- /dev/null +++ b/web/app/components/billing/pricing/header.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import DifyLogo from '../../base/logo/dify-logo' +import { useTranslation } from 'react-i18next' +import Button from '../../base/button' +import { RiCloseLine } from '@remixicon/react' + +type HeaderProps = { + onClose: () => void +} + +const Header = ({ + onClose, +}: HeaderProps) => { + const { t } = useTranslation() + + return ( +
+
+
+
+ +
+ + {t('billing.plansCommon.title.plans')} + +
+

+ {t('billing.plansCommon.title.description')} +

+ +
+
+ ) +} + +export default React.memo(Header) diff --git a/web/app/components/billing/pricing/index.tsx b/web/app/components/billing/pricing/index.tsx index 37d85023af..28a43482e9 100644 --- a/web/app/components/billing/pricing/index.tsx +++ b/web/app/components/billing/pricing/index.tsx @@ -1,51 +1,63 @@ 'use client' import type { FC } from 'react' -import React from 'react' +import React, { useState } from 'react' import { createPortal } from 'react-dom' -import { useTranslation } from 'react-i18next' -import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react' -import Link from 'next/link' +import Header from './header' +import PlanSwitcher from './plan-switcher' +import { PlanRange } from './plan-switcher/plan-range-switcher' import { useKeyPress } from 'ahooks' -import { Plan, SelfHostedPlan } from '../type' -import TabSlider from '../../base/tab-slider' -import SelectPlanRange, { PlanRange } from './select-plan-range' -import PlanItem from './plan-item' -import SelfHostedPlanItem from './self-hosted-plan-item' -import { useProviderContext } from '@/context/provider-context' -import GridMask from '@/app/components/base/grid-mask' -import { useAppContext } from '@/context/app-context' -import classNames from '@/utils/classnames' -import { useGetPricingPageLanguage } from '@/context/i18n' +// import { useTranslation } from 'react-i18next' +// import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react' +// import Link from 'next/link' +// import { Plan, SelfHostedPlan } from '../type' +// import TabSlider from '../../base/tab-slider' +// import PlanItem from './plan-item' +// import SelfHostedPlanItem from './self-hosted-plan-item' +// import { useProviderContext } from '@/context/provider-context' +// import GridMask from '@/app/components/base/grid-mask' +// import { useAppContext } from '@/context/app-context' +// import classNames from '@/utils/classnames' +// import { useGetPricingPageLanguage } from '@/context/i18n' -type Props = { +export type Category = 'cloud' | 'self' + +type PricingProps = { onCancel: () => void } -const Pricing: FC = ({ +const Pricing: FC = ({ onCancel, }) => { - const { t } = useTranslation() - const { plan } = useProviderContext() - const { isCurrentWorkspaceManager } = useAppContext() - const canPay = isCurrentWorkspaceManager + // const { t } = useTranslation() + // const { plan } = useProviderContext() + // const { isCurrentWorkspaceManager } = useAppContext() + // const canPay = isCurrentWorkspaceManager const [planRange, setPlanRange] = React.useState(PlanRange.monthly) - const [currentPlan, setCurrentPlan] = React.useState('cloud') + const [currentCategory, setCurrentCategory] = useState('cloud') useKeyPress(['esc'], onCancel) - const pricingPageLanguage = useGetPricingPageLanguage() - const pricingPageURL = pricingPageLanguage - ? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features` - : 'https://dify.ai/pricing#plans-and-features' + // const pricingPageLanguage = useGetPricingPageLanguage() + // const pricingPageURL = pricingPageLanguage + // ? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features` + // : 'https://dify.ai/pricing#plans-and-features' return createPortal(
e.stopPropagation()} > -
-
+
+ + + {/*
@@ -137,8 +149,8 @@ const Pricing: FC = ({
- -
+ */} +
, document.body, ) diff --git a/web/app/components/billing/pricing/plan-switcher/index.tsx b/web/app/components/billing/pricing/plan-switcher/index.tsx new file mode 100644 index 0000000000..8fa9fe9a9f --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/index.tsx @@ -0,0 +1,64 @@ +import type { FC } from 'react' +import React from 'react' +import type { Category } from '../index' +import { useTranslation } from 'react-i18next' +import { Ri24HoursFill, RiTerminalBoxFill } from '@remixicon/react' +import Tab from './tab' +import Divider from '@/app/components/base/divider' +import type { PlanRange } from './plan-range-switcher' +import PlanRangeSwitcher from './plan-range-switcher' + +type PlanSwitcherProps = { + currentCategory: Category + currentPlanRange: PlanRange + onChangeCategory: (category: Category) => void + onChangePlanRange: (value: PlanRange) => void +} + +const PlanSwitcher: FC = ({ + currentCategory, + currentPlanRange, + onChangeCategory, + onChangePlanRange, +}) => { + const { t } = useTranslation() + + const tabs = { + cloud: { + value: 'cloud' as Category, + label: t('billing.plansCommon.cloud'), + Icon: Ri24HoursFill, + }, + self: { + value: 'self' as Category, + label: t('billing.plansCommon.self'), + Icon: RiTerminalBoxFill, + }, + } + + return ( +
+
+
+ + {...tabs.cloud} + isActive={currentCategory === tabs.cloud.value} + onClick={onChangeCategory} + /> + + + {...tabs.self} + isActive={currentCategory === tabs.self.value} + onClick={onChangeCategory} + /> +
+ +
+
+ ) +} + +export default React.memo(PlanSwitcher) diff --git a/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.tsx b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.tsx new file mode 100644 index 0000000000..75276429fb --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.tsx @@ -0,0 +1,38 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import Switch from '../../../base/switch' + +export enum PlanRange { + monthly = 'monthly', + yearly = 'yearly', +} + +type PlanRangeSwitcherProps = { + value: PlanRange + onChange: (value: PlanRange) => void +} + +const PlanRangeSwitcher: FC = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + + return ( +
+ { + onChange(v ? PlanRange.yearly : PlanRange.monthly) + }} + /> + + {t('billing.plansCommon.annualBilling', { percent: 17 })} + +
+ ) +} +export default React.memo(PlanRangeSwitcher) diff --git a/web/app/components/billing/pricing/plan-switcher/tab.tsx b/web/app/components/billing/pricing/plan-switcher/tab.tsx new file mode 100644 index 0000000000..c961b2da0b --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/tab.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from 'react' +import cn from '@/utils/classnames' + +type TabProps = { + Icon: React.ComponentType + value: T + label: string + isActive: boolean + onClick: (value: T) => void +} + +const Tab = ({ + Icon, + value, + label, + isActive, + onClick, +}: TabProps) => { + const handleClick = useCallback(() => { + onClick(value) + }, [onClick, value]) + + return ( +
+ + {label} +
+ ) +} + +export default React.memo(Tab) as typeof Tab diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 46afd95b97..60bd734f0c 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -10,6 +10,8 @@ import './styles/globals.css' import './styles/markdown.scss' import GlobalPublicStoreProvider from '@/context/global-public-context' import { DatasetAttr } from '@/types/feature' +import { Instrument_Serif } from 'next/font/google' +import cn from '@/utils/classnames' export const viewport: Viewport = { width: 'device-width', @@ -19,6 +21,13 @@ export const viewport: Viewport = { userScalable: false, } +const instrumentSerif = Instrument_Serif({ + weight: ['400'], + style: ['normal', 'italic'], + subsets: ['latin'], + variable: '--font-instrument-serif', +}) + const LocaleLayout = async ({ children, }: { @@ -51,15 +60,15 @@ const LocaleLayout = async ({ } return ( - + - - - - + + + +