Update base template (#2027)

This commit is contained in:
Nikhil Rao 2023-10-26 17:59:14 -07:00 committed by GitHub
parent f66c6c3361
commit 8133aa10c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 346 additions and 229 deletions

View File

@ -0,0 +1,72 @@
# Welcome to Reflex!
This is the base Reflex template - installed when you run `reflex init`.
If you want to use a different template, pass the `--template` flag to `reflex init`.
For example, if you want a more basic starting point, you can run:
```bash
reflex init --template blank
```
## About this Template
This template has the following directory structure:
```bash
.
├── assets
├── requirements.txt
├── rxconfig.py
└── {your_app}
├── __init__.py
├── components
│ └── sidebar.py
├── pages
│ ├── __init__.py
│ ├── dashboard.py
│ ├── index.py
│ ├── settings.py
│ └── template.py
├── state.py
├── styles.py
└── {your_app}.py
```
See the [Project Structure docs](https://reflex.dev/docs/getting-started/project-structure/) for more information on general Reflex project structure.
### Adding Pages
In this template, the pages in your app are defined in `{your_app}/pages/`.
Each page is a function that returns a Reflex component.
For example, to edit this page you can modify `{your_app}/pages/index.py`.
See the [pages docs](https://reflex.dev/docs/components/pages/) for more information on pages.
In this template, instead of using `rx.add_page` or the `@rx.page` decorator,
we use the `@template` decorator from `{your_app}/pages/template.py`.
To add a new page:
1. Add a new file in `{your_app}/pages/`. We recommend using one file per page, but you can also group pages in a single file.
2. Add a new function with the `@template` decorator, which takes the same arguments as `@rx.page`.
3. Import the page in your `{your_app}/{your_app}.py` file and it will automatically be added to the app.
### Adding Components
In order to keep your code organized, we recommend putting components that are
used across multiple pages in the `{your_app}/components/` directory.
In this template, we have a sidebar component in `{your_app}/components/sidebar.py`.
### Adding State
In this template, we define the base state of the app in `{your_app}/state.py`.
The base state is useful for general app state that is used across multiple pages.
In this template, the base state handles the toggle for the sidebar.
As your app grows, we recommend using [substates](https://reflex.dev/docs/state/substates/)
to organize your state. You can either define substates in their own files, or if the state is
specific to a page, you can define it in the page file itself.

View File

@ -1,101 +1,12 @@
"""Welcome to Reflex! This file outlines the steps to create a basic app."""
from typing import Callable
"""Welcome to Reflex!."""
from code import styles
# Import all the pages.
from code.pages import *
import reflex as rx
from .pages import dashboard_page, home_page, settings_page
from .sidebar import sidebar
from .state import State
from .styles import *
meta = [
{
"name": "viewport",
"content": "width=device-width, shrink-to-fit=no, initial-scale=1",
},
]
def template(main_content: Callable[[], rx.Component]) -> rx.Component:
"""The template for each page of the app.
Args:
main_content (Callable[[], rx.Component]): The main content of the page.
Returns:
rx.Component: The template for each page of the app.
"""
menu_button = rx.box(
rx.menu(
rx.menu_button(
rx.icon(
tag="hamburger",
size="4em",
color=text_color,
),
),
rx.menu_list(
rx.menu_item(rx.link("Home", href="/", width="100%")),
rx.menu_divider(),
rx.menu_item(
rx.link("About", href="https://github.com/reflex-dev", width="100%")
),
rx.menu_item(
rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%")
),
),
),
position="fixed",
right="1.5em",
top="1.5em",
z_index="500",
)
return rx.hstack(
sidebar(),
main_content(),
rx.spacer(),
menu_button,
align_items="flex-start",
transition="left 0.5s, width 0.5s",
position="relative",
left=rx.cond(State.sidebar_displayed, "0px", f"-{sidebar_width}"),
)
@rx.page("/", meta=meta)
@template
def home() -> rx.Component:
"""Home page.
Returns:
rx.Component: The home page.
"""
return home_page()
@rx.page("/settings", meta=meta)
@template
def settings() -> rx.Component:
"""Settings page.
Returns:
rx.Component: The settings page.
"""
return settings_page()
@rx.page("/dashboard", meta=meta)
@template
def dashboard() -> rx.Component:
"""Dashboard page.
Returns:
rx.Component: The dashboard page.
"""
return dashboard_page()
# Add state and page to the app.
app = rx.App(style=base_style)
# Create the app and compile it.
app = rx.App(style=styles.base_style)
app.compile()

View File

@ -1,23 +1,25 @@
"""Sidebar component for the app."""
import reflex as rx
from code import styles
from code.state import State
from .state import State
from .styles import *
import reflex as rx
def sidebar_header() -> rx.Component:
"""Sidebar header.
Returns:
rx.Component: The sidebar header component.
The sidebar header component.
"""
return rx.hstack(
# The logo.
rx.image(
src="/icon.svg",
height="2em",
),
rx.spacer(),
# Link to Reflex GitHub repo.
rx.link(
rx.center(
rx.image(
@ -25,17 +27,17 @@ def sidebar_header() -> rx.Component:
height="3em",
padding="0.5em",
),
box_shadow=box_shadow,
box_shadow=styles.box_shadow,
bg="transparent",
border_radius=border_radius,
border_radius=styles.border_radius,
_hover={
"bg": accent_color,
"bg": styles.accent_color,
},
),
href="https://github.com/reflex-dev/reflex",
),
width="100%",
border_bottom=border,
border_bottom=styles.border,
padding="1em",
)
@ -44,7 +46,7 @@ def sidebar_footer() -> rx.Component:
"""Sidebar footer.
Returns:
rx.Component: The sidebar footer component.
The sidebar footer component.
"""
return rx.hstack(
rx.link(
@ -55,15 +57,15 @@ def sidebar_footer() -> rx.Component:
padding="0.5em",
),
bg="transparent",
border_radius=border_radius,
**hover_accent_bg,
border_radius=styles.border_radius,
**styles.hover_accent_bg,
),
on_click=State.toggle_sidebar_displayed,
transform=rx.cond(~State.sidebar_displayed, "rotate(180deg)", ""),
transition="transform 0.5s, left 0.5s",
position="relative",
left=rx.cond(State.sidebar_displayed, "0px", "20.5em"),
**overlapping_button_style,
**styles.overlapping_button_style,
),
rx.spacer(),
rx.link(
@ -79,7 +81,7 @@ def sidebar_footer() -> rx.Component:
href="https://reflex.dev/blog/",
),
width="100%",
border_top=border,
border_top=styles.border,
padding="1em",
)
@ -88,13 +90,18 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
"""Sidebar item.
Args:
text (str): The text of the item.
icon (str): The icon of the item.
url (str): The URL of the item.
text: The text of the item.
icon: The icon of the item.
url: The URL of the item.
Returns:
rx.Component: The sidebar item component.
"""
# Whether the item is active.
active = (State.router.page.path == f"/{text.lower()}") | (
(State.router.page.path == "/") & text == "Home"
)
return rx.link(
rx.hstack(
rx.image(
@ -106,17 +113,17 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
text,
),
bg=rx.cond(
State.origin_url == f"/{text.lower()}/",
accent_color,
active,
styles.accent_color,
"transparent",
),
color=rx.cond(
State.origin_url == f"/{text.lower()}/",
accent_text_color,
text_color,
active,
styles.accent_text_color,
styles.text_color,
),
border_radius=border_radius,
box_shadow=box_shadow,
border_radius=styles.border_radius,
box_shadow=styles.box_shadow,
width="100%",
padding_x="1em",
),
@ -126,25 +133,26 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
def sidebar() -> rx.Component:
"""Sidebar.
"""The sidebar.
Returns:
rx.Component: The sidebar component.
The sidebar component.
"""
# Get all the decorated pages and add them to the sidebar.
from reflex.page import get_decorated_pages
return rx.box(
rx.vstack(
sidebar_header(),
rx.vstack(
sidebar_item(
"Dashboard",
"/github.svg",
"/dashboard",
),
sidebar_item(
"Settings",
"/github.svg",
"/settings",
),
*[
sidebar_item(
text=page.get("title", page["route"].strip("/").capitalize()),
icon=page.get("image", "/github.svg"),
url=page["route"],
)
for page in get_decorated_pages()
],
width="100%",
overflow_y="auto",
align_items="flex-start",
@ -154,9 +162,9 @@ def sidebar() -> rx.Component:
sidebar_footer(),
height="100dvh",
),
min_width=sidebar_width,
min_width=styles.sidebar_width,
height="100%",
position="sticky",
top="0px",
border_right=border,
border_right=styles.border,
)

View File

@ -1,4 +1,3 @@
"""The pages of the app."""
from .dashboard import dashboard_page
from .home import home_page
from .settings import settings_page
from .dashboard import dashboard
from .index import index
from .settings import settings

View File

@ -1,28 +1,21 @@
"""The dashboard page for the template."""
"""The dashboard page."""
from code.templates import template
import reflex as rx
from ..styles import *
def dashboard_page() -> rx.Component:
"""The UI for the dashboard page.
@template(route="/dashboard", title="Dashboard")
def dashboard() -> rx.Component:
"""The dashboard page.
Returns:
rx.Component: The UI for the dashboard page.
The UI for the dashboard page.
"""
return rx.box(
rx.vstack(
rx.heading(
"Dashboard",
font_size="3em",
),
rx.text(
"Welcome to Reflex!",
),
rx.text(
"You can use this template to get started with Reflex.",
),
style=template_content_style,
return rx.vstack(
rx.heading("Dashboard", font_size="3em"),
rx.text("Welcome to Reflex!"),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/dashboard.py"),
),
style=template_page_style,
)

View File

@ -1,28 +0,0 @@
"""The home page of the app."""
import reflex as rx
from ..styles import *
def home_page() -> rx.Component:
"""The UI for the home page.
Returns:
rx.Component: The UI for the home page.
"""
return rx.box(
rx.vstack(
rx.heading(
"Home",
font_size="3em",
),
rx.text(
"Welcome to Reflex!",
),
rx.text(
"You can use this template to get started with Reflex.",
),
style=template_content_style,
),
style=template_page_style,
)

View File

@ -0,0 +1,18 @@
"""The home page of the app."""
from code import styles
from code.templates import template
import reflex as rx
@template(route="/", title="Home", image="/logo.svg")
def index() -> rx.Component:
"""The home page.
Returns:
The UI for the home page.
"""
with open("README.md") as readme:
content = readme.read()
return rx.markdown(content, component_map=styles.markdown_style)

View File

@ -1,28 +1,22 @@
"""The settings page for the template."""
"""The settings page."""
from code.templates import template
import reflex as rx
from ..styles import *
def settings_page() -> rx.Component:
"""The UI for the settings page.
@template(route="/settings", title="Settings")
def settings() -> rx.Component:
"""The settings page.
Returns:
rx.Component: The UI for the settings page.
The UI for the settings page.
"""
return rx.box(
rx.vstack(
rx.heading(
"Settings",
font_size="3em",
),
rx.text(
"Welcome to Reflex!",
),
rx.text(
"You can use this template to get started with Reflex.",
),
style=template_content_style,
return rx.vstack(
rx.heading("Settings", font_size="3em"),
rx.text("Welcome to Reflex!"),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/settings.py"),
),
style=template_page_style,
)

View File

@ -6,17 +6,9 @@ import reflex as rx
class State(rx.State):
"""State for the app."""
# Whether the sidebar is displayed.
sidebar_displayed: bool = True
@rx.var
def origin_url(self) -> str:
"""Get the url of the current page.
Returns:
str: The url of the current page.
"""
return self.router_data.get("asPath", "")
def toggle_sidebar_displayed(self) -> None:
"""Toggle the sidebar displayed."""
self.sidebar_displayed = not self.sidebar_displayed

View File

@ -1,7 +1,7 @@
"""Styles for the app."""
import reflex as rx
from code.state import State
from .state import State
import reflex as rx
border_radius = "0.375rem"
box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
@ -53,3 +53,20 @@ base_style = {
},
rx.MenuItem: hover_accent_bg,
}
markdown_style = {
"code": lambda text: rx.code(text, color="#1F1944", bg="#EAE4FD"),
"a": lambda text, **props: rx.link(
text,
**props,
font_weight="bold",
color="#03030B",
text_decoration="underline",
text_decoration_color="#AD9BF8",
_hover={
"color": "#AD9BF8",
"text_decoration": "underline",
"text_decoration_color": "#03030B",
},
),
}

View File

@ -0,0 +1 @@
from .template import template

View File

@ -0,0 +1,99 @@
"""Common templates used between pages in the app."""
from code import styles
from code.components.sidebar import sidebar
from code.state import State
from typing import Callable
import reflex as rx
# Meta tags for the app.
default_meta = [
{
"name": "viewport",
"content": "width=device-width, shrink-to-fit=no, initial-scale=1",
},
]
def menu_button() -> rx.Component:
"""The menu button on the top right of the page.
Returns:
The menu button component.
"""
return rx.box(
rx.menu(
rx.menu_button(
rx.icon(
tag="hamburger",
size="4em",
color=styles.text_color,
),
),
rx.menu_list(
rx.menu_item(rx.link("Home", href="/", width="100%")),
rx.menu_divider(),
rx.menu_item(
rx.link("About", href="https://github.com/reflex-dev", width="100%")
),
rx.menu_item(
rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%")
),
),
),
position="fixed",
right="1.5em",
top="1.5em",
z_index="500",
)
def template(
**page_kwargs: dict,
) -> Callable[[Callable[[], rx.Component]], rx.Component]:
"""The template for each page of the app.
Args:
page_kwargs: Keyword arguments to pass to the page.
Returns:
The template with the page content.
"""
def decorator(page_content: Callable[[], rx.Component]) -> rx.Component:
"""The template for each page of the app.
Args:
page_content: The content of the page.
Returns:
The template with the page content.
"""
# Get the meta tags for the page.
page_kwargs["meta"] = [*default_meta, *page_kwargs.get("meta", [])]
@rx.page(**page_kwargs)
def templated_page():
return rx.hstack(
sidebar(),
rx.box(
rx.box(
page_content(),
**styles.template_content_style,
),
**styles.template_page_style,
),
rx.spacer(),
menu_button(),
align_items="flex-start",
transition="left 0.5s, width 0.5s",
position="relative",
left=rx.cond(
State.sidebar_displayed, "0px", f"-{styles.sidebar_width}"
),
)
return templated_page
return decorator

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -62,7 +62,7 @@ def get_base_component_map() -> dict[str, Callable]:
"p": lambda value: Text.create(value, margin_y="1em"),
"ul": lambda value: UnorderedList.create(value, margin_y="1em"), # type: ignore
"ol": lambda value: OrderedList.create(value, margin_y="1em"), # type: ignore
"li": lambda value: ListItem.create(value),
"li": lambda value: ListItem.create(value, margin_y="0.5em"),
"a": lambda value: Link.create(value),
"code": lambda value: Code.create(value),
"codeblock": lambda *_, **props: CodeBlock.create(

View File

@ -89,6 +89,8 @@ class Templates(SimpleNamespace):
WEB_TEMPLATE = os.path.join(BASE, "web")
# The jinja template directory.
JINJA_TEMPLATE = os.path.join(BASE, "jinja")
# Where the code for the templates is stored.
CODE = "code"
class Next(SimpleNamespace):

View File

@ -61,3 +61,15 @@ def page(
return render_fn
return decorator
def get_decorated_pages() -> list[dict]:
"""Get the decorated pages.
Returns:
The decorated pages.
"""
return sorted(
[page_data for render_fn, page_data in DECORATED_PAGES],
key=lambda x: x["route"],
)

View File

@ -71,7 +71,7 @@ def init(
None, metavar="APP_NAME", help="The name of the app to initialize."
),
template: constants.Templates.Kind = typer.Option(
constants.Templates.Kind.DEFAULT.value,
constants.Templates.Kind.BASE.value,
help="The template to initialize the app with.",
),
loglevel: constants.LogLevel = typer.Option(

View File

@ -154,7 +154,7 @@ class AppHarness:
with chdir(self.app_path):
reflex.reflex.init(
name=self.app_name,
template=reflex.constants.Templates.Kind.DEFAULT,
template=reflex.constants.Templates.Kind.BLANK,
loglevel=reflex.constants.LogLevel.INFO,
)
self.app_module_path.write_text(source_code)

View File

@ -34,7 +34,7 @@ def detect_package_change(json_file_path: str) -> str:
"""Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string.
Args:
json_file_path (str): The path to the JSON file to be hashed.
json_file_path: The path to the JSON file to be hashed.
Returns:
str: The SHA-256 hash of the JSON file as a hexadecimal string.

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import json
import os
import re
import shutil
from pathlib import Path
@ -173,3 +174,21 @@ def update_json_file(file_path: str, update_dict: dict[str, int | str]):
# Write the updated json object to the file
with open(fp, "w") as f:
json.dump(json_object, f, ensure_ascii=False)
def find_replace(directory: str, find: str, replace: str):
"""Recursively find and replace text in files in a directory.
Args:
directory: The directory to search.
find: The text to find.
replace: The text to replace.
"""
for root, _dirs, files in os.walk(directory):
for file in files:
filepath = os.path.join(root, file)
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
text = re.sub(find, replace, text)
with open(filepath, "w") as f:
f.write(text)

View File

@ -249,17 +249,25 @@ def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
template: The template to use.
"""
console.log("Initializing the app directory.")
path_ops.cp(
os.path.join(constants.Templates.Dirs.BASE, "apps", template.value, "code"),
app_name,
)
# Copy the template to the current directory.
template_dir = os.path.join(constants.Templates.Dirs.BASE, "apps", template.value)
for file in os.listdir(template_dir):
# Copy the file but keep the name the same.
path_ops.cp(os.path.join(template_dir, file), file)
# Rename the template app to the app name.
path_ops.mv(constants.Templates.Dirs.CODE, app_name)
path_ops.mv(
os.path.join(app_name, template.value + ".py"),
os.path.join(app_name, template.value + constants.Ext.PY),
os.path.join(app_name, app_name + constants.Ext.PY),
)
path_ops.cp(
os.path.join(constants.Templates.Dirs.BASE, "apps", template.value, "assets"),
constants.Dirs.APP_ASSETS,
# Fix up the imports.
path_ops.find_replace(
app_name,
f"from {constants.Templates.Dirs.CODE}",
f"from {app_name}",
)