diff --git a/cloud/app/components/pricing-page.tsx b/cloud/app/components/pricing-page.tsx new file mode 100644 index 0000000000..9d55e11bc6 --- /dev/null +++ b/cloud/app/components/pricing-page.tsx @@ -0,0 +1,425 @@ +import React from "react"; +import { cn } from "@/app/lib/utils"; +import { ButtonLink } from "@/app/components/ui/button-link"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/app/components/ui/tabs"; +import { Check, X } from "lucide-react"; + +interface FeatureRowProps { + feature: string; + free: string | boolean; + pro: string | boolean; + team: string | boolean; +} +// Feature row component for displaying features with the same value across tiers +const FeatureRow = ({ feature, free, pro, team }: FeatureRowProps) => { + // If all tiers have the exact same value (and it's not a boolean) + const allSameNonBoolean = + free === pro && pro === team && typeof free === "string" && free !== ""; + + return ( +
+
{feature}
+ + {allSameNonBoolean ? ( +
+ {free} +
+ ) : ( + <> +
+ {typeof free === "boolean" ? ( +
+ {free ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ) : ( + + {free} + + )} +
+ +
+ {typeof pro === "boolean" ? ( +
+ {pro ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ) : ( + + {pro} + + )} +
+ +
+ {typeof team === "boolean" ? ( +
+ {team ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ) : ( + + {team} + + )} +
+ + )} +
+ ); +}; + +interface PricingTierProps { + name: string; + price: string; + description: string; + button: React.ReactNode; + badge?: "Open Beta" | "Closed Beta"; +} +// Pricing tier component +const PricingTier = ({ + name, + price, + description, + button, + badge, +}: PricingTierProps) => ( +
+
+
+

{name}

+ {badge && ( + + {badge} + + )} +
+

{description}

+
+ {price} + {price !== "TBD" && price !== "N/A" && ( + / month + )} +
+ {button} +
+
+); + +// Feature comparison table component +export const FeatureComparisonTable = ({ + features, +}: { + features: FeatureRowProps[]; +}) => ( +
+
+

+ Feature Comparison +

+
+
+ {/* Table header */} +
+
Feature
+
+ Free +
+
+ Pro +
+
+ Team +
+
+ + {/* Table rows */} + {features.map((feat, i) => ( + + ))} +
+
+); + +interface TierAction { + button: React.ReactNode; +} + +interface PricingActions { + hosted: { + free: TierAction; + pro: TierAction; + team: TierAction; + }; + selfHosted: { + free: TierAction; + pro: TierAction; + team: TierAction; + }; +} + +// Cloud hosted features +export const cloudHostedFeatures = [ + { + feature: "Projects", + free: "Unlimited", + pro: "Unlimited", + team: "Unlimited", + }, + { feature: "Users", free: "2", pro: "10", team: "Unlimited" }, + { + feature: "Tracing", + free: "30k spans / month", + pro: "100k spans / month (thereafter $1 per 10k)", + team: "1M spans / month (thereafter $1 per 10k)", + }, + { + feature: "Data Retention", + free: "30 days", + pro: "90 days", + team: "180 days", + }, + { feature: "Versioned Functions", free: true, pro: true, team: true }, + { feature: "Playground", free: true, pro: true, team: true }, + { feature: "Comparisons", free: true, pro: true, team: true }, + { feature: "Annotations", free: true, pro: true, team: true }, + { feature: "Support (Community)", free: true, pro: true, team: true }, + { feature: "Support (Chat / Email)", free: false, pro: true, team: true }, + { feature: "Support (Private Slack)", free: false, pro: false, team: true }, + { + feature: "API Rate Limits", + free: "10 / minute", + pro: "100 / minute", + team: "1000 / minute", + }, +]; + +// Self-hosted features +export const selfHostedFeatures = [ + { + feature: "Projects", + free: "Unlimited", + pro: "Unlimited", + team: "Unlimited", + }, + { + feature: "Users", + free: "Unlimited", + pro: "As licensed", + team: "As licensed", + }, + { + feature: "Tracing", + free: "No limits", + pro: "No limits", + team: "No limits", + }, + { + feature: "Data Retention", + free: "No limits", + pro: "No limits", + team: "No limits", + }, + { feature: "Versioned Functions", free: true, pro: true, team: true }, + { feature: "Playground", free: false, pro: true, team: true }, + { feature: "Comparisons", free: false, pro: true, team: true }, + { feature: "Annotations", free: false, pro: true, team: true }, + { feature: "Support (Community)", free: true, pro: true, team: true }, + { feature: "Support (Chat / Email)", free: false, pro: true, team: true }, + { feature: "Support (Private Slack)", free: false, pro: false, team: true }, + { + feature: "API Rate Limits", + free: "No limits", + pro: "No limits", + team: "No limits", + }, +]; +interface PricingProps { + actions: PricingActions; +} + +export function PricingPage({ actions }: PricingProps) { + return ( +
+
+
+

+ Pricing +

+

+ Get started with the Free plan today. +

+

+ No credit card required. +

+
+ + +
+ + Hosted By Us + Self Hosting + +
+ + {/* Hosted By Us Tab Content */} + + + + {/* Feature comparison table */} + + + + {/* Self Hosting Tab Content */} + +
+ + + +
+ + {/* Feature comparison table */} + +
+
+ + {/* FAQ Section */} +
+

+ Frequently Asked Questions +

+
+
+

+ How long will the open beta last? +

+

+ The open beta period is ongoing, and we'll provide advance + notice before moving to paid plans. +

+
+
+

+ What happens when the beta ends? +

+

+ All existing users will receive a grace period to evaluate which + plan is right for them before making any changes. +

+
+
+
+ +
+

+ Have questions about our pricing? +

+

+ Join our{" "} + + community + {" "} + and ask the team directly! +

+
+
+
+ ); +} + +export const CloudPricing = ({ + hostedActions, +}: { + hostedActions: { + free: TierAction; + pro: TierAction; + team: TierAction; + }; +}) => { + const pricingTiers: PricingTierProps[] = [ + { + name: "Free", + price: "$0", + description: "For individuals just getting started", + badge: "Open Beta", + button: hostedActions.free.button, + }, + { + name: "Pro", + price: "TBD", + description: "For teams with more advanced needs", + badge: "Closed Beta", + button: hostedActions.pro.button, + }, + { + name: "Team", + price: "TBD", + description: "For larger teams requiring dedicated support", + badge: "Closed Beta", + button: hostedActions.team.button, + }, + ]; + return ( +
+ {pricingTiers.map((tier) => ( + + ))} +
+ ); +}; diff --git a/cloud/app/routes/pricing.tsx b/cloud/app/routes/pricing.tsx index 2ce6ae8029..025adafe62 100644 --- a/cloud/app/routes/pricing.tsx +++ b/cloud/app/routes/pricing.tsx @@ -1,7 +1,66 @@ import { createFileRoute } from "@tanstack/react-router"; -import { TempPage } from "@/app/components/temp-page"; +import { PricingPage } from "@/app/components/pricing-page"; +import { ButtonLink } from "@/app/components/ui/button-link"; + +const marketingActions = { + hosted: { + free: { + button: ( + + Get Started + + ), + }, + pro: { + button: ( + + Contact Us + + ), + }, + team: { + button: ( + + Contact Us + + ), + }, + }, + selfHosted: { + free: { + button: ( + + Get Started + + ), + }, + pro: { + button: ( + + Request License + + ), + }, + team: { + button: ( + + Request License + + ), + }, + }, +}; export const Route = createFileRoute("/pricing")({ - // todo(sebastian): temp route ahead of porting md/mdx files - component: () => , + // todo(sebastian): simplify and add other SEO metadata + head: () => ({ + meta: [ + { title: "Pricing" }, + { + name: "description", + content: "Mirascope cloud's pricing plans and features", + }, + ], + }), + component: () => , });