This commit is contained in:
2024-01-04 09:02:44 +01:00
parent ed19c7c6cb
commit 689c71deb1
48 changed files with 1464 additions and 0 deletions

1
reflex_ipad/__init__.py Normal file
View File

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

View File

@@ -0,0 +1,6 @@
from . import medicine
from . import scan
def register_at(app):
medicine.register_at(app)
scan.register_at(app)

View File

@@ -0,0 +1,13 @@
import reflex as rx
from reflex_ipad.models import *
from sqlmodel import Field, Session, SQLModel, create_engine, select
async def medicine_scan(uuid: str):
with rx.session() as session:
statement = select(Medicine).where(Medicine.uuid == uuid)
results = session.exec(statement)
med = results.first()
return med
def register_at(app):
app.api.add_api_route("/medicine/scan/{uuid}", medicine_scan)

17
reflex_ipad/api/scan.py Normal file
View File

@@ -0,0 +1,17 @@
import reflex as rx
from reflex_ipad.models import *
from sqlmodel import Field, Session, SQLModel, create_engine, select
import time
async def scan(uuid: str):
print(uuid)
with rx.session() as session:
scan = Scan(uuid=uuid, timestamp=time.time())
session.add(scan)
session.commit()
return scan.as_dict()
def register_at(app):
print("register scan")
app.api.add_api_route("/scan/{uuid}", scan)

View File

View File

@@ -0,0 +1,119 @@
"""Sidebar component for the app."""
from reflex_ipad import styles
import reflex as rx
def sidebar_header() -> rx.Component:
"""Sidebar header.
Returns:
The sidebar header component.
"""
return rx.hstack(
# The logo.
rx.heading("Hallo"),
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(),
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,
),
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,
)

View File

@@ -0,0 +1,2 @@
from .medicine import *
from .scan import *

View File

@@ -0,0 +1,6 @@
import reflex as rx
class BaseModel(rx.Model):
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}

View File

@@ -0,0 +1,29 @@
import reflex as rx
from typing import Optional, List
from sqlmodel import Field, Session, SQLModel, create_engine, select, Relationship
class Owner(rx.Model, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
medicines: List["Medicine"] = Relationship(back_populates="owner")
class Medicine(rx.Model, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
owner_id: int = Field(default=None, foreign_key="owner.id")
owner: Owner = Relationship(back_populates="medicines")
pzn: str
package_size: Optional[int] = Field(default=None)
uuid: str = Field(index=True)
log: List["MedicineLog"] = Relationship(back_populates="medicine")
cron: Optional[str] = Field(default=None)
class MedicineLog(rx.Model, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
medicine_id: Optional[int] = Field(default=None, foreign_key="medicine.id")
medicine: Optional[Medicine] = Relationship(back_populates="log")
timestamp: int

View File

@@ -0,0 +1,11 @@
import reflex as rx
from .base import *
from typing import Optional, List
from sqlmodel import Field, Session, SQLModel, create_engine, select, Relationship
class Scan(BaseModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
uuid: str = Field(index=True)
timestamp: int = Field(index=True)

View File

@@ -0,0 +1,6 @@
from .dashboard import dashboard
from .index import index
from .settings import settings
from .medicine import medicine

View File

@@ -0,0 +1,21 @@
"""The dashboard page."""
from reflex_ipad.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", font_size="3em"),
rx.text("Welcome to Reflex!"),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/dashboard.py"),
),
)

View File

@@ -0,0 +1,18 @@
"""The home page of the app."""
from reflex_ipad import styles
from reflex_ipad.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)

View File

@@ -0,0 +1,246 @@
"""The meds page."""
from reflex_ipad import styles
from reflex_ipad.templates import template
from reflex_ipad.state import State
import datetime
import asyncio
import time
import reflex as rx
from reflex_ipad.models import *
from sqlmodel import Field, Session, SQLModel, create_engine, select
class NewMedicineState(rx.State):
"""Define your app state here."""
show_med_add_form: bool = False
medicine: Optional[Medicine] = None
medicine_name: str = ""
last_scan: Scan = Scan(uuid="", timestamp=0)
last_scan_uuid: str = ""
last_scan_time: str = ""
updated_uuid: bool = False
scanning: bool = False
rate: float = 2
lastupdatetime: int = time.time()
def start_scan(self):
self.set_scanning(True)
self.lastupdatetime = time.time()
self.last_scan_uuid = ""
return NewMedicineState.do_scanning
def load_last_scan(self):
with rx.session() as session:
statement = select(Scan).order_by(Scan.timestamp.desc()).limit(1)
results = session.exec(statement)
self.last_scan = results.first()
if self.last_scan.timestamp > self.lastupdatetime:
self.last_scan_uuid = self.last_scan.uuid
if not self.show_med_add_form:
self.update_medicine(self.last_scan.uuid)
self.updated_uuid = True
self.lastupdatetime = time.time()
ts = datetime.datetime.fromtimestamp(self.last_scan.timestamp)
self.last_scan_time = ts.strftime("%Y-%m-%d %H:%M")
def uuid_used(self):
self.updated_uuid = False
def update_medicine(self, uuid):
with rx.session() as session:
statement = select(Medicine).where(Medicine.uuid == uuid)
results = session.exec(statement)
self.medicine = results.first()
if self.medicine is not None:
self.medicine_name = f"{self.medicine.name} - {self.medicine.owner.name}"
@rx.background
async def do_scanning(self):
while True:
await asyncio.sleep(1 / self.rate)
if not self.scanning:
break
async with self:
self.load_last_scan()
def do_show_med_add_form(self):
self.set_show_med_add_form(True)
def stop_show_med_add_form(self):
self.set_show_med_add_form(False)
def handle_submit(self, form_data: dict):
self.form_data = form_data
with rx.session() as session:
statement = select(Owner).where(Owner.name == form_data["owner"])
results = session.exec(statement)
owner = results.first()
medicine = Medicine(
name=form_data["name"],
package_size=form_data["pkg_size"],
pzn=form_data["pzn"] or "",
owner_id=owner.id,
uuid=State.last_scan_uuid,
cron=form_data["schedule"],
)
session.add(medicine)
session.commit()
self.uuid_used()
self.set_show_med_add_form(False)
def handle_log(self, form_data: dict):
with rx.session() as session:
statement = select(Medicine).where(Medicine.uuid == form_data["uuid"])
results = session.exec(statement)
medicine = results.first()
medicineLog = MedicineLog(medicine_id=medicine.id, timestamp=time.time())
session.add(medicineLog)
session.commit()
self.uuid_used()
def cancel_log(self, form_data: dict):
self.uuid_used()
def new_medicine_form():
with rx.session() as session:
statement = select(Owner)
results = session.exec(statement)
owners = results.all()
return rx.vstack(
rx.form(
rx.vstack(
rx.input(
placeholder="Name",
name="name",
),
rx.input(
placeholder="Packungsgröße",
name="pkg_size",
),
rx.input(
placeholder="PZN",
name="pzn",
),
rx.select(
[owner.name for owner in owners],
placeholder="Für wen",
name="owner",
),
rx.hstack(
rx.text("Crontab lines für Einnahme"),
rx.popover(
rx.popover_trigger(rx.button("Help")),
rx.popover_content(
rx.popover_header("Crontab help"),
rx.popover_body(
rx.html("<pre>0 7 * * * </pre>"),
"Täglich sieben Uhr",
),
rx.popover_body(
rx.html("<pre>0 21 * * 0 </pre>"),
"Jeden Sonntag um 21:00 Uhr.",
),
rx.popover_body(
rx.html("<pre>0 7 * * 1-5 </pre>"),
"Montags bis Freitags jeweils um 07:00",
),
rx.popover_close_button(),
),
),
),
rx.text_area(name="schedule"),
rx.input(
value=NewMedicineState.last_scan_uuid,
name="uuid",
placeholder="UUID",
disabled=True,
),
rx.hstack(
rx.text("Gescannt:"),
rx.text(
NewMedicineState.last_scan_time,
),
),
rx.button("Submit", type_="submit"),
),
on_submit=NewMedicineState.handle_submit,
reset_on_submit=True,
),
rx.divider(),
)
def taken_form():
with rx.session() as session:
if NewMedicineState.medicine is None:
return rx.vstack()
return rx.vstack(
rx.heading(NewMedicineState.medicine_name, size="lg", color="darkblue"),
rx.hstack(
rx.form(
rx.button("Ja", type_="submit", color_scheme="green", size="lg"),
rx.input(
value=NewMedicineState.last_scan_uuid,
name="uuid",
disabled=True,
hidden=True,
type_="hidden",
),
on_submit=NewMedicineState.handle_log,
),
rx.form(
rx.button("Nein", type_="submit", color_scheme="red", size="lg"),
on_submit=NewMedicineState.cancel_log,
),
reset_on_submit=True,
),
rx.divider(),
)
@template(route="/medicine", title="Medikamente")
def medicine() -> rx.Component:
"""The dashboard page.
Returns:
The UI for the dashboard page.
"""
return rx.vstack(
rx.hstack(
rx.spacer(),
rx.heading("Medikamente", font_size="3em"),
rx.spacer(),
rx.cond(
NewMedicineState.show_med_add_form,
rx.button(
rx.text(
"-",
),
on_click=NewMedicineState.stop_show_med_add_form(),
),
rx.button(
rx.text(
"+",
),
on_click=NewMedicineState.do_show_med_add_form(),
),
),
width="100%",
border_bottom=styles.border,
padding="1em",
),
rx.cond(NewMedicineState.show_med_add_form, new_medicine_form()),
rx.cond(
NewMedicineState.updated_uuid,
taken_form(),
rx.text(
"Scan the Med",
),
),
on_mount=NewMedicineState.start_scan,
)

20
reflex_ipad/pages/meds.py Normal file
View File

@@ -0,0 +1,20 @@
"""The meds page."""
from reflex_ipad.templates import template
import reflex as rx
@template(route="/medicine", title="Medikamente")
def dashboard() -> rx.Component:
"""The dashboard page.
Returns:
The UI for the dashboard page.
"""
return rx.vstack(
rx.heading("Medikamente", font_size="3em"),
rx.text("Heute schon genommen?"),
rx.text(
"Scan the Med",
),
)

View File

@@ -0,0 +1,22 @@
"""The settings page."""
from reflex_ipad.templates import 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", font_size="3em"),
rx.text("Welcome to Reflex!"),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/settings.py"),
),
)

View File

@@ -0,0 +1,17 @@
"""Welcome to Reflex!."""
from reflex_ipad import styles
# Import all the pages.
from reflex_ipad.pages import *
from reflex_ipad import api
from reflex_ipad.models import *
import reflex as rx
# Create the app and compile it.
app = rx.App(style=styles.base_style)
api.register_at(app)
app.compile()

9
reflex_ipad/state.py Normal file
View File

@@ -0,0 +1,9 @@
import reflex as rx
import time
import datetime
import asyncio
from reflex_ipad.models import *
class State(rx.State):
"""Define your app state here."""
pass

62
reflex_ipad/styles.py Normal file
View File

@@ -0,0 +1,62 @@
"""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 = "1px solid #F4F3F6"
text_color = "black"
accent_text_color = "#1A1060"
accent_color = "#F5EFFE"
hover_accent_color = {"_hover": {"color": accent_color}}
hover_accent_bg = {"_hover": {"bg": 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": text_color,
"text_decoration": "none",
**hover_accent_color,
}
overlapping_button_style = {
"background_color": "white",
"border": border,
"border_radius": border_radius,
}
base_style = {
rx.MenuButton: {
"width": "3em",
"height": "3em",
**overlapping_button_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,127 @@
"""Common templates used between pages in the app."""
from __future__ import annotations
from reflex_ipad import styles
from reflex_ipad.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_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(
rx.menu_button(
rx.icon(
tag="hamburger",
size="4em",
color=styles.text_color,
),
),
rx.menu_list(
*[
rx.menu_item(
rx.link(
page["title"],
href=page["route"],
width="100%",
)
)
for page in get_decorated_pages()
],
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(
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 [])]
@rx.page(
route=route,
title=title,
image=image,
description=description,
meta=all_meta,
script_tags=script_tags,
on_load=on_load,
)
def templated_page():
return rx.hstack(
sidebar(),
rx.box(
rx.box(
page_content(),
**styles.template_content_style,
),
**styles.template_page_style,
),
menu_button(),
align_items="flex-start",
transition="left 0.5s, width 0.5s",
position="relative",
)
return templated_page
return decorator