Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactElement, ReactNode } from 'react';
import { Stat, Table, TBody, Td, TFoot, Th, THead, Tr } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { BillingPlanType } from '@/gql/graphql';
import { CurrencyFormatter } from './helpers';
import { CurrencyFormatter, formatOperations } from './helpers';

const PriceEstimationTable_PlanFragment = graphql(`
fragment PriceEstimationTable_PlanFragment on BillingPlan {
Expand Down Expand Up @@ -48,15 +48,15 @@ function PriceEstimationTable(props: {
<Td>
Included Operations <span className="text-neutral-10">(free)</span>
</Td>
<Td align="right">{includedOperationsInMillions}M</Td>
<Td align="right">{formatOperations(includedOperationsInMillions)}</Td>
<Td align="right">{CurrencyFormatter.format(0)}</Td>
<Td align="right">{CurrencyFormatter.format(0)}</Td>
</Tr>
)}
{plan.planType === BillingPlanType.Pro && (
<Tr>
<Td>Operations</Td>
<Td align="right">{additionalOperations}M</Td>
<Td align="right">{formatOperations(additionalOperations)}</Td>
<Td align="right">{CurrencyFormatter.format(plan.pricePerOperationsUnit ?? 0)}</Td>
<Td align="right">{CurrencyFormatter.format(operationsTotal)}</Td>
</Tr>
Expand Down Expand Up @@ -114,7 +114,7 @@ export function PlanSummary({
<Stat>
<Stat.Label>Operations Limit</Stat.Label>
<Stat.HelpText>up to</Stat.HelpText>
<Stat.Number>{operationsRateLimit}M</Stat.Number>
<Stat.Number>{formatOperations(operationsRateLimit)}</Stat.Number>
<Stat.HelpText>per month</Stat.HelpText>
</Stat>
<Stat className="mb-8">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ export const DateFormatter = Intl.DateTimeFormat('en', {
month: 'short',
day: 'numeric',
});

/** Format millions of operations as "500M" or "2.5B" */
export function formatOperations(millions: number): string {
if (millions >= 1000) {
const b = parseFloat((millions / 1000).toFixed(3));
return `${b}B`;
}
return `${millions}M`;
}
10 changes: 6 additions & 4 deletions packages/web/app/src/pages/organization-subscription-manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,16 +379,18 @@ function Inner(props: {
<div className="mt-5 pl-2.5">
<Slider
min={1}
max={300}
max={5000}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Increasing the slider maximum to 5000 (5B) on a linear scale makes it extremely difficult to select lower values precisely. For instance, 100M is now at the 2% mark. Consider using a logarithmic scale or adding a numeric input field for better precision, especially since intermediate labels like 100M/200M were removed.

disabled={isFetching}
value={[operationsRateLimit]}
onValueChange={onOperationsRateLimitChange}
/>
<div className="flex justify-between">
<span>1M</span>
<span>100M</span>
<span>200M</span>
<span>300M</span>
<span>1B</span>
<span>2B</span>
<span>3B</span>
<span>4B</span>
<span>5B</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/web/docs/src/components/pricing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function Pricing({ className }: { className?: string }): ReactElement {
<PricingSlider
className="mt-6 lg:mt-12"
onChange={value => {
const newPlan = value === 1 ? 'Hobby' : value < 280 ? 'Pro' : 'Enterprise';
const newPlan = value === 1 ? 'Hobby' : value < 4800 ? 'Pro' : 'Enterprise';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Enterprise threshold (4800) is very close to the maximum, and the slider continues to show a calculated Pro price for values >= 4800 even though the Enterprise card is highlighted. The slider should ideally show 'Contact us' or hide the price once the Enterprise threshold is reached to avoid confusion.

if (newPlan !== highlightedPlan) {
setHighlightedPlan(newPlan);
if (!scrollviewRef.current) return;
Expand Down
28 changes: 21 additions & 7 deletions packages/web/docs/src/components/pricing/pricing-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { CallToAction, cn } from '@theguild/components';
import { BookIcon } from '../book-icon';
import { Slider } from '../slider';

function formatOps(millions: number): { label: string; chars: number } {
if (millions >= 1000) {
const b = parseFloat((millions / 1000).toFixed(3));
const label = `${b} B`;
return { label, chars: label.length - 1 };
}
const label = `${millions} M`;
return { label, chars: label.length - 1 };
}

export function PricingSlider({
className,
onChange,
Expand All @@ -13,29 +23,30 @@ export function PricingSlider({
onChange: (value: number) => void;
}) {
const min = 1;
const max = 300;
const max = 5000;

const [popoverOpen, setPopoverOpen] = useState(false);
const [opsLabel, setOpsLabel] = useState(() => formatOps(min).label);
const rootRef = useRef<HTMLDivElement>(null);

return (
<div
ref={rootRef}
className={cn(
'relative isolate block select-none rounded-3xl border border-green-400 p-4 [counter-set:ops_calc(var(--ops))] sm:p-8',
'relative isolate block select-none rounded-3xl border border-green-400 p-4 sm:p-8',
className,
)}
// 10$ base price + 10$ per 1M
style={{ '--ops': min, '--price': 'calc(10 + var(--ops) * 10)' }}
style={{ '--ops': min, '--ops-chars': 0, '--price': 'calc(10 + var(--ops) * 10)' }}
{...rest}
>
<div
aria-hidden
className="text-green-1000 flex flex-wrap items-center text-2xl font-medium md:h-12 md:w-[calc(100%-260px)]"
>
<div className="relative min-w-[clamp(calc(60.95px+14.47px*round(down,log(max(var(--ops),1),10),1)),(2-var(--ops))*111px,111px)] max-w-[clamp(calc(60.95px+14.47px*round(down,log(max(var(--ops),1),10),1)),(2-var(--ops))*111px,111px)] shrink grow motion-safe:transition-all">
<div className="flex w-full whitespace-pre rounded-[40px] bg-blue-300 px-3 py-1 tabular-nums leading-8 opacity-[calc(var(--ops)-1)] [transition-duration:calc(clamp(0,var(--ops)-1,1)*350ms)] before:tracking-[-0.12em] before:content-[''_counter(ops)_'_'] motion-safe:transition-all">
M
<div className="relative min-w-[clamp(calc(60.95px+14.47px*var(--ops-chars)),(2-var(--ops))*111px,111px)] max-w-[clamp(calc(60.95px+14.47px*var(--ops-chars)),(2-var(--ops))*111px,111px)] shrink grow motion-safe:transition-all">
<div className="flex w-full whitespace-pre rounded-[40px] bg-blue-300 px-3 py-1 tabular-nums leading-8 opacity-[calc(var(--ops)-1)] [transition-duration:calc(clamp(0,var(--ops)-1,1)*350ms)] tracking-[-0.12em] motion-safe:transition-all">
{opsLabel}
</div>
<div className="absolute left-0 top-0 whitespace-pre leading-10 opacity-[calc(2-var(--ops))] [transition-duration:calc(clamp(0,2-var(--ops),1)*350ms)] motion-safe:transition">
How many
Expand All @@ -60,11 +71,14 @@ export function PricingSlider({
counter="after:content-['$'_counter(price)_'_/_month'] after:[counter-set:price_calc(var(--price))]"
onChange={event => {
const value = event.currentTarget.valueAsNumber;
const display = formatOps(value);
rootRef.current!.style.setProperty('--ops', `${value}`);
rootRef.current!.style.setProperty('--ops-chars', `${display.chars}`);
setOpsLabel(display.label);
onChange(value);
}}
/>
<span className="font-medium">{max}M</span>
<span className="font-medium">5B</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The end label '5B' is inconsistent with the start label '1M' (line 52) and the current value display (line 38), which still appends 'M' to the raw number (resulting in '5000 M' at the maximum). Units should be consistent across the component.

</div>
<Root delayDuration={0} open={popoverOpen} onOpenChange={setPopoverOpen}>
<Trigger asChild>
Expand Down
Loading