From ad0bbd6444dd641af3a325a8070a4a73eb5d2836 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 22 Nov 2024 20:30:11 +0100 Subject: [PATCH 1/2] 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 000000000..1df943381 --- /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 000000000..d113cc841 --- /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 f3e7b7c77..fe594d5ca 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 feb05bbb5..c920408ad 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 2/2] 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 d113cc841..67c03c18e 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: