Skip to content

Commit d9e362a

Browse files
authored
add shared reflex web components (#63)
1 parent dbcc42e commit d9e362a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+9119
-4
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ repos:
55
rev: v0.15.5
66
hooks:
77
- id: ruff-check
8-
files: ^reflex_ui/
8+
files: ^(reflex_ui|shared)/
99
args: ["--fix", "--exit-non-zero-on-fix", "--no-unsafe-fixes"]
1010
- id: ruff-format
11-
files: ^reflex_ui/
11+
files: ^(reflex_ui|shared)/
1212

1313
- repo: https://github.com/codespell-project/codespell
1414
rev: v2.4.2
1515
hooks:
1616
- id: codespell
17-
files: ^reflex_ui/
17+
files: ^(reflex_ui|shared)/
1818

1919
# Run pyi check before pyright because pyright can fail if pyi files are wrong.
2020
# - repo: local
@@ -31,5 +31,5 @@ repos:
3131
rev: v1.1.408
3232
hooks:
3333
- id: pyright
34-
files: ^reflex_ui/
34+
files: ^(reflex_ui|shared)/
3535
language: system

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ lint.pydocstyle.convention = "google"
6969
"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"]
7070
"**/alembic/*.py" = ["D", "ERA"]
7171
"__init__.py" = ["ERA"]
72+
"shared/**" = ["D100", "D101", "D102", "D103", "D104", "T201"]
7273

7374
[tool.pyright]
7475
reportIncompatibleMethodOverride = false

shared/__init__.py

Whitespace-only changes.

shared/backend/__init__.py

Whitespace-only changes.

shared/backend/get_blogs.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import TypedDict
2+
3+
import httpx
4+
import reflex as rx
5+
6+
from shared.constants import RECENT_BLOGS_API_URL
7+
8+
9+
class BlogPostDict(TypedDict):
10+
title: str
11+
description: str
12+
author: str
13+
date: str
14+
image: str
15+
tag: str
16+
url: str
17+
18+
19+
class RecentBlogsState(rx.State):
20+
posts: rx.Field[list[BlogPostDict]] = rx.field(default_factory=list)
21+
_fetched: bool = False
22+
23+
@rx.event(background=True, temporal=True)
24+
async def fetch_recent_blogs(self):
25+
if self._fetched:
26+
return
27+
try:
28+
async with httpx.AsyncClient() as client:
29+
resp = await client.get(RECENT_BLOGS_API_URL, timeout=10)
30+
resp.raise_for_status()
31+
data = resp.json()
32+
async with self:
33+
self.posts = data.get("posts", [])
34+
self._fetched = True
35+
except Exception:
36+
async with self:
37+
self.posts = []

shared/backend/signup.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import contextlib
2+
import os
3+
from datetime import datetime
4+
from typing import Any
5+
6+
import httpx
7+
import reflex as rx
8+
from email_validator import EmailNotValidError, ValidatedEmail, validate_email
9+
from sqlmodel import Field
10+
11+
from shared.constants import (
12+
API_BASE_URL_LOOPS,
13+
REFLEX_DEV_WEB_NEWSLETTER_FORM_WEBHOOK_URL,
14+
)
15+
16+
17+
class Waitlist(rx.Model, table=True):
18+
email: str
19+
date_created: datetime = Field(default_factory=datetime.utcnow, nullable=False)
20+
21+
22+
class IndexState(rx.State):
23+
"""Hold the state for the home page."""
24+
25+
# Whether the user signed up for the newsletter.
26+
signed_up: bool = False
27+
28+
# Whether to show the confetti.
29+
show_confetti: bool = False
30+
31+
@rx.event(background=True)
32+
async def send_contact_to_webhook(
33+
self,
34+
email: str | None,
35+
) -> None:
36+
with contextlib.suppress(httpx.HTTPError):
37+
async with httpx.AsyncClient() as client:
38+
response = await client.post(
39+
REFLEX_DEV_WEB_NEWSLETTER_FORM_WEBHOOK_URL,
40+
json={
41+
"email": email,
42+
},
43+
)
44+
response.raise_for_status()
45+
46+
@rx.event(background=True)
47+
async def add_contact_to_loops(
48+
self,
49+
email: str | None,
50+
):
51+
url: str = f"{API_BASE_URL_LOOPS}/contacts/create"
52+
loops_api_key: str | None = os.getenv("LOOPS_API_KEY")
53+
if loops_api_key is None:
54+
print("Loops API key does not exist")
55+
return
56+
57+
headers = {
58+
"Accept": "application/json",
59+
"Authorization": f"Bearer {loops_api_key}",
60+
}
61+
try:
62+
async with httpx.AsyncClient() as client:
63+
response = await client.post(
64+
url,
65+
headers=headers,
66+
json={
67+
"email": email,
68+
},
69+
)
70+
response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx)
71+
72+
except httpx.HTTPError as e:
73+
print(f"An error occurred: {e}")
74+
75+
@rx.event
76+
def signup_for_another_user(self):
77+
self.signed_up = False
78+
79+
@rx.event(background=True)
80+
async def signup(
81+
self,
82+
form_data: dict[str, Any],
83+
):
84+
"""Sign the user up for the newsletter."""
85+
email: str | None = None
86+
if email_to_validate := form_data.get("input_email"):
87+
try:
88+
validated_email: ValidatedEmail = validate_email(
89+
email_to_validate,
90+
check_deliverability=True,
91+
)
92+
email = validated_email.normalized
93+
94+
except EmailNotValidError as e:
95+
# Alert the error message.
96+
yield rx.toast.warning(
97+
str(e),
98+
style={
99+
"border": "1px solid #3C3646",
100+
"background": "linear-gradient(218deg, #1D1B23 -35.66%, #131217 100.84%)",
101+
},
102+
)
103+
return
104+
yield IndexState.send_contact_to_webhook(email)
105+
yield IndexState.add_contact_to_loops(email)
106+
async with self:
107+
self.signed_up = True
108+
yield
109+
yield [
110+
rx.toast.success("Thanks for signing up to the Newsletter!"),
111+
]

shared/components/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .code import *
2+
from .demo import *
3+
from .headings import *
4+
from .typography import *

shared/components/blocks/code.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Code block components for documentation pages."""
2+
3+
import reflex as rx
4+
5+
import shared.styles.fonts as fonts
6+
from shared import styles
7+
8+
9+
@rx.memo
10+
def code_block(code: str, language: str):
11+
return rx.box(
12+
rx._x.code_block(
13+
code,
14+
language=language,
15+
class_name="code-block",
16+
can_copy=True,
17+
),
18+
class_name="relative mb-4",
19+
)
20+
21+
22+
@rx.memo
23+
def code_block_dark(code: str, language: str):
24+
return rx.box(
25+
rx._x.code_block(
26+
code,
27+
language=language,
28+
class_name="code-block",
29+
can_copy=True,
30+
),
31+
class_name="relative mb-4",
32+
)
33+
34+
35+
def code_block_markdown(*children, **props):
36+
language = props.get("language", "plain")
37+
return code_block(code=children[0], language=language)
38+
39+
40+
def code_block_markdown_dark(*children, **props):
41+
language = props.get("language", "plain")
42+
return code_block_dark(code=children[0], language=language)
43+
44+
45+
def doccmdoutput(
46+
command: str,
47+
output: str,
48+
) -> rx.Component:
49+
"""Create a documentation code snippet.
50+
51+
Args:
52+
command: The command to display.
53+
output: The output of the command.
54+
theme: The theme of the component.
55+
56+
Returns:
57+
The styled command and its example output.
58+
"""
59+
return rx.vstack(
60+
rx._x.code_block(
61+
command,
62+
can_copy=True,
63+
border_radius=styles.DOC_BORDER_RADIUS,
64+
background="transparent",
65+
theme="ayu-dark",
66+
language="bash",
67+
code_tag_props={
68+
"style": {
69+
"fontFamily": "inherit",
70+
}
71+
},
72+
style=fonts.code,
73+
font_family="JetBrains Mono",
74+
width="100%",
75+
),
76+
rx._x.code_block(
77+
output,
78+
can_copy=False,
79+
border_radius="12px",
80+
background="transparent",
81+
theme="ayu-dark",
82+
language="log",
83+
code_tag_props={
84+
"style": {
85+
"fontFamily": "inherit",
86+
}
87+
},
88+
style=fonts.code,
89+
font_family="JetBrains Mono",
90+
width="100%",
91+
),
92+
padding_y="1em",
93+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Collapsible accordion box used by alert and video blocks."""
2+
3+
from collections.abc import Sequence
4+
5+
import reflex as rx
6+
from reflex_core.constants.colors import ColorType
7+
8+
9+
def collapsible_box(
10+
trigger_children: Sequence[rx.Component],
11+
body: rx.Component,
12+
color: ColorType,
13+
*,
14+
item_border_radius: str = "12px",
15+
) -> rx.Component:
16+
"""Collapsible accordion wrapper shared by alert and video directives."""
17+
return rx.box(
18+
rx.accordion.root(
19+
rx.accordion.item(
20+
rx.accordion.header(
21+
rx.accordion.trigger(
22+
rx.hstack(
23+
*trigger_children,
24+
rx.spacer(),
25+
rx.accordion.icon(color=f"{rx.color(color, 11)}"),
26+
align_items="center",
27+
justify_content="left",
28+
text_align="left",
29+
spacing="2",
30+
width="100%",
31+
),
32+
padding="0px",
33+
color=f"{rx.color(color, 11)} !important",
34+
background_color="transparent !important",
35+
border_radius="12px",
36+
_hover={},
37+
),
38+
),
39+
body,
40+
border_radius=item_border_radius,
41+
padding=["16px", "24px"],
42+
background_color=f"{rx.color(color, 3)}",
43+
border="none",
44+
),
45+
background="transparent !important",
46+
border_radius="12px",
47+
box_shadow="none !important",
48+
collapsible=True,
49+
width="100%",
50+
),
51+
border=f"1px solid {rx.color(color, 4)}",
52+
border_radius="12px",
53+
background_color=f"{rx.color(color, 3)} !important",
54+
width="100%",
55+
margin_bottom="16px",
56+
margin_top="16px",
57+
overflow="hidden",
58+
)

0 commit comments

Comments
 (0)