Skip to content

Commit

Permalink
Merge branch 'carlos/new-hosting-pricing' of https://github.com/refle…
Browse files Browse the repository at this point in the history
…x-dev/reflex-web into carlos/new-hosting-pricing
  • Loading branch information
Alek Petuskey authored and Alek Petuskey committed Nov 22, 2024
2 parents 9a2795f + 92eade5 commit d296c00
Show file tree
Hide file tree
Showing 4 changed files with 374 additions and 1 deletion.
90 changes: 90 additions & 0 deletions pcweb/pages/pricing/button.py
Original file line number Diff line number Diff line change
@@ -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,
)
281 changes: 281 additions & 0 deletions pcweb/pages/pricing/calculator.py
Original file line number Diff line number Diff line change
@@ -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 64

@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(
"""<svg xmlns="http://www.w3.org/2000/svg" width="216" height="88" viewBox="0 0 216 88" fill="none">
<path d="M0 44C0 68.3005 48.3532 88 108 88C167.647 88 216 68.3005 216 44C216 19.6995 167.647 0 108 0C48.3532 0 0 19.6995 0 44Z" fill="url(#paint0_radial_13427_11205)"/>
<defs>
<radialGradient id="paint0_radial_13427_11205" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(108 44) rotate(90) scale(44 108)">
<stop stop-color="var(--c-violet-3)"/>
<stop offset="1" stop-color="var(--c-slate-2)" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
""",
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",
)
2 changes: 2 additions & 0 deletions pcweb/pages/pricing/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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",
),
Expand Down
2 changes: 1 addition & 1 deletion pcweb/pages/pricing/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,5 +244,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",
)

0 comments on commit d296c00

Please sign in to comment.