Skip to content

Commit 092ed59

Browse files
LineIndentAlek99
andauthored
ENG-6284: new landing for pricing (#1429)
* new landing for pricing * updates to header landing * more fixes more to come * backend set * more banned emails * add more backend * ui fix * remove price cards * more ui fixes --------- Co-authored-by: Alek Petuskey <[email protected]>
1 parent 28867f2 commit 092ed59

File tree

3 files changed

+352
-10
lines changed

3 files changed

+352
-10
lines changed

pcweb/pages/framework/views/companies.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,20 @@ def companies() -> rx.Component:
4444
),
4545
class_name="flex flex-col justify-center gap-4 w-full h-auto max-w-[64.19rem] lg:border-x border-slate-3 lg:px-[8.5rem] lg:py-16 py-12 border-b lg:border-b-0",
4646
)
47+
48+
49+
def pricing_page_companies() -> rx.Component:
50+
return rx.el.section(
51+
rx.box(
52+
logo("amazon", "34px"),
53+
logo("nasa", "21px"),
54+
logo("dell", "36px"),
55+
logo("samsung", "26px"),
56+
logo("ibm", "21px"),
57+
logo("accenture", "21px"),
58+
logo("rappi", "21px"),
59+
logo("nike", "19px"),
60+
class_name="flex flex-row flex-wrap justify-center md:justify-start items-center gap-8 h-auto",
61+
),
62+
class_name="flex flex-col justify-center gap-4 w-full h-auto",
63+
)

pcweb/pages/pricing/header.py

Lines changed: 334 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,342 @@
11
import reflex as rx
22
from pcweb.components.hosting_banner import HostingBannerState
3+
from pcweb.pages.framework.views.companies import pricing_page_companies
4+
from pcweb.components.new_button import button
5+
from typing import Literal, Any
36

7+
import urllib.parse
8+
from datetime import datetime
9+
from reflex.event import EventType
410

5-
def header() -> rx.Component:
6-
return rx.box(
7-
rx.el.h1(
8-
"Get a custom quote",
9-
class_name="gradient-heading font-semibold text-3xl lg:text-5xl text-center",
11+
12+
SelectVariant = Literal["primary", "secondary", "outline", "transparent"]
13+
SelectSize = Literal["sm", "md", "lg"]
14+
SelectItemVariant = Literal["selectable", "actions", "projects"]
15+
16+
DEFAULT_CLASS_NAME = "inline-flex transition-bg shrink-0 items-center w-auto cursor-pointer disabled:cursor-not-allowed disabled:border disabled:border-slate-5 disabled:!bg-slate-3 disabled:!text-slate-8 outline-none focus:outline-none"
17+
18+
VARIANT_STYLES: dict[SelectVariant, str] = {
19+
"primary": "text-slate-9 font-medium border border-slate-5 bg-slate-1 hover:bg-slate-3 radix-state-open:bg-slate-3",
20+
"secondary": "text-slate-11 font-medium bg-slate-4 hover:bg-slate-6 radix-state-open:bg-slate-6",
21+
"transparent": "bg-transparent text-slate-9 font-medium hover:bg-slate-3 radix-state-open:bg-slate-3",
22+
"outline": "text-slate-9 font-medium border border-slate-5 hover:bg-slate-3 radix-state-open:bg-slate-3 bg-slate-1",
23+
}
24+
25+
SIZE_STYLES: dict[SelectSize, str] = {
26+
"xs": "text-sm px-1.5 h-7 rounded-md gap-1.5",
27+
"sm": "text-sm px-2 h-8 rounded-lg",
28+
"md": "text-sm px-2.5 min-h-9 max-h-9 rounded-[10px] gap-2.5",
29+
"lg": "text-sm px-3 h-10 rounded-xl gap-3",
30+
}
31+
32+
def select_item(
33+
content: tuple[str | rx.Component, EventType[()]],
34+
is_selected: bool = False,
35+
size: SelectSize = "sm",
36+
variant: SelectItemVariant = "actions",
37+
loading: bool = False,
38+
**props,
39+
) -> rx.Component:
40+
"""A select item component."""
41+
text, on_click_event = content
42+
base_classes = [
43+
"flex transition-bg items-center w-full max-w-32 cursor-pointer disabled:cursor-not-allowed disabled:border disabled:border-slate-5 disabled:!bg-slate-3 disabled:!text-slate-8 outline-none focus:outline-none",
44+
"bg-transparent text-slate-9 font-medium hover:bg-slate-3 font-sans",
45+
SIZE_STYLES[size],
46+
]
47+
48+
common_props = {
49+
"class_name": " ".join(filter(None, base_classes)),
50+
"type": "button",
51+
"on_click": on_click_event,
52+
**props,
53+
}
54+
55+
return rx.popover.close(
56+
rx.el.button(
57+
text,
58+
rx.box(class_name="flex-1"),
59+
**common_props,
1060
),
11-
rx.el.p(
12-
"The complete platform for building and deploying your apps.",
13-
class_name="text-slate-9 text-md lg:text-xl font-semibold text-center",
61+
class_name="w-full outline-none focus:outline-none",
62+
)
63+
# return rx.el.button(text, **common_props)
64+
65+
def select(
66+
content: rx.Component,
67+
variant: SelectVariant = "primary",
68+
size: SelectSize = "xs",
69+
placeholder: str = "Select an option",
70+
align: Literal["start", "center", "end"] = "start",
71+
class_name: str = "",
72+
icon: rx.Component | None = None,
73+
show_arrow: bool = True,
74+
unstyled: bool = False,
75+
tier: str = "",
76+
disabled: bool = False,
77+
**props,
78+
) -> rx.Component:
79+
"""A dropdown select component."""
80+
classes = (
81+
[
82+
DEFAULT_CLASS_NAME,
83+
VARIANT_STYLES[variant],
84+
SIZE_STYLES[size],
85+
class_name,
86+
]
87+
if not unstyled
88+
else [class_name]
89+
)
90+
91+
return rx.popover.root(
92+
rx.popover.trigger(
93+
rx.el.button(
94+
rx.box(placeholder),
95+
class_name=" ".join(filter(None, classes)),
96+
disabled=disabled,
97+
type="button",
98+
),
99+
),
100+
rx.popover.content(
101+
content,
102+
class_name="bg-transparent !shadow-none !p-0 border-none overflow-visible font-sans pointer-events-auto max-w-32",
103+
),
104+
**props,
105+
)
106+
107+
class QuoteFormState(rx.State):
108+
"""State management for the quote form."""
109+
num_employees: str = "500+"
110+
referral_source: str = "Google Search"
111+
banned_email: bool = False
112+
113+
114+
def set_select_value(self, field: str, value: str):
115+
"""Update the selected value for a given field."""
116+
setattr(self, field, value)
117+
118+
@rx.event
119+
def submit(self, form_data: dict[str, Any]):
120+
# Email domain validation
121+
banned_domains = [
122+
'gmail.com',
123+
'outlook.com',
124+
'hotmail.com',
125+
'yahoo.com',
126+
'icloud.com',
127+
'aol.com',
128+
'protonmail.com',
129+
'proton.me',
130+
'mail.com',
131+
'yandex.com',
132+
'zoho.com',
133+
'live.com',
134+
'msn.com',
135+
'me.com',
136+
'mac.com',
137+
'googlemail.com',
138+
'yahoo.co.uk',
139+
'yahoo.ca',
140+
'yahoo.co.in',
141+
'outlook.co.uk',
142+
'hotmail.co.uk'
143+
]
144+
145+
email = form_data.get("email", "").lower()
146+
if "@" in email:
147+
domain = email.split("@")[1]
148+
if domain in banned_domains:
149+
self.banned_email = True
150+
yield rx.set_focus("email")
151+
return
152+
153+
154+
self.banned_email = False
155+
now = datetime.now()
156+
current_month = now.strftime("%Y-%m")
157+
current_date = now.strftime("%Y-%m-%d")
158+
159+
params = {
160+
"First Name": form_data.get("first_name", ""),
161+
"Last Name": form_data.get("last_name", ""),
162+
"Business Email Address": form_data.get("email", ""),
163+
"Job Title": form_data.get("job_title", ""),
164+
"Company name": form_data.get("company_name", ""),
165+
"Phone Number": form_data.get("phone_number", ""),
166+
"Number of Employees": self.num_employees,
167+
"What internal tools are you looking to build?": form_data.get("internal_tools", ""),
168+
"Where did you first hear about Reflex?": self.referral_source,
169+
"month": current_month,
170+
"date": current_date,
171+
}
172+
173+
query_string = urllib.parse.urlencode(params)
174+
cal_url = f"https://cal.com/team/reflex/talk-to-a-reflex-expert?{query_string}"
175+
176+
return rx.redirect(cal_url)
177+
178+
def quote_input(placeholder: str, name: str, **props):
179+
return rx.el.input(
180+
placeholder=placeholder,
181+
name=name,
182+
class_name="box-border w-full border-slate-5 bg-slate-1 focus:shadow-[0px_0px_0px_2px_var(--c-violet-4)] px-6 pr-8 border rounded-[0.625rem] h-[2.25rem] font-medium text-slate-12 text-sm placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 peer pl-2.5 disabled:cursor-not-allowed disabled:border disabled:border-slate-5 disabled:!bg-slate-3 disabled:text-slate-8 disabled:placeholder:text-slate-8",
183+
**props,
184+
)
185+
186+
def form_field(label: str, input_component, required: bool = False, class_name: str = "mb-6"):
187+
"""Reusable form field component with label and input."""
188+
label_text = f"{label} {'*' if required else ''}"
189+
return rx.box(
190+
rx.text(label_text, class_name="text-slate-11 text-sm font-medium mb-2"),
191+
input_component,
192+
class_name=class_name,
193+
)
194+
195+
def text_input_field(label: str, name: str, placeholder: str, required: bool = False, input_type: str = "text", class_name: str = "mb-6"):
196+
"""Helper for creating text input fields."""
197+
input_component = quote_input(
198+
name=name,
199+
placeholder=placeholder,
200+
type=input_type,
201+
required=required,
202+
)
203+
return form_field(label, input_component, required, class_name)
204+
205+
def select_field(label: str, name: str, options: list, placeholder: str, required: bool = False, state_var: str = ""):
206+
"""Helper for creating custom select fields."""
207+
# Create scroll area with selectable options
208+
scroll_content = rx.box(
209+
*[
210+
select_item(
211+
content=(option, lambda opt=option, var=state_var: QuoteFormState.set_select_value(var, opt)),
212+
size="sm",
213+
variant="selectable",
214+
class_name="w-full justify-start px-4 py-2 hover:bg-slate-2 rounded-md",
215+
)
216+
for option in options
217+
],
218+
class_name="!pt-0 !mt-0 !max-h-[15rem] bg-slate-1 border border-slate-5 rounded-lg shadow-lg py-0 overflow-y-scroll w-full",
219+
)
220+
221+
# Get the current selected value for this field
222+
current_value = getattr(QuoteFormState, state_var, placeholder)
223+
224+
select_component = select(
225+
content=scroll_content,
226+
placeholder=current_value,
227+
variant="primary",
228+
size="md",
229+
class_name="w-full justify-between flex-1",
230+
show_arrow=True,
231+
)
232+
return form_field(label, select_component, required)
233+
234+
def textarea_field(label: str, name: str, placeholder: str, required: bool = False):
235+
"""Helper for creating textarea fields."""
236+
textarea_component = rx.el.textarea(
237+
name=name,
238+
placeholder=placeholder,
239+
required=required,
240+
class_name="w-full px-3 py-2 font-medium text-slate-12 text-sm placeholder:text-slate-9 border border-slate-5 bg-slate-1 focus:shadow-[0px_0px_0px_2px_var(--c-violet-4)] rounded-lg focus:border-violet-8 focus:outline-none bg-transparent min-h-[100px] resize-y transition-colors",
241+
)
242+
return form_field(label, textarea_component, required)
243+
244+
def custom_quote_form() -> rx.Component:
245+
"""Custom quote form component with clean, maintainable structure."""
246+
return rx.box(
247+
rx.box(
248+
# Left column - Content
249+
rx.box(
250+
rx.el.h2(
251+
"Book a Demo",
252+
class_name="text-slate-12 text-4xl font-bold mb-8",
253+
),
254+
rx.el.p(
255+
"Enterprise-ready solutions designed for scale, compliance, and support. Contact us for a tailored quote based on your infrastructure and team size.",
256+
class_name="text-slate-11 text-md leading-relaxed mb-12 max-w-lg",
257+
),
258+
rx.box(
259+
pricing_page_companies(),
260+
class_name="flex flex-col",
261+
),
262+
class_name="mb-8 lg:mb-0 text-center sm:text-left",
263+
),
264+
# Right column - Form
265+
rx.box(
266+
rx.el.form(
267+
# Personal Information
268+
rx.el.div(
269+
text_input_field("First name", "first_name", "John", required=True, class_name="mb-0"),
270+
text_input_field("Last name", "last_name", "Smith", required=True, class_name="mb-0"),
271+
class_name="flex-row flex gap-x-2 mb-6",
272+
),
273+
rx.cond(
274+
QuoteFormState.banned_email,
275+
rx.box(
276+
rx.el.div(
277+
rx.text("Business email", class_name="text-slate-11 text-sm font-medium mb-2"),
278+
rx.text("Personal emails not allowed!", class_name="text-red-8 text-sm font-medium mb-2"),
279+
class_name="flex flex-row items-center justify-between w-full",
280+
),
281+
rx.el.input(
282+
placeholder="Personal emails not allowed!",
283+
name="email",
284+
class_name="box-border w-full border-2 border-red-5 bg-slate-1 px-6 pr-8 border rounded-[0.625rem] h-[2.25rem] font-medium text-slate-12 text-sm placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 peer pl-2.5 disabled:cursor-not-allowed disabled:border disabled:border-slate-5 disabled:!bg-slate-3 disabled:text-slate-8 disabled:placeholder:text-slate-8",
285+
)
286+
),
287+
text_input_field("Business email", "email", "[email protected]", required=True, input_type="email"),
288+
),
289+
rx.el.div(
290+
text_input_field("Job title", "job_title", "CTO", required=True, class_name="mb-0"),
291+
text_input_field("Company name", "company_name", "Pynecone, Inc.", required=True, class_name="mb-0"),
292+
class_name="flex-row flex gap-x-2 mb-6",
293+
),
294+
295+
text_input_field("Phone number", "phone_number", "(555) 123-4567", required=True, input_type="tel"),
296+
297+
# Project Details
298+
textarea_field("What are you looking to build?", "internal_tools", "Please list any apps, requirements, or data sources you plan on using", required=True),
299+
300+
# Company Size and Referral
301+
rx.el.div(
302+
select_field("Number of employees", "num_employees", ["1-10", "11-50", "51-100", "101-500", "500+"], "500+", required=True, state_var="num_employees"),
303+
select_field("How did you hear about us?", "referral_source", ["Google Search", "Social Media", "Word of Mouth", "Blog", "Conference", "Other"], "Google Search", required=True, state_var="referral_source"),
304+
class_name="w-full flex-row flex flex-wrap justify-between mb-6",
305+
),
306+
307+
# Submit button
308+
button(
309+
"Submit",
310+
variant="primary",
311+
type="submit",
312+
size="lg",
313+
class_name="w-full mt-2",
314+
),
315+
class_name="w-full space-y-6",
316+
on_submit=QuoteFormState.submit,
317+
),
318+
rx.box(
319+
"1 Month Free Trial",
320+
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]",
321+
),
322+
class_name="relative bg-slate-1 p-6 sm:p-8 rounded-2xl border-2 border-[--violet-9] shadow-lg w-full max-w-md mx-auto lg:max-w-none lg:mx-0",
323+
),
324+
class_name="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 max-w-7xl mx-auto items-start",
14325
),
15-
class_name="flex flex-col gap-2 justify-center items-center max-w-[64.19rem] 2xl:border-x border-slate-4 w-full pb-16 "
326+
class_name="py-12 sm:py-20 px-4 sm:px-8",
327+
)
328+
329+
def header() -> rx.Component:
330+
return rx.box(
331+
custom_quote_form(),
332+
# rx.el.h1(
333+
# "Get a custom quote",
334+
# class_name="gradient-heading font-semibold text-3xl lg:text-5xl text-center",
335+
# ),
336+
# rx.el.p(
337+
# "The complete platform for building and deploying your apps.",
338+
# class_name="text-slate-9 text-md lg:text-xl font-semibold text-center",
339+
# ),
340+
class_name="flex flex-col gap-2 justify-center items-center max-w-[64.19rem] 2xl:border-x border-slate-4 w-full -mb-10 "
16341
+ rx.cond(HostingBannerState.show_banner, "pt-[11rem]", "pt-[12rem]"),
17342
)

pcweb/pages/pricing/pricing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def pricing() -> rx.Component:
2525
rx.el.main(
2626
rx.box(
2727
header(),
28-
plan_cards(),
28+
# plan_cards(),
2929
comparison_table_ai(),
3030
comparison_table_hosting(),
3131
calculator_section(),

0 commit comments

Comments
 (0)