From 6b86b076b95c42eccc26bafd4be9fecd42f83b89 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 20 Nov 2024 20:37:25 +0100 Subject: [PATCH 01/16] add pricing page --- assets/logos/dark/radial_circle.svg | 156 ++++++++ assets/logos/light/radial_circle.svg | 156 ++++++++ pcweb/components/docpage/navbar/navbar.py | 4 +- pcweb/constants.py | 1 + pcweb/pages/__init__.py | 3 +- pcweb/pages/pricing.py | 444 ---------------------- pcweb/pages/pricing/faq.py | 138 +++++++ pcweb/pages/pricing/header.py | 17 + pcweb/pages/pricing/plan_cards.py | 157 ++++++++ pcweb/pages/pricing/pricing.py | 33 ++ pcweb/pages/pricing/table.py | 205 ++++++++++ pcweb/styles/tailwind_config.py | 28 +- 12 files changed, 893 insertions(+), 449 deletions(-) create mode 100644 assets/logos/dark/radial_circle.svg create mode 100644 assets/logos/light/radial_circle.svg delete mode 100644 pcweb/pages/pricing.py create mode 100644 pcweb/pages/pricing/faq.py create mode 100644 pcweb/pages/pricing/header.py create mode 100644 pcweb/pages/pricing/plan_cards.py create mode 100644 pcweb/pages/pricing/pricing.py create mode 100644 pcweb/pages/pricing/table.py diff --git a/assets/logos/dark/radial_circle.svg b/assets/logos/dark/radial_circle.svg new file mode 100644 index 0000000000..fc2abf7649 --- /dev/null +++ b/assets/logos/dark/radial_circle.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/logos/light/radial_circle.svg b/assets/logos/light/radial_circle.svg new file mode 100644 index 0000000000..da2ab0d21b --- /dev/null +++ b/assets/logos/light/radial_circle.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pcweb/components/docpage/navbar/navbar.py b/pcweb/components/docpage/navbar/navbar.py index a3036a20b2..f8c34a079f 100644 --- a/pcweb/components/docpage/navbar/navbar.py +++ b/pcweb/components/docpage/navbar/navbar.py @@ -21,7 +21,7 @@ from pcweb.pages.docs import getting_started, hosting from pcweb.pages.faq import faq -from pcweb.pages.pricing import pricing +from pcweb.pages.pricing.pricing import pricing from pcweb.pages.errors import errors from pcweb.pages.docs.library import library from pcweb.pages.blog import blogs @@ -324,7 +324,7 @@ def new_component_section() -> rx.Component: ), ), nav_menu.item( - link_item("Pricing", pricing.path, "pricing"), + link_item("Pricing", "/pricing", "pricing"), ), class_name="desktop-only flex flex-row items-center gap-0 lg:gap-7 m-0 h-full list-none", ), diff --git a/pcweb/constants.py b/pcweb/constants.py index f5ee63cc68..1b8d8c2490 100644 --- a/pcweb/constants.py +++ b/pcweb/constants.py @@ -30,6 +30,7 @@ GALLERY_FORM_URL = "https://docs.google.com/forms/d/e/1FAIpQLSfB30hXB09CZ_H0Zi684w1y1zQSScyT3Qhd1mOUrAAIq9dj3Q/viewform?usp=sf_link" NPMJS_URL = "https://www.npmjs.com/" SPLINE_URL = "https://github.com/splinetool/react-spline" +HOSTING_URL = "https://cloud.staging.reflexcorp.run" # Install urls. BUN_URL = "https://bun.sh" diff --git a/pcweb/pages/__init__.py b/pcweb/pages/__init__.py index 13b2ba0a58..bf9a0fca80 100644 --- a/pcweb/pages/__init__.py +++ b/pcweb/pages/__init__.py @@ -5,7 +5,6 @@ from .docs import doc_routes from .faq import faq from .index import index -from .pricing import pricing from .page404 import page404 from .errors import errors from .gallery import gallery @@ -13,7 +12,7 @@ from .customers.data.customers import customers_routes from .gallery.apps import gallery_apps_routes from .hosting_countdown.hosting_countdown import hosting_countdown - +from .pricing.pricing import pricing routes = [ *[r for r in locals().values() if isinstance(r, Route) and r.add_as_page], *blog_routes, diff --git a/pcweb/pages/pricing.py b/pcweb/pages/pricing.py deleted file mode 100644 index 2f4f9fd14f..0000000000 --- a/pcweb/pages/pricing.py +++ /dev/null @@ -1,444 +0,0 @@ -import httpx -import reflex as rx -from httpx import Response - -from pcweb.components.button import button -from pcweb.components.webpage.comps import h1_title -from pcweb.constants import ( - REFLEX_DEV_WEB_LANDING_FORM_SALES_CALL_WEBHOOK_URL, - REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL, -) -from pcweb.pages.docs import getting_started, hosting -from pcweb.templates.webpage import webpage - - -class FormState(rx.State): - is_loading: bool = False - email_sent: bool = False - - @rx.event - async def submit(self, form_data: dict): - self.is_loading = True - yield - - try: - with httpx.Client() as client: - response = client.post( - REFLEX_DEV_WEB_LANDING_FORM_SALES_CALL_WEBHOOK_URL, - json=form_data, - ) - response.raise_for_status() - - self.is_loading = False - self.email_sent = True - yield rx.toast.success("Demo request submitted successfully!") - - except httpx.HTTPError: - self.is_loading = False - self.email_sent = False - yield rx.toast.error("Failed to submit request. Please try again later.") - - @rx.event - async def submit_pro_waitlist(self, form_data: dict): - try: - with httpx.Client() as client: - response: Response = client.post( - REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL, - json=form_data, - ) - response.raise_for_status() - - yield rx.toast.success("Thank you for joining the waitlist!") - - except httpx.HTTPError: - yield rx.toast.error("Failed to submit request. Please try again later.") - - -def dialog(trigger: rx.Component, content: rx.Component) -> rx.Component: - return rx.dialog.root( - rx.dialog.trigger( - trigger, - ), - rx.dialog.content( - content, - class_name="bg-white-1 p-4 rounded-[1.625rem] w-[26rem]", - ), - ) - - -def form() -> rx.Component: - input_class_name = "box-border border-slate-5 focus:border-violet-9 focus:border-1 bg-slate-1 p-[0.5rem_0.75rem] border rounded-[10px] w-full font-small text-slate-11 placeholder:text-slate-9 outline-none focus:outline-none" - return rx.box( - rx.form( - rx.box( - rx.text( - "Get an enterprise quote", - class_name="text-2xl text-slate-12 font-bold leading-6 scroll-m-[7rem]", - id="form-title", - ), - rx.text( - "Explore custom plans and pricing", - class_name="font-base text-slate-9", - ), - class_name="flex flex-col gap-2 mb-4 items-start", - ), - rx.box( - rx.hstack( - rx.el.input( - name="first_name", - type="text", - placeholder="First name *", - required=True, - class_name=input_class_name, - ), - rx.el.input( - name="last_name", - type="text", - placeholder="Last name *", - required=True, - class_name=input_class_name, - ), - spacing="2", - width="100%", - class_name="mb-2.5 gap-2", - ), - rx.hstack( - rx.el.input( - name="email", - type="email", - placeholder="Business email *", - required=True, - class_name=input_class_name, - ), - rx.el.input( - name="linkedin_profile", - type="text", - placeholder="LinkedIn profile", - required=False, - class_name=input_class_name, - ), - spacing="2", - width="100%", - class_name="mb-2.5 gap-2", - ), - rx.hstack( - rx.el.input( - name="company_name", - type="text", - placeholder="Company name *", - required=True, - class_name=input_class_name, - ), - rx.el.input( - name="title", - type="text", - placeholder="Title *", - required=True, - class_name=input_class_name, - ), - spacing="2", - width="100%", - class_name="mb-2.5 gap-2", - ), - rx.el.textarea( - name="project_description", - placeholder="Your company needs", - class_name=input_class_name + " h-24 mb-4 resize-none", - ), - class_name="flex flex-col", - ), - rx.cond( - FormState.is_loading, - button( - "Sending...", - variant="muted", - type="submit", - class_name="opacity-80 !cursor-not-allowed pointer-events-none !w-min", - ), - button( - "Submit", - type="submit", - class_name="!w-min", - ), - ), - on_submit=FormState.submit, - class_name="flex flex-col", - ), - rx.box( - rx.text( - "If you have any questions, feel free to contact us", - class_name="font-small text-slate-9", - ), - rx.link( - "sales@reflex.dev", - href="mailto:sales@reflex.dev", - underline="always", - class_name="text-slate-9 font-small", - ), - class_name="flex flex-row justify-between items-center gap-2 mt-4", - ), - class_name="relative flex flex-col gap-4 border-slate-4 bg-slate-2 shadow-large p-8 border rounded-[1.125rem] self-stretch scroll-[3rem]", - ) - - -# TODO(elvis): refactor the boolean logic to use enums -# https://linear.app/reflex-dev/issue/ENG-3837/refactor-the-included-variable-param-on-our-pricing-page-to-use-enums -def features(text: str, included: bool) -> rx.Component: - if included: - return rx.hstack( - rx.icon( - "circle-check", color=rx.color("green", 9), stroke_width=1.2, size=22 - ), - rx.text(text, class_name="font-base font-normal"), - align_items="center", - spacing="2", - ) - else: - return rx.hstack( - rx.icon("circle-x", color=rx.color("mauve", 9), stroke_width=1.2, size=22), - rx.text(text, class_name="font-base"), - align_items="center", - spacing="2", - ) - - -def hobby_tier() -> rx.Component: - return rx.box( - rx.vstack( - rx.hstack( - rx.text("Hobby", class_name="text-2xl font-bold"), - rx.badge( - "Free", - color_scheme="gray", - size="3", - variant="surface", - radius="full", - ), - width="100%", - justify_content="space-between", - class_name="mb-2", - ), - rx.text( - "Everything you need to kickstart your project", - class_name="text-slate-10 mb-4", - ), - rx.vstack( - features("Community support", True), - features("Single developer workspace", True), - features("A single deployed app", True), - features("1024 MB Machine Size", True), - features("A single CPU", True), - features("1 day log retention", True), - features("Multi-region", False), - features("Custom domain", False), - align_items="start", - spacing="3", - class_name="mb-6", - ), - rx.link( - button( - "Start building", - variant="secondary", - class_name="!w-full !text-slate-12", - ), - href=hosting.deploy_quick_start.path, - width="100%", - underline="none", - ), - align_items="start", - class_name="h-full z-10 p-8", - ), - class_name="relative flex flex-col gap-4 border-slate-4 bg-slate-2 shadow-large p-4 border rounded-[1.125rem] self-stretch", - ) - - -def pro_tier() -> rx.Component: - return rx.box( - rx.vstack( - rx.hstack( - rx.text("Pro", class_name="text-2xl font-bold"), - rx.badge( - "Coming Soon", - color_scheme="gray", - size="3", - variant="surface", - radius="full", - ), - width="100%", - justify_content="space-between", - class_name="mb-2", - ), - rx.text( - "Professional devs and small teams shipping quickly", - class_name="text-slate-10 mb-4", - ), - rx.vstack( - features("Priority support", True), - features("5 Team members", True), - features("5 Deployed apps", True), - features("2048 MB machine size", True), - features("2 CPU", True), - features("30 days log retention", True), - features("Multi-region", True), - features("Custom domain", True), - align_items="start", - spacing="3", - class_name="mb-6", - ), - dialog( - trigger=button( - "Join waitlist", - variant="secondary", - class_name="!w-full !text-slate-12", - ), - content=rx.form( - rx.box( - rx.text( - "Join waitlist", - class_name="text-slate-12 font-large", - ), - rx.text( - "Be the first to know when the Pro hosting plan is ready", - class_name="font-medium text-slate-11", - ), - class_name="flex flex-col gap-2 w-full font-instrument-sans", - ), - rx.box( - rx.el.input( - placeholder="Your email", - name="email", - type="email", - class_name="relative box-border border-slate-4 focus:border-violet-9 focus:border-1 bg-slate-2 p-[0.5rem_0.75rem] border rounded-xl font-base text-slate-11 placeholder:text-slate-9 outline-none focus:outline-none w-full", - ), - rx.dialog.close( - button( - "Submit", - type="submit", - ), - ), - class_name="flex flex-row justify-between items-center gap-4 w-full", - ), - rx.dialog.close( - rx.icon( - "x", - class_name="absolute top-2 right-2 !text-slate-9 hover:!text-slate-11 cursor-pointer transition-color", - ), - ), - on_submit=FormState.submit_pro_waitlist, - class_name="flex flex-col gap-5 relative p-1", - ), - ), - align_items="start", - class_name="h-full z-10 p-8", - ), - class_name="relative flex flex-col gap-4 border-slate-4 bg-slate-2 shadow-large p-4 border rounded-[1.125rem] self-stretch", - ) - - -def enterprise_tier() -> rx.Component: - return rx.box( - rx.vstack( - rx.hstack( - rx.text("Enterprise", class_name="text-2xl font-bold"), - rx.badge( - "Custom", - color_scheme="violet", - size="3", - variant="surface", - radius="full", - ), - width="100%", - justify_content="space-between", - class_name="mb-2", - ), - rx.text( - "A plan based on your specific needs", - class_name="text-m text-slate-10 mb-4", - ), - rx.vstack( - features("Dedicated support", True), - features("Support SLAs", True), - features("Unlimited team members", True), - features("Unlimited apps", True), - features("Customized machine size", True), - features("90 day log Retention", True), - features("Multi-region", True), - features("Custom domain", True), - align_items="start", - spacing="3", - class_name="mb-6", - ), - rx.text( - "* With enterprise, you can deploy on-prem or host your app with us.", - class_name="text-sm text-slate-10", - ), - align_items="start", - class_name="h-full z-10 p-8", - ), - class_name="relative flex flex-col gap-4 border-slate-4 bg-slate-2 shadow-large p-4 border rounded-[1.125rem] self-stretch", - ) - - -@webpage(path="/pricing", title="Pricing · Reflex") -def pricing(): - return rx.el.section( - rx.box( - rx.box( - h1_title(title="Find a plan that's right for you", class_name="mb-4"), - rx.el.h2( - "Start for free using the open-source and scale as you grow.", - class_name="font-md text-balance text-slate-10 mb-10", - ), - class_name="section-header", - ), - rx.grid( - rx.box( - hobby_tier(), - class_name="w-full", - ), - rx.box( - pro_tier(), - class_name="w-full", - ), - rx.box( - enterprise_tier(), - class_name="w-full", - ), - columns=rx.breakpoints( - xs="1", - sm="1", - md="3", - lg="3", - xl="3", - ), - spacing="4", - width="100%", - ), - rx.box( - rx.cond( - FormState.email_sent, - rx.box( - rx.box( - rx.text( - """Thanks for your interest in Reflex! -You'll get a reply from us soon.""", - class_name="font-large text-slate-12 whitespace-pre text-center", - ), - class_name="flex flex-row items-center gap-2", - ), - button( - "Back", - on_click=FormState.setvar("email_sent", False), - class_name="mt-4", - ), - class_name="flex flex-col items-center gap-2", - ), - form(), - ), - class_name="mt-12 w-full", - ), - class_name="flex flex-col justify-center items-center w-full max-w-[84.5rem]", - ), - id="pricing", - class_name="section-content", - ) diff --git a/pcweb/pages/pricing/faq.py b/pcweb/pages/pricing/faq.py new file mode 100644 index 0000000000..16d1bafd24 --- /dev/null +++ b/pcweb/pages/pricing/faq.py @@ -0,0 +1,138 @@ +import reflex as rx +from pcweb.components.button import button +from pcweb.constants import HOSTING_URL + + +def glow() -> rx.Component: + return rx.box( + class_name="absolute flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[-1] top-[-1.25rem] pointer-events-none w-[20rem] h-[5rem]", + background_image=rx.color_mode_cond( + "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.661) 0%, rgba(252, 252, 253, 0.00) 100%) !important", + "radial-gradient(50% 50% at 50% 50%, rgba(58, 45, 118, 0.207) 0%, rgba(21, 22, 24, 0.00) 100%) !important", + ), + ) + + +def header() -> rx.Component: + return rx.box( + rx.el.h3( + "Have questions?", + class_name="text-slate-12 text-3xl font-semibold", + ), + rx.el.p( + "Check FAQ", + class_name="text-slate-9 text-3xl font-semibold", + ), + class_name="flex items-center justify-between text-slate-11 flex-col pt-[5rem] pb-82xl:border-x border-slate-4", + ) + + +def sales_button() -> rx.Component: + return rx.link( + glow(), + button( + "Need more help? Contact sales", + variant="secondary", + class_name="!text-slate-11 !font-semibold !text-sm", + ), + href=HOSTING_URL, # TODO: Change to sales page when we have it + is_external=True, + class_name="self-center relative", + ) + + +def accordion(title: str, content: rx.Component) -> rx.Component: + """The accordion component. + + Args: + title (str): The title of the accordion. + content (rx.Component): The content of the accordion. + + Returns: + The accordion component. + + """ + return rx.accordion.root( + rx.accordion.item( + rx.accordion.trigger( + rx.el.h3( + title, class_name="font-semibold text-base text-slate-12 text-start" + ), + rx.icon( + tag="plus", + size=16, + class_name="!text-slate-9 group-data-[state=open]:rotate-45 transition-transform", + ), + class_name="hover:!bg-transparent !p-[0.5rem_0rem] !justify-between gap-4 group", + ), + rx.accordion.content( + content, + class_name="before:!h-0 after:!h-0 radix-state-open:animate-accordion-down radix-state-closed:animate-accordion-up transition-all !px-0", + ), + ), + collapsible=True, + class_name="!p-0 w-full overflow-hidden !bg-slate-1 !rounded-none !shadow-none", + ) + + +def accordion_text(text: str) -> rx.Component: + return rx.el.p(text, class_name="text-slate-9 text-sm font-medium") + + +faq_items = [ + ( + "Can I use Reflex for free?", + "Yes! Reflex is open source and free to use. You can self-host your apps or use our hosting platform which has a generous free tier.", + ), + ( + "How usage based billing is calculated?", + "Usage is calculated based on compute resources (CPU/RAM) consumed by your app. We measure this in compute units per hour.", + ), + ( + "What is your privacy and security policy?", + "We take security seriously. All apps are deployed with SSL certificates, DDoS protection, and web application firewall. Your code and data remain private and secure.", + ), + ( + "What happens when I upgrade?", + "When you upgrade your plan, you'll immediately get access to the new features and increased resource limits. Your app will continue running without interruption.", + ), + ( + "How are prorations calculated?", + "If you upgrade mid-billing cycle, we'll prorate the new charges for the remainder of the billing period. You'll only pay for what you use.", + ), + ( + "What happens if I cancel the plan?", + "If you cancel, you'll maintain access until the end of your current billing period. After that, your app will be downgraded to the free tier limits.", + ), + ( + "How can I influence Reflex roadmap?", + "We welcome feedback and feature requests from our community. You can submit ideas through GitHub issues or discuss them in our Discord community.", + ), + ( + "What determines the total amount billed?", + "Your bill is determined by your base plan plus any usage-based charges for compute resources that exceed the plan limits.", + ), + ( + "How to cancel subscription?", + "You can cancel your subscription anytime from your account dashboard. No long-term commitments required.", + ), + ( + "Can I add members to my project?", + "Yes! Pro plan allows up to 5 team members, Team plan up to 25 members, and Enterprise plan has unlimited team members.", + ), +] + + +def faq() -> rx.Component: + return rx.el.section( + header(), + rx.box( + *[ + accordion(title, accordion_text(content)) + for title, content in faq_items + ], + class_name="max-w-[40rem] flex justify-center items-center flex-col mx-auto w-full gap-2", + ), + sales_button(), + class_name="flex flex-col gap-8 w-full max-w-[64.19rem] 2xl:border-x border-slate-4 2xl:border-b pb-[6rem]", + ) diff --git a/pcweb/pages/pricing/header.py b/pcweb/pages/pricing/header.py new file mode 100644 index 0000000000..964462dc16 --- /dev/null +++ b/pcweb/pages/pricing/header.py @@ -0,0 +1,17 @@ +import reflex as rx +from pcweb.components.hosting_banner import HostingBannerState + + +def header() -> rx.Component: + return rx.box( + rx.el.h1( + "Find a plan that's right for you", + class_name="gradient-heading font-semibold text-4xl xl:text-5xl text-center", + ), + rx.el.h2( + "Start for free using the open-source and scale as you grow.", + class_name="font-medium text-slate-9 text-lg text-center text-wrap", + ), + class_name="flex flex-col gap-4 justify-center items-center max-w-[64.19rem] 2xl:border-x border-slate-4 w-full pb-16 " + + rx.cond(HostingBannerState.show_banner, "pt-[13rem]", "pt-[10rem]"), + ) diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py new file mode 100644 index 0000000000..27faecd195 --- /dev/null +++ b/pcweb/pages/pricing/plan_cards.py @@ -0,0 +1,157 @@ +import reflex as rx +from pcweb.components.button import button +from pcweb.constants import HOSTING_URL + + +def radial_circle(violet: bool = False) -> rx.Component: + """Create a radial circle background image component. + + Args: + violet: Whether to use the violet variant. Defaults to False. + + Returns: + A Reflex image component configured as a radial circle background. + + """ + theme = "violet" if violet else "" + return rx.image( + src=rx.color_mode_cond( + light=f"/logos/light/radial_circle{theme}.svg", + dark=f"/logos/dark/radial_circle{theme}.svg", + ), + alt="Radial circle", + loading="lazy", + class_name="top-0 right-0 absolute pointer-events-none z-[-1]", + ) + + +def card( + title: str, description: str, features: list[str], button_text: str +) -> rx.Component: + return rx.box( + rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl mb-2"), + rx.el.p( + description, class_name="text-sm font-medium text-slate-9 mb-8 text-pretty" + ), + rx.el.ul( + *[ + rx.el.li( + rx.icon("circle-check", class_name="!text-violet-9", size=16), + feature, + class_name="text-sm font-medium text-slate-11 flex items-center gap-1.5", + ) + for feature in features + ], + class_name="flex flex-col gap-2", + ), + rx.box(class_name="flex-1"), + rx.link( + button( + button_text, + variant="secondary", + class_name="w-full !text-sm !font-semibold !text-slate-11", + ), + href=HOSTING_URL, + is_external=True, + underline="none", + ), + class_name="flex flex-col p-10 border border-slate-4 rounded-[1.125rem] shadow-small bg-slate-2 w-full min-w-[20.375rem] h-[30.5rem]", + ) + + +def popular_card( + title: str, description: str, features: list[str], button_text: str +) -> rx.Component: + return rx.box( + radial_circle(), + rx.box( + "Most popular", + class_name="absolute top-[-0.75rem] left-8 rounded-md bg-[--violet-9] h-[1.5rem] z-[1] text-sm font-medium text-center px-2 flex items-center justify-center text-[#FCFCFD]", + ), + rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl mb-2"), + rx.el.p(description, class_name="text-sm font-medium text-slate-9 mb-8"), + rx.el.ul( + *[ + rx.el.li( + rx.icon("circle-check", class_name="!text-violet-9", size=16), + feature, + class_name="text-sm font-medium text-slate-11 flex items-center gap-1.5", + ) + for feature in features + ], + class_name="flex flex-col gap-2", + ), + rx.box(class_name="flex-1"), + rx.link( + button( + button_text, + variant="primary", + class_name="w-full !text-sm !font-semibold", + ), + href=HOSTING_URL, + is_external=True, + underline="none", + ), + class_name="flex flex-col p-10 border border-slate-4 rounded-[1.125rem] shadow-small bg-slate-2 w-full min-w-[20.375rem] h-[30.5rem] relative z-[1]", + ) + + +def plan_cards() -> rx.Component: + return rx.box( + card( + "Hobby", + "Get started for free and switch plan as you grow.", + [ + "Community support", + "1 team member", + "1 deployed app", + "1 day log retention", + "Basic analytics", + ], + "Start building for free", + ), + popular_card( + "Pro", + "Start a project with all you need for $19/mo per member. Plus usage.", + [ + "Community support", + "Up to 5 team members", + "Max 5 deployed apps", + "30 days log retention", + "Multi-region", + "1 custom domain", + ], + "Start with Pro plan", + ), + card( + "Team", + "Get the most comfort for $249/mo and $29/mo per member. Plus usage.", + [ + "Dedicated support: Slack, Teams", + "Up to 25 team members", + "Unlimited Apps", + "90 days log retention", + "Metrics and analytics", + "One-click Auth", + "5 custom domains", + "All in Pro", + ], + "Start with Team plan", + ), + card( + "Enterprise", + "Get our priority support and a plan tailored to your needs.", + [ + "Custom onboarding", + "Support SLAs", + "Advanced app analytics", + "AI tools for app building", + "Unlimited team members", + "Customized machine size", + "Influence Reflex roadmap", + "All in Team", + ], + "Contact sales", + ), + class_name="grid 2xl:grid-cols-4 xl:grid-cols-2 sm:grid-cols-1 gap-4", + ) diff --git a/pcweb/pages/pricing/pricing.py b/pcweb/pages/pricing/pricing.py new file mode 100644 index 0000000000..f3e7b7c774 --- /dev/null +++ b/pcweb/pages/pricing/pricing.py @@ -0,0 +1,33 @@ +import reflex as rx +from pcweb.components.webpage.badge import badge +from pcweb.pages.index.index_colors import index_colors +from pcweb.pages.index.views.footer_index import footer_index +from pcweb.pages.pricing.header import header +from pcweb.pages.pricing.plan_cards import plan_cards +from pcweb.pages.pricing.table import comparison_table +from pcweb.views.bottom_section.get_started import get_started +from pcweb.pages.pricing.faq import faq + + +@rx.page(route="/pricing", title="Reflex · Pricing") +def pricing() -> rx.Component: + """Get the Pricing landing page.""" + from pcweb.components.docpage.navbar import navbar + + return rx.box( + index_colors(), + navbar(), + rx.el.main( + rx.box( + header(), + plan_cards(), + comparison_table(), + faq(), + class_name="flex flex-col relative justify-center items-center w-full", + ), + class_name="flex flex-col w-full relative h-full justify-center items-center", + ), + footer_index(), + badge(), + class_name="flex flex-col w-full max-w-[94.5rem] justify-center items-center mx-auto px-4 lg:px-5 relative overflow-hidden", + ) diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py new file mode 100644 index 0000000000..193b162da1 --- /dev/null +++ b/pcweb/pages/pricing/table.py @@ -0,0 +1,205 @@ +import reflex as rx +from pcweb.components.button import button +from pcweb.constants import HOSTING_URL + +# Constants for styling +STYLES = { + "cell": "text-slate-12 font-medium text-sm whitespace-nowrap", + "header_cell": "text-slate-12 font-semibold text-lg", + "feature_cell": "text-slate-9 font-medium text-sm whitespace-nowrap", + "button_base": "!text-sm !font-semibold w-full text-nowrap", +} + +TABLE_STYLE = """ +.rt-TableCell { + background-color: transparent !important; + box-shadow: none !important; + vertical-align: 0 !important; + padding: 0 !important; + height: 0 !important; + gap: 0 !important; + width: 100% !important; + min-height: max-content !important; +} +.rt-TableRow { + display: grid !important; + grid-template-columns: minmax(200px, 1fr) repeat(3, minmax(150px, 1fr)) !important; + padding: 1rem 2.5rem; + gap: 6rem !important; +} +.rt-ScrollAreaViewport { + padding-top: 2rem; +} +""" + +# Data configuration +TOP_SECTION = [ + ("Compute", "Up to 5 CPU 10GB", "Usage Based", "Usage Based"), + ("Regions", "Multiple", "Multiple", "Multiple"), + ("Team size", "Up to 5", "Up to 25", "Unlimited"), + ("Build logs", "30 days", "90 days", "Custom"), + ("Runtime logs", "1 day", "1 week", "Custom"), + ("Support", "Community support", "Slack, Teams, Email", "Custom"), +] + +SECOND_SECTION = [ + ("Web app firewall", True, True, True), + ("HTTP/SSL", True, True, True), + ("DDos", True, True, True), + ("Custom onboarding", False, False, True), +] + +THIRD_SECTION = [ + ("Custom onboarding", False, False, True), + ("Migrate existing apps", False, False, True), +] + +PLAN_BUTTONS = [ + ("Start with Pro plan", "primary", "!text-[#FCFCFD]"), + ("Start with Team plan", "secondary", "!text-slate-11"), + ("Contact sales", "secondary", "!text-slate-11"), +] + + +def glow() -> rx.Component: + return rx.table.row( + class_name="absolute flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[5] top-[-1rem] pointer-events-none", + background_image=rx.color_mode_cond( + "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.661) 0%, rgba(252, 252, 253, 0.00) 100%) !important", + "radial-gradient(50% 50% at 50% 50%, rgba(58, 45, 118, 0.241) 0%, rgba(21, 22, 24, 0.00) 100%) !important", + ), + height="6rem !important", + width="60.75rem !important", + ) + + +def header() -> rx.Component: + return rx.box( + rx.el.h3( + "Compare features across plans.", + class_name="text-slate-12 text-3xl font-semibold text-center", + ), + rx.el.p( + "Find a perfect fit", + class_name="text-slate-9 text-3xl font-semibold text-center", + ), + class_name="flex items-center justify-between text-slate-11 flex-col py-[5rem] 2xl:border-x border-slate-4 max-w-[64.125rem] mx-auto w-full", + ) + + +def create_table_cell(content: str | rx.Component) -> rx.Component: + return rx.table.cell(content, class_name=STYLES["cell"]) + + +def create_action_button( + text: str, variant: str, extra_styles: str = "" +) -> rx.Component: + return rx.link( + button( + text, + variant=variant, + class_name=f"{STYLES['button_base']} {extra_styles}", + ), + href=HOSTING_URL, + is_external=True, + underline="none", + class_name="w-full flex justify-center items-center", + ) + + +def create_table_row(cells: list) -> rx.Component: + row_cells = [create_table_cell(cell) for cell in cells] + return rx.table.row( + *row_cells, + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 relative z-[2]", + ) + + +def create_table_row_header(cells: list) -> rx.Component: + return rx.table.row( + *[ + rx.table.column_header_cell(cell, class_name=STYLES["header_cell"]) + for cell in cells + ], + class_name="w-full [&>*:not(:first-child)]:text-center relative bg-slate-2 border border-slate-3 rounded-2xl z-[6]", + padding_x="5rem !important", + ) + + +def create_table_body(*body_content) -> rx.Component: + return rx.table.body( + *body_content, + class_name="w-full divide-y divide-slate-4 border border-slate-4 md:border-t-0 flex flex-col items-center justify-center border-x max-w-[64.125rem] mx-auto border-b-0", + ) + + +def create_checkmark_row(feature: str, checks: tuple[bool, ...]) -> rx.Component: + cells = [ + feature, + *[ + rx.box( + rx.icon("check", class_name="text-slate-12", size=16) if c else "", + class_name="flex justify-center items-center", + ) + for c in checks + ], + ] + return create_table_row(cells) + + +def table_body() -> rx.Component: + return rx.table.root( + rx.el.style(TABLE_STYLE), + rx.table.header( + create_table_row_header(["Features", "Pro", "Team", "Enterprise"]), + glow(), + class_name="relative", + ), + # Section 1 + create_table_body( + *[create_table_row(row) for row in TOP_SECTION], + ), + # Section 2 + rx.table.header( + create_table_row_header(["Section 2", "", "", ""]), + glow(), + class_name="relative", + ), + create_table_body( + *[ + create_checkmark_row(feature, checks) + for feature, *checks in SECOND_SECTION + ], + ), + # Section 3 + rx.table.header( + create_table_row_header(["Section 3", "", "", ""]), + glow(), + class_name="relative", + ), + create_table_body( + *[ + create_checkmark_row(feature, checks) + for feature, *checks in THIRD_SECTION + ], + ), + create_table_body( + rx.table.row( + rx.table.cell(), + *[ + rx.table.cell(create_action_button(text, variant, extra)) + for text, variant, extra in PLAN_BUTTONS + ], + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 !py-[1.25rem] border-y border-slate-4", + ), + ), + class_name="w-full overflow-x-auto max-w-[69.125rem] -mt-[2rem]", + ) + + +def comparison_table() -> rx.Component: + return rx.box( + header(), + table_body(), + class_name="flex flex-col w-full max-w-[69.125rem]", + ) diff --git a/pcweb/styles/tailwind_config.py b/pcweb/styles/tailwind_config.py index 566e5a9527..f7f31b3146 100644 --- a/pcweb/styles/tailwind_config.py +++ b/pcweb/styles/tailwind_config.py @@ -5,7 +5,9 @@ "darkMode": "class", "theme": { "fontFamily": { - "code": ["JetBrains Mono", "monospace"], + "sans": ["Instrument Sans", "sans-serif"], + "mono": ["JetBrains Mono", "monospace"], + "body": ["Instrument Sans", "sans-serif"], }, "fontSize": { "xs": [ @@ -90,5 +92,29 @@ "medium": "0px 4px 8px 0px light-dark(rgba(28, 32, 36, 0.04), rgba(0, 0, 0, 0.00))", "large": "0px 24px 12px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00)), 0px 8px 8px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00)), 0px 2px 6px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00))", }, + "keyframes": { + "accordion-down": { + "from": {"height": "0"}, + "to": {"height": "var(--radix-accordion-content-height)"}, + }, + "accordion-up": { + "from": {"height": "var(--radix-accordion-content-height)"}, + "to": {"height": "0"}, + }, + "spin": { + "from": {"transform": "rotate(0deg)"}, + "to": {"transform": "rotate(360deg)"}, + }, + "blur-in": { + "0%": {"filter": "blur(4px)"}, + "100%": {"filter": "blur(0)"}, + }, + }, + "animation": { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "spin": "spin 1s linear infinite", + "blur-in": "blur-in 0.15s ease forwards", + }, }, } From a17bfe8ecba1e40a3afcd629de0907991d25413a Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 20 Nov 2024 20:47:45 +0100 Subject: [PATCH 02/16] sales@reflex.dev link --- pcweb/pages/pricing/faq.py | 2 +- pcweb/pages/pricing/plan_cards.py | 6 +++++- pcweb/pages/pricing/table.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pcweb/pages/pricing/faq.py b/pcweb/pages/pricing/faq.py index 16d1bafd24..d41630242d 100644 --- a/pcweb/pages/pricing/faq.py +++ b/pcweb/pages/pricing/faq.py @@ -35,7 +35,7 @@ def sales_button() -> rx.Component: variant="secondary", class_name="!text-slate-11 !font-semibold !text-sm", ), - href=HOSTING_URL, # TODO: Change to sales page when we have it + href="mailto:sales@reflex.dev", # TODO: Change to sales page when we have it is_external=True, class_name="self-center relative", ) diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index 27faecd195..4140d5a0df 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -51,7 +51,11 @@ def card( variant="secondary", class_name="w-full !text-sm !font-semibold !text-slate-11", ), - href=HOSTING_URL, + href=( + HOSTING_URL + if button_text != "Contact sales" + else "mailto:sales@reflex.dev" + ), is_external=True, underline="none", ), diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 193b162da1..ca27b48040 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -100,7 +100,7 @@ def create_action_button( variant=variant, class_name=f"{STYLES['button_base']} {extra_styles}", ), - href=HOSTING_URL, + href=HOSTING_URL if text != "Contact sales" else "mailto:sales@reflex.dev", is_external=True, underline="none", class_name="w-full flex justify-center items-center", From c52b3ef19cb75f94938612094e30f3aef03c3941 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Wed, 20 Nov 2024 21:40:10 +0100 Subject: [PATCH 03/16] hide glow --- pcweb/pages/pricing/table.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index ca27b48040..69be275876 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -63,7 +63,7 @@ def glow() -> rx.Component: return rx.table.row( - class_name="absolute flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[5] top-[-1rem] pointer-events-none", + class_name="absolute flex-shrink-0 left-1/2 -translate-x-1/2 z-[5] top-[-1rem] pointer-events-none", background_image=rx.color_mode_cond( "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.661) 0%, rgba(252, 252, 253, 0.00) 100%) !important", "radial-gradient(50% 50% at 50% 50%, rgba(58, 45, 118, 0.241) 0%, rgba(21, 22, 24, 0.00) 100%) !important", @@ -111,7 +111,7 @@ def create_table_row(cells: list) -> rx.Component: row_cells = [create_table_cell(cell) for cell in cells] return rx.table.row( *row_cells, - class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 relative z-[2]", + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 z-[2] !h-[56px]", ) @@ -121,7 +121,7 @@ def create_table_row_header(cells: list) -> rx.Component: rx.table.column_header_cell(cell, class_name=STYLES["header_cell"]) for cell in cells ], - class_name="w-full [&>*:not(:first-child)]:text-center relative bg-slate-2 border border-slate-3 rounded-2xl z-[6]", + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-2 border border-slate-3 rounded-2xl z-[6] !h-[3.625rem] relative", padding_x="5rem !important", ) @@ -152,7 +152,7 @@ def table_body() -> rx.Component: rx.el.style(TABLE_STYLE), rx.table.header( create_table_row_header(["Features", "Pro", "Team", "Enterprise"]), - glow(), + # glow(), class_name="relative", ), # Section 1 @@ -162,7 +162,7 @@ def table_body() -> rx.Component: # Section 2 rx.table.header( create_table_row_header(["Section 2", "", "", ""]), - glow(), + # glow(), class_name="relative", ), create_table_body( @@ -174,7 +174,7 @@ def table_body() -> rx.Component: # Section 3 rx.table.header( create_table_row_header(["Section 3", "", "", ""]), - glow(), + # glow(), class_name="relative", ), create_table_body( @@ -190,7 +190,7 @@ def table_body() -> rx.Component: rx.table.cell(create_action_button(text, variant, extra)) for text, variant, extra in PLAN_BUTTONS ], - class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 !py-[1.25rem] border-y border-slate-4", + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 !py-[1.25rem] border-y border-slate-4 !h-[76px] relative", ), ), class_name="w-full overflow-x-auto max-w-[69.125rem] -mt-[2rem]", From 306b8c969a7a7f6ec80a61c649181f2de37d6a05 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Wed, 20 Nov 2024 12:40:29 -0800 Subject: [PATCH 04/16] More updates --- docs/hosting/deploy-quick-start.md | 18 +-- docs/hosting/hosting-cli-commands.md | 157 +++---------------------- pcweb/pages/pricing/plan_cards.py | 20 ++-- pcweb/pages/pricing/table.py | 14 +++ pcweb/templates/docpage/blocks/code.py | 35 ++---- pcweb/whitelist.py | 2 +- 6 files changed, 58 insertions(+), 188 deletions(-) diff --git a/docs/hosting/deploy-quick-start.md b/docs/hosting/deploy-quick-start.md index 67acb90f0b..68a43e9b31 100644 --- a/docs/hosting/deploy-quick-start.md +++ b/docs/hosting/deploy-quick-start.md @@ -17,7 +17,7 @@ Reflex’s hosting service makes it easy to deploy your apps without worrying ab ### Prerequisites -1. Hosting service requires `reflex>=0.3.2`. +1. Hosting service requires `reflex>=0.6.5`. 2. This tutorial assumes you have successfully `reflex init` and `reflex run` your app. 3. Also make sure you have a `requirements.txt` file at the top level app directory that contains all your python dependencies! @@ -43,7 +43,13 @@ reflex deploy The command is by default interactive. It asks you a few questions for information required for the deployment. -**Name**: choose a name for the deployed app. This name will be part of the deployed app URL, i.e. `.reflex.run`. The name should only contain domain name safe characters: no slashes, no underscores. Domain names are case insensitive. To avoid confusion, the name you choose here is also case insensitive. If you enter letters in upper cases, we automatically convert them to lower cases. +**Name**: choose a name for the deployed app. This name will be part of the deployed app URL, i.e. `-randomword-randomword.reflex.run`. + +The name should only contain domain name safe characters: no slashes, no underscores. + +```md alert info +# Custom domains are available for paid plans. +``` **Regions**: enter the region code here or press `Enter` to accept the default. The default code `sjc` stands for San Jose, California in the US west coast. Check the list of supported regions at [reflex deployments regions](#reflex-deployments-regions). @@ -52,13 +58,7 @@ The command is by default interactive. It asks you a few questions for informati That’s it! You should receive some feedback on the progress of your deployment and in a few minutes your app should be up. 🎉 ```md alert info -# Once your code is uploaded, the hosting service will start the deployment. After a complete upload, exiting from the command **does not** affect the deployment process. The command prints a message when you can safely close it without affecting the deployment. -``` - -The hosting service does not currently handle database or file upload operations. It is necessary to set up an external database use it within your app. - -```md alert info -# If you want to deploy on a custom URL we recommend using https://redirect.pizza +# The hosting service does not currently handle database or file upload operations. Set up an external database use it within your app. ``` ## See it in Action diff --git a/docs/hosting/hosting-cli-commands.md b/docs/hosting/hosting-cli-commands.md index 75057b7112..ca2214065b 100644 --- a/docs/hosting/hosting-cli-commands.md +++ b/docs/hosting/hosting-cli-commands.md @@ -56,25 +56,8 @@ To redeploy or update your app, navigate to the project directory and type `refl All the `reflex` commands come with a help manual. The help manual lists additional command options that may be useful. You type `--help` to see the help manual. Some commands are organized under a `subcommands` series. Here is an example below. Note that the help manual may look different depending on the version of `reflex` or the `reflex-hosting-cli`. -```python eval -doccmdoutput( - command="reflex deployments --help", - output="""Usage: reflex deployments [OPTIONS] COMMAND [ARGS]... - - Subcommands for managing the Deployments. - -Options: - --help Show this message and exit. - -Commands: - build-logs Get the build logs for a deployment. - delete Delete a hosted instance. - list List all the hosted deployments of the authenticated user. - logs Get the logs for a deployment. - regions List all the regions of the hosting service. - status Check the status of a deployment. -""" -) +``` bash +reflex deployments --help ``` ### Authentication Commands @@ -83,13 +66,8 @@ Commands: When you type the `reflex login` command for the very first time, it opens the hosting service login page in your browser. We authenticate users through OAuth. At the moment the supported OAuth providers are Github and Gmail. You should be able to revoke such authorization on your Github and Google account settings page. We do not log into your Github or Gmail account. OAuth authorization provides us your email address and in case of Github your username handle. We use those to create an account for you. The email used in the original account creation is used to identify you as a user. If you have authenticated using different emails, those create separate accounts. To switch to another account, first log out using the `reflex logout` command. More details on the logout command are in [reflex logout](#reflex-logout) section. -```python eval -doccmdoutput( - command="reflex login", - output="""Opening https://control-plane.reflex.run ... -Successfully logged in. -""", -) +``` bash +reflex login ``` After authentication, the browser redirects to the original hosting service login page. It shows that you have logged in. Now you can return to the terminal where you type the login command. It should print a message such as `Successfully logged in`. @@ -108,55 +86,8 @@ This is the command to deploy a reflex app from its top level app directory. Thi A `requirements.txt` file is required. The deploy command checks the content of this file against the top level packages installed in your current Python environment. If the command detects new packages in your Python environment, or newer versions of the same packages, it prints the difference and asks if you would like to update your `requirements.txt`. Make sure you double check the suggested updates. This functionality is added in more recent versions of the hosting CLI package `reflex-hosting-cli>=0.1.3`. -```python eval -doccmdoutput( - command="reflex deploy", - output="""Info: The requirements.txt may need to be updated. ---- requirements.txt -+++ new_requirements.txt -@@ -1,3 +1,3 @@ --reflex>=0.2.0 --openai==0.28 -+openai==0.28.0 -+reflex==0.3.8 - -Would you like to update requirements.txt based on the changes above? [y/n]: y - -Choose a name for your deployed app (https://.reflex.run) -Enter to use default. (webui-gray-sun): demo-chat -Region to deploy to. See regions: https://bit.ly/46Qr3mF -Enter to use default. (sjc): lax -Environment variables for your production App ... - * env-1 name (enter to skip): OPENAI_API_KEY - env-1 value: sk-********************* - * env-2 name (enter to skip): -Finished adding envs. -──────────────── Compiling production app and preparing for export. ──────────────── -Zipping Backend: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 12/12 0:00:00 -Uploading Backend code and sending request ... -Backend deployment will start shortly. -──────────────── Compiling production app and preparing for export. ──────────────── -Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 9/9 0:00:00 -Creating Production Build: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 9/9 0:00:07 -Zipping Frontend: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 20/20 0:00:00 -Uploading Frontend code and sending request ... -Frontend deployment will start shortly. -───────────────────────────── Deploying production app. ──────────────────────────── -Deployment will start shortly: https://demo-chat.reflex.run -Closing this command now will not affect your deployment. -Waiting for server to report progress ... -2024-01-12 12:24:54.188271 PST | Updating frontend... -2024-01-12 12:24:55.074264 PST | Frontend updated! -2024-01-12 12:24:55.137679 PST | Deploy success (frontend) -2024-01-12 12:24:59.722384 PST | Updating backend... -2024-01-12 12:25:01.006386 PST | Building backend image... -2024-01-12 12:26:03.672379 PST | Deploying backend image... -2024-01-12 12:26:21.017946 PST | Backend updated! -2024-01-12 12:26:21.018003 PST | Deploy success (backend) -Waiting for the new deployment to come up -Your site [ demo-chat ] at ['lax'] is up: https://demo-chat.reflex.run -""", -) +``` bash +reflex deploy ``` The deploy command is by default interactive. To deploy without interaction, add `--no-interactive` and set the relevant command options as deployment settings. Type `reflex deploy --help` to see the help manual for explanations on each option. The deploy sequences are the same, whether the deploy command is interactive or not. @@ -169,40 +100,16 @@ reflex deploy --no-interactive -k todo -r sjc -r sea --env OPENAI_API_KEY=YOU-KE List all your deployments. -```python eval -doccmdoutput( - command="reflex deployments list", - output="""key regions app_name reflex_version cpus memory_mb url envs ----------------------------- ------- -------------------- ---------------- ------- ----------- ------------------------------------------ --------- -webui-navy-star ['sjc'] webui 0.3.7 1 1024 https://webui-navy-star.reflex.run ['OPENAI_API_KEY'] -chatroom-teal-ocean ['ewr'] chatroom 0.3.2 1 1024 https://chatroom-teal-ocean.reflex.run [] -sales-navy-moon ['phx'] sales 0.3.4 1 1024 https://sales-navy-moon.reflex.run [] -simple-background-tasks ['yul'] lorem_stream 0.3.7 1 1024 https://simple-background-tasks.reflex.run [] -snakegame ['sjc'] snakegame 0.3.3 1 1024 https://snakegame.reflex.run [] -basic-crud-navy-apple ['dfw'] basic_crud 0.3.8 1 1024 https://basic-crud-navy-apple.reflex.run [] -""", -) +``` bash +reflex deployments list ``` #### reflex deployments status `app-name` Get the status of a specific app, including backend and frontend. -```python eval -doccmdoutput( - command="reflex deployments status clock-gray-piano", - output="""Getting status for [ clock-gray-piano ] ... - -backend_url reachable updated_at ------------------------------------------ ----------- ------------ -https://rxh-prod-clock-gray-piano.fly.dev False N/A - - -frontend_url reachable updated_at ------------------------------------------ ----------- ----------------------- -https://clock-gray-piano.reflex.run True 2023-10-13 15:23:07 PDT -""", -) +``` bash +reflex deployments status clock-gray-piano ``` #### reflex deployments logs `app-name` @@ -213,52 +120,16 @@ The returned logs are the messages printed to console. If you have `print` state We have added more options to this command including `from` and `to` timestamps and the limit on how many lines of logs to fetch. Accepted timestamp formats include the ISO 8601 format, unix epoch and relative timestamp. A relative timestamp is some time units ago from `now`. The units are `d (day), h (hour), m (minute), s (second)`. For example, `--from 3d --to 4h` queries from 3 days ago up to 4 hours ago. For the exact syntax in the version of CLI you use, refer to the help manual. -```python eval -doccmdoutput( - command="reflex deployments logs todo", - output="""Note: there is a few seconds delay for logs to be available. -2023-10-13 22:18:39.696028 | rxh-dev-todo | info | Pulling container image registry.fly.io/rxh-dev-todo:depot-1697235471 -2023-10-13 22:18:41.462929 | rxh-dev-todo | info | Pulling container image registry.fly.io/rxh-dev-todo@sha256:60b7b531e99e037f2fb496b3e05893ee28f93a454ee618bda89a531a547c4002 -2023-10-13 22:18:45.963840 | rxh-dev-todo | info | Successfully prepared image registry.fly.io/rxh-dev-todo@sha256:60b7b531e99e037f2fb496b3e05893ee28f93a454ee618bda89a531a547c4002 (4.500906837s) -2023-10-13 22:18:46.134860 | rxh-dev-todo | info | Successfully prepared image registry.fly.io/rxh-dev-todo:depot-1697235471 (6.438815793s) -2023-10-13 22:18:46.210583 | rxh-dev-todo | info | Configuring firecracker -2023-10-13 22:18:46.434645 | rxh-dev-todo | info | [ 0.042971] Spectre V2 : WARNING: Unprivileged eBPF is enabled with eIBRS on, data leaks possible via Spectre v2 BHB attacks! -2023-10-13 22:18:46.477693 | rxh-dev-todo | info | [ 0.054250] PCI: Fatal: No config space access function found -2023-10-13 22:18:46.664016 | rxh-dev-todo | info | Configuring firecracker -""", -) +``` bash +reflex deployments logs todo ``` #### reflex deployments build-logs `app-name` Get the logs of the hosting service deploying the app. -```python eval -doccmdoutput( - command="reflex deployments build-logs webcam-demo", - output="""Note: there is a few seconds delay for logs to be available. -2024-01-08 11:02:46.109785 PST | fly-controller-prod | #8 extracting sha256:bd9ddc54bea929a22b334e73e026d4136e5b73f5cc29942896c72e4ece69b13d 0.0s done | None | None -2024-01-08 11:02:46.109811 PST | fly-controller-prod | #8 DONE 5.3s | None | None -2024-01-08 11:02:46.109834 PST | fly-controller-prod | | None | None -2024-01-08 11:02:46.109859 PST | fly-controller-prod | #8 [1/4] FROM public.ecr.aws/p3v4g4o2/reflex-hosting-base:v0.1.8-py3.11@sha256:9e8569507f349d78d41a86e1eb29a15ebc9dece487816875bbc874f69dcf7ecf | None | None -... -... -2024-01-08 11:02:50.913748 PST | fly-controller-prod | #11 [4/4] RUN . /home/reflexuser/venv/bin/activate && pip install --no-color --no-cache-dir -q -r /home/reflexuser/app/requirements.txt reflex==0.3.4 | None | None -... -... -2024-01-08 11:03:07.430922 PST | fly-controller-prod | #12 pushing layer sha256:d9212ef47485c9f363f105a05657936394354038a5ae5ce03c6025f7f8d2d425 3.5s done | None | None -2024-01-08 11:03:07.881471 PST | fly-controller-prod | #12 pushing layer sha256:ee46d14ae1959b0cacda828e5c4c1debe32c9c4c5139fb670cde66975a70c019 3.9s done | None | None -... -2024-01-08 11:03:13.943166 PST | fly-controller-prod | Built backend image | None | None -2024-01-08 11:03:13.943174 PST | fly-controller-prod | Deploying backend image... | None | None -2024-01-08 11:03:13.943311 PST | fly-controller-prod | Running sys_run | None | None -... -2024-01-08 11:03:31.005887 PST | fly-controller-prod | Checking for valid image digest to be deployed to machines... | None | None -2024-01-08 11:03:31.005893 PST | fly-controller-prod | Running sys_run | None | None -2024-01-08 11:03:32.411762 PST | fly-controller-prod | Backend updated! | None | None -2024-01-08 11:03:32.481276 PST | fly-controller-prod | Deploy success (backend) | None | None -""", -) +``` bash +reflex deployments build-logs webcam-demo ``` The hosting service prints log messages when preparing and deploying your app. These log messages are called build logs. Build logs are useful in troubleshooting deploy failures. For example, if there is a package `numpz==1.26.3` (supposed to be `numpy`) in the `requirements.txt`, hosting service will be unable to install it. That package does not exist. We expect to find a few lines in the build logs indicating that the `pip install` command fails. diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index 4140d5a0df..729e9c0a77 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -104,7 +104,7 @@ def plan_cards() -> rx.Component: return rx.box( card( "Hobby", - "Get started for free and switch plan as you grow.", + "Everything you need to get started with Reflex.", [ "Community support", "1 team member", @@ -116,14 +116,14 @@ def plan_cards() -> rx.Component: ), popular_card( "Pro", - "Start a project with all you need for $19/mo per member. Plus usage.", + "For professional projects $19/mo per member. Plus usage.", [ "Community support", "Up to 5 team members", "Max 5 deployed apps", "30 days log retention", "Multi-region", - "1 custom domain", + "Custom domains", ], "Start with Pro plan", ), @@ -131,14 +131,13 @@ def plan_cards() -> rx.Component: "Team", "Get the most comfort for $249/mo and $29/mo per member. Plus usage.", [ - "Dedicated support: Slack, Teams", - "Up to 25 team members", + "Email support", + "Up to 15 team members", "Unlimited Apps", "90 days log retention", "Metrics and analytics", "One-click Auth", - "5 custom domains", - "All in Pro", + "Everything in Pro", ], "Start with Team plan", ), @@ -146,14 +145,15 @@ def plan_cards() -> rx.Component: "Enterprise", "Get our priority support and a plan tailored to your needs.", [ + "Advanced support", "Custom onboarding", - "Support SLAs", + "On prem deployments", "Advanced app analytics", - "AI tools for app building", + "SOC 2 report", "Unlimited team members", "Customized machine size", "Influence Reflex roadmap", - "All in Team", + "Everything in Team", ], "Contact sales", ), diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 69be275876..4a6f672007 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -33,6 +33,12 @@ """ # Data configuration +PRICE_SECTION = [ + ("Per Seat Price", "$19/mo", "$29/mo", "Custom"), + ("Compute", "Usage Based", "Usage Based", "Custom"), +] + + TOP_SECTION = [ ("Compute", "Up to 5 CPU 10GB", "Usage Based", "Usage Based"), ("Regions", "Multiple", "Multiple", "Multiple"), @@ -150,6 +156,14 @@ def create_checkmark_row(feature: str, checks: tuple[bool, ...]) -> rx.Component def table_body() -> rx.Component: return rx.table.root( rx.el.style(TABLE_STYLE), + rx.table.header( + create_table_row_header(["Price", "Pro", "Team", "Enterprise"]), + glow(), + class_name="relative", + ), + create_table_body( + *[create_table_row(row) for row in PRICE_SECTION], + ), rx.table.header( create_table_row_header(["Features", "Pro", "Team", "Enterprise"]), # glow(), diff --git a/pcweb/templates/docpage/blocks/code.py b/pcweb/templates/docpage/blocks/code.py index f6c877d96b..16343ccc6a 100644 --- a/pcweb/templates/docpage/blocks/code.py +++ b/pcweb/templates/docpage/blocks/code.py @@ -34,7 +34,7 @@ def code_block_dark(code: str, language: str): class_name="relative", ) - + def code_block_markdown(*children, **props): language = props.get("language", "plain") return code_block(code=children[0], language=language) @@ -43,7 +43,7 @@ def code_block_markdown(*children, **props): def code_block_markdown_dark(*children, **props): language = props.get("language", "plain") return code_block_dark(code=children[0], language=language) - + def doccmdoutput( command: str, @@ -59,10 +59,8 @@ def doccmdoutput( Returns: The styled command and its example output. """ - return rx.flex( - rx.flex( - rx.lucide.icon(tag="chevrons-right", color="white", width=18, height=18), - rx._x.code_block( + return rx.vstack( + rx._x.code_block( command, can_copy=True, border_radius=styles.DOC_BORDER_RADIUS, @@ -76,20 +74,14 @@ def doccmdoutput( }, style=fonts.code, font_family="Source Code Pro", - ), - direction="row", - align="center", - spacing="1", - margin_left="1em", + width="100%", ), - rx.divider(size="4", color_scheme="green"), - rx.flex( - rx._x.code_block( + rx._x.code_block( output, - can_copy=True, + can_copy=False, border_radius="12px", background="transparent", - theme="nord", + theme="ayu-dark", language="log", code_tag_props={ "style": { @@ -98,14 +90,7 @@ def doccmdoutput( }, style=fonts.code, font_family="Source Code Pro", - ), + width="100%", ), - direction="column", - spacing="2", - border_radius="12px", - border=f"1px solid {c_color('slate', 5)}", - position="relative", - margin_y="1em", - width="100%", - background_color="black", + padding_y="1em", ) diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index c3f74420bb..838c9e638b 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = [] +WHITELISTED_PAGES = ["/docs/hosting"] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: From 37c917c3293e0c440c0f0e5b0babefc31530070b Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Wed, 20 Nov 2024 20:37:24 -0800 Subject: [PATCH 05/16] More updates --- pcweb/pages/pricing/faq.py | 22 ++++++------- pcweb/pages/pricing/plan_cards.py | 6 ++-- pcweb/pages/pricing/table.py | 52 ++++++++++++++++++++----------- pcweb/whitelist.py | 2 +- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/pcweb/pages/pricing/faq.py b/pcweb/pages/pricing/faq.py index d41630242d..12f57fa740 100644 --- a/pcweb/pages/pricing/faq.py +++ b/pcweb/pages/pricing/faq.py @@ -85,29 +85,25 @@ def accordion_text(text: str) -> rx.Component: "Yes! Reflex is open source and free to use. You can self-host your apps or use our hosting platform which has a generous free tier.", ), ( - "How usage based billing is calculated?", - "Usage is calculated based on compute resources (CPU/RAM) consumed by your app. We measure this in compute units per hour.", + "What is the difference between Reflex and Reflex Hosting?", + "Reflex is our open-source framework for building web apps. Reflex Hosting is our platform for hosting Reflex apps.", ), ( - "What is your privacy and security policy?", - "We take security seriously. All apps are deployed with SSL certificates, DDoS protection, and web application firewall. Your code and data remain private and secure.", + "Why was I charged $1 after adding a credit card?", + "A $1 USD transaction is performed as a credit security check to ensure your card details are correct and authorized. The charge is refunded after the transaction completes.", ), ( - "What happens when I upgrade?", - "When you upgrade your plan, you'll immediately get access to the new features and increased resource limits. Your app will continue running without interruption.", + "How usage based billing is calculated?", + "Usage is calculated based on compute resources (CPU/RAM) consumed by your app. We measure this in compute units per hour.", ), ( - "How are prorations calculated?", - "If you upgrade mid-billing cycle, we'll prorate the new charges for the remainder of the billing period. You'll only pay for what you use.", + "What happens when I upgrade?", + "When you upgrade your plan, you'll immediately get access to the new features and increased resource limits. Your app will continue running without interruption.", ), ( "What happens if I cancel the plan?", "If you cancel, you'll maintain access until the end of your current billing period. After that, your app will be downgraded to the free tier limits.", ), - ( - "How can I influence Reflex roadmap?", - "We welcome feedback and feature requests from our community. You can submit ideas through GitHub issues or discuss them in our Discord community.", - ), ( "What determines the total amount billed?", "Your bill is determined by your base plan plus any usage-based charges for compute resources that exceed the plan limits.", @@ -118,7 +114,7 @@ def accordion_text(text: str) -> rx.Component: ), ( "Can I add members to my project?", - "Yes! Pro plan allows up to 5 team members, Team plan up to 25 members, and Enterprise plan has unlimited team members.", + "Yes! Pro plan allows up to 5 team members, Team plan up to 15 members, and Enterprise plan have unlimited team members.", ), ] diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index 729e9c0a77..df80a56a18 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -145,14 +145,12 @@ def plan_cards() -> rx.Component: "Enterprise", "Get our priority support and a plan tailored to your needs.", [ - "Advanced support", - "Custom onboarding", + "Priority Support + Custom Onboarding", "On prem deployments", "Advanced app analytics", - "SOC 2 report", "Unlimited team members", "Customized machine size", - "Influence Reflex roadmap", + "SOC 2 report", "Everything in Team", ], "Contact sales", diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 4a6f672007..6b6b6f094f 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -39,23 +39,36 @@ ] -TOP_SECTION = [ - ("Compute", "Up to 5 CPU 10GB", "Usage Based", "Usage Based"), +COMPUTE_SECTION = [ + ("On Premise (Optional)", False, False, True), + ("Compute Per Project", "5 CPU, 10GB", "Unlimited", "Unlimited"), ("Regions", "Multiple", "Multiple", "Multiple"), - ("Team size", "Up to 5", "Up to 25", "Unlimited"), - ("Build logs", "30 days", "90 days", "Custom"), + ("Team size", "< 5", "< 15", "Unlimited"), ("Runtime logs", "1 day", "1 week", "Custom"), - ("Support", "Community support", "Slack, Teams, Email", "Custom"), + ("Build logs", "30 days", "90 days", "Custom"), + ] -SECOND_SECTION = [ +FEATURE_SECTION = [ + ("Custom domains", True, True, True), + ("Secrets", True, True, True), + ("Metrics and analytics", True, True, True), + ("Automatic CI/CD", True, True, True), + ("Multi-region", True, True, True), + ("One-click Auth", False, True, True), + ("Cron jobs", False, True, True), + ("SSO", False, False, True), +] + +SECURITY_SECTION = [ ("Web app firewall", True, True, True), ("HTTP/SSL", True, True, True), ("DDos", True, True, True), ("Custom onboarding", False, False, True), ] -THIRD_SECTION = [ +SUPPORT_SECTION = [ + ("Support", "Community support", "Email (1 Business Day)", "Support SLAs available"), ("Custom onboarding", False, False, True), ("Migrate existing apps", False, False, True), ] @@ -165,36 +178,37 @@ def table_body() -> rx.Component: *[create_table_row(row) for row in PRICE_SECTION], ), rx.table.header( - create_table_row_header(["Features", "Pro", "Team", "Enterprise"]), - # glow(), + create_table_row_header(["Compute", "", "", ""]), + class_name="relative", + ), + create_table_body( + *[create_table_row(row) for row in COMPUTE_SECTION], + ), + rx.table.header( + create_table_row_header(["Features", "", "", ""]), class_name="relative", ), - # Section 1 create_table_body( - *[create_table_row(row) for row in TOP_SECTION], + *[create_checkmark_row(feature, checks) for feature, *checks in FEATURE_SECTION], ), - # Section 2 rx.table.header( - create_table_row_header(["Section 2", "", "", ""]), - # glow(), + create_table_row_header(["Security", "", "", ""]), class_name="relative", ), create_table_body( *[ create_checkmark_row(feature, checks) - for feature, *checks in SECOND_SECTION + for feature, *checks in SECURITY_SECTION ], ), - # Section 3 rx.table.header( - create_table_row_header(["Section 3", "", "", ""]), - # glow(), + create_table_row_header(["Support", "", "", ""]), class_name="relative", ), create_table_body( *[ create_checkmark_row(feature, checks) - for feature, *checks in THIRD_SECTION + for feature, *checks in SUPPORT_SECTION ], ), create_table_body( diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index 838c9e638b..d95607b0dc 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = ["/docs/hosting"] +WHITELISTED_PAGES = ["/pricing"] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: From b64df6b40bb0246a69c2187647fa9a1667d77168 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 22 Nov 2024 12:47:28 +0100 Subject: [PATCH 06/16] hobby column --- pcweb/pages/pricing/table.py | 76 ++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 6b6b6f094f..feb05bbb5c 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -23,7 +23,7 @@ } .rt-TableRow { display: grid !important; - grid-template-columns: minmax(200px, 1fr) repeat(3, minmax(150px, 1fr)) !important; + grid-template-columns: minmax(100px, 1fr) repeat(4, minmax(100px, 1fr)) !important; padding: 1rem 2.5rem; gap: 6rem !important; } @@ -34,49 +34,52 @@ # Data configuration PRICE_SECTION = [ - ("Per Seat Price", "$19/mo", "$29/mo", "Custom"), - ("Compute", "Usage Based", "Usage Based", "Custom"), + ("Per Seat Price", "$19/mo", "$19/mo", "$29/mo", "Custom"), + ("Compute", "Usage Based", "Usage Based", "Usage Based", "Custom"), ] COMPUTE_SECTION = [ - ("On Premise (Optional)", False, False, True), - ("Compute Per Project", "5 CPU, 10GB", "Unlimited", "Unlimited"), - ("Regions", "Multiple", "Multiple", "Multiple"), - ("Team size", "< 5", "< 15", "Unlimited"), - ("Runtime logs", "1 day", "1 week", "Custom"), - ("Build logs", "30 days", "90 days", "Custom"), - + ("Compute Per Project", "5 CPU, 10GB", "5 CPU, 10GB", "Unlimited", "Unlimited"), + ("Regions", "Multiple", "Multiple", "Multiple", "Multiple"), + ("Team size", "< 5", "< 5", "< 15", "Unlimited"), + ("Runtime logs", "1 day", "1 day", "1 week", "Custom"), + ("Build logs", "30 days", "30 days", "90 days", "Custom"), ] +ON_PREMISE_ROW = [("On Premise (Optional)", False, False, False, True)] + FEATURE_SECTION = [ - ("Custom domains", True, True, True), - ("Secrets", True, True, True), - ("Metrics and analytics", True, True, True), - ("Automatic CI/CD", True, True, True), - ("Multi-region", True, True, True), - ("One-click Auth", False, True, True), - ("Cron jobs", False, True, True), - ("SSO", False, False, True), + ("Custom domains", True, True, True, True), + ("Secrets", True, True, True, True), + ("Metrics and analytics", True, True, True, True), + ("Automatic CI/CD", True, True, True, True), + ("Multi-region", False, True, True, True), + ("One-click Auth", False, False, True, True), + ("Cron jobs", False, False, True, True), + ("SSO", False, False, False, True), ] SECURITY_SECTION = [ - ("Web app firewall", True, True, True), - ("HTTP/SSL", True, True, True), - ("DDos", True, True, True), - ("Custom onboarding", False, False, True), + ("Web app firewall", True, True, True, True), + ("HTTP/SSL", True, True, True, True), + ("DDos", True, True, True, True), + ("Custom onboarding", False, False, True, True), ] SUPPORT_SECTION = [ - ("Support", "Community support", "Email (1 Business Day)", "Support SLAs available"), - ("Custom onboarding", False, False, True), - ("Migrate existing apps", False, False, True), + ("Community support", True, True, True, True), + ("Email (1 Business Day)", False, False, False, True), + ("Support SLAs available", False, False, False, True), + ("Custom onboarding", False, False, False, True), + ("Migrate existing apps", False, False, False, True), ] PLAN_BUTTONS = [ - ("Start with Pro plan", "primary", "!text-[#FCFCFD]"), - ("Start with Team plan", "secondary", "!text-slate-11"), - ("Contact sales", "secondary", "!text-slate-11"), + ("Start building for free", "secondary", "!text-slate-11 !w-fit"), + ("Start with Pro plan", "primary", "!text-[#FCFCFD] !w-fit"), + ("Start with Team plan", "secondary", "!text-slate-11 !w-fit"), + ("Contact sales", "secondary", "!text-slate-11 !w-fit"), ] @@ -170,7 +173,7 @@ def table_body() -> rx.Component: return rx.table.root( rx.el.style(TABLE_STYLE), rx.table.header( - create_table_row_header(["Price", "Pro", "Team", "Enterprise"]), + create_table_row_header(["Price", "Hobby", "Pro", "Team", "Enterprise"]), glow(), class_name="relative", ), @@ -182,17 +185,24 @@ def table_body() -> rx.Component: class_name="relative", ), create_table_body( + *[ + create_checkmark_row(feature, checks) + for feature, *checks in ON_PREMISE_ROW + ], *[create_table_row(row) for row in COMPUTE_SECTION], ), rx.table.header( - create_table_row_header(["Features", "", "", ""]), + create_table_row_header(["Features", "", "", "", ""]), class_name="relative", ), create_table_body( - *[create_checkmark_row(feature, checks) for feature, *checks in FEATURE_SECTION], + *[ + create_checkmark_row(feature, checks) + for feature, *checks in FEATURE_SECTION + ], ), rx.table.header( - create_table_row_header(["Security", "", "", ""]), + create_table_row_header(["Security", "", "", "", ""]), class_name="relative", ), create_table_body( @@ -202,7 +212,7 @@ def table_body() -> rx.Component: ], ), rx.table.header( - create_table_row_header(["Support", "", "", ""]), + create_table_row_header(["Support", "", "", "", ""]), class_name="relative", ), create_table_body( From ad0bbd6444dd641af3a325a8070a4a73eb5d2836 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 22 Nov 2024 20:30:11 +0100 Subject: [PATCH 07/16] pricing calculator --- pcweb/pages/pricing/button.py | 90 ++++++++++ pcweb/pages/pricing/calculator.py | 281 ++++++++++++++++++++++++++++++ pcweb/pages/pricing/pricing.py | 2 + pcweb/pages/pricing/table.py | 2 +- 4 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 pcweb/pages/pricing/button.py create mode 100644 pcweb/pages/pricing/calculator.py diff --git a/pcweb/pages/pricing/button.py b/pcweb/pages/pricing/button.py new file mode 100644 index 0000000000..1df9433816 --- /dev/null +++ b/pcweb/pages/pricing/button.py @@ -0,0 +1,90 @@ +from typing import Any, Dict, Literal, Optional + +import reflex as rx + +LiteralButtonVariant = Literal[ + "primary", "secondary", "transparent", "destructive", "outline" +] +LiteralButtonSize = Literal["sm", "md", "lg", "icon-sm", "icon-md", "icon-lg"] + +DEFAULT_CLASS_NAME = "text-sm cursor-pointer inline-flex items-center justify-center relative transition-bg shrink-0 font-sans disabled:cursor-not-allowed disabled:border disabled:border-slate-5 disabled:!bg-slate-3 disabled:!text-slate-8 transition-bg" + + +def get_variant_bg_cn(variant: str) -> str: + """Get the background color class name for a button variant. + + Args: + variant (str): The variant of the button. + + Returns: + str: The background color class name. + + """ + return f"enabled:bg-gradient-to-b from-[--{variant}-9] to-[--{variant}-10] hover:to-[--{variant}-9] disabled:hover:bg-[--{variant}-9]" + + +BUTTON_STYLES: Dict[str, Dict[str, Dict[str, str]]] = { + "size": { + "xs": "px-1.5 h-7 rounded-md gap-1.5", + "sm": "px-2 h-8 rounded-lg gap-2", + "md": "px-2.5 h-9 rounded-[10px] gap-2.5", + "lg": "px-3 h-10 rounded-xl gap-3", + "icon-xs": "size-7 rounded-md", + "icon-sm": "size-8 rounded-lg", + "icon-md": "size-9 rounded-[10px]", + "icon-lg": "size-10 rounded-md", + }, + "variant": { + "primary": lambda: f"{get_variant_bg_cn('violet')} text-[#FCFCFD] font-semibold", + "secondary": "bg-slate-4 hover:bg-slate-5 text-slate-11 font-semibold", + "transparent": "bg-transparent hover:bg-slate-3 text-slate-9 font-medium", + "destructive": lambda: f"{get_variant_bg_cn('red')} text-[#FCFCFD] font-semibold", + "outline": "bg-slate-1 hover:bg-slate-3 text-slate-9 font-medium border border-slate-5", + }, +} + + +def button( + text: str = "", + variant: LiteralButtonVariant = "primary", + size: LiteralButtonSize = "sm", + style: Dict[str, Any] = None, + class_name: str = "", + icon: Optional[rx.Component] = None, + **props, +) -> rx.Component: + """Create a button component. + + Args: + text (str): The text to display on the button. + variant (LiteralButtonVariant, optional): The button variant. Defaults to "primary". + size (LiteralButtonSize, optional): The button size. Defaults to "sm". + style (Dict[str, Any], optional): Additional styles to apply to the button. Defaults to {}. + class_name (str, optional): Additional CSS classes to apply to the button. Defaults to "". + icon (Optional[rx.Component], optional): An optional icon component to display before the text. Defaults to None. + **props: Additional props to pass to the button element. + + Returns: + rx.Component: A button component with the specified properties. + + """ + if style is None: + style = {} + variant_class = BUTTON_STYLES["variant"][variant] + variant_class = variant_class() if callable(variant_class) else variant_class + + classes = [ + DEFAULT_CLASS_NAME, + BUTTON_STYLES["size"][size], + variant_class, + class_name, + ] + + content = [icon, text] if icon else [text] + + return rx.el.button( + *content, + style=style, + class_name=" ".join(filter(None, classes)), + **props, + ) diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py new file mode 100644 index 0000000000..d113cc841c --- /dev/null +++ b/pcweb/pages/pricing/calculator.py @@ -0,0 +1,281 @@ +import reflex as rx +from typing import Optional +from reflex.event import EventType, BASE_STATE +from .button import button +import enum + +MONTH_MINUTES = 60 * 24 * 30 + + +class Tiers(enum.Enum): + PRO = "Pro" + TEAM = "Team" + + +class BillingState(rx.State): + + selected_plan: str = Tiers.PRO.value + # Rates + cpu_rate: float = 0.000463 + mem_rate: float = 0.000231 + + # Estimated numbers for the widget calculator + estimated_cpu_number: int = 0 + estimated_ram_gb: int = 0 + estimated_seats: int = 1 + + @rx.var(cache=True) + def seat_rate(self) -> int: + if self.selected_plan == Tiers.PRO.value: + return 19 + elif self.selected_plan == Tiers.TEAM.value: + return 29 + + @rx.var(cache=True) + def max_seats(self) -> int: + if self.selected_plan == Tiers.PRO.value: + return 5 + elif self.selected_plan == Tiers.TEAM.value: + return 15 + + @rx.var(cache=True) + def max_cpu(self) -> int: + if self.selected_plan == Tiers.PRO.value: + return 5 + elif self.selected_plan == Tiers.TEAM.value: + return 32 + + @rx.var(cache=True) + def max_ram(self) -> int: + if self.selected_plan == Tiers.PRO.value: + return 10 + elif self.selected_plan == Tiers.TEAM.value: + return 48 + + @rx.event + def change_plan(self, plan: str) -> None: + self.selected_plan = plan + if plan == Tiers.PRO.value: + if self.estimated_cpu_number > 5: + self.estimated_cpu_number = 5 + if self.estimated_ram_gb > 10: + self.estimated_ram_gb = 10 + if self.estimated_seats > 5: + self.estimated_seats = 5 + + +def calculator(text: str, component: rx.Component, total: str) -> rx.Component: + return rx.box( + rx.text(text, class_name="text-sm text-slate-12 font-medium text-nowrap"), + rx.box(component, class_name="flex justify-center items-center mx-auto"), + rx.text(total, class_name="text-sm text-slate-9 font-medium text-right"), + class_name="grid grid-cols-3 items-center gap-4", + ) + + +def stepper( + value: rx.Var[int], + default_value: str, + min_value: int, + max_value: int, + on_click_decrement: Optional[EventType[[], BASE_STATE]], + on_click_increment: Optional[EventType[[], BASE_STATE]], +) -> rx.Component: + return rx.box( + # Number of seats/cpu/tam + rx.box( + rx.el.input( + value=value, + placeholder="0", + default_value=default_value, + min=min_value, + max=max_value, + name="token_days", + on_click=on_click_decrement, + max_length=1000, + class_name="flex flex-row flex-1 gap-2 px-2.5 py-1.5 font-medium text-slate-12 text-sm placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 absolute left-0 h-full bg-transparent w-[4rem] pointer-events-none", + type="number", + style={ + "appearance": "textfield", + "-webkit-appearance": "textfield", + "-moz-appearance": "textfield", + "&::-webkit-inner-spin-button": {"-webkit-appearance": "none"}, + "&::-webkit-outer-spin-button": {"-webkit-appearance": "none"}, + }, + ), + rx.box( + button( + icon=rx.icon( + "minus", + ), + variant="transparent", + size="icon-xs", + disabled=rx.cond( + value <= min_value, + True, + False, + ), + type="button", + on_click=on_click_decrement, + ), + button( + icon=rx.icon( + "plus", + ), + variant="transparent", + size="icon-xs", + disabled=rx.cond( + value >= max_value, + True, + False, + ), + type="button", + on_click=on_click_increment, + ), + class_name="flex flex-row items-center absolute right-0 border-l border-slate-5 h-full px-1 gap-1", + ), + class_name="!w-[8.5rem] relative border-slate-5 bg-slate-1 border rounded-[0.625rem] h-[2.25rem] flex items-center", + ), + class_name="flex flex-row gap-2.5 h-[2.25rem]", + ) + + +def pricing_widget() -> rx.Component: + return rx.box( + rx.box( + # Team seats + calculator( + "Members", + stepper( + BillingState.estimated_seats, + default_value="1", + min_value=1, + max_value=BillingState.max_seats, + on_click_decrement=BillingState.setvar( + "estimated_seats", (BillingState.estimated_seats - 1) + ), + on_click_increment=BillingState.setvar( + "estimated_seats", (BillingState.estimated_seats + 1) + ), + ), + f"${BillingState.estimated_seats * BillingState.seat_rate}", + ), + # GB RAM + calculator( + "GB RAM", + stepper( + BillingState.estimated_ram_gb, + default_value="1", + min_value=0, + max_value=BillingState.max_ram, + on_click_decrement=BillingState.setvar( + "estimated_ram_gb", (BillingState.estimated_ram_gb - 1) + ), + on_click_increment=BillingState.setvar( + "estimated_ram_gb", (BillingState.estimated_ram_gb + 1) + ), + ), + f"${round(BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES))}", + ), + # CPU + calculator( + "CPU", + stepper( + BillingState.estimated_cpu_number, + default_value="0", + min_value=0, + max_value=BillingState.max_cpu, + on_click_decrement=BillingState.setvar( + "estimated_cpu_number", (BillingState.estimated_cpu_number - 1) + ), + on_click_increment=BillingState.setvar( + "estimated_cpu_number", (BillingState.estimated_cpu_number + 1) + ), + ), + f"${round(BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES))}", + ), + class_name="flex flex-col gap-2", + ), + # Total 1 month + rx.text( + f"Total: ${round(BillingState.estimated_seats * BillingState.seat_rate + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES))}/month", + class_name="text-base font-medium text-slate-12 text-center mt-6", + ), + class_name="flex-1 flex flex-col relative h-full w-full max-w-[25rem] pb-2.5 z-[2]", + ) + + +def header() -> rx.Component: + return rx.box( + rx.el.h3( + "Calculate costs.", + class_name="text-slate-12 text-3xl font-semibold text-center", + ), + rx.el.p( + "Simply usage based pricing.", + class_name="text-slate-9 text-3xl font-semibold text-center", + ), + class_name="flex items-center justify-between text-slate-11 flex-col pt-[5rem] 2xl:border-x border-slate-4 max-w-[64.125rem] mx-auto w-full", + ) + + +def tag_item(tag: str): + return rx.el.button( + rx.text( + tag, + class_name="font-small shrink-0", + color=rx.cond( + BillingState.selected_plan == tag, + "var(--c-white-1)", + "var(--c-slate-9)", + ), + ), + class_name="flex items-center justify-center px-3 py-1.5 cursor-pointer transition-bg shrink-0", + background_=rx.cond( + BillingState.selected_plan == tag, + "var(--c-violet-9)", + "var(--c-slate-2)", + ), + _hover={ + "background": rx.cond( + BillingState.selected_plan == tag, + "var(--c-violet-9)", + "var(--c-slate-3)", + ) + }, + on_click=BillingState.change_plan(tag), + ) + + +def filtering_tags(): + return rx.box( + # Glow + rx.html( + """ + + + + + + + + +""", + class_name="w-[13.5rem] h-[5.5rem] shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[0] pointer-events-none -mt-2", + ), + rx.box( + tag_item(Tiers.PRO.value), + tag_item(Tiers.TEAM.value), + class_name="shadow-large bg-slate-1 rounded-lg border border-slate-3 flex items-center divide-x divide-slate-3 mt-8 mb-12 relative overflow-hidden z-[1] overflow-x-auto", + ), + class_name="relative", + ) + + +def calculator_section() -> rx.Component: + return rx.el.section( + header(), + filtering_tags(), + pricing_widget(), + class_name="flex flex-col w-full max-w-[64.19rem] 2xl:border-x border-slate-4 2xl:border-b pb-[6rem] justify-center items-center", + ) diff --git a/pcweb/pages/pricing/pricing.py b/pcweb/pages/pricing/pricing.py index f3e7b7c774..fe594d5ca3 100644 --- a/pcweb/pages/pricing/pricing.py +++ b/pcweb/pages/pricing/pricing.py @@ -7,6 +7,7 @@ from pcweb.pages.pricing.table import comparison_table from pcweb.views.bottom_section.get_started import get_started from pcweb.pages.pricing.faq import faq +from pcweb.pages.pricing.calculator import calculator_section @rx.page(route="/pricing", title="Reflex · Pricing") @@ -22,6 +23,7 @@ def pricing() -> rx.Component: header(), plan_cards(), comparison_table(), + calculator_section(), faq(), class_name="flex flex-col relative justify-center items-center w-full", ), diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index feb05bbb5c..c920408ad0 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -239,5 +239,5 @@ def comparison_table() -> rx.Component: return rx.box( header(), table_body(), - class_name="flex flex-col w-full max-w-[69.125rem]", + class_name="flex-col w-full max-w-[69.125rem] desktop-only", ) From 92eade5c02e953fa0dc2b31b0fb2ac9d6cd677db Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 22 Nov 2024 20:31:43 +0100 Subject: [PATCH 08/16] update max ram --- pcweb/pages/pricing/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py index d113cc841c..67c03c18ed 100644 --- a/pcweb/pages/pricing/calculator.py +++ b/pcweb/pages/pricing/calculator.py @@ -50,7 +50,7 @@ def max_ram(self) -> int: if self.selected_plan == Tiers.PRO.value: return 10 elif self.selected_plan == Tiers.TEAM.value: - return 48 + return 64 @rx.event def change_plan(self, plan: str) -> None: From 9a2795f6745b89a5cfd4ef0fbe9e799bfb440d61 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 11:34:54 -0800 Subject: [PATCH 09/16] Changes --- pcweb/pages/pricing/plan_cards.py | 63 ++++++++++++++++--------------- pcweb/pages/pricing/table.py | 33 +++++++++------- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index df80a56a18..f6d20adafc 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -26,7 +26,7 @@ def radial_circle(violet: bool = False) -> rx.Component: def card( - title: str, description: str, features: list[str], button_text: str + title: str, description: str, features: list[tuple[str, str]], button_text: str ) -> rx.Component: return rx.box( rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl mb-2"), @@ -36,8 +36,8 @@ def card( rx.el.ul( *[ rx.el.li( - rx.icon("circle-check", class_name="!text-violet-9", size=16), - feature, + rx.icon(feature[0], class_name="!text-violet-9", size=16), + feature[1], class_name="text-sm font-medium text-slate-11 flex items-center gap-1.5", ) for feature in features @@ -64,7 +64,7 @@ def card( def popular_card( - title: str, description: str, features: list[str], button_text: str + title: str, description: str, features: list[tuple[str, str]], button_text: str ) -> rx.Component: return rx.box( radial_circle(), @@ -77,8 +77,8 @@ def popular_card( rx.el.ul( *[ rx.el.li( - rx.icon("circle-check", class_name="!text-violet-9", size=16), - feature, + rx.icon(feature[0], class_name="!text-violet-9", size=16), + feature[1], class_name="text-sm font-medium text-slate-11 flex items-center gap-1.5", ) for feature in features @@ -106,11 +106,11 @@ def plan_cards() -> rx.Component: "Hobby", "Everything you need to get started with Reflex.", [ - "Community support", - "1 team member", - "1 deployed app", - "1 day log retention", - "Basic analytics", + ("heart-handshake", "Community support"), + ("user", "1 team member"), + ("app-window", "1 deployed app"), + ("list-minus", "1 day log retention"), + ("map-pinned", "Single region"), ], "Start building for free", ), @@ -118,12 +118,13 @@ def plan_cards() -> rx.Component: "Pro", "For professional projects $19/mo per member. Plus usage.", [ - "Community support", - "Up to 5 team members", - "Max 5 deployed apps", - "30 days log retention", - "Multi-region", - "Custom domains", + ("heart-handshake", "Community support"), + ("users", "Up to 5 team members"), + ("app-window", "Max 5 deployed apps"), + ("globe", "Multi-region"), + ("brush", "Custom domains"), + ("activity", "Basic analytics"), + ("circle-plus", "Everything in Hobby"), ], "Start with Pro plan", ), @@ -131,13 +132,13 @@ def plan_cards() -> rx.Component: "Team", "Get the most comfort for $249/mo and $29/mo per member. Plus usage.", [ - "Email support", - "Up to 15 team members", - "Unlimited Apps", - "90 days log retention", - "Metrics and analytics", - "One-click Auth", - "Everything in Pro", + ("mail", "Email support"), + ("users", "Up to 15 team members"), + ("app-window", "Unlimited Apps"), + ("list-minus", "90 days log retention"), + ("activity", "Metrics and analytics"), + ("lock-keyhole", "One-click Auth"), + ("circle-plus", "Everything in Pro"), ], "Start with Team plan", ), @@ -145,13 +146,13 @@ def plan_cards() -> rx.Component: "Enterprise", "Get our priority support and a plan tailored to your needs.", [ - "Priority Support + Custom Onboarding", - "On prem deployments", - "Advanced app analytics", - "Unlimited team members", - "Customized machine size", - "SOC 2 report", - "Everything in Team", + ("heart-handshake", "Priority Support + Custom Onboarding"), + ("users", "Unlimited team members"), + ("hard-drive", "On prem deployments"), + ("list-minus", "Advanced app analytics"), + ("cpu", "Customized machine size"), + ("shield-check", "SOC 2 report"), + ("circle-plus", "Everything in Team"), ], "Contact sales", ), diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index feb05bbb5c..832dfee76e 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -34,26 +34,26 @@ # Data configuration PRICE_SECTION = [ - ("Per Seat Price", "$19/mo", "$19/mo", "$29/mo", "Custom"), - ("Compute", "Usage Based", "Usage Based", "Usage Based", "Custom"), + ("Per Seat Price", "Free", "$19/mo", "Contact Sales", "Contact Sales"), + ("Compute", "Free", "Usage Based", "Usage Based", "Custom"), ] COMPUTE_SECTION = [ - ("Compute Per Project", "5 CPU, 10GB", "5 CPU, 10GB", "Unlimited", "Unlimited"), - ("Regions", "Multiple", "Multiple", "Multiple", "Multiple"), - ("Team size", "< 5", "< 5", "< 15", "Unlimited"), - ("Runtime logs", "1 day", "1 day", "1 week", "Custom"), - ("Build logs", "30 days", "30 days", "90 days", "Custom"), + ("Compute Limits", "1 CPU, .5GB", "5 CPU, 10GB", "32 CPU, 64GB", "No Limit"), + ("Regions", "Single", "Multiple", "Multiple", "Multiple"), + ("Team size", "1", "< 5", "< 15", "Unlimited"), + ("Runtime logs", "1 day", "7 days", "30 days", "Custom"), + ("Build logs", "1 days", "30 days", "90 days", "Custom"), ] -ON_PREMISE_ROW = [("On Premise (Optional)", False, False, False, True)] +ON_PREMISE_ROW = [("On Premises (Optional)", False, False, False, True)] FEATURE_SECTION = [ - ("Custom domains", True, True, True, True), ("Secrets", True, True, True, True), - ("Metrics and analytics", True, True, True, True), - ("Automatic CI/CD", True, True, True, True), + ("Custom domains", False, True, True, True), + ("Metrics and analytics", False, True, True, True), + ("Automatic CI/CD", False, True, True, True), ("Multi-region", False, True, True, True), ("One-click Auth", False, False, True, True), ("Cron jobs", False, False, True, True), @@ -64,12 +64,12 @@ ("Web app firewall", True, True, True, True), ("HTTP/SSL", True, True, True, True), ("DDos", True, True, True, True), - ("Custom onboarding", False, False, True, True), ] + SUPPORT_SECTION = [ ("Community support", True, True, True, True), - ("Email (1 Business Day)", False, False, False, True), + ("Email (1 Business Day)", False, False, True, True), ("Support SLAs available", False, False, False, True), ("Custom onboarding", False, False, False, True), ("Migrate existing apps", False, False, False, True), @@ -108,8 +108,13 @@ def header() -> rx.Component: class_name="flex items-center justify-between text-slate-11 flex-col py-[5rem] 2xl:border-x border-slate-4 max-w-[64.125rem] mx-auto w-full", ) - + def create_table_cell(content: str | rx.Component) -> rx.Component: + if content == "Usage Based": + return rx.table.cell( + rx.link(content, color=rx.color("violet", 12), text_decoration="underline"), + class_name=STYLES["cell"], + ) return rx.table.cell(content, class_name=STYLES["cell"]) From 0af0739da115306dd602d6da370975a85c55ef1c Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 12:10:11 -0800 Subject: [PATCH 10/16] Pricing page touch ups --- pcweb/pages/pricing/calculator.py | 90 +++++++++++++++++-------------- pcweb/pages/pricing/plan_cards.py | 4 +- pcweb/pages/pricing/table.py | 2 +- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py index 67c03c18ed..09ed24e7a5 100644 --- a/pcweb/pages/pricing/calculator.py +++ b/pcweb/pages/pricing/calculator.py @@ -2,6 +2,7 @@ from typing import Optional from reflex.event import EventType, BASE_STATE from .button import button +from .plan_cards import radial_circle import enum MONTH_MINUTES = 60 * 24 * 30 @@ -20,8 +21,8 @@ class BillingState(rx.State): mem_rate: float = 0.000231 # Estimated numbers for the widget calculator - estimated_cpu_number: int = 0 - estimated_ram_gb: int = 0 + estimated_cpu_number: int = 1 + estimated_ram_gb: int = 1 estimated_seats: int = 1 @rx.var(cache=True) @@ -166,7 +167,7 @@ def pricing_widget() -> rx.Component: stepper( BillingState.estimated_ram_gb, default_value="1", - min_value=0, + min_value=1, max_value=BillingState.max_ram, on_click_decrement=BillingState.setvar( "estimated_ram_gb", (BillingState.estimated_ram_gb - 1) @@ -183,7 +184,7 @@ def pricing_widget() -> rx.Component: stepper( BillingState.estimated_cpu_number, default_value="0", - min_value=0, + min_value=1, max_value=BillingState.max_cpu, on_click_decrement=BillingState.setvar( "estimated_cpu_number", (BillingState.estimated_cpu_number - 1) @@ -197,9 +198,21 @@ def pricing_widget() -> rx.Component: class_name="flex flex-col gap-2", ), # Total 1 month - rx.text( - f"Total: ${round(BillingState.estimated_seats * BillingState.seat_rate + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES))}/month", - class_name="text-base font-medium text-slate-12 text-center mt-6", + rx.center( + rx.flex( + rx.badge( + f"Total: ${ + round( + BillingState.estimated_seats * BillingState.seat_rate + + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) + + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES) + - 25 + ) + }/month", + size='3', + class_name="mt-6", + ) + ) ), class_name="flex-1 flex flex-col relative h-full w-full max-w-[25rem] pb-2.5 z-[2]", ) @@ -210,40 +223,17 @@ def header() -> rx.Component: rx.el.h3( "Calculate costs.", class_name="text-slate-12 text-3xl font-semibold text-center", + id="calculator-header", ), rx.el.p( "Simply usage based pricing.", class_name="text-slate-9 text-3xl font-semibold text-center", ), - class_name="flex items-center justify-between text-slate-11 flex-col pt-[5rem] 2xl:border-x border-slate-4 max-w-[64.125rem] mx-auto w-full", - ) - - -def tag_item(tag: str): - return rx.el.button( - rx.text( - tag, - class_name="font-small shrink-0", - color=rx.cond( - BillingState.selected_plan == tag, - "var(--c-white-1)", - "var(--c-slate-9)", - ), - ), - class_name="flex items-center justify-center px-3 py-1.5 cursor-pointer transition-bg shrink-0", - background_=rx.cond( - BillingState.selected_plan == tag, - "var(--c-violet-9)", - "var(--c-slate-2)", + rx.el.p( + "We subtract the hobby tier free CPU and RAM from your usage.", + class_name="text-slate-9 text-md font-medium text-center mt-2", ), - _hover={ - "background": rx.cond( - BillingState.selected_plan == tag, - "var(--c-violet-9)", - "var(--c-slate-3)", - ) - }, - on_click=BillingState.change_plan(tag), + class_name="flex items-center mb-5 justify-between text-slate-11 flex-col pt-[5rem] 2xl:border-x border-slate-4 max-w-[64.125rem] mx-auto w-full", ) @@ -264,9 +254,14 @@ def filtering_tags(): class_name="w-[13.5rem] h-[5.5rem] shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[0] pointer-events-none -mt-2", ), rx.box( - tag_item(Tiers.PRO.value), - tag_item(Tiers.TEAM.value), - class_name="shadow-large bg-slate-1 rounded-lg border border-slate-3 flex items-center divide-x divide-slate-3 mt-8 mb-12 relative overflow-hidden z-[1] overflow-x-auto", + rx.segmented_control.root( + rx.segmented_control.item(Tiers.PRO.value, value=Tiers.PRO.value), + rx.segmented_control.item(Tiers.TEAM.value, value=Tiers.TEAM.value), + on_change=BillingState.change_plan, + value=BillingState.selected_plan, + class_name="shadow-large bg-slate-1 rounded-lg border border-slate-3", + ), + class_name="mb-5 relative z-[1] overflow-x-auto", ), class_name="relative", ) @@ -275,7 +270,22 @@ def filtering_tags(): def calculator_section() -> rx.Component: return rx.el.section( header(), - filtering_tags(), - pricing_widget(), + rx.box( + radial_circle(), + rx.box( + rx.flex( + filtering_tags(), + align_items="center", + justify_content="center", + width="100%", + ), + align_items="center", + width="100%", + ), + rx.box(pricing_widget()), + class_name="flex flex-col p-8 border border-slate-4 rounded-[1.125rem] shadow-small bg-slate-2 relative z-[1]", + ), class_name="flex flex-col w-full max-w-[64.19rem] 2xl:border-x border-slate-4 2xl:border-b pb-[6rem] justify-center items-center", ) + + diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index f6d20adafc..b394f49f86 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -130,7 +130,7 @@ def plan_cards() -> rx.Component: ), card( "Team", - "Get the most comfort for $249/mo and $29/mo per member. Plus usage.", + "For teams looking to scale. Plus usage.", [ ("mail", "Email support"), ("users", "Up to 15 team members"), @@ -140,7 +140,7 @@ def plan_cards() -> rx.Component: ("lock-keyhole", "One-click Auth"), ("circle-plus", "Everything in Pro"), ], - "Start with Team plan", + "Contact sales", ), card( "Enterprise", diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 84a8dfeeac..8575b7c4ec 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -112,7 +112,7 @@ def header() -> rx.Component: def create_table_cell(content: str | rx.Component) -> rx.Component: if content == "Usage Based": return rx.table.cell( - rx.link(content, color=rx.color("violet", 12), text_decoration="underline"), + rx.link(content, color=rx.color("violet", 12), href="#calculator-header", text_decoration="underline"), class_name=STYLES["cell"], ) return rx.table.cell(content, class_name=STYLES["cell"]) From 2951c839580ae1eea9c8c87de7cafc409517f5f0 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 12:27:25 -0800 Subject: [PATCH 11/16] Update sales page --- pcweb/pages/__init__.py | 1 + pcweb/pages/pricing/plan_cards.py | 4 +- pcweb/pages/sales.py | 215 ++++++++++++++++++++++++++++++ pcweb/whitelist.py | 2 +- 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 pcweb/pages/sales.py diff --git a/pcweb/pages/__init__.py b/pcweb/pages/__init__.py index bf9a0fca80..39c573f208 100644 --- a/pcweb/pages/__init__.py +++ b/pcweb/pages/__init__.py @@ -13,6 +13,7 @@ from .gallery.apps import gallery_apps_routes from .hosting_countdown.hosting_countdown import hosting_countdown from .pricing.pricing import pricing +from .sales import sales routes = [ *[r for r in locals().values() if isinstance(r, Route) and r.add_as_page], *blog_routes, diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index b394f49f86..996d730928 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -54,7 +54,7 @@ def card( href=( HOSTING_URL if button_text != "Contact sales" - else "mailto:sales@reflex.dev" + else "/sales" ), is_external=True, underline="none", @@ -130,7 +130,7 @@ def plan_cards() -> rx.Component: ), card( "Team", - "For teams looking to scale. Plus usage.", + "For teams looking to scale their applications. Plus usage.", [ ("mail", "Email support"), ("users", "Up to 15 team members"), diff --git a/pcweb/pages/sales.py b/pcweb/pages/sales.py new file mode 100644 index 0000000000..b3a02e081e --- /dev/null +++ b/pcweb/pages/sales.py @@ -0,0 +1,215 @@ +import httpx +import reflex as rx +from httpx import Response + +from pcweb.components.button import button +from pcweb.components.webpage.comps import h1_title +from pcweb.constants import ( + REFLEX_DEV_WEB_LANDING_FORM_SALES_CALL_WEBHOOK_URL, + REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL, +) +from pcweb.pages.docs import getting_started, hosting +from pcweb.templates.webpage import webpage + + +class FormState(rx.State): + is_loading: bool = False + email_sent: bool = False + + @rx.event + async def submit(self, form_data: dict): + self.is_loading = True + yield + + try: + with httpx.Client() as client: + response = client.post( + REFLEX_DEV_WEB_LANDING_FORM_SALES_CALL_WEBHOOK_URL, + json=form_data, + ) + response.raise_for_status() + + self.is_loading = False + self.email_sent = True + yield rx.toast.success("Demo request submitted successfully!") + + except httpx.HTTPError: + self.is_loading = False + self.email_sent = False + yield rx.toast.error("Failed to submit request. Please try again later.") + + @rx.event + async def submit_pro_waitlist(self, form_data: dict): + try: + with httpx.Client() as client: + response: Response = client.post( + REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL, + json=form_data, + ) + response.raise_for_status() + + yield rx.toast.success("Thank you for joining the waitlist!") + + except httpx.HTTPError: + yield rx.toast.error("Failed to submit request. Please try again later.") + + +def dialog(trigger: rx.Component, content: rx.Component) -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + trigger, + ), + rx.dialog.content( + content, + class_name="bg-white-1 p-4 rounded-[1.625rem] w-[26rem]", + ), + ) + + +def form() -> rx.Component: + input_class_name = "box-border border-slate-5 focus:border-violet-9 focus:border-1 bg-slate-1 p-[0.5rem_0.75rem] border rounded-[10px] w-full font-small text-slate-11 placeholder:text-slate-9 outline-none focus:outline-none" + return rx.box( + rx.form( + rx.box( + rx.text( + "Get an enterprise quote", + class_name="text-2xl text-slate-12 font-bold leading-6 scroll-m-[7rem]", + id="form-title", + ), + rx.text( + "Explore custom plans and pricing", + class_name="font-base text-slate-9", + ), + class_name="flex flex-col gap-2 mb-4 items-start", + ), + rx.box( + rx.hstack( + rx.el.input( + name="first_name", + type="text", + placeholder="First name *", + required=True, + class_name=input_class_name, + ), + rx.el.input( + name="last_name", + type="text", + placeholder="Last name *", + required=True, + class_name=input_class_name, + ), + spacing="2", + width="100%", + class_name="mb-2.5 gap-2", + ), + rx.hstack( + rx.el.input( + name="email", + type="email", + placeholder="Business email *", + required=True, + class_name=input_class_name, + ), + rx.el.input( + name="linkedin_profile", + type="text", + placeholder="LinkedIn profile", + required=False, + class_name=input_class_name, + ), + spacing="2", + width="100%", + class_name="mb-2.5 gap-2", + ), + rx.hstack( + rx.el.input( + name="company_name", + type="text", + placeholder="Company name *", + required=True, + class_name=input_class_name, + ), + rx.el.input( + name="title", + type="text", + placeholder="Title *", + required=True, + class_name=input_class_name, + ), + spacing="2", + width="100%", + class_name="mb-2.5 gap-2", + ), + rx.el.textarea( + name="project_description", + placeholder="Your company needs", + class_name=input_class_name + " h-24 mb-4 resize-none", + ), + class_name="flex flex-col", + ), + rx.cond( + FormState.is_loading, + button( + "Sending...", + variant="muted", + type="submit", + class_name="opacity-80 !cursor-not-allowed pointer-events-none !w-min", + ), + button( + "Submit", + type="submit", + class_name="!w-min", + ), + ), + on_submit=FormState.submit, + class_name="flex flex-col", + ), + rx.box( + rx.text( + "If you have any questions, feel free to contact us", + class_name="font-small text-slate-9", + ), + rx.link( + "sales@reflex.dev", + href="mailto:sales@reflex.dev", + underline="always", + class_name="text-slate-9 font-small", + ), + class_name="flex flex-row justify-between items-center gap-2 mt-4", + ), + class_name="relative flex flex-col gap-4 border-slate-4 bg-slate-2 shadow-large p-8 border rounded-[1.125rem] self-stretch scroll-[3rem]", + ) + + +@webpage(path="/sales", title="Pricing · Reflex") +def sales(): + return rx.el.section( + rx.box( + rx.box( + rx.cond( + FormState.email_sent, + rx.box( + rx.box( + rx.text( + """Thanks for your interest in Reflex! +You'll get a reply from us soon.""", + class_name="font-large text-slate-12 whitespace-pre text-center", + ), + class_name="flex flex-row items-center gap-2", + ), + button( + "Back", + on_click=FormState.setvar("email_sent", False), + class_name="mt-4", + ), + class_name="flex flex-col items-center gap-2", + ), + form(), + ), + class_name="mt-12 w-full", + ), + class_name="flex flex-col justify-center items-center w-full max-w-[84.5rem]", + ), + id="pricing", + class_name="section-content", + ) \ No newline at end of file diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index d95607b0dc..2b6cb2a907 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = ["/pricing"] +WHITELISTED_PAGES = ["/sales", "/pricing"] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: From 53d7797f91cde4e7912cf62a8562a74159d613f5 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 12:29:56 -0800 Subject: [PATCH 12/16] Whitelist --- pcweb/whitelist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index 2b6cb2a907..c3f74420bb 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = ["/sales", "/pricing"] +WHITELISTED_PAGES = [] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: From b8f2c90049324dbacdce5bb567a1a3a70a8ab312 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 12:58:37 -0800 Subject: [PATCH 13/16] Update button for team --- pcweb/pages/pricing/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index 8575b7c4ec..f594398ecc 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -78,7 +78,7 @@ PLAN_BUTTONS = [ ("Start building for free", "secondary", "!text-slate-11 !w-fit"), ("Start with Pro plan", "primary", "!text-[#FCFCFD] !w-fit"), - ("Start with Team plan", "secondary", "!text-slate-11 !w-fit"), + ("Contact sales", "secondary", "!text-slate-11 !w-fit"), ("Contact sales", "secondary", "!text-slate-11 !w-fit"), ] From aa727eea4ddaa48062e16028826a3689e7fb8066 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 13:18:32 -0800 Subject: [PATCH 14/16] Update docs/hosting/deploy-quick-start.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Brandého --- docs/hosting/deploy-quick-start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hosting/deploy-quick-start.md b/docs/hosting/deploy-quick-start.md index 68a43e9b31..41a69988d5 100644 --- a/docs/hosting/deploy-quick-start.md +++ b/docs/hosting/deploy-quick-start.md @@ -58,7 +58,7 @@ The name should only contain domain name safe characters: no slashes, no undersc That’s it! You should receive some feedback on the progress of your deployment and in a few minutes your app should be up. 🎉 ```md alert info -# The hosting service does not currently handle database or file upload operations. Set up an external database use it within your app. +# The hosting service does not currently handle database or file upload operations. Set up an external database and use it within your app. ``` ## See it in Action From a737c2e45a54b17e95fb73a24f465c580099e3ab Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 15:35:53 -0800 Subject: [PATCH 15/16] Fix f string --- pcweb/pages/pricing/calculator.py | 20 +++++++++++--------- pcweb/whitelist.py | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py index 09ed24e7a5..cba0145ffc 100644 --- a/pcweb/pages/pricing/calculator.py +++ b/pcweb/pages/pricing/calculator.py @@ -201,23 +201,25 @@ def pricing_widget() -> rx.Component: rx.center( rx.flex( rx.badge( - f"Total: ${ - round( - BillingState.estimated_seats * BillingState.seat_rate - + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) - + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES) - - 25 - ) - }/month", + f"Total: ${calculate_total()}/month", + ), size='3', class_name="mt-6", ) - ) ), class_name="flex-1 flex flex-col relative h-full w-full max-w-[25rem] pb-2.5 z-[2]", ) +def calculate_total(): + return round( + BillingState.estimated_seats * BillingState.seat_rate + + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) + + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES) + - 25 + ) + + def header() -> rx.Component: return rx.box( rx.el.h3( diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index c3f74420bb..2b6cb2a907 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = [] +WHITELISTED_PAGES = ["/sales", "/pricing"] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: From e65a3b838cae4dea355e48ba4abde40ef12d4678 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 22 Nov 2024 15:36:44 -0800 Subject: [PATCH 16/16] whiteist --- pcweb/whitelist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcweb/whitelist.py b/pcweb/whitelist.py index 2b6cb2a907..c3f74420bb 100644 --- a/pcweb/whitelist.py +++ b/pcweb/whitelist.py @@ -9,7 +9,7 @@ # - Correct: WHITELISTED_PAGES = ["/docs/getting-started/introduction"] # - Incorrect: WHITELISTED_PAGES = ["/docs/getting-started/introduction/"] -WHITELISTED_PAGES = ["/sales", "/pricing"] +WHITELISTED_PAGES = [] def _check_whitelisted_path(path): if len(WHITELISTED_PAGES) == 0: