Peter Oertel 2 éve
commit
4828b1bac8

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+*.db
+*.py[cod]
+.web
+/venv
+__pycache__/

+ 69 - 0
README.md

@@ -0,0 +1,69 @@
+# 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
+├── README.md
+├── assets
+├── rxconfig.py
+└── {your_app}
+    ├── __init__.py
+    ├── components
+    │   ├── __init__.py
+    │   └── sidebar.py
+    ├── pages
+    │   ├── __init__.py
+    │   ├── dashboard.py
+    │   ├── index.py
+    │   └── settings.py
+    ├── styles.py
+    ├── templates
+    │   ├── __init__.py
+    │   └── template.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/pages/routes/) 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}/templates/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}/pages/__init__.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
+
+As your app grows, we recommend using [substates](https://reflex.dev/docs/substates/overview/)
+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.

BIN
assets/favicon.ico


+ 10 - 0
assets/github.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Github" clip-path="url(#clip0_469_1929)">
+<path id="Vector" d="M8.0004 0.587524C3.80139 0.587524 0.400391 3.98851 0.400391 8.1875C0.400391 11.5505 2.57589 14.391 5.59689 15.398C5.97689 15.4645 6.11939 15.2365 6.11939 15.037C6.11939 14.8565 6.10989 14.258 6.10989 13.6215C4.20039 13.973 3.70639 13.156 3.55439 12.7285C3.46889 12.51 3.09839 11.8355 2.77539 11.655C2.50939 11.5125 2.12939 11.161 2.76589 11.1515C3.36439 11.142 3.79189 11.7025 3.93439 11.9305C4.61839 13.08 5.71089 12.757 6.14789 12.5575C6.21439 12.0635 6.41388 11.731 6.6324 11.541C4.94139 11.351 3.17439 10.6955 3.17439 7.7885C3.17439 6.962 3.46889 6.27801 3.95339 5.74601C3.87739 5.55601 3.61139 4.77701 4.02939 3.73201C4.02939 3.73201 4.66589 3.53251 6.11939 4.51101C6.7274 4.34001 7.3734 4.25451 8.0194 4.25451C8.6654 4.25451 9.3114 4.34001 9.9194 4.51101C11.3729 3.52301 12.0094 3.73201 12.0094 3.73201C12.4274 4.77701 12.1614 5.55601 12.0854 5.74601C12.5699 6.27801 12.8644 6.9525 12.8644 7.7885C12.8644 10.705 11.0879 11.351 9.3969 11.541C9.6724 11.7785 9.9099 12.2345 9.9099 12.947C9.9099 13.9635 9.9004 14.7805 9.9004 15.037C9.9004 15.2365 10.0429 15.474 10.4229 15.398C13.5165 14.3536 15.5996 11.4527 15.6004 8.1875C15.6004 3.98851 12.1994 0.587524 8.0004 0.587524Z" fill="#494369"/>
+</g>
+<defs>
+<clipPath id="clip0_469_1929">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 68 - 0
assets/logo.svg

@@ -0,0 +1,68 @@
+<svg width="80" height="78" viewBox="0 0 80 78" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_ddddi_449_2821)">
+<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" fill="url(#paint0_radial_449_2821)"/>
+<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" fill="url(#paint1_radial_449_2821)"/>
+<g filter="url(#filter1_i_449_2821)">
+<path d="M31 37.5C30.4477 37.5 30 37.0523 30 36.5V13.5001C30 12.9478 30.4477 12.5001 31 12.5001H49C49.5523 12.5001 50 12.9478 50 13.5001V21.5001C50 22.0524 49.5523 22.5001 49 22.5001H45V18.5001C45 17.9478 44.5523 17.5001 44 17.5001H36C35.4477 17.5001 35 17.9478 35 18.5001V21.5001C35 22.0524 35.4477 22.5001 36 22.5001H45V27.5001H36C35.4477 27.5001 35 27.9478 35 28.5001V36.5C35 37.0523 34.5523 37.5 34 37.5H31ZM46 37.5C45.4477 37.5 45 37.0523 45 36.5V27.5001H49C49.5523 27.5001 50 27.9478 50 28.5001V36.5C50 37.0523 49.5523 37.5 49 37.5H46Z" fill="url(#paint2_radial_449_2821)"/>
+</g>
+<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" stroke="#20117E" stroke-opacity="0.04"/>
+</g>
+<defs>
+<filter id="filter0_ddddi_449_2821" x="0.5" y="0.5" width="79" height="77" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_449_2821"/>
+<feOffset dy="10"/>
+<feGaussianBlur stdDeviation="8"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.0784314 0 0 0 0 0.0705882 0 0 0 0 0.231373 0 0 0 0.06 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_449_2821"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="6" operator="erode" in="SourceAlpha" result="effect2_dropShadow_449_2821"/>
+<feOffset dy="12"/>
+<feGaussianBlur stdDeviation="3"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.0784314 0 0 0 0 0.0705882 0 0 0 0 0.231373 0 0 0 0.1 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow_449_2821" result="effect2_dropShadow_449_2821"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect3_dropShadow_449_2821"/>
+<feOffset dy="10"/>
+<feGaussianBlur stdDeviation="3"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.16 0"/>
+<feBlend mode="normal" in2="effect2_dropShadow_449_2821" result="effect3_dropShadow_449_2821"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feMorphology radius="1" operator="dilate" in="SourceAlpha" result="effect4_dropShadow_449_2821"/>
+<feOffset dy="2"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="effect3_dropShadow_449_2821" result="effect4_dropShadow_449_2821"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_449_2821" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="-8"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.607843 0 0 0 0 0.972549 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="shape" result="effect5_innerShadow_449_2821"/>
+</filter>
+<filter id="filter1_i_449_2821" x="30" y="12.5001" width="20" height="26.9999" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="2"/>
+<feGaussianBlur stdDeviation="1.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.32 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_449_2821"/>
+</filter>
+<radialGradient id="paint0_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 3) rotate(90) scale(52 54)">
+<stop stop-color="white" stop-opacity="0.9"/>
+<stop offset="1" stop-color="#4E3DB9" stop-opacity="0.24"/>
+</radialGradient>
+<radialGradient id="paint1_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 3) rotate(90) scale(52 54)">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#F7F7F7"/>
+</radialGradient>
+<radialGradient id="paint2_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 12.5001) rotate(90) scale(24.9999 20)">
+<stop stop-color="#F5F3FF"/>
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#E1DDF4"/>
+</radialGradient>
+</defs>
+</svg>

+ 13 - 0
assets/paneleft.svg

@@ -0,0 +1,13 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="PaneLeft" clip-path="url(#clip0_469_1942)">
+<g id="Vector">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80217 0.525009C7.34654 0.525009 6.97717 0.894373 6.97717 1.35001V10.65C6.97717 11.1056 7.34654 11.475 7.80217 11.475H10.6522C11.1078 11.475 11.4772 11.1056 11.4772 10.65V1.35001C11.4772 0.894373 11.1078 0.525009 10.6522 0.525009H7.80217ZM8.02717 10.425V1.57501H10.4272V10.425H8.02717Z" fill="#494369"/>
+<path d="M3.78215 8.14502L2.16213 6.525H5.92717V5.475H2.16213L3.78215 3.85498L3.03969 3.11252L0.523438 5.62877V6.37123L3.03969 8.88748L3.78215 8.14502Z" fill="#494369"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_469_1942">
+<rect width="12" height="12" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 37 - 0
assets/reflex_black.svg

@@ -0,0 +1,37 @@
+<svg width="67" height="14" viewBox="0 0 67 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="67" height="14" fill="#1E1E1E"/>
+<g id="Nav Template &#62; Initial" clip-path="url(#clip0_0_1)">
+<rect width="1440" height="1024" transform="translate(-16 -17)" fill="white"/>
+<g id="Sidebar">
+<g clip-path="url(#clip1_0_1)">
+<path d="M-16 -17H264V1007H-16V-17Z" fill="white"/>
+<g id="Header">
+<path d="M-16 -17H264V31H-16V-17Z" fill="white"/>
+<g id="Button">
+<rect x="-4" y="-3" width="74.316" height="20" rx="6" fill="white"/>
+<g id="Logo">
+<g id="Reflex">
+<path d="M0 13.6316V0.368408H10.6106V5.67369H7.95792V3.02105H2.65264V5.67369H7.95792V8.32633H2.65264V13.6316H0ZM7.95792 13.6316V8.32633H10.6106V13.6316H7.95792Z" fill="#110F1F"/>
+<path d="M13.2632 13.6316V0.368408H21.2211V3.02105H15.9158V5.67369H21.2211V8.32633H15.9158V10.979H21.2211V13.6316H13.2632Z" fill="#110F1F"/>
+<path d="M23.8738 13.6316V0.368408H31.8317V3.02105H26.5264V5.67369H31.8317V8.32633H26.5264V13.6316H23.8738Z" fill="#110F1F"/>
+<path d="M34.4843 13.6316V0.368408H37.137V10.979H42.4422V13.6316H34.4843Z" fill="#110F1F"/>
+<path d="M45.0949 13.6316V0.368408H53.0528V3.02105H47.7475V5.67369H53.0528V8.32633H47.7475V10.979H53.0528V13.6316H45.0949Z" fill="#110F1F"/>
+<path d="M55.7054 5.67369V0.368408H58.3581V5.67369H55.7054ZM63.6634 5.67369V0.368408H66.316V5.67369H63.6634ZM58.3581 8.32633V5.67369H63.6634V8.32633H58.3581ZM55.7054 13.6316V8.32633H58.3581V13.6316H55.7054ZM63.6634 13.6316V8.32633H66.316V13.6316H63.6634Z" fill="#110F1F"/>
+</g>
+</g>
+</g>
+<path d="M264 30.5H-16V31.5H264V30.5Z" fill="#F4F3F6"/>
+</g>
+</g>
+<path d="M263.5 -17V1007H264.5V-17H263.5Z" fill="#F4F3F6"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_0_1">
+<rect width="1440" height="1024" fill="white" transform="translate(-16 -17)"/>
+</clipPath>
+<clipPath id="clip1_0_1">
+<path d="M-16 -17H264V1007H-16V-17Z" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 8 - 0
assets/reflex_white.svg

@@ -0,0 +1,8 @@
+<svg width="56" height="12" viewBox="0 0 56 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z" fill="white"/>
+<path d="M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z" fill="white"/>
+<path d="M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z" fill="white"/>
+<path d="M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z" fill="white"/>
+<path d="M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z" fill="white"/>
+<path d="M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z" fill="white"/>
+</svg>

+ 1 - 0
boppersbizza/__init__.py

@@ -0,0 +1 @@
+"""Base template for Reflex."""

+ 16 - 0
boppersbizza/boppersbizza.py

@@ -0,0 +1,16 @@
+"""Welcome to Reflex!."""
+
+from boppersbizza import styles
+
+# Import all the pages.
+from boppersbizza.pages import *
+
+import reflex as rx
+
+
+class State(rx.State):
+    """Define empty state to allow access to rx.State.router."""
+
+
+# Create the app.
+app = rx.App(style=styles.base_style)

+ 0 - 0
boppersbizza/components/__init__.py


+ 152 - 0
boppersbizza/components/sidebar.py

@@ -0,0 +1,152 @@
+"""Sidebar component for the app."""
+
+from boppersbizza import styles
+
+import reflex as rx
+
+
+def sidebar_header() -> rx.Component:
+    """Sidebar header.
+
+    Returns:
+        The sidebar header component.
+    """
+    return rx.hstack(
+        # The logo.
+        rx.color_mode_cond(
+            rx.image(src="/reflex_black.svg", height="2em"),
+            rx.image(src="/reflex_white.svg", height="2em"),
+        ),
+        rx.spacer(),
+        # Link to Reflex GitHub repo.
+        rx.link(
+            rx.center(
+                rx.image(
+                    src="/github.svg",
+                    height="3em",
+                    padding="0.5em",
+                ),
+                box_shadow=styles.box_shadow,
+                bg="transparent",
+                border_radius=styles.border_radius,
+                _hover={
+                    "bg": styles.accent_color,
+                },
+            ),
+            href="https://github.com/reflex-dev/reflex",
+        ),
+        align="center",
+        width="100%",
+        border_bottom=styles.border,
+        padding="1em",
+    )
+
+
+def sidebar_footer() -> rx.Component:
+    """Sidebar footer.
+
+    Returns:
+        The sidebar footer component.
+    """
+    return rx.hstack(
+        rx.spacer(),
+        rx.link(
+            rx.text("Docs"),
+            href="https://reflex.dev/docs/getting-started/introduction/",
+            style=styles.link_style,
+        ),
+        rx.link(
+            rx.text("Blog"),
+            href="https://reflex.dev/blog/",
+            style=styles.link_style,
+        ),
+        width="100%",
+        border_top=styles.border,
+        padding="1em",
+    )
+
+
+def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
+    """Sidebar item.
+
+    Args:
+        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 = (rx.State.router.page.path == f"/{text.lower()}") | (
+        (rx.State.router.page.path == "/") & text == "Home"
+    )
+
+    return rx.link(
+        rx.hstack(
+            rx.image(
+                src=icon,
+                height="2.5em",
+                padding="0.5em",
+            ),
+            rx.text(
+                text,
+            ),
+            bg=rx.cond(
+                active,
+                styles.accent_color,
+                "transparent",
+            ),
+            color=rx.cond(
+                active,
+                styles.accent_text_color,
+                styles.text_color,
+            ),
+            align="center",
+            border_radius=styles.border_radius,
+            box_shadow=styles.box_shadow,
+            width="100%",
+            padding_x="1em",
+        ),
+        href=url,
+        width="100%",
+    )
+
+
+def sidebar() -> rx.Component:
+    """The sidebar.
+
+    Returns:
+        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(
+                        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",
+                padding="1em",
+            ),
+            rx.spacer(),
+            sidebar_footer(),
+            height="100dvh",
+        ),
+        display=["none", "none", "block"],
+        min_width=styles.sidebar_width,
+        height="100%",
+        position="sticky",
+        top="0px",
+        border_right=styles.border,
+    )

+ 4 - 0
boppersbizza/pages/__init__.py

@@ -0,0 +1,4 @@
+from .dashboard import dashboard
+from .index import index
+from .settings import settings
+from .testing import testing

+ 22 - 0
boppersbizza/pages/dashboard.py

@@ -0,0 +1,22 @@
+"""The dashboard page."""
+
+from boppersbizza.templates import template
+
+import reflex as rx
+
+
+@template(route="/dashboard", title="Dashboard")
+def dashboard() -> rx.Component:
+    """The dashboard page.
+
+    Returns:
+        The UI for the dashboard page.
+    """
+    return rx.vstack(
+        rx.heading("Dashboard", size="8"),
+        rx.text("Welcome to Reflex!"),
+        rx.text(
+            "You can edit this page in ",
+            rx.code("{your_app}/pages/dashboard.py"),
+        ),
+    )

+ 18 - 0
boppersbizza/pages/index.py

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

+ 61 - 0
boppersbizza/pages/settings.py

@@ -0,0 +1,61 @@
+"""The settings page."""
+
+from boppersbizza.templates import ThemeState, template
+
+import reflex as rx
+
+
+@template(route="/settings", title="Settings")
+def settings() -> rx.Component:
+    """The settings page.
+
+    Returns:
+        The UI for the settings page.
+    """
+    return rx.vstack(
+        rx.heading("Settings", size="8"),
+        rx.hstack(
+            rx.text("Dark mode: "),
+            rx.color_mode.switch(),
+        ),
+        rx.hstack(
+            rx.text("Theme color: "),
+            rx.select(
+                [
+                    "tomato",
+                    "red",
+                    "ruby",
+                    "crimson",
+                    "pink",
+                    "plum",
+                    "purple",
+                    "violet",
+                    "iris",
+                    "indigo",
+                    "blue",
+                    "cyan",
+                    "teal",
+                    "jade",
+                    "green",
+                    "grass",
+                    "brown",
+                    "orange",
+                    "sky",
+                    "mint",
+                    "lime",
+                    "yellow",
+                    "amber",
+                    "gold",
+                    "bronze",
+                    "gray",
+                ],
+                value=ThemeState.accent_color,
+                on_change=ThemeState.set_accent_color,
+            ),
+        ),
+        rx.text(
+            "You can edit this page in ",
+            rx.code("{your_app}/pages/settings.py"),
+            size="1",
+        ),
+    )

+ 32 - 0
boppersbizza/pages/testing.py

@@ -0,0 +1,32 @@
+from boppersbizza import styles
+from boppersbizza.templates import template
+
+import reflex as rx
+
+
+class TestingState(rx.State):
+    msg_list: list
+    message: str
+
+    def new_msg(self):
+        self.msg_list.append(self.message)
+        self.message = ""
+
+
+def msg_box(msg) -> rx.Component:
+    return rx.box(msg)
+
+
+@template(route="/testing", title="Home", image="/github.svg")
+def testing() -> rx.Component:
+    return rx.container(
+        rx.foreach(TestingState.msg_list, lambda msg: msg_box(msg)),
+        rx.hstack(
+            rx.input(
+                placeholder="Test Message",
+                value=TestingState.message,
+                on_change=TestingState.set_message,
+            ),
+            rx.button("Send", on_click=TestingState.new_msg),
+        ),
+    )

+ 60 - 0
boppersbizza/styles.py

@@ -0,0 +1,60 @@
+"""Styles for the app."""
+
+import reflex as rx
+
+border_radius = "0.375rem"
+box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
+border = f"1px solid {rx.color('accent', 12)}"
+text_color = rx.color("gray", 11)
+accent_text_color = rx.color("accent", 10)
+accent_color = rx.color("accent", 1)
+hover_accent_color = {"_hover": {"color": accent_text_color}}
+hover_accent_bg = {"_hover": {"background_color": accent_color}}
+content_width_vw = "90vw"
+sidebar_width = "20em"
+
+template_page_style = {"padding_top": "5em", "padding_x": ["auto", "2em"], "flex": "1"}
+
+template_content_style = {
+    "align_items": "flex-start",
+    "box_shadow": box_shadow,
+    "border_radius": border_radius,
+    "padding": "1em",
+    "margin_bottom": "2em",
+}
+
+link_style = {
+    "color": accent_text_color,
+    "text_decoration": "none",
+    **hover_accent_color,
+}
+
+overlapping_button_style = {
+    "background_color": "white",
+    "border": border,
+    "border_radius": border_radius,
+}
+
+base_style = {
+    rx.menu.trigger: {
+        **overlapping_button_style,
+    },
+    rx.menu.item: hover_accent_bg,
+}
+
+markdown_style = {
+    "code": lambda text: rx.code(text, color=accent_text_color, bg=accent_color),
+    "a": lambda text, **props: rx.link(
+        text,
+        **props,
+        font_weight="bold",
+        color=accent_text_color,
+        text_decoration="underline",
+        text_decoration_color=accent_text_color,
+        _hover={
+            "color": accent_color,
+            "text_decoration": "underline",
+            "text_decoration_color": accent_color,
+        },
+    ),
+}

+ 1 - 0
boppersbizza/templates/__init__.py

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

+ 145 - 0
boppersbizza/templates/template.py

@@ -0,0 +1,145 @@
+"""Common templates used between pages in the app."""
+
+from __future__ import annotations
+
+from boppersbizza import styles
+from boppersbizza.components.sidebar import sidebar
+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_item_link(text, href):
+    return rx.menu.item(
+        rx.link(
+            text,
+            href=href,
+            width="100%",
+            color="inherit",
+        ),
+        _hover={
+            "color": styles.accent_color,
+            "background_color": styles.accent_text_color,
+        },
+    )
+
+
+def menu_button() -> rx.Component:
+    """The menu button on the top right of the page.
+
+    Returns:
+        The menu button component.
+    """
+    from reflex.page import get_decorated_pages
+
+    return rx.box(
+        rx.menu.root(
+            rx.menu.trigger(
+                rx.icon(
+                    "menu",
+                    size=36,
+                    color=styles.accent_text_color,
+                ),
+                background_color=styles.accent_color,
+            ),
+            rx.menu.content(
+                *[
+                    menu_item_link(page["title"], page["route"])
+                    for page in get_decorated_pages()
+                ],
+                rx.menu.separator(),
+                menu_item_link("About", "https://github.com/reflex-dev"),
+                menu_item_link("Contact", "mailto:founders@=reflex.dev"),
+            ),
+        ),
+        position="fixed",
+        right="1.5em",
+        top="1.5em",
+        z_index="500",
+    )
+
+
+class ThemeState(rx.State):
+    """The state for the theme of the app."""
+
+    accent_color: str = "crimson"
+
+
+def template(
+    route: str | None = None,
+    title: str | None = None,
+    image: str | None = None,
+    description: str | None = None,
+    meta: str | None = None,
+    script_tags: list[rx.Component] | None = None,
+    on_load: rx.event.EventHandler | list[rx.event.EventHandler] | None = None,
+) -> Callable[[Callable[[], rx.Component]], rx.Component]:
+    """The template for each page of the app.
+
+    Args:
+        route: The route to reach the page.
+        title: The title of the page.
+        image: The favicon of the page.
+        description: The description of the page.
+        meta: Additionnal meta to add to the page.
+        on_load: The event handler(s) called when the page load.
+        script_tags: Scripts to attach 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.
+        all_meta = [*default_meta, *(meta or [])]
+
+        def templated_page():
+            return rx.hstack(
+                sidebar(),
+                rx.box(
+                    rx.box(
+                        page_content(),
+                        **styles.template_content_style,
+                    ),
+                    **styles.template_page_style,
+                ),
+                menu_button(),
+                align="start",
+                transition="left 0.5s, width 0.5s",
+                position="relative",
+            )
+
+        @rx.page(
+            route=route,
+            title=title,
+            image=image,
+            description=description,
+            meta=all_meta,
+            script_tags=script_tags,
+            on_load=on_load,
+        )
+        def theme_wrap():
+            return rx.theme(
+                templated_page(),
+                accent_color=ThemeState.accent_color,
+            )
+
+        return theme_wrap
+
+    return decorator

+ 1 - 0
requirements.txt

@@ -0,0 +1 @@
+reflex==0.4.6

+ 5 - 0
rxconfig.py

@@ -0,0 +1,5 @@
+import reflex as rx
+
+config = rx.Config(
+    app_name="boppersbizza",
+)