Skip to content

Commit e75728f

Browse files
committed
intro form
1 parent b5d1a2b commit e75728f

File tree

1 file changed

+396
-0
lines changed

1 file changed

+396
-0
lines changed

reflex_ui/blocks/intro_form.py

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
"""Intro form component for collecting user information.
2+
3+
This module provides a comprehensive intro form that validates company emails.
4+
"""
5+
6+
import reflex as rx
7+
from reflex.event import EventType
8+
from reflex.experimental.client_state import ClientStateVar
9+
from reflex.vars.base import get_unique_variable_name
10+
11+
import reflex_ui as ui
12+
from reflex_ui.components.base.button import BUTTON_VARIANTS, DEFAULT_CLASS_NAME
13+
14+
intro_form_error_message = ClientStateVar.create("intro_form_error_message", "")
15+
intro_form_open_cs = ClientStateVar.create("intro_form_open", False)
16+
17+
PERSONAL_EMAIL_PROVIDERS = r"^(?!.*@(gmail|outlook|hotmail|yahoo|icloud|aol|protonmail|mail|yandex|zoho|live|msn|me|mac|googlemail)\.com$|.*@(yahoo|outlook|hotmail)\.co\.uk$|.*@yahoo\.ca$|.*@yahoo\.co\.in$|.*@proton\.me$).*$"
18+
19+
20+
def get_element_value(element_id: str) -> str:
21+
"""Get the value of an element by ID."""
22+
return f"document.getElementById('{element_id}')?.value"
23+
24+
25+
def check_if_company_email(email: str) -> bool:
26+
"""Check if an email address is from a company domain (not a personal email provider).
27+
28+
Args:
29+
email: The email address to check
30+
31+
Returns:
32+
True if it's likely a company email, False if it's from a personal provider
33+
"""
34+
if not email or "@" not in email:
35+
return False
36+
37+
domain = email.split("@")[-1].lower()
38+
39+
# List of common personal email providers
40+
personal_domains = {
41+
"gmail.com",
42+
"outlook.com",
43+
"hotmail.com",
44+
"yahoo.com",
45+
"icloud.com",
46+
"aol.com",
47+
"protonmail.com",
48+
"proton.me",
49+
"mail.com",
50+
"yandex.com",
51+
"zoho.com",
52+
"live.com",
53+
"msn.com",
54+
"me.com",
55+
"mac.com",
56+
"googlemail.com",
57+
"yahoo.co.uk",
58+
"yahoo.ca",
59+
"yahoo.co.in",
60+
"outlook.co.uk",
61+
"hotmail.co.uk",
62+
}
63+
64+
return domain not in personal_domains and ".edu" not in domain
65+
66+
67+
def check_if_default_value_is_selected(value: str) -> bool:
68+
"""Check if the default value is selected."""
69+
return bool(value.strip())
70+
71+
72+
class IntroFormStateUI(rx.State):
73+
"""State for handling intro form submissions and validation."""
74+
75+
@rx.event
76+
def validate_email(self, email: str):
77+
"""Validate the email address."""
78+
if not check_if_company_email(email):
79+
yield [
80+
intro_form_error_message.push(
81+
"Please enter a valid company email - gmails, aol, me, etc are not allowed"
82+
),
83+
]
84+
else:
85+
yield intro_form_error_message.push("")
86+
87+
88+
def input_field(
89+
label: str,
90+
placeholder: str,
91+
name: str,
92+
type: str = "text",
93+
required: bool = False,
94+
) -> rx.Component:
95+
"""Create a labeled input field component.
96+
97+
Args:
98+
label: The label text to display above the input
99+
placeholder: Placeholder text for the input
100+
name: The name attribute for the input field
101+
type: The input type (text, email, tel, etc.)
102+
required: Whether the field is required
103+
104+
Returns:
105+
A Reflex component containing the labeled input field
106+
"""
107+
return rx.el.div(
108+
rx.el.label(
109+
label + (" *" if required else ""),
110+
class_name="block text-sm font-medium text-secondary-12",
111+
),
112+
ui.input(
113+
placeholder=placeholder,
114+
name=name,
115+
type=type,
116+
required=required,
117+
max_length=255,
118+
class_name="w-full",
119+
),
120+
class_name="flex flex-col gap-1.5",
121+
)
122+
123+
124+
def validation_input_field(
125+
label: str,
126+
placeholder: str,
127+
name: str,
128+
type: str = "text",
129+
required: bool = False,
130+
pattern: str | None = None,
131+
on_blur: EventType[()] | None = None,
132+
id: str = "",
133+
) -> rx.Component:
134+
"""Create a labeled input field component.
135+
136+
Args:
137+
label: The label text to display above the input
138+
placeholder: Placeholder text for the input
139+
name: The name attribute for the input field
140+
type: The input type (text, email, tel, etc.)
141+
pattern: Regex pattern for input validation
142+
required: Whether the field is required
143+
on_blur: Event handler for when the input is blurred
144+
id: The ID attribute for the input field
145+
146+
Returns:
147+
A Reflex component containing the labeled input field
148+
"""
149+
return rx.el.div(
150+
rx.el.label(
151+
label + (" *" if required else ""),
152+
class_name="block text-sm font-medium text-secondary-12",
153+
),
154+
ui.input(
155+
placeholder=placeholder,
156+
id=id,
157+
name=name,
158+
type=type,
159+
required=required,
160+
max_length=255,
161+
pattern=pattern,
162+
class_name="w-full",
163+
on_blur=on_blur,
164+
),
165+
class_name="flex flex-col gap-1.5",
166+
)
167+
168+
169+
def text_area_field(
170+
label: str, placeholder: str, name: str, required: bool = False
171+
) -> rx.Component:
172+
"""Create a labeled textarea field component.
173+
174+
Args:
175+
label: The label text to display above the textarea
176+
placeholder: Placeholder text for the textarea
177+
name: The name attribute for the textarea field
178+
required: Whether the field is required
179+
180+
Returns:
181+
A Reflex component containing the labeled textarea field
182+
"""
183+
return rx.el.div(
184+
rx.el.label(label, class_name="block text-sm font-medium text-secondary-12"),
185+
ui.textarea(
186+
placeholder=placeholder,
187+
name=name,
188+
required=required,
189+
class_name="w-full min-h-14",
190+
max_length=800,
191+
),
192+
class_name="flex flex-col gap-1.5",
193+
)
194+
195+
196+
def select_field(
197+
label: str,
198+
name: str,
199+
items: list[str],
200+
required: bool = False,
201+
) -> rx.Component:
202+
"""Create a labeled select field component.
203+
204+
Args:
205+
label: The label text to display above the select
206+
name: The name attribute for the select field
207+
items: List of options to display in the select
208+
required: Whether the field is required
209+
210+
Returns:
211+
A Reflex component containing the labeled select field
212+
"""
213+
return rx.el.div(
214+
rx.el.label(
215+
label + (" *" if required else ""),
216+
class_name="block text-xs lg:text-sm font-medium text-secondary-12 truncate min-w-0",
217+
),
218+
rx.el.div(
219+
rx.el.select(
220+
rx.el.option("Select", value=""),
221+
*[rx.el.option(item, value=item) for item in items],
222+
default_value="",
223+
name=name,
224+
required=required,
225+
class_name=ui.cn(
226+
"w-full appearance-none pr-9",
227+
DEFAULT_CLASS_NAME,
228+
BUTTON_VARIANTS["variant"]["outline"],
229+
BUTTON_VARIANTS["size"]["md"],
230+
"outline-primary-6 focus:border-primary-6",
231+
),
232+
),
233+
ui.select_arrow(
234+
class_name="size-4 text-secondary-9 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none"
235+
),
236+
class_name="relative",
237+
),
238+
class_name="flex flex-col gap-1.5 min-w-0",
239+
)
240+
241+
242+
def intro_form(
243+
id_prefix: str = "", on_submit: EventType[()] | None = None, **props
244+
) -> rx.Component:
245+
"""Create and return the intro form component.
246+
247+
Builds a complete form with all required fields, validation,
248+
and styling. The form includes personal info, company details,
249+
and preferences.
250+
251+
Args:
252+
id_prefix: Optional prefix for all element IDs to ensure uniqueness when multiple forms exist.
253+
If empty, a unique prefix will be auto-generated.
254+
on_submit: Optional event handler called when the form is submitted.
255+
**props: Additional properties to pass to the form component
256+
257+
Returns:
258+
A Reflex form component with all intro form fields
259+
"""
260+
prefix = id_prefix or get_unique_variable_name()
261+
email_id = f"{prefix}_user_email"
262+
form = rx.el.form(
263+
rx.el.div(
264+
input_field("First name", "John", "first_name", "text", True),
265+
input_field("Last name", "Smith", "last_name", "text", True),
266+
class_name="grid grid-cols-2 gap-4",
267+
),
268+
rx.el.div(
269+
validation_input_field(
270+
"Business Email",
271+
"john@company.com",
272+
"email",
273+
"email",
274+
True,
275+
PERSONAL_EMAIL_PROVIDERS,
276+
id=email_id,
277+
on_blur=IntroFormStateUI.validate_email(
278+
rx.Var(get_element_value(email_id))
279+
),
280+
),
281+
input_field("Phone number", "+1234567890", "phone_number", "tel", True),
282+
class_name="grid grid-cols-2 gap-4",
283+
),
284+
rx.el.div(
285+
input_field("Job title", "CTO", "job_title", "text", True),
286+
input_field("Company name", "Pynecone, Inc.", "company_name", "text", True),
287+
class_name="grid grid-cols-2 gap-4",
288+
),
289+
text_area_field(
290+
"What are you looking to build? *",
291+
"Please list any apps, requirements, or data sources you plan on using",
292+
"internal_tools",
293+
True,
294+
),
295+
rx.el.div(
296+
select_field(
297+
"Number of employees?",
298+
"number_of_employees",
299+
["1", "2-5", "6-10", "11-50", "51-100", "101-500", "500+"],
300+
required=True,
301+
),
302+
select_field(
303+
"How did you hear about us?",
304+
"how_did_you_hear_about_us",
305+
[
306+
"Google Search",
307+
"Social Media",
308+
"Word of Mouth",
309+
"Blog",
310+
"Conference",
311+
"Other",
312+
],
313+
required=True,
314+
),
315+
class_name="grid grid-cols-1 md:grid-cols-2 gap-4",
316+
),
317+
select_field(
318+
"How technical are you?",
319+
"technical_level",
320+
["Non-technical", "Neutral", "Technical"],
321+
True,
322+
),
323+
rx.cond(
324+
intro_form_error_message.value,
325+
rx.el.span(
326+
intro_form_error_message.value,
327+
class_name="text-destructive-10 text-sm font-medium px-2 py-1 rounded-md bg-destructive-3 border border-destructive-4",
328+
),
329+
),
330+
ui.button(
331+
"Submit",
332+
type="submit",
333+
class_name="w-full",
334+
),
335+
class_name=ui.cn(
336+
"@container flex flex-col lg:gap-6 gap-2 p-6",
337+
props.pop("class_name", ""),
338+
),
339+
on_submit=on_submit,
340+
**props,
341+
)
342+
return rx.fragment(
343+
form,
344+
)
345+
346+
347+
def intro_form_dialog(
348+
trigger: rx.Component | None = None,
349+
id_prefix: str = "",
350+
on_submit: EventType[()] | None = None,
351+
**props,
352+
) -> rx.Component:
353+
"""Return a intro form dialog container element.
354+
355+
Args:
356+
trigger: The component that triggers the dialog
357+
id_prefix: Optional prefix for all element IDs to ensure uniqueness when multiple dialogs exist
358+
on_submit: Optional event handler called when the form is submitted.
359+
**props: Additional properties to pass to the dialog root
360+
361+
Returns:
362+
A Reflex dialog component containing the intro form
363+
"""
364+
if trigger is None:
365+
trigger = rx.fragment()
366+
class_name = ui.cn("w-auto", props.pop("class_name", ""))
367+
return ui.dialog.root(
368+
ui.dialog.trigger(render_=trigger),
369+
ui.dialog.portal(
370+
ui.dialog.backdrop(),
371+
ui.dialog.popup(
372+
rx.el.div(
373+
rx.el.div(
374+
rx.el.h1(
375+
"Get Started",
376+
class_name="text-xl font-bold text-secondary-12",
377+
),
378+
class_name="flex flex-row justify-between items-center gap-1 px-6 pt-4 -mb-4",
379+
),
380+
intro_form(
381+
id_prefix=id_prefix,
382+
class_name="w-full max-w-md",
383+
on_submit=on_submit,
384+
),
385+
class_name="relative isolate overflow-hidden -m-px w-full max-w-md",
386+
),
387+
class_name="h-fit mt-1 overflow-hidden w-full max-w-md",
388+
),
389+
),
390+
open=intro_form_open_cs.value,
391+
on_open_change_complete=[
392+
rx.call_function(intro_form_error_message.set_value(""))
393+
],
394+
class_name=class_name,
395+
**props,
396+
)

0 commit comments

Comments
 (0)