self-hosted-plan-item.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. 'use client'
  2. import type { FC, ReactNode } from 'react'
  3. import React from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { RiArrowRightUpLine, RiBrain2Line, RiCheckLine, RiQuestionLine } from '@remixicon/react'
  6. import { SelfHostedPlan } from '../type'
  7. import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../config'
  8. import Toast from '../../base/toast'
  9. import Tooltip from '../../base/tooltip'
  10. import { Asterisk, AwsMarketplace, Azure, Buildings, Diamond, GoogleCloud } from '../../base/icons/src/public/billing'
  11. import type { PlanRange } from './select-plan-range'
  12. import cn from '@/utils/classnames'
  13. import { useAppContext } from '@/context/app-context'
  14. type Props = {
  15. plan: SelfHostedPlan
  16. planRange: PlanRange
  17. canPay: boolean
  18. }
  19. const KeyValue = ({ label, tooltip, textColor, tooltipIconColor }: { icon: ReactNode; label: string; tooltip?: string; textColor: string; tooltipIconColor: string }) => {
  20. return (
  21. <div className={cn('flex', textColor)}>
  22. <div className='flex size-4 items-center justify-center'>
  23. <RiCheckLine />
  24. </div>
  25. <div className={cn('system-sm-regular ml-2 mr-0.5', textColor)}>{label}</div>
  26. {tooltip && (
  27. <Tooltip
  28. asChild
  29. popupContent={tooltip}
  30. popupClassName='w-[200px]'
  31. >
  32. <div className='flex size-4 items-center justify-center'>
  33. <RiQuestionLine className={cn(tooltipIconColor)} />
  34. </div>
  35. </Tooltip>
  36. )}
  37. </div>
  38. )
  39. }
  40. const style = {
  41. [SelfHostedPlan.community]: {
  42. icon: <Asterisk className='size-7 text-text-primary' />,
  43. title: 'text-text-primary',
  44. price: 'text-text-primary',
  45. priceTip: 'text-text-tertiary',
  46. description: 'text-util-colors-gray-gray-600',
  47. bg: 'border-effects-highlight-lightmode-off bg-background-section-burn',
  48. btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary',
  49. values: 'text-text-secondary',
  50. tooltipIconColor: 'text-text-tertiary',
  51. },
  52. [SelfHostedPlan.premium]: {
  53. icon: <Diamond className='size-7 text-text-warning' />,
  54. title: 'text-text-primary',
  55. price: 'text-text-primary',
  56. priceTip: 'text-text-tertiary',
  57. description: 'text-text-warning',
  58. bg: 'border-effects-highlight bg-background-section-burn',
  59. btnStyle: 'bg-third-party-aws hover:bg-third-party-aws-hover border border-components-button-primary-border text-text-primary-on-surface shadow-xs',
  60. values: 'text-text-secondary',
  61. tooltipIconColor: 'text-text-tertiary',
  62. },
  63. [SelfHostedPlan.enterprise]: {
  64. icon: <Buildings className='size-7 text-text-primary-on-surface' />,
  65. title: 'text-text-primary-on-surface',
  66. price: 'text-text-primary-on-surface',
  67. priceTip: 'text-text-primary-on-surface',
  68. description: 'text-text-primary-on-surface',
  69. bg: 'border-effects-highlight bg-[#155AEF] text-text-primary-on-surface',
  70. btnStyle: 'bg-white bg-opacity-96 hover:opacity-85 border-[0.5px] border-components-button-secondary-border text-[#155AEF] shadow-xs',
  71. values: 'text-text-primary-on-surface',
  72. tooltipIconColor: 'text-text-primary-on-surface',
  73. },
  74. }
  75. const SelfHostedPlanItem: FC<Props> = ({
  76. plan,
  77. }) => {
  78. const { t } = useTranslation()
  79. const isFreePlan = plan === SelfHostedPlan.community
  80. const isPremiumPlan = plan === SelfHostedPlan.premium
  81. const i18nPrefix = `billing.plans.${plan}`
  82. const isEnterprisePlan = plan === SelfHostedPlan.enterprise
  83. const { isCurrentWorkspaceManager } = useAppContext()
  84. const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[]
  85. const handleGetPayUrl = () => {
  86. // Only workspace manager can buy plan
  87. if (!isCurrentWorkspaceManager) {
  88. Toast.notify({
  89. type: 'error',
  90. message: t('billing.buyPermissionDeniedTip'),
  91. className: 'z-[1001]',
  92. })
  93. return
  94. }
  95. if (isFreePlan) {
  96. window.location.href = getStartedWithCommunityUrl
  97. return
  98. }
  99. if (isPremiumPlan) {
  100. window.location.href = getWithPremiumUrl
  101. return
  102. }
  103. if (isEnterprisePlan)
  104. window.location.href = contactSalesUrl
  105. }
  106. return (
  107. <div className={cn(`relative flex w-[374px] flex-col overflow-hidden rounded-2xl
  108. border-[0.5px] hover:border-effects-highlight hover:shadow-lg hover:backdrop-blur-[5px]`, style[plan].bg)}>
  109. <div>
  110. <div className={cn(isEnterprisePlan ? 'z-1 absolute bottom-0 left-0 right-0 top-0 bg-price-enterprise-background' : '')} >
  111. </div>
  112. {isEnterprisePlan && <div className='z-15 absolute -left-[90px] -top-[104px] size-[341px] rounded-full bg-[#09328c] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
  113. {isEnterprisePlan && <div className='z-15 absolute -bottom-[72px] -right-[40px] size-[341px] rounded-full bg-[#e2eafb] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
  114. </div>
  115. <div className='relative z-10 min-h-[559px] w-full p-6'>
  116. <div className=' flex min-h-[108px] flex-col gap-y-1'>
  117. {style[plan].icon}
  118. <div className='flex items-center'>
  119. <div className={cn('system-md-semibold uppercase leading-[125%]', style[plan].title)}>{t(`${i18nPrefix}.name`)}</div>
  120. </div>
  121. <div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
  122. </div>
  123. <div className='my-3'>
  124. <div className='flex items-end'>
  125. <div className={cn('shrink-0 text-[28px] font-bold leading-[125%]', style[plan].price)}>{t(`${i18nPrefix}.price`)}</div>
  126. {!isFreePlan
  127. && <span className={cn('ml-2 py-1 text-[14px] font-normal leading-normal', style[plan].priceTip)}>
  128. {t(`${i18nPrefix}.priceTip`)}
  129. </span>}
  130. </div>
  131. </div>
  132. <div
  133. className={cn('system-md-semibold flex h-[44px] cursor-pointer items-center justify-center rounded-full px-5 py-3',
  134. style[plan].btnStyle)}
  135. onClick={handleGetPayUrl}
  136. >
  137. {t(`${i18nPrefix}.btnText`)}
  138. {isPremiumPlan
  139. && <>
  140. <div className='mx-1 pt-[6px]'>
  141. <AwsMarketplace className='h-6' />
  142. </div>
  143. <RiArrowRightUpLine className='size-4' />
  144. </>}
  145. </div>
  146. <div className={cn('system-sm-semibold mb-2 mt-6', style[plan].values)}>{t(`${i18nPrefix}.includesTitle`)}</div>
  147. <div className='flex flex-col gap-y-3'>
  148. {features.map(v =>
  149. <KeyValue key={`${plan}-${v}`}
  150. textColor={style[plan].values}
  151. tooltipIconColor={style[plan].tooltipIconColor}
  152. icon={<RiBrain2Line />}
  153. label={v}
  154. />)}
  155. </div>
  156. {isPremiumPlan && <div className='mt-[68px]'>
  157. <div className='flex items-center gap-x-1'>
  158. <div className='flex size-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default shadow-xs'>
  159. <Azure />
  160. </div>
  161. <div className='flex size-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default shadow-xs'>
  162. <GoogleCloud />
  163. </div>
  164. </div>
  165. <span className={cn('system-xs-regular mt-2', style[plan].tooltipIconColor)}>{t('billing.plans.premium.comingSoon')}</span>
  166. </div>}
  167. </div>
  168. </div>
  169. )
  170. }
  171. export default React.memo(SelfHostedPlanItem)