diff --git a/assets/hosting/dark/hosting-preview.jpg b/assets/hosting/dark/hosting-preview.jpg new file mode 100644 index 0000000000..c44e4f458d Binary files /dev/null and b/assets/hosting/dark/hosting-preview.jpg differ diff --git a/assets/hosting/dark/hosting_patterns.svg b/assets/hosting/dark/hosting_patterns.svg new file mode 100644 index 0000000000..b0242c6700 --- /dev/null +++ b/assets/hosting/dark/hosting_patterns.svg @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/hosting/light/hosting-preview.jpg b/assets/hosting/light/hosting-preview.jpg new file mode 100644 index 0000000000..e5b05244a8 Binary files /dev/null and b/assets/hosting/light/hosting-preview.jpg differ diff --git a/assets/hosting/light/hosting_patterns.svg b/assets/hosting/light/hosting_patterns.svg new file mode 100644 index 0000000000..b97bf86ec5 --- /dev/null +++ b/assets/hosting/light/hosting_patterns.svg @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcweb/components/docpage/navbar/navbar.py b/pcweb/components/docpage/navbar/navbar.py index f8c34a079f..0a3f2a99b5 100644 --- a/pcweb/components/docpage/navbar/navbar.py +++ b/pcweb/components/docpage/navbar/navbar.py @@ -323,6 +323,9 @@ def new_component_section() -> rx.Component: "none", ), ), + nav_menu.item( + link_item("Hosting", "/hosting", "hosting"), + ), nav_menu.item( link_item("Pricing", "/pricing", "pricing"), ), diff --git a/pcweb/components/icons/icons.py b/pcweb/components/icons/icons.py index 54e5237ec9..fcc8b1da14 100644 --- a/pcweb/components/icons/icons.py +++ b/pcweb/components/icons/icons.py @@ -379,6 +379,21 @@ """ +infinity = """ + + + +""" + +analytics = """ + + +""" + +globe = """ + +""" + ICONS = { # Socials "github": github, @@ -427,6 +442,9 @@ "python": python, "package": package, "document_code": document_code, + "infinity": infinity, + "analytics": analytics, + "globe": globe, } LiteralIcon = Literal[ @@ -474,7 +492,10 @@ "forum", "python", "package", - "document_code" + "document_code", + "infinity", + "analytics", + "globe", ] diff --git a/pcweb/components/icons/patterns.py b/pcweb/components/icons/patterns.py index c2a2215e4f..2029287255 100644 --- a/pcweb/components/icons/patterns.py +++ b/pcweb/components/icons/patterns.py @@ -1,6 +1,6 @@ import reflex as rx from pcweb.components.icons.icons import get_icon - +from pcweb.components.hosting_banner import HostingBannerState def create_pattern( pattern: str, @@ -13,6 +13,7 @@ def create_pattern( + class_name, ) + def default_patterns() -> rx.Component: return [ # Left @@ -39,6 +40,7 @@ def default_patterns() -> rx.Component: ), ] + def index_patterns() -> rx.Component: return [ rx.box( @@ -55,3 +57,21 @@ def index_patterns() -> rx.Component: class_name="bg-[radial-gradient(50%_50%_at_50%_50%,_var(--glow)_0%,_rgba(21,_22,_24,_0.00)_100%)] w-[56.125rem] h-[11.625rem] rounded-[56.125rem] overflow-hidden pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-1] mt-[65.75rem] absolute top-0" ), ] + + +def hosting_patterns() -> rx.Component: + return [ + rx.image( + src=rx.color_mode_cond( + light="/hosting/light/hosting_patterns.svg", + dark="/hosting/dark/hosting_patterns.svg", + ), + alt="Reflex Hosting Patterns", + class_name="desktop-only absolute top-0 z-[-1] w-[1028px] h-[478px] pointer-events-none shrink-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2" + + rx.cond( + HostingBannerState.show_banner, + " lg:mt-[24rem] mt-[3.5rem]", + " lg:mt-[19rem] mt-[8.5rem]", + ), + ) + ] diff --git a/pcweb/components/new_button.py b/pcweb/components/new_button.py new file mode 100644 index 0000000000..6ed9c07f1f --- /dev/null +++ b/pcweb/components/new_button.py @@ -0,0 +1,92 @@ +from typing import Any, Dict, Literal, Optional + +import reflex as rx + +LiteralButtonVariant = Literal[ + "primary", "secondary", "transparent", "destructive", "outline" +] +LiteralButtonSize = Literal["sm", "md", "lg", "xl", "icon-sm", "icon-md", "icon-lg", "icon-xl"] + +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] dark:to-[--{variant}-9] hover:to-[--{variant}-9] dark:hover:to-[--{variant}-10] 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", + "xl": "px-3.5 h-12 rounded-[14px] gap-3.5 !text-base text-nowrap", + "icon-xs": "size-7 rounded-md", + "icon-sm": "size-8 rounded-lg", + "icon-md": "size-9 rounded-[10px]", + "icon-lg": "size-10 rounded-md", + "icon-xl": "size-12 rounded-[14px]", + }, + "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/constants.py b/pcweb/constants.py index 1b8d8c2490..39b56bc660 100644 --- a/pcweb/constants.py +++ b/pcweb/constants.py @@ -30,7 +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" +HOSTING_URL = "https://cloud.reflexcorp.run" # Install urls. BUN_URL = "https://bun.sh" diff --git a/pcweb/pages/__init__.py b/pcweb/pages/__init__.py index 39c573f208..b48db533f7 100644 --- a/pcweb/pages/__init__.py +++ b/pcweb/pages/__init__.py @@ -11,9 +11,11 @@ from .customers.landing import customers from .customers.data.customers import customers_routes from .gallery.apps import gallery_apps_routes -from .hosting_countdown.hosting_countdown import hosting_countdown +# from .hosting_countdown.hosting_countdown import hosting_countdown from .pricing.pricing import pricing from .sales import sales +from .hosting.hosting import hosting_landing + routes = [ *[r for r in locals().values() if isinstance(r, Route) and r.add_as_page], *blog_routes, diff --git a/pcweb/pages/hosting/hosting.py b/pcweb/pages/hosting/hosting.py new file mode 100644 index 0000000000..ed189d0103 --- /dev/null +++ b/pcweb/pages/hosting/hosting.py @@ -0,0 +1,40 @@ +import reflex as rx + +from .views.hero import hero +from .views.preview import preview +from .views.deploy_animation import deploy_animation +from .views.pricing_cards import pricing_cards +from .views.templates import templates +from .views.features import features +from pcweb.pages.index.views.companies import companies +from pcweb.components.icons.patterns import hosting_patterns +from pcweb.components.docpage.navbar import navbar +from pcweb.pages.index.views.get_started import get_started +from pcweb.pages.index.views.footer_index import footer_index +from pcweb.components.webpage.badge import badge +from pcweb.pages.index.index_colors import index_colors +from pcweb.meta.meta import meta_tags + + +@rx.page(route="/hosting", title="Reflex ยท Hosting", meta=meta_tags) +def hosting_landing() -> rx.Component: + """Get the main Reflex landing page.""" + return rx.box( + index_colors(), + *hosting_patterns(), + navbar(), + rx.el.main( + hero(), + preview(), + # companies(), + deploy_animation(), + features(), + pricing_cards(), + templates(), + get_started(), + class_name="flex flex-col w-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/hosting/views/deploy_animation.py b/pcweb/pages/hosting/views/deploy_animation.py new file mode 100644 index 0000000000..2d1adfa8bc --- /dev/null +++ b/pcweb/pages/hosting/views/deploy_animation.py @@ -0,0 +1,153 @@ +import reflex as rx +from pcweb.pages.hosting_countdown.animated_box import ( + terminal_box, + deploy_box, + typing_text_script, +) + + +def grid() -> rx.Component: + return rx.html( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""", + class_name="shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-2] pointer-events-none w-full h-full", + ) + + +def animated_box() -> rx.Component: + return rx.box( + rx.box( + terminal_box(), + deploy_box(), + # Glow + rx.html( + """ + + + + + + + + + + """, + class_name="absolute bottom-[-6.5rem] left-1/2 transform -translate-x-1/2 w-[26.625rem] h-[6rem] flex-shrink-0 pointer-events-none", + ), + class_name="justify-center flex flex-col items-center max-w-[34.5rem] max-h-[17.875rem] shrink-0 relative w-full h-full overflow-hidden", + ), + on_mount=rx.call_script(typing_text_script()), # On dev it will run twice + class_name="flex items-center justify-center w-full h-[6rem] mx-auto", + ) + + +def deploy_animation() -> rx.Component: + return rx.el.section( + grid(), + rx.box( + rx.el.h2( + "Deploy, manage, and scale.", + class_name="lg:text-3xl text-xl font-semibold text-slate-12 text-balance", + ), + rx.el.span( + "A complete infrastructure for your apps", + class_name="lg:text-3xl text-xl font-semibold text-slate-9 z-[1] text-balance", + ), + class_name="flex flex-col text-center", + ), + animated_box(), + class_name="overflow-hidden flex flex-col justify-center gap-[3.5rem] w-full h-auto max-w-[64.19rem] lg:border-x border-slate-3 lg:px-[8.5rem] lg:pt-[5.5rem] pt-12 border-b border-t relative", + ) diff --git a/pcweb/pages/hosting/views/features.py b/pcweb/pages/hosting/views/features.py new file mode 100644 index 0000000000..d09d905aca --- /dev/null +++ b/pcweb/pages/hosting/views/features.py @@ -0,0 +1,178 @@ +import reflex as rx +from pcweb.components.icons import get_icon +from pcweb.components.new_button import button +from pcweb.constants import HOSTING_URL + + +def grid() -> rx.Component: + return rx.html( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""", + class_name="shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-2] pointer-events-none w-full h-full ml-[-2px] mt-[1px]", + ) + + +def feature_card(icon: str, title: str, description: str) -> rx.Component: + return rx.box( + rx.box( + rx.box( + get_icon(icon, class_name="!text-slate-12 !size-5"), + rx.text(title, class_name="text-slate-12 text-base font-medium"), + class_name="flex flex-row gap-2 items-center", + ), + rx.text( + description, class_name="text-slate-9 font-medium text-sm text-start" + ), + class_name="flex flex-col gap-2 w-[22rem] h-[8rem] px-8 py-5", + ), + class_name="border-slate-3 border-b box-border", + ) + + +def features() -> rx.Component: + return rx.el.section( + # TODO: Fill with updated comments + grid(), + rx.box( + rx.box( + feature_card( + "backend_db", + "Build and deploy", + "Skip the boilerplate and get started faster. No need to write API endpoints.", + ), + feature_card( + "infinity", + "Integrate with CI/CD", + "Skip the boilerplate and get started faster. No need to write API endpoints.", + ), + feature_card( + "globe", + "Scale to multiple regions", + "Skip the boilerplate and get started faster. No need to write API endpoints.", + ), + rx.box(class_name="h-[4rem]"), + class_name="flex flex-col pt-8 lg:border-r border-slate-3", + ), + rx.box( + feature_card( + "backend_auth", + "Add team members", + "Skip the boilerplate and get started faster. No need to write API endpoints.", + ), + feature_card( + "analytics", + "Get alerts and metrics", + "Skip the boilerplate and get started faster. No need to write API endpoints.", + ), + class_name="flex flex-col -mt-[32px]", + ), + class_name="flex lg:flex-row flex-col justify-center items-center", + ), + rx.link( + button("Start building", variant="primary", size="xl"), + href=HOSTING_URL, + is_external=True, + class_name="p-2 border border-slate-3 rounded-[1.375rem] border-solid lg:mt-0 mt-4", + ), + class_name="flex flex-col justify-center items-center max-w-[64.19rem] lg:border-x border-slate-3 w-full mx-auto lg:pb-[5.5rem] pb-4 relative overflow-hidden", + ) diff --git a/pcweb/pages/hosting/views/hero.py b/pcweb/pages/hosting/views/hero.py new file mode 100644 index 0000000000..f9e80a23ad --- /dev/null +++ b/pcweb/pages/hosting/views/hero.py @@ -0,0 +1,53 @@ +import reflex as rx +from pcweb.components.new_button import button +from pcweb.constants import REFLEX_DEV_WEB_LANDING_FORM_URL_GET_DEMO +from pcweb.constants import HOSTING_URL +from pcweb.components.hosting_banner import HostingBannerState + + +def hero() -> rx.Component: + """Render the hero section of the landing page.""" + return rx.el.section( + # Headings + rx.el.h1( + "Deploy with a single command", + class_name="max-w-full inline-block bg-clip-text bg-gradient-to-r from-slate-12 to-slate-11 w-full text-4xl lg:text-5xl text-center text-transparent text-balance mx-auto break-words font-semibold", + ), + # TODO: Change this wording + rx.el.h2( + """A unified platform to build and manage your Reflex apps""", + class_name="max-w-full w-full font-large text-center text-slate-11 -mt-2 font-normal text-[1.25rem] mx-auto text-balance word-wrap break-words md:whitespace-pre", + ), + # Buttons + rx.box( + rx.link( + button( + "Start building", + size="xl", + class_name="w-full lg:w-fit", + ), + underline="none", + href=HOSTING_URL, + class_name="w-full lg:w-fit", + ), + rx.link( + button( + "Get a demo", + size="xl", + variant="secondary", + class_name="lg:!w-[8.25rem] w-full", + ), + href=REFLEX_DEV_WEB_LANDING_FORM_URL_GET_DEMO, + is_external=True, + underline="none", + class_name="w-full lg:w-fit", + ), + class_name="flex flex-col lg:flex-row items-center gap-4 mt-4 w-full lg:w-auto lg:max-w-full max-w-[24rem]", + ), + class_name="flex flex-col justify-center items-center gap-4 mx-auto w-full max-w-[64.19rem] lg:border-x border-slate-3 pb-4 lg:pb-[7.875rem]" + + rx.cond( + HostingBannerState.show_banner, + " lg:pt-[18.2rem] pt-[11rem]", + " lg:pt-[13.2rem] pt-[6rem]", + ), + ) diff --git a/pcweb/pages/hosting/views/preview.py b/pcweb/pages/hosting/views/preview.py new file mode 100644 index 0000000000..230adedda7 --- /dev/null +++ b/pcweb/pages/hosting/views/preview.py @@ -0,0 +1,38 @@ +import reflex as rx + + +def pattern_1() -> rx.Component: + return rx.html( + """ + + + + + + + + + + + + + + """, + class_name="shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-2] pointer-events-none", + ) + + +def preview() -> rx.Component: + return rx.box( + pattern_1(), + rx.image( + src=rx.color_mode_cond( + light="/hosting/light/hosting-preview.jpg", + dark="/hosting/dark/hosting-preview.jpg", + ), + alt="Reflex Hosting Preview", + class_name="w-full h-full rounded-lg lg:rounded-2xl object-cover bg-center border border-slate-3", + loading="lazy", + ), + class_name="w-full h-full max-w-[71rem] max-h-[41rem] rounded-[2rem] backdrop-blur-[6px] bg-[rgba(26, 27, 29, 0.48)] lg:p-4 lg:border border-slate-3 relative lg:mt-0 mt-[3.5rem]", + ) diff --git a/pcweb/pages/hosting/views/pricing_cards.py b/pcweb/pages/hosting/views/pricing_cards.py new file mode 100644 index 0000000000..19e5f47e7b --- /dev/null +++ b/pcweb/pages/hosting/views/pricing_cards.py @@ -0,0 +1,34 @@ +import reflex as rx +from pcweb.pages.pricing.plan_cards import plan_cards +from pcweb.components.new_button import button + + +def pricing_cards() -> rx.Component: + return rx.el.section( + rx.box( + rx.el.h2( + "Start for free and scale as you grow.", + class_name="lg:text-3xl text-xl font-semibold text-slate-12 text-balance", + ), + rx.el.span( + "Flexible pricing", + class_name="lg:text-3xl text-xl font-semibold text-slate-9 z-[1] text-balance", + ), + class_name="flex flex-col text-center pb-[3.5rem] max-w-[64.19rem] lg:border-x border-slate-3 border-t pt-[5rem] w-full mx-auto", + ), + plan_cards(), + rx.box( + rx.link( + button( + "Compare plans", + size="md", + icon=rx.icon("chevron-right", size=16), + variant="transparent", + class_name="mt-[3.5rem] flex-row-reverse mb-[5rem]", + ), + href="/pricing", + ), + class_name="flex flex-col text-center max-w-[64.19rem] lg:border-x border-slate-3 w-full mx-auto", + ), + class_name="flex flex-col justify-center items-center w-full relative", + ) diff --git a/pcweb/pages/hosting/views/templates.py b/pcweb/pages/hosting/views/templates.py new file mode 100644 index 0000000000..de0730ad57 --- /dev/null +++ b/pcweb/pages/hosting/views/templates.py @@ -0,0 +1,86 @@ +import reflex as rx + + +def gallery_app_card(app: dict) -> rx.Component: + return rx.box( + rx.box( + rx.link( + rx.image( + src=app["image"], + loading="lazy", + alt="Image preview for app: " + app["title"], + class_name="w-full h-full duration-150 object-top object-cover hover:scale-105 transition-transform ease-out", + ), + href=f"/templates/{app['url'].replace(' ', '-').lower()}", + ), + class_name="relative border-slate-5 border-b border-solid w-full overflow-hidden h-[11.5rem]", + ), + rx.box( + rx.text( + app["title"], + class_name="font-smbold text-slate-12 truncate", + width="100%", + ), + rx.text( + app["description"], + class_name="text-slate-10 font-small truncate text-pretty", + width="100%", + ), + class_name="flex flex-col items-start gap-2 p-6 w-full h-auto", + ), + class_name="box-border flex flex-col border-slate-4 bg-slate-2 shadow-large border rounded-xl w-full h-auto overflow-hidden", + ) + + +templates_name_map = { + "dalle": "DALL-E", + "sales": "Sales App", + "nba": "NBA App", + "reflex-llamaindex-template": "LLamaIndex App", + "ai_image_gen": "AI Image Gen", + "dashboard": "Dashboard", + "customer_data_app": "Customer Data App", + "ci_template": "CI/CD Template", + "reflex-chat": "Chat App", + "api_admin_panel": "API Admin Panel", +} + + +def component_grid() -> rx.Component: + from pcweb.pages.gallery.apps import gallery_apps_data + + posts = [] + for path, document in list(gallery_apps_data.items()): + document.metadata["url"] = document.metadata["title"] + document.metadata["title"] = templates_name_map.get( + document.metadata["title"], document.metadata["title"] + ) + # Skip the DALL-E template + if document.metadata["title"] == "DALL-E": + continue + posts.append(gallery_app_card(app=document.metadata)) + return rx.box( + *posts, + class_name="gap-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 [&>*]:min-w-[22.5rem] w-full", + ) + + +def templates() -> rx.Component: + return rx.el.section( + rx.box( + rx.el.h2( + "Quickly start with a template.", + class_name="lg:text-3xl text-xl font-semibold text-slate-12 text-balance", + ), + rx.el.span( + "Select one to continue", + class_name="lg:text-3xl text-xl font-semibold text-slate-9 z-[1] text-balance", + ), + class_name="flex flex-col text-center pb-[3rem] max-w-[64.19rem] lg:border-x border-slate-3 border-t pt-[5rem] w-full mx-auto", + ), + component_grid(), + rx.box( + class_name="flex flex-col text-center pb-[3rem] max-w-[64.19rem] lg:border-x border-slate-3 pt-[5rem] w-full mx-auto", + ), + class_name="flex flex-col justify-center w-full h-auto relative max-w-[70.19rem] mx-auto", + ) diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py index cba0145ffc..5734a23b0a 100644 --- a/pcweb/pages/pricing/calculator.py +++ b/pcweb/pages/pricing/calculator.py @@ -235,7 +235,7 @@ def header() -> rx.Component: "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", ), - 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", + class_name="flex items-center mb-5 justify-between text-slate-11 flex-col pt-[5rem] mx-auto w-full", ) @@ -276,7 +276,7 @@ def calculator_section() -> rx.Component: radial_circle(), rx.box( rx.flex( - filtering_tags(), + # filtering_tags(), align_items="center", justify_content="center", width="100%", diff --git a/pcweb/pages/pricing/faq.py b/pcweb/pages/pricing/faq.py index 12f57fa740..520fb5a1e9 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="mailto:sales@reflex.dev", # TODO: Change to sales page when we have it + href="/sales", 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 996d730928..6539f87916 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -1,5 +1,5 @@ import reflex as rx -from pcweb.components.button import button +from pcweb.components.new_button import button from pcweb.constants import HOSTING_URL @@ -25,6 +25,114 @@ def radial_circle(violet: bool = False) -> rx.Component: ) +def glow() -> rx.Component: + return rx.html( + """ + + + + + + + +""", + class_name="shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-2] pointer-events-none", + ) + + +def grid() -> rx.Component: + return rx.html( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""", + class_name="shrink-0 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[-2] pointer-events-none", + ) + + def card( title: str, description: str, features: list[tuple[str, str]], button_text: str ) -> rx.Component: @@ -36,9 +144,9 @@ def card( rx.el.ul( *[ rx.el.li( - rx.icon(feature[0], class_name="!text-violet-9", size=16), + rx.icon(feature[0], class_name="!text-slate-9", size=16), feature[1], - class_name="text-sm font-medium text-slate-11 flex items-center gap-1.5", + class_name="text-sm font-medium text-slate-11 flex items-center gap-3", ) for feature in features ], @@ -49,17 +157,14 @@ def card( button( button_text, variant="secondary", - class_name="w-full !text-sm !font-semibold !text-slate-11", - ), - href=( - HOSTING_URL - if button_text != "Contact sales" - else "/sales" + size="lg", + class_name="w-full", ), + href=(HOSTING_URL if button_text != "Contact sales" else "/sales"), 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]", + class_name="flex flex-col p-8 border border-slate-4 rounded-[1.125rem] shadow-small bg-slate-2 w-full min-w-[20.375rem] h-[33.5rem]", ) @@ -67,36 +172,41 @@ def popular_card( title: str, description: str, features: list[tuple[str, 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(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 - ], - class_name="flex flex-col gap-2", + class_name="absolute top-[-0.75rem] left-8 rounded-md bg-[--violet-9] h-[1.5rem] text-sm font-medium text-center px-2 flex items-center justify-center text-[#FCFCFD] z-[10]", ), - rx.box(class_name="flex-1"), - rx.link( - button( - button_text, - variant="primary", - class_name="w-full !text-sm !font-semibold", + rx.box( + glow(), + grid(), + 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(feature[0], class_name="!text-violet-9", size=16), + feature[1], + class_name="text-sm font-medium text-slate-11 flex items-center gap-3", + ) + for feature in features + ], + class_name="flex flex-col gap-2", ), - href=HOSTING_URL, - is_external=True, - underline="none", + rx.box(class_name="flex-1"), + rx.link( + button( + button_text, + variant="primary", + size="lg", + class_name="w-full !text-sm !font-semibold", + ), + href=f"{HOSTING_URL}/?redirect_url={HOSTING_URL}/billing/", + is_external=True, + underline="none", + ), + class_name="flex flex-col p-8 border border-[--violet-9] rounded-[1.125rem] w-full min-w-[20.375rem] h-[33.5rem] relative z-[1] backdrop-blur-[6px] bg-[rgba(249,_249,_251,_0.48)] dark:bg-[rgba(26,_27,_29,_0.48)] shadow-[0px_2px_5px_0px_rgba(28_32_36_0.03)] overflow-hidden", ), - 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]", + class_name="relative", ) @@ -106,11 +216,11 @@ def plan_cards() -> rx.Component: "Hobby", "Everything you need to get started with Reflex.", [ + ("code", "Open Source Framework"), ("heart-handshake", "Community support"), ("user", "1 team member"), - ("app-window", "1 deployed app"), - ("list-minus", "1 day log retention"), - ("map-pinned", "Single region"), + ("app-window", "1 Deployed app"), + ("file-code", "Starter Templates"), ], "Start building for free", ), @@ -118,12 +228,13 @@ def plan_cards() -> rx.Component: "Pro", "For professional projects $19/mo per member. Plus usage.", [ - ("heart-handshake", "Community support"), + ("server", "Larger machine sizes"), ("users", "Up to 5 team members"), - ("app-window", "Max 5 deployed apps"), + ("app-window", "Up to 5 Deployed apps"), + ("clock", "30 days log retention"), ("globe", "Multi-region"), ("brush", "Custom domains"), - ("activity", "Basic analytics"), + ("wand", "AI Tools for App Building and Debugging"), ("circle-plus", "Everything in Hobby"), ], "Start with Pro plan", @@ -133,11 +244,13 @@ def plan_cards() -> rx.Component: "For teams looking to scale their applications. Plus usage.", [ ("mail", "Email support"), - ("users", "Up to 15 team members"), + ("users", "Up to 25 team members"), ("app-window", "Unlimited Apps"), - ("list-minus", "90 days log retention"), - ("activity", "Metrics and analytics"), - ("lock-keyhole", "One-click Auth"), + ("signal", "Full Website Analytics"), + ("lock-keyhole", "One Click Auth"), + ("git-branch", "Dev, Stage & Prod Environments"), + ("database", "Database Editor UI and Migration Tool"), + ("test-tube", "Built-in Testing Framework"), ("circle-plus", "Everything in Pro"), ], "Contact sales", @@ -146,12 +259,15 @@ def plan_cards() -> rx.Component: "Enterprise", "Get our priority support and a plan tailored to your needs.", [ - ("heart-handshake", "Priority Support + Custom Onboarding"), + ("headset", "Priority Engineering Support"), + ("user-round-plus", "White Glove Onboarding"), ("users", "Unlimited team members"), - ("hard-drive", "On prem deployments"), - ("list-minus", "Advanced app analytics"), - ("cpu", "Customized machine size"), - ("shield-check", "SOC 2 report"), + ("hard-drive", "On Premise Deployment"), + ("signal", "Full Analytics Dashboard of App Users"), + ("clock", "Unlimited log retention"), + ("activity", "Error Monitoring and Observability"), + ("git-pull-request", "Influence Reflex Roadmap"), + ("shield-check", "Custom SSO"), ("circle-plus", "Everything in Team"), ], "Contact sales", diff --git a/pcweb/pages/pricing/pricing.py b/pcweb/pages/pricing/pricing.py index fe594d5ca3..7433113607 100644 --- a/pcweb/pages/pricing/pricing.py +++ b/pcweb/pages/pricing/pricing.py @@ -4,7 +4,7 @@ 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.pages.pricing.table import comparison_table_hosting, comparison_table_oss 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 @@ -22,7 +22,8 @@ def pricing() -> rx.Component: rx.box( header(), plan_cards(), - comparison_table(), + comparison_table_hosting(), + comparison_table_oss(), 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 f594398ecc..e4bc7913e5 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -33,46 +33,76 @@ """ # Data configuration -PRICE_SECTION = [ - ("Per Seat Price", "Free", "$19/mo", "Contact Sales", "Contact Sales"), - ("Compute", "Free", "Usage Based", "Usage Based", "Custom"), +USERS_SECTION = [ + ("Per Seat Price", "Free", "$19/mo/user", "Contact Sales", "Contact Sales"), + ("User Limit", "1", "5", "25", "Unlimited"), ] +FRAMEWORK_SECTION = [ + ("Open Source Framework", True, True, True, True), + ("Starter Templates", True, True, True, True), + ("Enterprise Templates", False, False, True, True), + ("One Click Auth", False, False, True, True), + ("Embed Reflex Apps", False, False, True, True), + ("Built-in Testing", False, False, True, True), +] + +THEME_SECTION = [("Theming", "Builtin Themes", "Builtin Themes", "Custom Themes", "Custom Themes")] -COMPUTE_SECTION = [ - ("Compute Limits", "1 CPU, .5GB", "5 CPU, 10GB", "32 CPU, 64GB", "No Limit"), +REFLEX_AI_SECTION = [ + ("Flexgen Website Builder", "5/day", "20/day", "100/day", "Custom"), + ("Full-Stack AI Agent", "5/day", "50/day", "250/day", "Custom"), + ("AI Assistant / Debugger", "5/day", "50/day", "250/day", "Custom"), +] + +DATABASE_SECTION = [ + ("Connect your own SQL DB", True, True, True, True), + ("Database Editor UI", False, False, True, True), + ("Database Migration Tool", False, False, True, True), +] + +HOSTING_TEXT_SECTION = [ + ("Compute Limits", "1 CPU, .5GB", "5 CPU, 10GB", "Custom", "Custom"), ("Regions", "Single", "Multiple", "Multiple", "Multiple"), - ("Team size", "1", "< 5", "< 15", "Unlimited"), + ("Custom Domains", "None", "1", "5", "Unlimited"), + ("Build logs", "7 day", "30 days", "90 days", "Custom"), ("Runtime logs", "1 day", "7 days", "30 days", "Custom"), - ("Build logs", "1 days", "30 days", "90 days", "Custom"), ] -ON_PREMISE_ROW = [("On Premises (Optional)", False, False, False, True)] - -FEATURE_SECTION = [ +HOSTING_BOOLEAN_SECTION = [ + ("CLI Deployments", True, True, True, True), + ("Automatic CI / CD Deploy (Github)", False, False, True, True), ("Secrets", 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), - ("SSO", False, False, False, True), + ("Secret Manager", False, False, True, True), + ("App Analytics", False, False, True, True), + ("Traces", False, False, True, True), + ("Custom Alerts", False, False, True, True), + ("Rollbacks", False, False, True, True), + ("Large File Support", False, False, True, True), + ("On Prem Hosting", False, False, False, True), ] SECURITY_SECTION = [ - ("Web app firewall", True, True, True, True), + ("Web App Firewall", True, True, True, True), ("HTTP/SSL", True, True, True, True), - ("DDos", True, True, True, True), + ("DDos Protection", True, True, True, True), + ("2 Factor Auth", True, True, True, True), + ("Rich Permissions Control", False, False, True, True), + ("Connect to Analytics Vendors", False, False, True, True), + ("Audit Logs", False, False, False, True), + ("Custom SSO", False, False, False, True), ] +SUPPORT_TEXT_SECTION = [ + ("Support", "Community", "Community", "Email Support", "Dedicated Support") +] -SUPPORT_SECTION = [ - ("Community support", True, True, True, 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), +SUPPORT_BOOLEAN_SECTION = [ + ("White Glove Onboarding", False, False, False, True), + ("Support SLAs Available", False, False, False, True), + ("Migrate Existing Apps", False, False, False, True), + ("Priority Support with Reflex Engineering Team", False, False, False, True), + ("", "", "", "", ""), ] PLAN_BUTTONS = [ @@ -95,20 +125,6 @@ def glow() -> rx.Component: ) -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: if content == "Usage Based": return rx.table.cell( @@ -127,7 +143,7 @@ def create_action_button( variant=variant, class_name=f"{STYLES['button_base']} {extra_styles}", ), - href=HOSTING_URL if text != "Contact sales" else "mailto:sales@reflex.dev", + href=HOSTING_URL if text != "Contact sales" else "/sales", is_external=True, underline="none", class_name="w-full flex justify-center items-center", @@ -142,10 +158,10 @@ def create_table_row(cells: list) -> rx.Component: ) -def create_table_row_header(cells: list) -> rx.Component: +def create_table_row_header(cells: list, coming_soon: bool = False) -> rx.Component: return rx.table.row( *[ - rx.table.column_header_cell(cell, class_name=STYLES["header_cell"]) + rx.table.column_header_cell(cell, rx.badge("coming soon", margin_left="0.5rem"), class_name=STYLES["header_cell"]) if cell and coming_soon else rx.table.column_header_cell(cell, class_name=STYLES["header_cell"]) for cell in cells ], class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-2 border border-slate-3 rounded-2xl z-[6] !h-[3.625rem] relative", @@ -156,7 +172,7 @@ def create_table_row_header(cells: list) -> rx.Component: 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", + 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.19rem] mx-auto border-b-0", ) @@ -174,7 +190,36 @@ def create_checkmark_row(feature: str, checks: tuple[bool, ...]) -> rx.Component return create_table_row(cells) -def table_body() -> rx.Component: + +def header_hosting() -> rx.Component: + return rx.box( + rx.el.h3( + "Secure and Scalable Hosting", + class_name="text-slate-12 text-3xl font-semibold text-center", + ), + rx.el.p( + "Compare features across plans.", + class_name="text-slate-9 text-2xl 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.19rem] mx-auto w-full", + ) + + +def header_oss() -> rx.Component: + return rx.box( + rx.el.h3( + "Supercharged Features to Build Faster", + class_name="text-slate-12 text-3xl font-semibold text-center", + ), + rx.el.p( + "Premium Features to help you get the most out of Reflex", + class_name="text-slate-9 text-2xl 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.19rem] mx-auto w-full", + ) + + +def table_body_hosting() -> rx.Component: return rx.table.root( rx.el.style(TABLE_STYLE), rx.table.header( @@ -183,48 +228,74 @@ def table_body() -> rx.Component: class_name="relative", ), create_table_body( - *[create_table_row(row) for row in PRICE_SECTION], + *[create_table_row(row) for row in USERS_SECTION], ), rx.table.header( - create_table_row_header(["Compute", "", "", ""]), + create_table_row_header(["Hosting", "", "", ""]), class_name="relative", ), create_table_body( + *[create_table_row(row) for row in HOSTING_TEXT_SECTION], *[ create_checkmark_row(feature, checks) - for feature, *checks in ON_PREMISE_ROW + for feature, *checks in HOSTING_BOOLEAN_SECTION ], - *[create_table_row(row) for row in COMPUTE_SECTION], ), rx.table.header( - create_table_row_header(["Features", "", "", "", ""]), + create_table_row_header(["Security", "", "", "", ""]), class_name="relative", ), create_table_body( *[ create_checkmark_row(feature, checks) - for feature, *checks in FEATURE_SECTION + for feature, *checks in SECURITY_SECTION ], ), rx.table.header( - create_table_row_header(["Security", "", "", "", ""]), + create_table_row_header(["Support", "", "", "", ""]), class_name="relative", ), create_table_body( + *[create_table_row(row) for row in SUPPORT_TEXT_SECTION], *[ create_checkmark_row(feature, checks) - for feature, *checks in SECURITY_SECTION + for feature, *checks in SUPPORT_BOOLEAN_SECTION ], ), + class_name="w-full overflow-x-auto max-w-[69.125rem] -mt-[2rem]", + ) + + +def table_body_oss() -> rx.Component: + return rx.table.root( + rx.el.style(TABLE_STYLE), rx.table.header( - create_table_row_header(["Support", "", "", "", ""]), + create_table_row_header(["Framework","Hobby", "Pro", "Team", "Enterprise"]), class_name="relative", ), create_table_body( *[ create_checkmark_row(feature, checks) - for feature, *checks in SUPPORT_SECTION + for feature, *checks in FRAMEWORK_SECTION ], + *[create_table_row(row) for row in THEME_SECTION], + ), + rx.table.header( + create_table_row_header(["Database", "", "", ""]), + class_name="relative", + ), + create_table_body( + *[ + create_checkmark_row(feature, checks) + for feature, *checks in DATABASE_SECTION + ], + ), + rx.table.header( + create_table_row_header(["AI", "", "", ""], coming_soon=True), + class_name="relative", + ), + create_table_body( + *[create_table_row(row) for row in REFLEX_AI_SECTION], ), create_table_body( rx.table.row( @@ -239,10 +310,17 @@ def table_body() -> rx.Component: class_name="w-full overflow-x-auto max-w-[69.125rem] -mt-[2rem]", ) - -def comparison_table() -> rx.Component: +def comparison_table_hosting() -> rx.Component: return rx.box( - header(), - table_body(), + header_hosting(), + table_body_hosting(), class_name="flex-col w-full max-w-[69.125rem] desktop-only", ) + + +def comparison_table_oss() -> rx.Component: + return rx.box( + header_oss(), + table_body_oss(), + class_name="flex-col w-full max-w-[69.125rem] desktop-only", + ) \ No newline at end of file diff --git a/pcweb/pages/sales.py b/pcweb/pages/sales.py index b3a02e081e..8851344db5 100644 --- a/pcweb/pages/sales.py +++ b/pcweb/pages/sales.py @@ -72,7 +72,7 @@ def form() -> rx.Component: rx.form( rx.box( rx.text( - "Get an enterprise quote", + "Contact the Sales Team", class_name="text-2xl text-slate-12 font-bold leading-6 scroll-m-[7rem]", id="form-title", ), @@ -151,7 +151,7 @@ def form() -> rx.Component: FormState.is_loading, button( "Sending...", - variant="muted", + variant="secondary", type="submit", class_name="opacity-80 !cursor-not-allowed pointer-events-none !w-min", ),