Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add calculator to pricing page #205

Merged
merged 5 commits into from
Nov 21, 2024
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
174 changes: 174 additions & 0 deletions src/components/PricingCalculator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { useState } from 'react';
import { formatMoney, formatNumber } from '~/utils/formatters';

export function PricingCalculator() {
const proPlanIncludedPatches = 50_000;
const initialMauCount = 10_000;
const [mauCount, setMauCount] = useState(initialMauCount);
const initialNumPatchesPerMonth = 1;
const [numPatchesPerMonth, setNumPatchesPerMonth] = useState(
initialNumPatchesPerMonth,
);
const totalPatchInstalls = mauCount * numPatchesPerMonth;
const [recommendedPlan, helpText, cost] = (() => {
if (totalPatchInstalls <= 5_000) {
return [
'Free',
<span>
Our free tier has everything you need.{' '}
<a className="link underline" href="https://console.shorebird.dev">
Sign up
</a>{' '}
here.
</span>,
0,
];
}

if (totalPatchInstalls <= 50_000) {
return [
'Pro',
<span>
Our Pro plan includes {formatNumber(proPlanIncludedPatches)} patch
installs.{' '}
<a className="link underline" href="https://console.shorebird.dev">
Sign up
</a>{' '}
here.
</span>,
2000,
];
}

if (totalPatchInstalls < 2_000_000) {
const numOverageInstalls = totalPatchInstalls - proPlanIncludedPatches;
return [
'Pro',
<span>
Our Pro plan includes ${formatNumber(proPlanIncludedPatches)} patches
and supports{' '}
<a
className="link underline"
href="https://shorebird.dev/blog/simplified-pricing/"
>
configurable overages
</a>{' '}
at $1 per 2,500 patch installs.{' '}
<a className="link underline" href="https://console.shorebird.dev">
Sign up
</a>{' '}
here.
</span>,
2000 + numOverageInstalls * 0.04,
];
}

return [
'Enterprise',
<span>
<a className="link underline" href="contact#sales">
Contact us
</a>{' '}
for a discounted quote.
</span>,
null,
];
})();

return (
<div className="mt-8 flex flex-col items-center text-white">
<div className="mt-4 text-center text-2xl font-semibold">
Not sure which plan you should choose? Use our pricing calculator to
find out.
</div>
<div className="mx-auto mb-8 mt-2 min-w-full px-8 text-center text-sm text-shorebirdTextGray"></div>
<div className="min-w-full items-start px-28">
<div className="flex flex-col text-lg">
<div className="">
<span className="text-sm font-bold text-shorebirdTextGray">
How many monthly active users do you have?
</span>
<div className="relative">
<input
type="range"
onChange={(e) => {
setMauCount(+e.target.value);
}}
value={mauCount}
min="5000"
max="2000000"
step="5000"
className="w-full"
/>

<span className="absolute -bottom-6 start-0 text-sm text-gray-500 dark:text-gray-400">
5,000
</span>
<span className="absolute -bottom-6 start-1/4 -translate-x-1/2 text-sm text-gray-500 rtl:translate-x-1/2 dark:text-gray-400">
500,000
</span>
<span className="absolute -bottom-6 start-2/4 -translate-x-1/2 text-sm text-gray-500 rtl:translate-x-1/2 dark:text-gray-400">
1,000,000
</span>
<span className="absolute -bottom-6 start-3/4 -translate-x-1/2 text-sm text-gray-500 rtl:translate-x-1/2 dark:text-gray-400">
1,500,000
</span>
<span className="absolute -bottom-6 end-0 text-sm text-gray-500 dark:text-gray-400">
2,000,000+
</span>
</div>
</div>
<div className="w-full pt-12">
<span className="text-sm font-bold text-shorebirdTextGray">
How many times will you patch each month?
</span>
<div className="relative">
<input
type="range"
onChange={(e) => {
setNumPatchesPerMonth(+e.target.value);
}}
value={numPatchesPerMonth}
className="w-full"
min="1"
max="20"
/>

<span className="absolute -bottom-6 start-0 text-sm text-gray-500 dark:text-gray-400">
1
</span>
<span className="absolute -bottom-6 start-1/3 -translate-x-1/2 text-sm text-gray-500 rtl:translate-x-1/2 dark:text-gray-400">
5
</span>
<span className="absolute -bottom-6 start-2/3 -translate-x-1/2 text-sm text-gray-500 rtl:translate-x-1/2 dark:text-gray-400">
10
</span>
<span className="absolute -bottom-6 end-0 text-sm text-gray-500 dark:text-gray-400">
20
</span>
</div>
</div>
</div>

<div className="mt-12 block">
<div className="text-2xl">
We recommend the{' '}
<span className="font-bold">{recommendedPlan}</span> plan
{cost !== null && (
<span className="ml-2 text-sm">
({formatMoney(cost)} per month)
</span>
)}
</div>
<div className="text-m mt-2 max-w-2xl">
You will want a plan with{' '}
<span className="size-2 font-bold">
{formatNumber(totalPatchInstalls)}
</span>{' '}
patch installs per month. {helpText}
</div>
</div>
</div>
</div>
);
}
5 changes: 4 additions & 1 deletion src/components/PricingDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { config } from '../config';
import { CheckArrowIcon } from '~/assets/icons/CheckArrowIcon';
import { PricingCalculator } from './PricingCalculator';

interface Feature {
title: string;
Expand Down Expand Up @@ -176,7 +177,9 @@ export const PricingDetails = () => {
))}
</div>

<div className="mx-auto mb-8 mt-6 px-8 text-sm text-shorebirdTextGray">
<PricingCalculator />

<div className="mx-auto mb-8 mt-12 px-8 text-sm text-shorebirdTextGray">
<p>
*Prices are quoted in USD and sold as "patch installs per month",
reflecting successful installs of a given patch. For example, 1
Expand Down
25 changes: 25 additions & 0 deletions src/utils/formatters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
interface FormatMoneyOptions {
currency?: string;
options?: Intl.NumberFormatOptions;
}

/// Converts a number of cents into a currency string with the provided
/// currency.
export function formatMoney(
cents: number | undefined,
options?: FormatMoneyOptions,
) {
return Intl.NumberFormat('en-US', {
style: 'currency',
currency: options?.currency ?? 'USD',
...options?.options,
}).format((cents ?? 0) / 100);
}

/// Converts a number to a comma-separated string.
export function formatNumber(
number: number,
options?: Intl.NumberFormatOptions,
): string {
return Intl.NumberFormat('en-US', options).format(number);
}