initial
This commit is contained in:
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.web
|
||||
__pycache__/*
|
||||
68
.drone.jsonnet
Normal file
68
.drone.jsonnet
Normal file
@@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
kind: 'pipeline',
|
||||
type: 'docker',
|
||||
name: 'reflex-ipad',
|
||||
steps: [
|
||||
{
|
||||
name: 'builder',
|
||||
image: 'plugins/docker',
|
||||
settings: {
|
||||
tags: ['builder'],
|
||||
dockerfile: 'Dockerfile.builder',
|
||||
registry: 'gitea.pb42.de',
|
||||
repo: 'gitea.pb42.de/matthias/reflex-ipad',
|
||||
config: { from_secret: 'dockerconfigjson' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'frontend-builder',
|
||||
image: 'gitea.pb42.de/matthias/reflex-ipad',
|
||||
commands:[
|
||||
'reflex export --frontend-only --no-zip',
|
||||
'mv .web/_static /drone/src/web',
|
||||
],
|
||||
depends_on: [
|
||||
'builder'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'backend',
|
||||
image: 'plugins/docker',
|
||||
settings: {
|
||||
tags: ['backend-latest'],
|
||||
dockerfile: 'Dockerfile.backend',
|
||||
registry: 'gitea.pb42.de',
|
||||
repo: 'gitea.pb42.de/matthias/reflex-ipad',
|
||||
config: { from_secret: 'dockerconfigjson' },
|
||||
build_args: ['API_URL=https://ipad.pb42.de/api'],
|
||||
},
|
||||
depends_on: [
|
||||
'builder'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "deploy_web",
|
||||
image: "appleboy/drone-scp",
|
||||
settings: {
|
||||
host: "pb42.de",
|
||||
target: "/",
|
||||
source: "web/*",
|
||||
username: {
|
||||
from_secret: "deploy_username",
|
||||
},
|
||||
password: {
|
||||
from_secret: "deploy_password",
|
||||
},
|
||||
port: 42022,
|
||||
},
|
||||
depends_on: [
|
||||
'frontend'
|
||||
],
|
||||
},
|
||||
]
|
||||
trigger: { event: ['push'] },
|
||||
image_pull_secrets: ['dockerconfigjson'],
|
||||
},
|
||||
]
|
||||
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*.db
|
||||
*.py[cod]
|
||||
.web
|
||||
__pycache__/
|
||||
venv
|
||||
|
||||
4
Caddy.Dockerfile
Normal file
4
Caddy.Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM library/caddy
|
||||
|
||||
COPY --from=local/reflex-app /app/.web/_static /srv
|
||||
ADD Caddyfile /etc/caddy/Caddyfile
|
||||
14
Caddyfile
Normal file
14
Caddyfile
Normal file
@@ -0,0 +1,14 @@
|
||||
{$DOMAIN}
|
||||
|
||||
encode gzip
|
||||
|
||||
@backend_routes path /_event/* /_upload /ping
|
||||
handle @backend_routes {
|
||||
reverse_proxy app:8000
|
||||
}
|
||||
|
||||
root * /srv
|
||||
route {
|
||||
try_files {path} {path}/ /404.html
|
||||
file_server
|
||||
}
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Stage 1: init
|
||||
FROM python:3.11 as init
|
||||
|
||||
# Pass `--build-arg API_URL=http://app.example.com:8000` during build
|
||||
ARG API_URL
|
||||
|
||||
# Copy local context to `/app` inside container (see .dockerignore)
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
|
||||
# Create virtualenv which will be copied into final container
|
||||
ENV VIRTUAL_ENV=/app/.venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN python3.11 -m venv $VIRTUAL_ENV
|
||||
|
||||
# Install app requirements and reflex inside virtualenv
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Deploy templates and prepare app
|
||||
RUN reflex init
|
||||
10
Dockerfile.backend
Normal file
10
Dockerfile.backend
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
ARG API_URL
|
||||
WORKDIR /app
|
||||
RUN adduser --disabled-password --home /app reflex
|
||||
COPY --chown=reflex --from=gitea.pb42.de/matthias/reflex-ipad:builder /app /app
|
||||
COPY --chown=reflex deploy/* .
|
||||
USER reflex
|
||||
ENV PATH="/app/.venv/bin:$PATH" API_URL=$API_URL
|
||||
|
||||
CMD reflex db migrate && reflex run --env prod --backend-only
|
||||
22
Dockerfile.builder
Normal file
22
Dockerfile.builder
Normal file
@@ -0,0 +1,22 @@
|
||||
# Stage 1: init
|
||||
FROM python:3.11 as init
|
||||
|
||||
# Pass `--build-arg API_URL=http://app.example.com:8000` during build
|
||||
ARG API_URL
|
||||
|
||||
# Copy local context to `/app` inside container (see .dockerignore)
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
|
||||
# Create virtualenv which will be copied into final container
|
||||
ENV VIRTUAL_ENV=/app/.venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN python3.11 -m venv $VIRTUAL_ENV
|
||||
|
||||
# Install app requirements and reflex inside virtualenv
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Deploy templates and prepare app
|
||||
RUN reflex init
|
||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Reflex IPAD
|
||||
|
||||
This is a small webinterface for several usages.
|
||||
|
||||
## Medicine
|
||||
|
||||
- Log medicine
|
||||
- get reminder if not taken
|
||||
-
|
||||
116
alembic.ini
Normal file
116
alembic.ini
Normal file
@@ -0,0 +1,116 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
1
alembic/README
Normal file
1
alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
78
alembic/env.py
Normal file
78
alembic/env.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
39
alembic/versions/44b022dca7ce_.py
Normal file
39
alembic/versions/44b022dca7ce_.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 44b022dca7ce
|
||||
Revises: d64b9935ffd3
|
||||
Create Date: 2024-01-01 16:46:40.302737
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '44b022dca7ce'
|
||||
down_revision: Union[str, None] = 'd64b9935ffd3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('scan',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('timestamp', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_scan_timestamp'), 'scan', ['timestamp'], unique=False)
|
||||
op.create_index(op.f('ix_scan_uuid'), 'scan', ['uuid'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_scan_uuid'), table_name='scan')
|
||||
op.drop_index(op.f('ix_scan_timestamp'), table_name='scan')
|
||||
op.drop_table('scan')
|
||||
# ### end Alembic commands ###
|
||||
30
alembic/versions/5faf3b4df7ee_.py
Normal file
30
alembic/versions/5faf3b4df7ee_.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 5faf3b4df7ee
|
||||
Revises: 44b022dca7ce
|
||||
Create Date: 2024-01-02 14:01:13.882740
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5faf3b4df7ee'
|
||||
down_revision: Union[str, None] = '44b022dca7ce'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('medicine', sa.Column('cron', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('medicine', 'cron')
|
||||
# ### end Alembic commands ###
|
||||
54
alembic/versions/b21476e11a57_.py
Normal file
54
alembic/versions/b21476e11a57_.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: b21476e11a57
|
||||
Revises:
|
||||
Create Date: 2024-01-01 14:11:31.822410
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b21476e11a57'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('owner',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('medicine',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=True),
|
||||
sa.Column('pzn', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['owner.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_medicine_uuid'), 'medicine', ['uuid'], unique=False)
|
||||
op.create_table('medicinelog',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('medicine_id', sa.Integer(), nullable=True),
|
||||
sa.Column('timestamp', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['medicine_id'], ['medicine.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('medicinelog')
|
||||
op.drop_index(op.f('ix_medicine_uuid'), table_name='medicine')
|
||||
op.drop_table('medicine')
|
||||
op.drop_table('owner')
|
||||
# ### end Alembic commands ###
|
||||
30
alembic/versions/d64b9935ffd3_.py
Normal file
30
alembic/versions/d64b9935ffd3_.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: d64b9935ffd3
|
||||
Revises: b21476e11a57
|
||||
Create Date: 2024-01-01 16:24:12.182247
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd64b9935ffd3'
|
||||
down_revision: Union[str, None] = 'b21476e11a57'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('medicine', sa.Column('package_size', sa.Integer(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('medicine', 'package_size')
|
||||
# ### end Alembic commands ###
|
||||
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
10
assets/github.svg
Normal file
10
assets/github.svg
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
37
assets/icon.svg
Normal file
37
assets/icon.svg
Normal file
@@ -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 > 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>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
68
assets/logo.svg
Normal file
68
assets/logo.svg
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
13
assets/paneleft.svg
Normal file
13
assets/paneleft.svg
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 807 B |
20
compose.yaml
Normal file
20
compose.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# During build and run, set environment DOMAIN pointing
|
||||
# to publicly accessible domain where app will be hosted
|
||||
services:
|
||||
app:
|
||||
image: local/reflex-app
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
API_URL: https://${DOMAIN:-localhost}
|
||||
|
||||
webserver:
|
||||
environment:
|
||||
DOMAIN: ${DOMAIN:-localhost}
|
||||
ports:
|
||||
- 80:80 # for acme-challenge via HTTP
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Caddy.Dockerfile
|
||||
depends_on:
|
||||
- app
|
||||
7
deploy/rxconfig.py
Normal file
7
deploy/rxconfig.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="reflex_ipad",
|
||||
db_url="postgresql://ipad:uizJOcDZR3qKLUJuAeSr8WG8onJ1vsUQB2zgE5NIKcpLGT5EF3x7JBkPs@db:5432/ipad",
|
||||
env=rx.Env.prod,
|
||||
)
|
||||
17
docker_compose.yml
Normal file
17
docker_compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
app:
|
||||
image: gitea.pb42.de/matthias/reflex-ipad:latest
|
||||
force_pull: true
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=ipad
|
||||
- POSTGRES_PASSWORD=uizJOcDZR3qKLUJuAeSr8WG8onJ1vsUQB2zgE5NIKcpLGT5EF3x7JBkPs
|
||||
- POSTGRES_DB=ipad
|
||||
networks:
|
||||
- gitea2
|
||||
volumes:
|
||||
- /var/docker/ipad/db/data:/var/lib/postgresql/data
|
||||
shm_size: 256mb
|
||||
1
reflex_ipad/__init__.py
Normal file
1
reflex_ipad/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Base template for Reflex."""
|
||||
6
reflex_ipad/api/__init__.py
Normal file
6
reflex_ipad/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from . import medicine
|
||||
from . import scan
|
||||
|
||||
def register_at(app):
|
||||
medicine.register_at(app)
|
||||
scan.register_at(app)
|
||||
13
reflex_ipad/api/medicine.py
Normal file
13
reflex_ipad/api/medicine.py
Normal 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
17
reflex_ipad/api/scan.py
Normal 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)
|
||||
0
reflex_ipad/components/__init__.py
Normal file
0
reflex_ipad/components/__init__.py
Normal file
119
reflex_ipad/components/sidebar.py
Normal file
119
reflex_ipad/components/sidebar.py
Normal 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,
|
||||
)
|
||||
2
reflex_ipad/models/__init__.py
Normal file
2
reflex_ipad/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .medicine import *
|
||||
from .scan import *
|
||||
6
reflex_ipad/models/base.py
Normal file
6
reflex_ipad/models/base.py
Normal 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}
|
||||
29
reflex_ipad/models/medicine.py
Normal file
29
reflex_ipad/models/medicine.py
Normal 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
|
||||
11
reflex_ipad/models/scan.py
Normal file
11
reflex_ipad/models/scan.py
Normal 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)
|
||||
6
reflex_ipad/pages/__init__.py
Normal file
6
reflex_ipad/pages/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .dashboard import dashboard
|
||||
from .index import index
|
||||
from .settings import settings
|
||||
from .medicine import medicine
|
||||
|
||||
|
||||
21
reflex_ipad/pages/dashboard.py
Normal file
21
reflex_ipad/pages/dashboard.py
Normal 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"),
|
||||
),
|
||||
)
|
||||
18
reflex_ipad/pages/index.py
Normal file
18
reflex_ipad/pages/index.py
Normal 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)
|
||||
246
reflex_ipad/pages/medicine.py
Normal file
246
reflex_ipad/pages/medicine.py
Normal 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
20
reflex_ipad/pages/meds.py
Normal 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",
|
||||
),
|
||||
)
|
||||
22
reflex_ipad/pages/settings.py
Normal file
22
reflex_ipad/pages/settings.py
Normal 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"),
|
||||
),
|
||||
)
|
||||
17
reflex_ipad/reflex_ipad.py
Normal file
17
reflex_ipad/reflex_ipad.py
Normal 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
9
reflex_ipad/state.py
Normal 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
62
reflex_ipad/styles.py
Normal 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",
|
||||
},
|
||||
),
|
||||
}
|
||||
1
reflex_ipad/templates/__init__.py
Normal file
1
reflex_ipad/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .template import template
|
||||
127
reflex_ipad/templates/template.py
Normal file
127
reflex_ipad/templates/template.py
Normal 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
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
reflex==0.3.7
|
||||
croniter==2.0.1
|
||||
6
rxconfig.py
Normal file
6
rxconfig.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="reflex_ipad",
|
||||
db_url="sqlite:///reflex.db",
|
||||
)
|
||||
Reference in New Issue
Block a user