diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..d99f1a3 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,118 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations +revision_id = alembic.util.rev_id() +project = htmx-patterns + +# 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 migrations/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:migrations/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 diff --git a/htmx_patterns/app.py b/htmx_patterns/app.py index fcd4483..8a848e5 100644 --- a/htmx_patterns/app.py +++ b/htmx_patterns/app.py @@ -5,11 +5,15 @@ from htmx_patterns.__about__ import __version__ from htmx_patterns.boosted.router import boosted_router from htmx_patterns.config import get_config from htmx_patterns.infinite.router import infinite_router +from htmx_patterns.toast.router import toast_router +from htmx_patterns.websocket.router import websocket_router def set_prefers( - request: Request, + request: Request = None, ): + if request is None: + return hx_request_header = request.headers.get("hx-request") user_agent = request.headers.get("user-agent", "").lower() if hx_request_header: @@ -38,6 +42,8 @@ config = get_config() app.include_router(infinite_router) app.include_router(boosted_router) +app.include_router(toast_router) +app.include_router(websocket_router) @app.get("/") @@ -63,7 +69,19 @@ async def app_css(request: Request): return FileResponse("templates/app.css") -@app.get("/htmx") -async def htmx(request: Request): +@app.get("/htmx.js") +async def htmx_js(request: Request): "use a proper static file server like nginx or apache in production" return config.templates.TemplateResponse("htmx.js", {"request": request}) + + +@app.get("/ws.js") +async def ws_js(request: Request): + "use a proper static file server like nginx or apache in production" + return config.templates.TemplateResponse("ws.js", {"request": request}) + + +@app.get("/tailwind.js") +async def tailwind_js(request: Request): + "use a proper static file server like nginx or apache in production" + return config.templates.TemplateResponse("tailwind.js", {"request": request}) diff --git a/htmx_patterns/boosted/models.py b/htmx_patterns/boosted/models.py deleted file mode 100644 index a5ad575..0000000 --- a/htmx_patterns/boosted/models.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import date, datetime -from typing import List, Union - -from faker import Faker -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import UUID4, BaseModel - -faker = Faker() - - -class Person(BaseModel): - id: UUID4 - name: str - birthday: Union[datetime, date] - phone_number: str - - -class PersonFactory(ModelFactory): - name = faker.name - phone_number = faker.phone_number - __model__ = Person - - -# result = PersonFactory.build() diff --git a/htmx_patterns/boosted/router.py b/htmx_patterns/boosted/router.py index 87b8a90..b48aa13 100644 --- a/htmx_patterns/boosted/router.py +++ b/htmx_patterns/boosted/router.py @@ -1,24 +1,42 @@ -import asyncio -import time +from typing import Annotated -from fastapi import APIRouter +from fastapi import APIRouter, Depends, Header from fastapi.requests import Request +from sqlmodel import Session -boosted_router = APIRouter(prefix="/boosted", tags=["Shots Methods"]) -from htmx_patterns.boosted.models import PersonFactory -from htmx_patterns.config import get_config +from htmx_patterns.config import get_config, get_session +from htmx_patterns.models import Person + +boosted_router = APIRouter(prefix="/boosted", tags=["Boosted"]) config = get_config() @boosted_router.get("/") @boosted_router.get("") -async def boosted(request: Request, id: int = 0): - # simulate getting a person by id - person = PersonFactory.build() +async def boosted( + request: Request, + id: int = 1, + session: Session = Depends(get_session), + user_agent: Annotated[str | None, Header()] = None, +): + # person = PersonFactory.build() + person = Person.get_by_id(session, id) - if id > 0: + if person is None: + return config.templates.TemplateResponse( + "boosted/person.html", + { + "request": request, + "person": None, + "person_id": id, + "prev_id": None, + "next_id": 1, + }, + ) + + if id > 1: prev_id = id - 1 next_id = id + 1 else: diff --git a/htmx_patterns/cli/api.py b/htmx_patterns/cli/api.py index 1a97e0b..20711f8 100644 --- a/htmx_patterns/cli/api.py +++ b/htmx_patterns/cli/api.py @@ -1,5 +1,7 @@ +import alembic import typer import uvicorn +from alembic.config import Config from rich.console import Console from htmx_patterns.config import get_config @@ -7,7 +9,6 @@ from htmx_patterns.config import get_config api_app = typer.Typer() - @api_app.callback() def api(): "model cli" @@ -37,6 +38,10 @@ def run( ): config = get_config(env) Console().print(config.api_server) + Console().print(config.database_url) + alembic_cfg = Config("alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", config.database_url) + alembic.command.upgrade(config=alembic_cfg, revision=alembic_revision) uvicorn.run(**config.api_server.dict()) diff --git a/htmx_patterns/config.py b/htmx_patterns/config.py index 642a226..c501fca 100644 --- a/htmx_patterns/config.py +++ b/htmx_patterns/config.py @@ -1,17 +1,25 @@ -import os -import urllib.parse from datetime import datetime, timezone -from functools import lru_cache, partial -from typing import Any, Optional +from functools import lru_cache +import os +from typing import Optional from urllib.parse import quote_plus -import jinja2 from dotenv import load_dotenv -from fastapi import Request from fastapi.templating import Jinja2Templates +import jinja2 from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict from rich.console import Console +from sqlalchemy import create_engine +from sqlmodel import Session +from typing import TYPE_CHECKING + +from htmx_patterns import models + +if TYPE_CHECKING: + from sqlalchemy.engine import Engine + +__all__ = ["models"] console = Console() @@ -32,7 +40,6 @@ class ApiServer(BaseModel): proxy_headers: bool = True - @pass_context def url_for_query(context: dict, name: str, **params: dict) -> str: request = context["request"] @@ -89,9 +96,10 @@ def get_templates(config: BaseSettings) -> Jinja2Templates: class Config(BaseSettings): - env: str + env: str = "local" the_templates: Optional[Jinja2Templates] = Field(None, exclude=True) api_server: ApiServer = ApiServer() + database_url: str = "sqlite:///database.db" @property def templates(self) -> Jinja2Templates: @@ -102,6 +110,36 @@ class Config(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter="__") +class Database: + def __init__(self, config: Config) -> None: + self.config = config + + self.db_conf = {} + if "sqlite" in self.config.database_url: + self.db_conf = { + "connect_args": {"check_same_thread": False}, + "pool_recycle": 3600, + "pool_pre_ping": True, + } + + # if os.environ.get("ENV") == "test": + # self._engine = create_engine( + # "sqlite://", + # connect_args={"check_same_thread": False}, + # poolclass=StaticPool, + # ) + # else: + self._engine = create_engine(self.config.database_url, **self.db_conf) + + @property + def engine(self) -> "Engine": + return self._engine + + @property + def session(self) -> "Session": + return Session(self.engine) + + @lru_cache def get_config(env: Optional[str] = None): if env is None: @@ -110,3 +148,12 @@ def get_config(env: Optional[str] = None): config = Config() return config + + +config = get_config() +database = Database(config) + + +def get_session() -> "Session": + with Session(database.engine) as session: + yield session diff --git a/htmx_patterns/infinite/models.py b/htmx_patterns/infinite/models.py deleted file mode 100644 index a5ad575..0000000 --- a/htmx_patterns/infinite/models.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import date, datetime -from typing import List, Union - -from faker import Faker -from polyfactory.factories.pydantic_factory import ModelFactory -from pydantic import UUID4, BaseModel - -faker = Faker() - - -class Person(BaseModel): - id: UUID4 - name: str - birthday: Union[datetime, date] - phone_number: str - - -class PersonFactory(ModelFactory): - name = faker.name - phone_number = faker.phone_number - __model__ = Person - - -# result = PersonFactory.build() diff --git a/htmx_patterns/infinite/router.py b/htmx_patterns/infinite/router.py index e1096bb..8ce2bed 100644 --- a/htmx_patterns/infinite/router.py +++ b/htmx_patterns/infinite/router.py @@ -1,13 +1,12 @@ import asyncio -import time from fastapi import APIRouter from fastapi.requests import Request -infinite_router = APIRouter(prefix="/infinite", tags=["Shots Methods"]) - from htmx_patterns.config import get_config -from htmx_patterns.infinite.models import PersonFactory +from htmx_patterns.models import PersonFactory + +infinite_router = APIRouter(prefix="/infinite", tags=["Infinite"]) config = get_config() diff --git a/htmx_patterns/models.py b/htmx_patterns/models.py new file mode 100644 index 0000000..a3ab75e --- /dev/null +++ b/htmx_patterns/models.py @@ -0,0 +1,60 @@ +from datetime import datetime +from typing import Optional + +from faker import Faker +from polyfactory.factories.pydantic_factory import ModelFactory +from pydantic import UUID4 +from sqlmodel import Field, SQLModel + +faker = Faker() + + +class PersonBase(SQLModel, table=False): + id: UUID4 + name: str + birthday: datetime + phone_number: str + + def get_by_id(session, id): + return session.get(Person, id) + + def save(self, session): + session.add(self) + session.commit() + session.refresh(self) + return self + + def delete(self, session): + session.delete(self) + session.commit() + return self + + def update(self, session): + session.merge(self) + session.commit() + session.refresh(self) + return self + + def all(session): + return session.query(Person).all() + + def paginate(session, page=1, page_size=10): + return session.query(Person).offset((page - 1) * page_size).limit(page_size) + + +class Person(PersonBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class PersonCreate(PersonBase): + pass + + +class PersonRead(PersonBase): + id: int = Field(default=None, primary_key=True) + + +class PersonFactory(ModelFactory): + name = faker.name + phone_number = faker.phone_number + __model__ = Person diff --git a/htmx_patterns/toast/router.py b/htmx_patterns/toast/router.py new file mode 100644 index 0000000..bcc6db6 --- /dev/null +++ b/htmx_patterns/toast/router.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter +from fastapi.requests import Request +from fastapi.responses import PlainTextResponse + + +from htmx_patterns.config import get_config + +toast_router = APIRouter(prefix="/toast", tags=["toast"]) + +config = get_config() + + +@toast_router.get("/") +@toast_router.get("") +async def get_toast( + request: Request, +): + return config.templates.TemplateResponse( + "toast/toast.html", + { + "request": request, + }, + ) + + +@toast_router.post("/") +@toast_router.post("") +async def post_toast( + request: Request, +): + return config.templates.TemplateResponse( + "toast/toast-message.html", + { + "request": request, + "message": "Submitted", + }, + ) + + +@toast_router.get("/null/") +@toast_router.get("/null") +@toast_router.delete("/null/") +@toast_router.delete("/null") +@toast_router.post("/null/") +@toast_router.post("/null") +async def null( + request: Request, +): + return PlainTextResponse("") diff --git a/htmx_patterns/websocket/dependencies.py b/htmx_patterns/websocket/dependencies.py new file mode 100644 index 0000000..d9f556e --- /dev/null +++ b/htmx_patterns/websocket/dependencies.py @@ -0,0 +1,22 @@ +from fastapi import WebSocket + + +class ConnectionManager: + """Class defining socket events""" + + def __init__(self): + """init method, keeping track of connections""" + self.active_connections = [] + + async def connect(self, websocket: WebSocket): + """connect event""" + await websocket.accept() + self.active_connections.append(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + """Direct Message""" + await websocket.send_text(message) + + def disconnect(self, websocket: WebSocket): + """disconnect event""" + self.active_connections.remove(websocket) diff --git a/htmx_patterns/websocket/router.py b/htmx_patterns/websocket/router.py new file mode 100644 index 0000000..b2cb219 --- /dev/null +++ b/htmx_patterns/websocket/router.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect +from htmx_patterns.websocket.dependencies import ConnectionManager + +from htmx_patterns.config import get_config + + +websocket_router = APIRouter(prefix="/websocket", tags=["Websocket"]) + +manager = ConnectionManager() + +config = get_config() + + +@websocket_router.get("/") +def websocket_index(request: Request): + return config.templates.TemplateResponse( + "websocket/index.html", {"request": request} + ) + + +@websocket_router.websocket("/") +async def websocket_endpoint( + websocket: WebSocket, +): + await manager.connect(websocket) + await manager.send_personal_message("Hello", websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"Received:{data}", websocket) + # import time + # + # time.sleep(1) + # data = "hello" + # await manager.send_personal_message(f"Received:{data}", websocket) + # + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.send_personal_message("Bye!!!", websocket) diff --git a/justfile b/justfile index 447a0dc..5689950 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,24 @@ default: build-image: podman build -t docker.io/waylonwalker/htmx-patterns-waylonwalker-com:$(hatch version) . +tag-wayl-one: + podman tag docker.io/waylonwalker/htmx-patterns-waylonwalker-com:$(hatch version) registry.wayl.one/htmx-patterns-waylonwalker-com:latest + podman tag docker.io/waylonwalker/htmx-patterns-waylonwalker-com:$(hatch version) registry.wayl.one/htmx-patterns-waylonwalker-com:$(hatch version) +push-wayl-one: + podman push registry.wayl.one/htmx-patterns-waylonwalker-com:latest + podman push registry.wayl.one/htmx-patterns-waylonwalker-com:$(hatch version) + push-image: podman push docker.io/waylonwalker/htmx-patterns-waylonwalker-com:$(hatch version) +shell: + hatch shell + +run: + uv run htmx-patterns api run + +lint: + ruff format htmx_patterns + ruff check --fix htmx_patterns + diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..dfee355 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,90 @@ +from logging.config import fileConfig + +from alembic import context +from sqlmodel import SQLModel + +# 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 = SQLModel.metadata + +# 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, + # ) + from htmx_patterns.config import Database, get_config + + project_config = get_config() + database = Database(project_config) + + config.set_main_option("sqlalchemy.url", project_config.database_url) + # connectable = engine_from_config( + # config.get_section(config.config_ini_section, {}), + # prefix="sqlalchemy.", + # poolclass=pool.NullPool, + # ) + + with database.engine.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=True, + version_table=f'{config.get_main_option("project")}_alembic_version', + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..6ce3351 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,27 @@ +"""${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 +import sqlmodel +${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"} diff --git a/migrations/versions/bf7df4c30cea_init.py b/migrations/versions/bf7df4c30cea_init.py new file mode 100644 index 0000000..4770b42 --- /dev/null +++ b/migrations/versions/bf7df4c30cea_init.py @@ -0,0 +1,39 @@ +"""init + +Revision ID: bf7df4c30cea +Revises: +Create Date: 2024-04-07 09:47:07.245218 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = "bf7df4c30cea" +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( + "person", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("birthday", sa.DateTime(), nullable=False), + sa.Column("phone_number", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("person") + # ### end Alembic commands ### diff --git a/pyproject.toml b/pyproject.toml index 9dc6a6c..7bedfbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "jinja2", "uvicorn[standard]", "typer", +"alembic", ] [project.scripts] diff --git a/templates/app.css b/templates/app.css index ef9badd..1cfe8ff 100644 --- a/templates/app.css +++ b/templates/app.css @@ -8,47 +8,46 @@ } ::-webkit-scrollbar { - height: 1rem; - width: 1rem; + height: .5rem; + width: .5rem; } ::-webkit-scrollbar-track { border-radius: 0.25rem; border-radius: 9999px; --tw-bg-opacity: 1; - background-color: #450a0a; + background-color: #002600; } body::-webkit-scrollbar-track { border-radius: 0.25rem; border-radius: 9999px; --tw-bg-opacity: 1; - background-color: #450a0a; + background-color: #002600; } ::-webkit-scrollbar-thumb { border-radius: 0.25rem; border-radius: 9999px; --tw-bg-opacity: 1; -background-color: rgba(239,68,68,var(--tw-bg-opacity)); +background-color: #1aff1a; + background-color: #00b300; } ::-webkit-scrollbar-thumb:hover { --tw-bg-opacity: 1; -background-color: #f87171; -background-color: #dc2626 + background-color: #1aff1a; } body::-webkit-scrollbar-thumb { border-radius: 0.25rem; border-radius: 9999px; --tw-bg-opacity: 1; -background-color: rgba(239,68,68,var(--tw-bg-opacity)); + background-color: #00b300; } body::-webkit-scrollbar-thumb:hover { --tw-bg-opacity: 1; -background-color: #f87171; -background-color: #dc2626 + background-color: #1aff1a; } diff --git a/templates/base.html b/templates/base.html index abc126c..aeae2d6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,67 +1,155 @@ - - {% block head %} - - {% block title %}HTMX Patterns{% endblock %} - - - - - - - - - - - - - - + + {% block head %} + + {% block title %}HTMX Patterns{% endblock %} + + + + + + + + + + + + + + - - - - - {% if DEBUG %} - {{ hot_reload.script(url_for('hot-reload') ) | safe }} - {% endif %} - {% endblock %} - + + + + + + + {% if DEBUG %} + {{ hot_reload.script(url_for('hot-reload') ) | safe }} + {% endif %} + {% endblock %} + + + {% set links = { + "HTMX-PATTERNS": url_for("index"), + "Boosted Links": url_for('boosted'), + "Infinite Scroll": url_for('infinite'), + "Toast": url_for('get_toast'), + "WebSocket": url_for('websocket_index'), + } %} + + + +
+
+
+ - {% block content %} - {{ body | safe }} - {% endblock %} -
- + {% for link, url in links.items() %} + + {{ link }} + + {% endfor %} + + {% block content %} + {{ body | safe }} + {% endblock %} + +
+
+ diff --git a/templates/boosted/person.html b/templates/boosted/person.html index 2ad2ae2..7d47ae0 100644 --- a/templates/boosted/person.html +++ b/templates/boosted/person.html @@ -2,60 +2,62 @@ {% block title %}Contact - {{ person_id }} - {{ person.name }}{% endblock %} {% block content %}

+ class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none"> HTMX PATTERNS - BOOSTED

-

+

Contact - {{ person_id }}

-

- {{ person.name }} - +{% if person is not none %} +

+ {{ person.name.upper() }} - {{ person.phone_number }}

+{% else %} +

+ Person not found +

+{% endif %} -

+{% macro link(id, text, boosted=false) -%} + + {{ text }} + +{%- endmacro %} + +

Boosted Links

- {% if prev_id is not none %} - - Previous - - {% else %} - - Previous - - {% endif %} - - - Next - + {{ link(prev_id, 'Previous', boosted=True) }} + {{ link(next_id, 'Next', boosted=True) }}
-

+

Normal Links

- {% if prev_id is not none %} - - Previous - - {% else %} - - Previous - - {% endif %} - - - Next - + {{ link(prev_id, 'Previous', boosted=False) }} + {{ link(next_id, 'Next', boosted=False) }}
{% endblock %} diff --git a/templates/index.html b/templates/index.html index eccd008..050723a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,19 +1,34 @@ {% extends "base.html" %} {% block content %}

+ class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none"> HTMX PATTERNS

-

+

A collection of HTMX patterns

+

+ These are patterns that I have written based on content from the hypermedia.systems + book. There is lots of code duplication as each pattern is meant to be standalone. +

+ +

+ I currently make use of htmx with fastapi, sqlmodel, sqlite, and tailwindcss + for many of my projects. These patterns are here to serve for reference to + myself implemented using this stack in the most pure way possible to remain + simple and understandable. Sometimes real projects get complicated and are + hard to bring in new features correctly. This is a playground with completely + separate routers, models, and templates for each project. +

+