Skip to content

Commit 8d7fb94

Browse files
author
Alek99
committed
feat: redesign pricing cards with Team as popular plan and add anchor links - Restructured pricing cards layout: Title → Description → Button → Price → Features - Made Team the popular card instead of Pro plan with purple border and glow effects - Updated pricing: Pro to /month, Team to /user/month - Added messaging sections with Reflex Build limits and upgrade links - Improved feature lists and removed redundant features - Added helper functions for cleaner, more maintainable code - Fixed price display formatting and alignment - Added anchor link for Reflex Build section with proper scroll positioning
1 parent 7f5e9ee commit 8d7fb94

File tree

2 files changed

+204
-113
lines changed

2 files changed

+204
-113
lines changed

pcweb/pages/pricing/plan_cards.py

Lines changed: 179 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def radial_circle(violet: bool = False) -> rx.Component:
3030

3131

3232
def glow() -> rx.Component:
33+
"""Radial gradient glow effect for popular card."""
3334
return rx.html(
3435
"""<svg xmlns="http://www.w3.org/2000/svg" width="502" height="580" viewBox="0 0 502 580" fill="none">
3536
<path d="M0 290C0 450.163 112.377 580 251 580C389.623 580 502 450.163 502 290C502 129.837 389.623 0 251 0C112.377 0 0 129.837 0 290Z" fill="url(#paint0_radial_13685_26666)"/>
@@ -45,6 +46,7 @@ def glow() -> rx.Component:
4546

4647

4748
def grid() -> rx.Component:
49+
"""Animated grid background for popular card."""
4850
return rx.html(
4951
"""<svg width="326" height="472" viewBox="0 0 326 472" fill="none" xmlns="http://www.w3.org/2000/svg">
5052
<g clip-path="url(#clip0_13685_24040)">
@@ -137,6 +139,99 @@ def grid() -> rx.Component:
137139
)
138140

139141

142+
def _get_price_label(title: str) -> str:
143+
"""Get the appropriate price label for each plan."""
144+
if title == "Hobby":
145+
return "Free"
146+
elif title == "Enterprise":
147+
return "" # No label for Enterprise (Custom pricing)
148+
else:
149+
return "From"
150+
151+
152+
def _render_price_display(price: str, title: str) -> rx.Component:
153+
"""Render the price display section with proper formatting."""
154+
if "user" in price:
155+
# Handle user-based pricing (e.g., "$49 user/month")
156+
parts = price.split(" ", 1)
157+
return rx.el.div(
158+
rx.el.span(parts[0], class_name="text-4xl font-bold text-slate-12"),
159+
rx.el.span(f" {parts[1]}", class_name="text-sm text-slate-9 ml-2"),
160+
class_name="flex items-baseline"
161+
)
162+
else:
163+
# Handle regular pricing (e.g., "$25/month")
164+
main_price = price.split("/")[0] if "/" in price else price
165+
period = f" / {price.split('/')[1]}" if "/" in price else ""
166+
return rx.el.div(
167+
rx.el.span(main_price, class_name="text-4xl font-bold text-slate-12"),
168+
rx.el.span(period, class_name="text-sm text-slate-9"),
169+
class_name="flex items-baseline"
170+
)
171+
172+
173+
def _render_messaging_section(title: str) -> rx.Component:
174+
"""Render the messaging/features section for each plan."""
175+
messaging_config = {
176+
"Hobby": {
177+
"main": "Reflex build 5 msgs/day",
178+
"sub": rx.link("Monthly cap 30 messages", href="#reflex-build",
179+
class_name="text-xs text-slate-9 hover:text-slate-11 underline")
180+
},
181+
"Pro": {
182+
"main": "Reflex build 100 msgs/month",
183+
"sub": rx.link("Upgrade to Team for more messages", href="#reflex-build",
184+
class_name="text-xs text-slate-9 hover:text-slate-11 underline")
185+
},
186+
"Enterprise": {
187+
"main": "Reflex build 500+ msgs/month",
188+
"sub": rx.link("More messages available on request", href="#reflex-build",
189+
class_name="text-xs text-slate-9 hover:text-slate-11 underline")
190+
}
191+
}
192+
193+
if title in messaging_config:
194+
config = messaging_config[title]
195+
return rx.el.div(
196+
rx.el.p(config["main"], class_name="text-sm font-medium text-slate-12 mt-4"),
197+
rx.el.p(config["sub"]) if title == "Hobby" else config["sub"],
198+
class_name="mt-4"
199+
)
200+
else:
201+
# Default spacing for plans without messaging section
202+
return rx.el.div(class_name="h-[3.5rem]")
203+
204+
205+
def _get_features_header(title: str) -> str:
206+
"""Get the appropriate features section header for each plan."""
207+
headers = {
208+
"Hobby": "Get started with:",
209+
"Pro": "Everything in the Free Plan, plus:",
210+
"Team": "Everything in the Pro Plan, plus:",
211+
"Enterprise": "Everything in Team, plus:"
212+
}
213+
return headers.get(title, "Features:")
214+
215+
216+
def _render_feature_list(features: list[tuple[str, str]]) -> rx.Component:
217+
"""Render the feature list with consistent styling."""
218+
return rx.el.ul(
219+
*[
220+
rx.el.li(
221+
rx.icon("check", class_name="!text-green-500", size=16),
222+
feature[1],
223+
rx.tooltip(
224+
rx.icon("info", class_name="!text-slate-9", size=12),
225+
content=feature[2],
226+
) if len(feature) == 3 else "",
227+
class_name="text-sm font-medium text-slate-11 flex items-center gap-3 mb-2",
228+
)
229+
for feature in features
230+
],
231+
class_name="flex flex-col",
232+
)
233+
234+
140235
def card(
141236
title: str,
142237
description: str,
@@ -145,56 +240,43 @@ def card(
145240
price: str = None,
146241
redirect_url: str = None,
147242
) -> rx.Component:
243+
"""Standard pricing card component."""
148244
return rx.box(
149-
rx.el.div(
150-
rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl"),
151-
(
152-
rx.badge(
153-
price,
154-
color_scheme="gray",
155-
size="3",
156-
class_name="font-medium 2xl:text-lg text-base w-fit",
157-
)
158-
if price
159-
else rx.fragment()
160-
),
161-
class_name="flex 2xl:items-center mb-2 2xl:gap-4 gap-2 2xl:flex-row flex-col",
162-
),
163-
rx.el.p(
164-
description, class_name="text-sm font-medium text-slate-9 mb-8 text-pretty"
165-
),
166-
rx.el.ul(
167-
*[
168-
rx.el.li(
169-
rx.icon(feature[0], class_name="!text-slate-9", size=16),
170-
feature[1],
171-
(
172-
rx.tooltip(
173-
rx.icon("info", class_name="!text-slate-9", size=12),
174-
content=feature[2],
175-
)
176-
if len(feature) == 3
177-
else ""
178-
),
179-
class_name="text-sm font-medium text-slate-11 flex items-center gap-3",
180-
)
181-
for feature in features
182-
],
183-
class_name="flex flex-col gap-2",
184-
),
185-
rx.box(class_name="flex-1"),
245+
# Header
246+
rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl mb-4"),
247+
rx.el.p(description, class_name="text-sm font-medium text-slate-9 mb-6 text-pretty"),
248+
249+
# CTA Button
186250
rx.link(
187251
button(
188252
button_text,
189253
variant="secondary",
190254
size="lg",
191-
class_name="w-full",
255+
class_name="w-full mb-6",
192256
),
193257
href=redirect_url,
194258
is_external=True,
195259
underline="none",
196260
),
197-
class_name="flex flex-col p-8 border border-slate-4 rounded-[1.125rem] shadow-small bg-slate-2 w-full min-w-0 h-[33.5rem] overflow-hidden",
261+
262+
# Pricing Section
263+
rx.el.div(
264+
rx.el.span(_get_price_label(title), class_name="text-sm text-slate-9 block mb-1"),
265+
_render_price_display(price, title),
266+
_render_messaging_section(title),
267+
class_name="mb-6"
268+
),
269+
270+
# Divider
271+
rx.el.hr(class_name="border-slate-3 mb-6"),
272+
273+
# Features Section
274+
rx.el.div(
275+
rx.el.p(_get_features_header(title), class_name="text-sm font-medium text-slate-9 mb-4"),
276+
_render_feature_list(features),
277+
),
278+
279+
class_name="flex flex-col p-6 border border-slate-4 rounded-lg shadow-small bg-slate-2 w-full min-w-0 overflow-hidden h-[42rem]",
198280
)
199281

200282

@@ -205,62 +287,60 @@ def popular_card(
205287
button_text: str,
206288
price: str = None,
207289
) -> rx.Component:
290+
"""Popular pricing card component with special styling and effects."""
208291
return rx.box(
292+
# Popular Badge
209293
rx.box(
210-
"Most popular",
294+
"Most Popular",
211295
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]",
212296
),
297+
298+
# Card Content with Background Effects
213299
rx.box(
214300
glow(),
215301
grid(),
216-
rx.el.div(
217-
rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl"),
218-
(
219-
rx.badge(
220-
price,
221-
color_scheme="violet",
222-
size="3",
223-
class_name="font-medium 2xl:text-lg text-base w-fit",
224-
)
225-
if price
226-
else rx.fragment()
227-
),
228-
class_name="flex 2xl:items-center mb-2 2xl:gap-4 gap-2 2xl:flex-row flex-col",
229-
),
230-
rx.el.p(description, class_name="text-sm font-medium text-slate-9 mb-8"),
231-
rx.el.ul(
232-
*[
233-
rx.el.li(
234-
rx.icon(feature[0], class_name="!text-violet-9", size=16),
235-
feature[1],
236-
(
237-
rx.tooltip(
238-
rx.icon("info", class_name="!text-slate-9", size=12),
239-
content=feature[2],
240-
)
241-
if len(feature) == 3
242-
else ""
243-
),
244-
class_name="text-sm font-medium text-slate-11 flex items-center gap-3",
245-
)
246-
for feature in features
247-
],
248-
class_name="flex flex-col gap-2",
249-
),
250-
rx.box(class_name="flex-1"),
302+
303+
# Header
304+
rx.el.h3(title, class_name="font-semibold text-slate-12 text-2xl mb-4"),
305+
rx.el.p(description, class_name="text-sm font-medium text-slate-9 mb-6 text-pretty"),
306+
307+
# CTA Button
251308
rx.link(
252309
button(
253310
button_text,
254311
variant="primary",
255312
size="lg",
256-
class_name="w-full !text-sm !font-semibold",
313+
class_name="w-full mb-6 !text-sm !font-semibold",
257314
),
258315
href=f"{REFLEX_CLOUD_URL}/?redirect_url={REFLEX_CLOUD_URL}/billing/",
259316
is_external=True,
260317
underline="none",
261318
),
262-
class_name="flex flex-col p-8 border border-[--violet-9] rounded-[1.125rem] w-full min-w-0 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",
319+
320+
# Pricing Section
321+
rx.el.div(
322+
rx.el.span("From", class_name="text-sm text-slate-9 block mb-1"),
323+
_render_price_display(price, title),
324+
rx.el.div(
325+
rx.el.p("Reflex build 250 msgs/month", class_name="text-sm font-medium text-slate-12 mt-4"),
326+
rx.link("More messages available on request", href="#reflex-build", class_name="text-xs text-slate-9 hover:text-slate-11 underline"),
327+
class_name="mt-4"
328+
),
329+
class_name="mb-6"
330+
),
331+
332+
# Divider
333+
rx.el.hr(class_name="border-slate-3 mb-6"),
334+
335+
# Features Section
336+
rx.el.div(
337+
rx.el.p("Everything in the Pro Plan, plus:", class_name="text-sm font-medium text-slate-9 mb-4"),
338+
_render_feature_list(features),
339+
),
340+
341+
class_name="flex flex-col p-6 border-2 border-[--violet-9] rounded-lg w-full min-w-0 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 h-[42rem]",
263342
),
343+
264344
class_name="relative",
265345
)
266346

@@ -271,69 +351,65 @@ def plan_cards() -> rx.Component:
271351
"Hobby",
272352
"Everything you need to get started.",
273353
[
274-
("frame", "Open Source Framework"),
275-
("brain", "AI App Builder (Limited Access)"),
276354
(
277355
"app-window",
278-
"Cloud Unlimited Apps",
356+
"Cloud Limited Apps",
279357
"Free users are limited to 20 hours of 1 vCPU, 1 GB RAM machines per month.",
280358
),
281-
("code", "Reflex Open Source"),
282359
("heart-handshake", "Discord/Github Support"),
283360
("building", rx.link("Reflex Enterprise", href="https://reflex.dev/docs/enterprise/overview/", class_name="!text-slate-11"), "Free-tier users can access Reflex Enterprise features, with a required 'Built with Reflex' badge displayed on their apps."),
361+
("frame", "Open Source Framework"),
284362
],
285-
"Start building for free",
286-
price="Free",
363+
"Start for Free",
364+
price="$0/month",
287365
redirect_url=REFLEX_DOCS_URL,
288366
),
289-
popular_card(
367+
card(
290368
"Pro",
291369
"For professional projects and startups.",
292370
[
293-
("brain", "AI App Builder (Free $20 credits / month)"),
294-
("credit-card", "Cloud Compute (Free $10 credits / month)"),
371+
("credit-card", "Cloud Credits $10/month included"),
295372
("brush", "Custom domains"),
296373
("building", rx.link("Reflex Enterprise", href="https://reflex.dev/docs/enterprise/overview/", class_name="!text-slate-11"), "Pro-tier users can access Reflex Enterprise features without the 'Built with Reflex' badge when hosting their apps on Reflex Cloud"),
297-
("circle-plus", "Everything in Hobby"),
298374
],
299-
"Start with Pro plan",
300-
price="$20/mo",
375+
"Upgrade now",
376+
price="$20/month",
377+
redirect_url=f"{REFLEX_CLOUD_URL}/?redirect_url={REFLEX_CLOUD_URL}/billing/",
301378
),
302-
card(
379+
popular_card(
303380
"Team",
304381
"For teams looking to scale their applications.",
305-
[
306-
("users", "Invite your team mates"),
382+
[
383+
("credit-card", "Cloud Compute $20/mo included"),
384+
("users", "Invite your teammates"),
307385
(
308386
"cable",
309-
"Connect AI Builder to your Data",
310-
"Integrations include Databricks, Snowflake, etc.",
387+
"Reflex Build Integrations",
388+
"Databricks, Snowflake, etc.",
311389
),
312-
("credit-card", "Cloud Compute (Free $20 credits / user / month)"),
313-
("lock-keyhole", "One Click Auth"),
314390
("file-badge", "AG Grid with no Reflex Branding"),
315391
("mail", "Email support"),
316392
("building", rx.link("Reflex Enterprise", href="https://reflex.dev/docs/enterprise/overview/", class_name="!text-slate-11"), "Team-tier users can access Reflex Enterprise features without the 'Built with Reflex' badge when self-hosting their apps."),
317-
("circle-plus", "Everything in Pro"),
318393
],
319-
"Start with Team plan",
320-
redirect_url=f"{REFLEX_CLOUD_URL}/?redirect_url={REFLEX_CLOUD_URL}/billing/",
321-
price="$49/user/mo",
394+
"Upgrade now",
395+
price="$49 user/month",
322396
),
323397
card(
324398
"Enterprise",
325399
"Get a plan tailored to your business needs.",
326400
[
401+
("credit-card", "Cloud Compute $100/mo included"),
402+
("hard-drive", "On Premise Deployment", "Option to self-host your apps on your own infrastructure."),
327403
("hand-helping", "White Glove Onboarding"),
328404
("user-round-plus", "Personalized integration help"),
329-
("hard-drive", "On Premise Deployment"),
330405
("key", "Bring your own AI API keys"),
331406
("headset", "Dedicated Support Channel"),
332407
("git-pull-request", "Influence Reflex Roadmap"),
333408
("circle-plus", "Everything in Team"),
334409
],
335-
"Contact sales",
410+
"Contact Us",
411+
price="Custom",
336412
redirect_url=REFLEX_DEV_WEB_LANDING_FORM_URL_GET_DEMO,
337413
),
338-
class_name="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"
414+
class_name="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
339415
)

0 commit comments

Comments
 (0)