diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index d66616bcd5..b695302965 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -50,9 +50,15 @@ const PlanComp: FC = ({ const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE ? reset.triggerEvents ?? undefined : undefined - const apiRateLimitResetInDays = type === Plan.sandbox && total.apiRateLimit !== NUM_INFINITE - ? getDaysUntilEndOfMonth() - : undefined + const apiRateLimitResetInDays = (() => { + if (total.apiRateLimit === NUM_INFINITE) + return undefined + if (typeof reset.apiRateLimit === 'number') + return reset.apiRateLimit + if (type === Plan.sandbox) + return getDaysUntilEndOfMonth() + return undefined + })() const [showModal, setShowModal] = React.useState(false) const { mutateAsync } = useEducationVerify() diff --git a/web/app/components/billing/trigger-events-limit-modal/index.module.css b/web/app/components/billing/trigger-events-limit-modal/index.module.css new file mode 100644 index 0000000000..e8e86719e6 --- /dev/null +++ b/web/app/components/billing/trigger-events-limit-modal/index.module.css @@ -0,0 +1,30 @@ +.surface { + border: 0.5px solid var(--color-components-panel-border, rgba(16, 24, 40, 0.08)); + background: + linear-gradient(109deg, var(--color-background-section, #f9fafb) 0%, var(--color-background-section-burn, #f2f4f7) 100%), + var(--color-components-panel-bg, #fff); +} + +.heroOverlay { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='54' height='54' fill='none'%3E%3Crect x='1' y='1' width='48' height='48' rx='12' stroke='rgba(16, 24, 40, 0.3)' stroke-width='1' opacity='0.08'/%3E%3C/svg%3E"); + background-size: 54px 54px; + background-position: 31px -23px; + background-repeat: repeat; + mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%); + -webkit-mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%); +} + +.icon { + border: 0.5px solid transparent; + background: + linear-gradient(180deg, var(--color-components-avatar-bg-mask-stop-0, rgba(255, 255, 255, 0.12)) 0%, var(--color-components-avatar-bg-mask-stop-100, rgba(255, 255, 255, 0.08)) 100%), + var(--color-util-colors-blue-brand-blue-brand-500, #296dff); + box-shadow: 0 10px 20px color-mix(in srgb, var(--color-util-colors-blue-brand-blue-brand-500, #296dff) 35%, transparent); +} + +.highlight { + background: linear-gradient(97deg, var(--color-components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -4%, var(--color-components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/web/app/components/billing/trigger-events-limit-modal/index.stories.tsx b/web/app/components/billing/trigger-events-limit-modal/index.stories.tsx new file mode 100644 index 0000000000..3e2be3f72e --- /dev/null +++ b/web/app/components/billing/trigger-events-limit-modal/index.stories.tsx @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import React, { useEffect, useState } from 'react' +import i18next from 'i18next' +import { I18nextProvider } from 'react-i18next' +import TriggerEventsLimitModal from '.' +import { Plan } from '../type' + +const i18n = i18next.createInstance() +i18n.init({ + lng: 'en', + resources: { + en: { + translation: { + billing: { + triggerLimitModal: { + title: 'Upgrade to unlock more trigger events', + description: 'You’ve reached the limit of workflow event triggers for this plan.', + dismiss: 'Dismiss', + upgrade: 'Upgrade', + usageTitle: 'TRIGGER EVENTS', + }, + usagePage: { + triggerEvents: 'Trigger Events', + resetsIn: 'Resets in {{count, number}} days', + }, + upgradeBtn: { + encourage: 'Upgrade Now', + encourageShort: 'Upgrade', + plain: 'View Plan', + }, + }, + }, + }, + }, +}) + +const Template = (args: React.ComponentProps) => { + const [visible, setVisible] = useState(args.show ?? true) + useEffect(() => { + setVisible(args.show ?? true) + }, [args.show]) + const handleHide = () => setVisible(false) + return ( + +
+ + +
+
+ ) +} + +const meta = { + title: 'Billing/TriggerEventsLimitModal', + component: TriggerEventsLimitModal, + parameters: { + layout: 'centered', + }, + args: { + show: true, + usage: 120, + total: 120, + resetInDays: 5, + planType: Plan.professional, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Professional: Story = { + args: { + onDismiss: () => { /* noop */ }, + onUpgrade: () => { /* noop */ }, + }, + render: args =>