From 0500266b92f08ed29f69b946805171c32196e06c Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 8 May 2024 20:45:33 -0500 Subject: [PATCH] init --- justfile | 46 ++ play_outside/__about__.py | 4 + play_outside/__init__.py | 3 + play_outside/api.py | 270 ++++++++++++ play_outside/cli/api.py | 133 ++++++ play_outside/config.py | 98 +++++ play_outside/decorators.py | 104 +++++ play_outside/queries.py | 4 + pyproject.toml | 79 ++++ static/app.css | 822 +++++++++++++++++++++++++++++++++++ static/favicon.ico | Bin 0 -> 15406 bytes static/htmx.org@1.9.8 | 1 + tailwind.config.js | 17 + tailwind/app.css | 28 ++ templates/base.html | 49 +++ templates/card.html | 70 +++ templates/card.txt | 12 + templates/includestyles.html | 6 + templates/index.html | 13 + templates/index.txt | 4 + tests/__init__.py | 3 + 21 files changed, 1766 insertions(+) create mode 100644 justfile create mode 100644 play_outside/__about__.py create mode 100644 play_outside/__init__.py create mode 100644 play_outside/api.py create mode 100644 play_outside/cli/api.py create mode 100644 play_outside/config.py create mode 100644 play_outside/decorators.py create mode 100644 play_outside/queries.py create mode 100644 pyproject.toml create mode 100644 static/app.css create mode 100644 static/favicon.ico create mode 100644 static/htmx.org@1.9.8 create mode 100644 tailwind.config.js create mode 100644 tailwind/app.css create mode 100644 templates/base.html create mode 100644 templates/card.html create mode 100644 templates/card.txt create mode 100644 templates/includestyles.html create mode 100644 templates/index.html create mode 100644 templates/index.txt create mode 100644 tests/__init__.py diff --git a/justfile b/justfile new file mode 100644 index 0000000..48b27dc --- /dev/null +++ b/justfile @@ -0,0 +1,46 @@ +default: build tag push convert set-image deploy +fresh: create-ns cred convert deploy viz +update: convert patch + +regcred: + kubectl get secret -n default regcred --output=yaml -o yaml | sed 's/namespace: default/namespace: play-outside/' | kubectl apply -n play-outside -f - && echo deployed secret || echo secret exists +build: + podman build -t docker.io/waylonwalker/play-outside -f Dockerfile . +tag: + podman tag docker.io/waylonwalker/play-outside docker.io/waylonwalker/play-outside:$(hatch version) +push: + podman push docker.io/waylonwalker/play-outside docker.io/waylonwalker/play-outside:$(hatch version) + podman push docker.io/waylonwalker/play-outside docker.io/waylonwalker/play-outside:latest +set-image: + kubectl set image deployment/play-outside-wayl-one --namespace play-outside play-outside-wayl-one=docker.io/waylonwalker/play-outside:$(hatch version) + +create-ns: + kubectl create ns play-outside && echo created ns play-outside || echo namespace play-outside already exists +cred: + kubectl get secret regcred --output=yaml -o yaml | sed 's/namespace: default/namespace: play-outside/' | kubectl apply -n play-outside -f - && echo deployed secret || echo secret exists +convert: + kompose convert -o deployment.yaml -n play-outside --replicas 3 +deploy: + kubectl apply -f deployment.yaml +delete: + kubectl delete all --all -n play-outside --timeout=0s +viz: + k8sviz -n play-outside --kubeconfig $KUBECONFIG -t png -o play-outside-k8s.png +restart: + kubectl rollout restart -n play-outside deployment/play-outside-wayl-one +patch: + kubectl patch -f deployment.yaml + +describe: + kubectl get deployment -n play-outside + kubectl get rs -n play-outside + kubectl get pod -n play-outside + kubectl get svc -n play-outside + kubectl get ing -n play-outside + + +describe-pod: + kubectl describe pod -n play-outside + +logs: + kubectl logs --all-containers -l io.kompose.service=play-outside-wayl-one -n play-outside -f diff --git a/play_outside/__about__.py b/play_outside/__about__.py new file mode 100644 index 0000000..f08d8c2 --- /dev/null +++ b/play_outside/__about__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT +__version__ = "0.0.28" diff --git a/play_outside/__init__.py b/play_outside/__init__.py new file mode 100644 index 0000000..a2123ed --- /dev/null +++ b/play_outside/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT diff --git a/play_outside/api.py b/play_outside/api.py new file mode 100644 index 0000000..f6a461a --- /dev/null +++ b/play_outside/api.py @@ -0,0 +1,270 @@ +from dataclasses import dataclass +from fastapi.responses import PlainTextResponse +from fastapi import FastAPI +import time +from datetime import datetime +from fastapi import Request +from fastapi import Header +import httpx +from play_outside.config import get_config +from play_outside.decorators import cache, no_cache +from fastapi.responses import FileResponse +from fastapi.responses import JSONResponse +from fastapi.responses import RedirectResponse + +from fastapi import Depends +import arel + + +def set_prefers( + request: Request, +): + hx_request_header = request.headers.get("hx-request") + user_agent = request.headers.get("user-agent", "").lower() + if "mozilla" in user_agent or "webkit" in user_agent or hx_request_header: + request.state.prefers_html = True + request.state.prefers_json = False + else: + request.state.prefers_html = False + request.state.prefers_json = True + + +app = FastAPI( + dependencies=[Depends(set_prefers)], +) +config = get_config() + + +if config.env == "local": + hot_reload = arel.HotReload( + paths=[arel.Path("fokais"), arel.Path("templates"), arel.Path("static")] + ) + app.add_websocket_route("/hot-reload", route=hot_reload, name="hot-reload") + app.add_event_handler("startup", hot_reload.startup) + app.add_event_handler("shutdown", hot_reload.shutdown) + config.templates.env.globals["DEBUG"] = True + config.templates.env.globals["hot_reload"] = hot_reload + + +async def get_lat_long(ip_address): + if ip_address is None: + ip_address = "140.177.140.75" + async with httpx.AsyncClient() as client: + response = await client.get(f"https://ipwho.is/{ip_address}") + return response.json() + + +async def get_weather(lat_long=None): + if not lat_long: + lat_long = {"latitude": 40.7128, "longitude": -74.0060} + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.openweathermap.org/data/2.5/weather?units=imperial&lat={lat_long['latitude']}&lon={lat_long['longitude']}&appid={config.open_weather_api_key}" + ) + return response.json() + + +async def get_forecast(lat_long=None): + if not lat_long: + lat_long = {"latitude": 40.7128, "longitude": -74.0060} + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.openweathermap.org/data/2.5/forecast?units=imperial&lat={lat_long['latitude']}&lon={lat_long['longitude']}&appid={config.open_weather_api_key}" + ) + return response.json()["list"] + + +async def get_air_quality(lat_long): + async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.openweathermap.org/data/2.5/air_pollution?lat={lat_long['latitude']}&lon={lat_long['longitude']}&appid={config.open_weather_api_key}" + ) + return response.json() + + +@app.get("/htmx.org@1.9.8", include_in_schema=False) +@cache() +async def get_htmx(request: Request): + return FileResponse("static/htmx.org@1.9.8") + + +@app.get("/favicon.ico", include_in_schema=False) +@cache() +async def get_favicon(request: Request): + return FileResponse("static/favicon.ico") + + +@app.get("/app3.css", include_in_schema=False) +@cache() +async def get_app_css(request: Request): + """ + return app.css for development updates + """ + return FileResponse("static/app.css") + + +def make_text_response(request, data, color): + template_response = config.templates.TemplateResponse( + "index.txt", {"request": request, "color": color, **data} + ) + + return PlainTextResponse(template_response.body, status_code=200) + + +@app.get("/") +@no_cache +async def get_home(request: Request, color: bool = False, n_forecast: int = None): + data = await get_data(request, n_forecast) + print(n_forecast) + print(len(data["forecast"])) + if request.state.prefers_html: + return config.templates.TemplateResponse( + "index.html", {"request": request, **data} + ) + else: + return make_text_response(request, data, color=color) + + +def hours_till_sunset(weather): + if "sys" not in weather: + return "" + if "sunset" not in weather["sys"]: + return "" + + sunset = ( + datetime.fromtimestamp(weather["sys"]["sunset"]) + - datetime.fromtimestamp(weather["dt"]) + ).total_seconds() / 60 + + if sunset < 0: + return "it is after sunset" + elif sunset > 120: + return "" + elif sunset > 60: + return f"Time till sunset is {round(int(sunset)/60)} hours. " + else: + return f"Time till sunset is {round(int(sunset))} minutes. " + + +ANSI_RED = r"\x1b[1;31m" +ANSI_GREEN = r"\x1b[1;32m" +ANSI_YELLOW = r"\x1b[1;33m" + + +@dataclass +class PlayCondition: + message: str = "" + color: str = "bg-green-500" + ansi_color: str = ANSI_GREEN + + +def determine_play_condition(weather, aqi=0): + play_condition = PlayCondition() + + feels_like_temperature = weather["main"]["feels_like"] + # visibility = weather["visibility"] + + play_condition.message += hours_till_sunset(weather) + + if "after" in play_condition.message: + play_condition.color = "bg-red-500" + play_condition.ansi_color = ANSI_RED + + # if visibility < 1000: + # play_condition.message += "It's too foggy. Find better activities inside!" + # play_condition.color = "bg-red-500" + # play_condition.ansi_color = ANSI_RED + + if aqi > 150: + play_condition.message += "It's too polluted. Find better activities inside!" + play_condition.color = "bg-red-500" + play_condition.ansi_color = ANSI_RED + elif aqi > 100: + play_condition.message += "limit your time outside due to the poor air quality" + play_condition.color = "bg-yellow-500" + play_condition.ansi_color = ANSI_YELLOW + elif aqi > 50: + play_condition.message += "Check the air quality outside at your discression." + play_condition.color = "bg-yellow-500" + play_condition.ansi_color = ANSI_YELLOW + else: + play_condition.message += "" + + if feels_like_temperature < 10: + play_condition.message += "It's too cold. Stay indoors and keep warm!" + play_condition.color = "bg-red-500" + play_condition.ansi_color = ANSI_RED + elif feels_like_temperature < 30: + play_condition.message += ( + "You can play outside, but limit your time in this cold!" + ) + play_condition.color = "bg-yellow-500" + play_condition.ansi_color = ANSI_YELLOW + elif feels_like_temperature < 40: + play_condition.message += ( + "Coats and winter gear required for outdoor play. Stay cozy!" + ) + elif feels_like_temperature < 50: + play_condition.message += "Grab a warm jacket and enjoy your time outside!" + elif feels_like_temperature < 60: + play_condition.message += "Grab some long sleeves and enjoy your time outside!" + elif feels_like_temperature > 90: + play_condition.message += ( + "You can play outside, but limit your time in this heat!" + ) + play_condition.color = "bg-yellow-500" + play_condition.ansi_color = ANSI_YELLOW + elif feels_like_temperature > 109: + play_condition.message += ( + "It's too hot for outdoor play. Find cooler activities indoors!" + ) + play_condition.color = "bg-red-500" + play_condition.ansi_color = ANSI_RED + else: + play_condition.message += "Enjoy your time outside!" + return play_condition + + +async def get_data(request: Request, n_forecast: int = None): + user_ip = request.headers.get("CF-Connecting-IP") + lat_long = await get_lat_long(user_ip) + weather = await get_weather(lat_long) + forecast = await get_forecast(lat_long) + if n_forecast is not None: + forecast = forecast[: n_forecast + 2] + air_quality = await get_air_quality(lat_long) + weather["play_condition"] = determine_play_condition( + weather, + air_quality["list"][0]["main"]["aqi"], + ) + + forecast = [ + {"play_condition": determine_play_condition(x), **x} + for x in forecast + if datetime.fromtimestamp(x["dt"]).hour >= 6 + and datetime.fromtimestamp(x["dt"]).hour <= 21 + ] + + return { + "request.client": request.client, + "request.client.host": request.client.host, + "user_ip": user_ip, + "lat_long": lat_long, + "weather": weather, + "forecast": forecast, + "air_quality": air_quality, + "sunset": weather["sys"]["sunset"], + } + + +@app.get("/metadata") +async def root( + request: Request, +): + return await get_data(request) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8100) diff --git a/play_outside/cli/api.py b/play_outside/cli/api.py new file mode 100644 index 0000000..2906903 --- /dev/null +++ b/play_outside/cli/api.py @@ -0,0 +1,133 @@ +from pathlib import Path + +import typer +import uvicorn +from rich.console import Console +from play_outside.config import get_config + +api_app = typer.Typer() + + +@api_app.callback() +def api(): + "model cli" + + +@api_app.command() +def config( + env: str = typer.Option( + help="the environment to use", + ), +): + play_outside_config = get_config(env) + Console().print(play_outside_config) + + +@api_app.command() +def upgrade( + env: str = typer.Option( + help="the environment to use", + ), + alembic_revision: str = typer.Option( + "head", + help="the alembic revision to use", + ), +): + play_outside_config = get_config(env) + Console().print(play_outside_config) + alembic_cfg = Config("alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", play_outside_config.database_url) + alembic.command.upgrade(config=alembic_cfg, revision=alembic_revision) + + +@api_app.command() +def revision( + env: str = typer.Option( + help="the environment to use", + ), + alembic_revision: str = typer.Option( + "head", + help="the alembic revision to upgrade to before creating a new revision", + ), + message: str = typer.Option( + None, + "--message", + "-m", + help="the message to use for the new revision", + ), +): + play_outside_config = get_config(env) + Console().print(play_outside_config) + alembic_cfg = Config("alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", play_outside_config.database_url) + alembic.command.upgrade(config=alembic_cfg, revision=alembic_revision) + alembic.command.revision( + config=alembic_cfg, + message=message, + autogenerate=True, + ) + alembic.command.upgrade(config=alembic_cfg, revision="head") + + +@api_app.command() +def datasette( + env: str = typer.Option( + help="the environment to use", + ), + alembic_revision: str = typer.Option( + "head", + help="the alembic revision to use", + ), +): + from datasette.app import Datasette + + play_outside_config = get_config(env) + Console().print(play_outside_config) + alembic_cfg = Config("alembic.ini") + alembic_cfg.set_main_option("sqlalchemy.url", play_outside_config.database_url) + alembic.command.upgrade(config=alembic_cfg, revision=alembic_revision) + + ds = Datasette( + files=[Path(play_outside_config.database_url.replace("sqlite:///", ""))], + ) + uvicorn.run( + ds.app(), **play_outside_config.datasette_server.dict(exclude={"app", "reload"}) + ) + + +@api_app.command() +def run( + env: str = typer.Option( + help="the environment to use", + ), + alembic_revision: str = typer.Option( + "head", + help="the alembic revision to use", + ), +): + play_outside_config = get_config(env) + Console().print(play_outside_config) + # alembic_cfg = Config("alembic.ini") + # alembic_cfg.set_main_option("sqlalchemy.url", play_outside_config.database_url) + # alembic.command.upgrade(config=alembic_cfg, revision=alembic_revision) + uvicorn.run(**play_outside_config.api_server.dict()) + + +@api_app.command() +def justrun( + env: str = typer.Option( + "local", + help="the environment to use", + ), + alembic_revision: str = typer.Option( + "head", + help="the alembic revision to use", + ), +): + play_outside_config = get_config(env) + Console().print(play_outside_config) + uvicorn.run(**play_outside_config.api_server.dict()) + + +if __name__ == "__main__": + api_app() diff --git a/play_outside/config.py b/play_outside/config.py new file mode 100644 index 0000000..93e11bd --- /dev/null +++ b/play_outside/config.py @@ -0,0 +1,98 @@ +import pydantic +from dotenv import load_dotenv +from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import BaseModel +from pydantic import Field +from typing import Any +from fastapi.templating import Jinja2Templates +from urllib.parse import quote_plus +from typing import Optional +from datetime import datetime +from datetime import timezone +from functools import lru_cache +import os +import jinja2 + +from rich.console import Console + +console = Console() + + +class ApiServer(BaseModel): + app: str = "play_outside.api:app" + port: int = 8200 + reload: bool = True + log_level: str = "info" + host: str = "0.0.0.0" + workers: int = 1 + forwarded_allow_ips: str = "*" + proxy_headers: bool = True + + +if hasattr(jinja2, "pass_context"): + pass_context = jinja2.pass_context +else: + pass_context = jinja2.contextfunction + + +@pass_context +def https_url_for(context: dict, name: str, **path_params: Any) -> str: + request = context["request"] + http_url = request.url_for(name, **path_params) + return str(http_url).replace("http", "https", 1) + + +def get_templates(config: BaseSettings) -> Jinja2Templates: + templates = Jinja2Templates(directory="templates") + templates.env.filters["quote_plus"] = lambda u: quote_plus(str(u)) + templates.env.filters["timestamp"] = lambda u: datetime.fromtimestamp( + u, tz=timezone.utc + ).strftime("%B %d, %Y") + templates.env.globals["https_url_for"] = https_url_for + templates.env.globals["config"] = config + templates.env.globals["datetime"] = datetime + templates.env.globals["len"] = len + templates.env.globals["int"] = int + console.print(f'Using environment: {os.environ.get("ENV")}') + + if os.environ.get("ENV") in ["dev", "qa", "prod"]: + templates.env.globals["url_for"] = https_url_for + console.print("Using HTTPS") + else: + console.print("Using HTTP") + + return templates + + +class Config(BaseSettings): + env: str = "prod" + open_weather_api_key: Optional[str] = None + api_server: ApiServer = ApiServer() + the_templates: Optional[Jinja2Templates] = Field(None, exclude=True) + model_config = SettingsConfigDict( + env_prefix="PLAY_OUTSIDE_", + env_nested_delimiter="__", + case_sensitive=False, + ) + + @property + def templates(self) -> Jinja2Templates: + if self.the_templates is None: + self.the_templates = get_templates(self) + return self.the_templates + + @pydantic.validator("open_weather_api_key", pre=True, always=True) + def validate_open_weather_api_key(cls, v): + if v is None: + v = os.getenv("OPEN_WEATHER_API_KEY") + return v + + +@lru_cache +def get_config(env: Optional[str] = None): + if env is None: + env = os.environ.get("ENV", "prod") + load_dotenv(dotenv_path=f".env.{env}") + config = Config(env=env) + return config diff --git a/play_outside/decorators.py b/play_outside/decorators.py new file mode 100644 index 0000000..0da3e3f --- /dev/null +++ b/play_outside/decorators.py @@ -0,0 +1,104 @@ +import inspect +import time +from functools import wraps +from inspect import signature + +from fastapi import Request +from fastapi.responses import JSONResponse + +from play_outside.config import get_config + +config = get_config() + + +admin_routes = [] +not_cached_routes = [] +cached_routes = [] + + +def not_found(request): + hx_request_header = request.headers.get("hx-request") + user_agent = request.headers.get("user-agent", "").lower() + + if "mozilla" in user_agent or "webkit" in user_agent or hx_request_header: + return config.templates.TemplateResponse( + "error.html", + {"status_code": 404, "detail": "Not Found", "request": request}, + status_code=404, + ) + else: + return JSONResponse( + content={ + "status_code": 404, + "detail": "Not Found", + }, + status_code=404, + ) + + +def no_cache(func): + not_cached_routes.append(f"{func.__module__}.{func.__name__}") + + @wraps(func) + async def wrapper(*args, request: Request, **kwargs): + # my_header will be now available in decorator + if "request" in signature(func).parameters: + kwargs["request"] = request + + if inspect.iscoroutinefunction(func): + response = await func(*args, **kwargs) + else: + response = func(*args, **kwargs) + + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + return response + + return wrapper + + +def cache(max_age=86400): + def inner_wrapper(func): + cached_routes.append(f"{func.__module__}.{func.__name__}") + + @wraps(func) + async def wrapper(*args, request: Request, **kwargs): + if "request" in signature(func).parameters: + kwargs["request"] = request + if inspect.iscoroutinefunction(func): + response = await func(*args, **kwargs) + else: + response = func(*args, **kwargs) + response.headers[ + "Cache-Control" + ] = f"public, max-age={max_age}, stale-while-revalidate=31536000, stale-if-error=31536000" + response.headers["Expires"] = f"{int(time.time()) + max_age}" + + return response + + return wrapper + + return inner_wrapper + + +default_data = {} + + +def defaults(data=default_data): + def inner_wrapper(func): + default_data[f"{func.__module__}.{func.__name__}"] = data + + @wraps(func) + async def wrapper(*args, request: Request, **kwargs): + if "request" in signature(func).parameters: + kwargs["request"] = request + if inspect.iscoroutinefunction(func): + response = await func(*args, **kwargs) + else: + response = func(*args, **kwargs) + return response + + return wrapper + + return inner_wrapper diff --git a/play_outside/queries.py b/play_outside/queries.py new file mode 100644 index 0000000..bdbcc12 --- /dev/null +++ b/play_outside/queries.py @@ -0,0 +1,4 @@ +import httpx + + +# get weather from open weather api diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c9e9cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,79 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["play_outside"] + +[project] +name = "play-outside" +dynamic = ["version"] +description = 'Use Open Weather Api to determine if Kids can play outside.' +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +keywords = [] +authors = [{ name = "Waylon S. Walker", email = "waylon@waylonwalker.com" }] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + 'httpx', + 'fastapi', + 'uvicorn[standard]', + 'typer', + 'python-dotenv', + 'rich', + 'pydantic-settings', + 'jinja2', + 'arel', +] + + +[project.urls] +Documentation = "https://github.com/unknown/play-outside#readme" +Issues = "https://github.com/unknown/play-outside/issues" +Source = "https://github.com/unknown/play-outside" + +[project.scripts] +play-outside = "play_outside.cli.api:api_app" + +[tool.hatch.version] +path = "play_outside/__about__.py" + +[tool.hatch.envs.default] +dependencies = ["coverage[toml]>=6.5", "pytest"] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = ["- coverage combine", "coverage report"] +cov = ["test-cov", "cov-report"] + +[[tool.hatch.envs.all.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.types] +dependencies = ["mypy>=1.0.0"] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:play_outside tests}" + +[tool.coverage.run] +source_pkgs = ["play_outside", "tests"] +branch = true +parallel = true +omit = ["play_outside/__about__.py"] + +[tool.coverage.paths] +play_outside = ["play_outside", "*/play-outside/play_outside"] +tests = ["tests", "*/play-outside/tests"] + +[tool.coverage.report] +exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] diff --git a/static/app.css b/static/app.css new file mode 100644 index 0000000..12a829e --- /dev/null +++ b/static/app.css @@ -0,0 +1,822 @@ +/* +! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-\[-1rem\] { + margin-top: -1rem; + margin-bottom: -1rem; +} + +.my-\[-4rem\] { + margin-top: -4rem; + margin-bottom: -4rem; +} + +.mb-0 { + margin-bottom: 0px; +} + +.mt-24 { + margin-top: 6rem; +} + +.ml-\[10rem\] { + margin-left: 10rem; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.list-none { + list-style-type: none; +} + +.rounded-3xl { + border-radius: 1.5rem; +} + +.border-2 { + border-width: 2px; +} + +.border-green-500 { + --tw-border-opacity: 1; + border-color: rgb(34 197 94 / var(--tw-border-opacity)); +} + +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + +.border-yellow-500 { + --tw-border-opacity: 1; + border-color: rgb(234 179 8 / var(--tw-border-opacity)); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity)); +} + +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + +.bg-yellow-500 { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +.bg-zinc-900 { + --tw-bg-opacity: 1; + background-color: rgb(24 24 27 / var(--tw-bg-opacity)); +} + +.bg-opacity-10 { + --tw-bg-opacity: 0.1; +} + +.bg-gradient-to-r { + background-image: linear-gradient(to right, var(--tw-gradient-stops)); +} + +.from-red-600 { + --tw-gradient-from: #dc2626 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(220 38 38 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.via-pink-500 { + --tw-gradient-to: rgb(236 72 153 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), #ec4899 var(--tw-gradient-via-position), var(--tw-gradient-to); +} + +.to-yellow-400 { + --tw-gradient-to: #facc15 var(--tw-gradient-to-position); +} + +.bg-clip-text { + -webkit-background-clip: text; + background-clip: text; +} + +.p-4 { + padding: 1rem; +} + +.pb-0 { + padding-bottom: 0px; +} + +.text-center { + text-align: center; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-8xl { + font-size: 6rem; + line-height: 1; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-black { + font-weight: 900; +} + +.leading-loose { + line-height: 2; +} + +.leading-tight { + line-height: 1.25; +} + +.text-transparent { + color: transparent; +} + +.text-zinc-100 { + --tw-text-opacity: 1; + color: rgb(244 244 245 / var(--tw-text-opacity)); +} + +.ring-red-500 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity)); +} + +html { + scroll-behavior: smooth; + --tw-bg-opacity: 1; + background-color: rgb(39 39 42 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +html:-webkit-autofill { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +html:autofill { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +::-webkit-scrollbar { + height: 1rem; + width: 1rem; +} + +::-webkit-scrollbar-track { + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(24 24 27 / var(--tw-bg-opacity)); +} + +body::-webkit-scrollbar-track { + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(219 39 119 / var(--tw-bg-opacity)); +} + +::-webkit-scrollbar-thumb { + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(82 82 91 / var(--tw-bg-opacity)); +} + +::-webkit-scrollbar-thumb:hover { + --tw-bg-opacity: 1; + background-color: rgb(113 113 122 / var(--tw-bg-opacity)); +} + +body::-webkit-scrollbar-thumb { + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(6 182 212 / var(--tw-bg-opacity)); +} + +body::-webkit-scrollbar-thumb:hover { + --tw-bg-opacity: 1; + background-color: rgb(34 211 238 / var(--tw-bg-opacity)); +} + +.autofill\:bg-yellow-500:-webkit-autofill { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + +.autofill\:bg-yellow-500:autofill { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..566ebd9c00f6d14b6c568753266bf195a9fc20ec GIT binary patch literal 15406 zcmeHNO^h5z748X0fRrc(5u^kpR>HBJO}yUOS=$OI!ANj3mn2rM5H3E&l5deOL|H+E zPq{<{OY8-KGe6E+LY6t9STT|7lI`d6XCLU%H2OZMa7a1@h(A%GhmDP(<*x`z&}~ zcMlb!B+$_cJHG+%t(8&v9Cfe-v}1gv2HBONdk*zmgHd^s3WEw3i4a#0pKoCE!|$4y?1emIO!f0FY>Ipr;K9*@$2iQ~EzRkDFBaG0gVvUMa_b$nG{loI(UrOn%VbDdeh za|lmQN+@JLV$GVgW#PAks*HI1F??_X<9CDQjOvOM#QY22l^SEgdqJDhccsjiVEa5C z#J|je^V%+Q)t8d-o~6;S=8B0<^!p<1GoD$_iFlgYZRfCjF#SJab@+0x zpe%T3Ry9{$qW6!=z@{pG@;5!a{r_k@kxW*}f+}qf%nkC=Z17Ciu{ecZ@Ggl@r zm%!SG{!LA!bqZLl>Nc{A4gjDv57km z`HNW7F5rEl7uFM$-*2^Ft-X>{(mtP*pE|@obfSC$4{dPoIFCG7_E5@zCbE3XnudVj z+*WO{8 z@WWrp`UBlI_G{x1?W*rH>kn5~W78BZS$$Aje{*(~rtgTSl=au^v_@339fyj>lH?B9 zzqyC{ea!8FKb!jxZ0fwM#zV%*??0IHvJXR7HiwC3#o0s2M}Q4i!mAV53Ub>jcDW_WXGBAcQswm z{i7*4Ezfw0m*-5>Z?2pvpQ0`c)t1@T_E|aOo}!MZ6S1M^NRdetEv&n*qCY+M-m&qIK%PI{t}ZfNt7Qp4^)`GV@d&E3a6!RT3CmjC59QZ+SeXs}R07|4Vl~vGd{Th@#`#NS6V^&8 z8!}8vY7CZ5$IANlCdGpTU7PsUa-vl77CnDY{5}?0V%T~Ms1Gfpuj{ID_wYU>gS>L z-*qnn_ia3yZ`>wrs*WtMED>rvJ+`6{%h5=a5}RfeaDINHch;%S80 z3K2W&$^OcGWWAwvwyK7RC$tHIZkyW5S*;bb;ICpwbH*lqt0ysSo7yl}7Mt8k8T`?G z6Km>=gAKPAIUnwTn^GwSzBlm`axl(!}8%uHbw4RAaev`aL3CD{yU^qa%suySsc2DX(P(QHtz6$$Xq#78uqr4wvx0EajnlRK_kVhlWV|d;je4t6lPG z(W6}bv*iHmk6ohx=-VC}^dz<~U?g@?C zD4R2CXp9s1{}dkjysi^DFlg8!yJ|CvZ83;b7wq_4*x17FF87Fmab?A(mZ4Koan3^b z2MzpVcNesSO+Bu+%$#`SzK^raOR)cQKFE__5}Mr8He(sTVI6=z>a6(*ad%PH4Kbvs zSg~ba^^}B;^8@DaccC|pqkkNnxbKMC@YUaNi@^F@*t`ax{R?sPAIN=%vj85HNkbdj z-yyGv1#|m`?%Np~o3yQWF4k6~oWf2Rh!u7xcs>G-zvHZh--X~utabR!?nz>T{f7TQ ztNGK^?^1@ceoe-m|Iqz}y20}iZKeK6AkAHsGvLteL@a(ohL87l6ZbM~@l0Nwy%lpj zgSYjeyeG2Iq3a`c^Q(A1#nX0I(dPSd}tiThpb@A$x$n)AkIe|P1Vcn`ln-4E_mma!!B zVQ%EUzq5PjIber#mG?>0{T}0A@1QFFtn9>bKD;x(=er`ueFxS(tcNY@OcQ77;|}$^ zac%7mtIlZy$(DIfjrCpcb|RMO=gPmaZPplN+GPxS%{{IiBwA`X^PIeE0PHPfuwQD$&-*2gLH({qaY_96vxof8Mm~OK_G9jKHO3EPw>5r%nQe{v%lR?(^Oti+ zm!@?7PVtHMe|zs_{)Thx5n=+~bv~TGTytjL=`jeyr&>R|#|><4rw{e@Qymy^Bvpks z`;9#Xc$hnE2UAVP_bVS)+#`9m*fvh|q)pc64>+g9H8YRlKKl4T@&}JU--|E?)tNr_ zB^j42eQJN16C!_0J`qMNO!06cYpX##oELfKs>Q_ob=w#D?L6eq)@V|;T}Ya;)f_+1 zFF0!eubjQQ;`TA$+Gjbx__J^p$+n4dx5WFIKe>avCkW^E>ijr=oTTGs&X2AqcM-4! zzwFW6Wjc5`Ip=2xg*&}pBgf#~67&8|-V=zv`uQF6!N!C2b3(8BTVfoqaxC@i%Xj#JGr^;aI22U{TuH()7%Mn9C8Q2PHgA?#N&_mCwk_kPT=MJi5(y2ZEv9s_iq(8 z@n72)*??>CF7k?D=I)U9Gq~eO#rqrOVpyA!`zy)$s{1Rf@3}M9;63*}tjqrt{M8uo z|D5g-i_VQMy#S6SJk6XHz5#vQem`UG50L{ikL1I<#A)}3^aXW-car#D#sW5(>nv41 zo8AlD+c8(O2d)kquY!v_*Kl@GUwsVjc!)iM)oK&H0n_AwsU`;E}cMbfY)tG7( z*tMo()e>t09V+w6R{U5yUcmZyJN5zPng74h0nvkGsn8m&fyi?;)=hu6ggY9prMo~g ZoUxeIgVr{B6zs@j`M)TC;epfx{{=0)){return"unset"}else{return n}}function re(t,r){var n=null;c(t,function(e){return n=R(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=te().createDocumentFragment()}return i}function H(e){return e.match(/",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",3);case"script":case"style":return i("
"+e+"
",1);default:return i(e,0)}}}function ne(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ie(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function oe(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return te().body.contains(e.getRootNode().host)}else{return te().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function se(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){y(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return xr(te().body,function(){return eval(e)})}function t(t){var e=Y.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Y.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){Y.logger=null}function E(e,t){if(t){return e.querySelector(t)}else{return E(te(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(te(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);ae(e.parentElement.children,function(e){n(e,t)});F(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[E(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return te().querySelectorAll(z(t))}}var $=function(e,t){var r=te().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function le(e,t){if(t){return W(e,t)[0]}else{return W(te().body,e)[0]}}function s(e){if(L(e,"String")){return E(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:te().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Pr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Pr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var he=te().createElement("output");function de(e,t){var r=re(e,t);if(r){if(r==="this"){return[ve(e,t)]}else{var n=W(e,r);if(n.length===0){y('The selector "'+r+'" on '+t+" returned no matches!");return[he]}else{return n}}}}function ve(e,t){return c(e,function(e){return ee(e,t)!=null})}function ge(e){var t=re(e,"hx-target");if(t){if(t==="this"){return ve(e,"hx-target")}else{return le(e,t)}}else{var r=ie(e);if(r.boosted){return te().body}else{return e}}}function me(e){var t=Y.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=te().querySelectorAll(t);if(r){ae(r,function(e){var t;var r=i.cloneNode(true);t=te().createDocumentFragment();t.appendChild(r);if(!xe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!fe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}ae(a.elts,function(e){fe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ue(te().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=re(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Y.config.addedClass);Dt(e);Ct(e);Ce(e);fe(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Y.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Ue(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Be(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}fe(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=xr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ue(te().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if($e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function x(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Je="input, textarea, select";function Ze(e){var t=ee(e,"hx-trigger");var r=[];if(t){var n=We(t);do{x(n,ze);var i=n.length;var a=x(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};x(n,ze);o.pollInterval=v(x(n,/[,\[\s]/));x(n,ze);var s=Ge(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ge(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){x(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(x(n,p))}else if(u==="from"&&n[0]===":"){n.shift();var f=x(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();var c=x(n,p);if(c.length>0){f+=" "+c}}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=x(n,p)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(x(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=x(n,p)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=x(n,p)}else{ue(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ue(e,"htmx:syntax:error",{token:n.shift()})}x(n,ze)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,Je)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ke(e){ie(e).cancelled=true}function Ye(e,t,r){var n=ie(e);n.timeout=setTimeout(function(){if(oe(e)&&n.cancelled!==true){if(!nt(r,e,Ut("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ye(e,t,r)}},r.pollInterval)}function Qe(e){return location.hostname===e.hostname&&Q(e,"href")&&Q(e,"href").indexOf("#")!==0}function et(t,r,e){if(t.tagName==="A"&&Qe(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=Q(t,"href")}else{var a=Q(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=Q(t,"action")}e.forEach(function(e){it(t,function(e,t){if(d(e,Y.config.disableSelector)){m(e);return}ce(n,i,e,t)},r,e,true)})}}function tt(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function rt(e,t){return ie(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function nt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ue(te().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function it(a,o,e,s,l){var u=ie(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ie(e);t.lastValue=e.value})}ae(t,function(n){var i=function(e){if(!oe(a)){n.removeEventListener(s.trigger,i);return}if(rt(a,e)){return}if(l||tt(e,a)){e.preventDefault()}if(nt(s,a,e)){return}var t=ie(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ie(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{fe(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var at=false;var ot=null;function st(){if(!ot){ot=function(){at=true};window.addEventListener("scroll",ot);setInterval(function(){if(at){at=false;ae(te().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){lt(e)})}},200)}}function lt(t){if(!o(t,"data-hx-revealed")&&k(t)){t.setAttribute("data-hx-revealed","true");var e=ie(t);if(e.initHash){fe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){fe(t,"revealed")},{once:true})}}}function ut(e,t,r){var n=P(r);for(var i=0;i=0){var t=dt(n);setTimeout(function(){ft(s,r,n+1)},t)}};t.onopen=function(e){n=0};ie(s).webSocket=t;t.addEventListener("message",function(e){if(ct(s)){return}var t=e.data;C(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=I(n.children);for(var a=0;a0){fe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(tt(e,u)){e.preventDefault()}})}else{ue(u,"htmx:noWebSocketSourceError")}}function dt(e){var t=Y.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}y('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function vt(e,t,r){var n=P(r);for(var i=0;i0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Nt(o)}for(var l in r){It(e,l,r[l])}}}function Pt(t){Re(t);for(var e=0;eY.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ue(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function _t(e){if(!M()){return null}e=D(e);var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){fe(te().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Vt();var r=T(t);var n=Xe(this.response);if(n){var i=E("title");if(i){i.innerHTML=n}else{window.document.title=n}}Pe(t,e,r);Jt(r.tasks);Ft=a;fe(te().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ue(te().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Kt(e){Wt();e=e||location.pathname+location.search;var t=_t(e);if(t){var r=l(t.content);var n=Vt();var i=T(n);Pe(n,r,i);Jt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Ft=e;fe(te().body,"htmx:historyRestore",{path:e,item:t})}else{if(Y.config.refreshOnHistoryMiss){window.location.reload(true)}else{Zt(e)}}}function Yt(e){var t=de(e,"hx-indicator");if(t==null){t=[e]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Y.config.requestClass)});return t}function Qt(e){var t=de(e,"hx-disabled-elt");if(t==null){t=[]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function er(e,t){ae(e,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Y.config.requestClass)}});ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function tr(e,t){for(var r=0;r=0}function dr(e,t){var r=t?t:re(e,"hx-swap");var n={swapStyle:ie(e).boosted?"innerHTML":Y.config.defaultSwapStyle,swapDelay:Y.config.defaultSwapDelay,settleDelay:Y.config.defaultSettleDelay};if(Y.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hr(e)){n["show"]="top"}if(r){var i=P(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}else if(a==0){n["swapStyle"]=o}else{y("Unknown modifier in hx-swap: "+o)}}}}return n}function vr(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&Q(e,"enctype")==="multipart/form-data"}function gr(t,r,n){var i=null;C(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(vr(r)){return ur(n)}else{return lr(n)}}}function T(e){return{tasks:[],elts:[e]}}function mr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=le(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=le(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Y.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Y.config.scrollBehavior})}}}function pr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=ee(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=xr(e,function(){return Function("return ("+a+")")()},{})}else{s=S(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return pr(u(e),t,r,n)}function xr(e,t,r){if(Y.config.allowEval){return t()}else{ue(e,"htmx:evalDisallowedError");return r}}function yr(e,t){return pr(e,"hx-vars",true,t)}function br(e,t){return pr(e,"hx-vals",false,t)}function wr(e){return se(yr(e),br(e))}function Sr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Er(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ue(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return e.getAllResponseHeaders().match(t)}function Cr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return ce(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ce(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ce(e,t,null,null,{returnPromise:true})}}function Tr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Or(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Y.config.selfRequestsOnly){if(!n){return false}}return fe(e,"htmx:validateUrl",se({url:i,sameHost:n},r))}function ce(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=te().body}var M=a.handler||qr;if(!oe(n)){ne(o);return l}var u=a.targetOverride||ge(n);if(u==null||u==he){ue(n,"htmx:targetError",{target:ee(n,"hx-target")});ne(s);return l}var f=ie(n);var c=f.lastButtonClicked;if(c){var h=Q(c,"formaction");if(h!=null){r=h}var d=Q(c,"formmethod");if(d!=null){if(d.toLowerCase()!=="dialog"){t=d}}}var v=re(n,"hx-confirm");if(e===undefined){var D=function(e){return ce(t,r,n,i,a,!!e)};var X={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:v};if(fe(n,"htmx:confirm",X)===false){ne(o);return l}}var g=n;var m=re(n,"hx-sync");var p=null;var x=false;if(m){var U=m.split(":");var B=U[0].trim();if(B==="this"){g=ve(n,"hx-sync")}else{g=le(n,B)}m=(U[1]||"drop").trim();f=ie(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ne(o);return l}else if(m==="abort"){if(f.xhr){ne(o);return l}else{x=true}}else if(m==="replace"){fe(g,"htmx:abort")}else if(m.indexOf("queue")===0){var F=m.split(" ");p=(F[1]||"last").trim()}}if(f.xhr){if(f.abortable){fe(g,"htmx:abort")}else{if(p==null){if(i){var y=ie(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){p=y.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){ce(t,r,n,i,a)})}ne(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var V=re(n,"hx-prompt");if(V){var S=prompt(V);if(S===null||!fe(n,"htmx:prompt",{prompt:S,target:u})){ne(o);w();return l}}if(v&&!e){if(!confirm(v)){ne(o);w();return l}}var E=fr(n,u,S);if(a.headers){E=se(E,a.headers)}var j=or(n,t);var C=j.errors;var T=j.values;if(a.values){T=se(T,a.values)}var _=wr(n);var z=se(T,_);var O=cr(z,n);if(t!=="get"&&!vr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(Y.config.getCacheBusterParam&&t==="get"){O["org.htmx.cache-buster"]=Q(u,"id")||"true"}if(r==null||r===""){r=te().location.href}var R=pr(n,"hx-request");var W=ie(n).boosted;var q=Y.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:O,unfilteredParameters:z,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||R.credentials||Y.config.withCredentials,timeout:a.timeout||R.timeout||Y.config.timeout,path:r,triggeringEvent:i};if(!fe(n,"htmx:configRequest",H)){ne(o);w();return l}r=H.path;t=H.verb;E=H.headers;O=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){fe(n,"htmx:validation:halted",H);ne(o);w();return l}var $=r.split("#");var G=$[0];var L=$[1];var A=r;if(q){A=G;var J=Object.keys(O).length!==0;if(J){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=lr(O);if(L){A+="#"+L}}}if(!Or(n,A,H)){ue(n,"htmx:invalidPath",H);ne(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(R.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var Z=E[N];Sr(b,N,Z)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Tr(n);I.pathInfo.responsePath=Er(b);M(n,I);er(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:afterOnLoad",I);if(!oe(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(oe(r)){t=r}}if(t){fe(t,"htmx:afterRequest",I);fe(t,"htmx:afterOnLoad",I)}}ne(o);w()}catch(e){ue(n,"htmx:onLoadError",se({error:e},I));throw e}};b.onerror=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendError",I);ne(s);w()};b.onabort=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendAbort",I);ne(s);w()};b.ontimeout=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:timeout",I);ne(s);w()};if(!fe(n,"htmx:beforeRequest",I)){ne(o);w();return l}var k=Yt(n);var P=Qt(n);ae(["loadstart","loadend","progress","abort"],function(t){ae([b,b.upload],function(e){e.addEventListener(t,function(e){fe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});fe(n,"htmx:beforeSend",I);var K=q?null:gr(b,n,O);b.send(K);return l}function Rr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=re(e,"hx-push-url");var l=re(e,"hx-replace-url");var u=ie(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function qr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;if(!fe(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){Be(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){Wt();var r=f.getResponseHeader("HX-Location");var h;if(r.indexOf("{")===0){h=S(r);r=h["path"];delete h["path"]}Cr("GET",r,h).then(function(){$t(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){u.target=te().querySelector(f.getResponseHeader("HX-Retarget"))}var d=Rr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var a=f.status>=400;var g=Y.config.ignoreTitle;var o=se({shouldSwap:i,serverResponse:v,isError:a,ignoreTitle:g},u);if(!fe(c,"htmx:beforeSwap",o))return;c=o.target;v=o.serverResponse;a=o.isError;g=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){Ke(l)}C(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Wt()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var h=dr(l,s);if(h.hasOwnProperty("ignoreTitle")){g=h.ignoreTitle}c.classList.add(Y.config.swappingClass);var m=null;var p=null;var x=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=T(c);Ue(h.swapStyle,c,l,v,n,r);if(t.elt&&!oe(t.elt)&&Q(t.elt,"id")){var i=document.getElementById(Q(t.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!Y.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Y.config.swappingClass);ae(n.elts,function(e){if(e.classList){e.classList.add(Y.config.settlingClass)}fe(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!oe(l)){o=te().body}Be(f,"HX-Trigger-After-Swap",o)}var s=function(){ae(n.tasks,function(e){e.call()});ae(n.elts,function(e){if(e.classList){e.classList.remove(Y.config.settlingClass)}fe(e,"htmx:afterSettle",u)});if(d.type){fe(te().body,"htmx:beforeHistoryUpdate",se({history:d},u));if(d.type==="push"){$t(d.path);fe(te().body,"htmx:pushedIntoHistory",{path:d.path})}else{Gt(d.path);fe(te().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=te().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!g){var t=E("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}mr(n.elts,h);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!oe(l)){r=te().body}Be(f,"HX-Trigger-After-Settle",r)}ne(m)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ue(l,"htmx:swapError",u);ne(p);throw e}};var y=Y.config.globalViewTransitions;if(h.hasOwnProperty("transition")){y=h.transition}if(y&&fe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var b=new Promise(function(e,t){m=e;p=t});var w=x;x=function(){document.startViewTransition(function(){w();return b})}}if(h.swapDelay>0){setTimeout(x,h.swapDelay)}else{x()}}if(a){ue(l,"htmx:responseError",se({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Hr={};function Lr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ar(e,t){if(t.init){t.init(r)}Hr[e]=se(Lr(),t)}function Nr(e){delete Hr[e]}function Ir(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=ee(e,"hx-ext");if(t){ae(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Hr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Ir(u(e),r,n)}var kr=false;te().addEventListener("DOMContentLoaded",function(){kr=true});function Pr(e){if(kr||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Mr(){if(Y.config.includeIndicatorStyles!==false){te().head.insertAdjacentHTML("beforeend","")}}function Dr(){var e=te().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function Xr(){var e=Dr();if(e){Y.config=se(Y.config,e)}}Pr(function(){Xr();Mr();var e=te().body;Dt(e);var t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ie(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Kt();ae(t,function(e){fe(e,"htmx:restored",{document:te(),triggerEvent:fe})})}else{if(r){r(e)}}};setTimeout(function(){fe(e,"htmx:load",{});e=null},0)});return Y}()}); \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..b56aa7a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,17 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["templates/**/*.html"], + plugins: [require("@tailwindcss/typography")], + theme: { + extend: { + width: { + 128: "32rem", + }, + boxShadow: { + xlc: "0 0 60px 15px rgba(0, 0, 0, 0.3)", + lgc: "0 0 20px 0px rgba(0, 0, 0, 0.3)", + }, + + }, + }, +}; diff --git a/tailwind/app.css b/tailwind/app.css new file mode 100644 index 0000000..a0ccb27 --- /dev/null +++ b/tailwind/app.css @@ -0,0 +1,28 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + scroll-behavior: smooth; + @apply bg-zinc-800 text-white autofill:bg-yellow-500; +} + +::-webkit-scrollbar { + @apply h-4 w-4; +} + +::-webkit-scrollbar-track { + @apply rounded-full bg-zinc-900; +} + +body::-webkit-scrollbar-track { + @apply rounded-full bg-pink-600; +} + +::-webkit-scrollbar-thumb { + @apply rounded-full bg-zinc-600 hover:bg-zinc-500; +} + +body::-webkit-scrollbar-thumb { + @apply rounded-full bg-cyan-500 hover:bg-cyan-400; +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..705aef3 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,49 @@ + + + + {% block head %} + + {% block title %}Play Outside{% endblock %} + + + + + + + + + + + + + + + + + + + + {% if config.env == 'local' %} + {{ hot_reload.script(url_for('hot-reload') ) | safe }} + {% endif %} + {% endblock %} + + +
+

+ Disclaimer +

+

+ I use this site to determine if my kids can play outside, each kid is different, use your own discretion before allowing kids outside. +

+
+ {% block body %} + {{ content | safe }} + {% endblock %} + + diff --git a/templates/card.html b/templates/card.html new file mode 100644 index 0000000..af30adf --- /dev/null +++ b/templates/card.html @@ -0,0 +1,70 @@ +{% if datetime.fromtimestamp(weather.dt).hour == 6 %} + +
+

+ {{ datetime.fromtimestamp(weather.dt).strftime(format = '%A') }} +
+ + {{ datetime.fromtimestamp(weather.dt).strftime(format = '%Y-%m-%d') }} + +

+ + +
    + {% endif %} +
  • + {{ datetime.fromtimestamp(weather.dt).strftime(format = '%I:%M %p') }} +

    +
    +

    + + {{ weather.play_condition.message.replace('.', '
    ') | safe }} +

    + +

    + {{ weather.name }} +

    +

    + Feels Like: {{ weather.main.feels_like }} +

    +
      + {% for w in weather.weather %} +
    • +

      + {{ w.description }} + +

      +
    • + {% endfor %} +
    +

    + Air Quality: {{ air_quality.list.0.main.aqi }} +

    +

    + Sunset: {{ datetime.fromtimestamp(sunset) }} +

    + {% if weather.sys.sunset %} + {% if (datetime.fromtimestamp(weather.sys.sunset) - datetime.fromtimestamp(weather.dt)).total_seconds() > 0 + %} + +

    + Time till sunset: {{ datetime.fromtimestamp(weather.sys.sunset) - datetime.fromtimestamp(weather.dt) }} +

    + {% else %} +

    + Its after sunset +

    + {% endif %} + {% endif %} +

    + Visibility: {{ weather.visibility }} +

    + {% if weather.wind.speed > 5 %} +

    + Wind Speed: {{ weather.wind.speed }} +

    + {% endif %} +
    +
  • diff --git a/templates/card.txt b/templates/card.txt new file mode 100644 index 0000000..559c3b1 --- /dev/null +++ b/templates/card.txt @@ -0,0 +1,12 @@ +{% set w = int((len(weather.play_condition.message) - len(datetime.fromtimestamp(weather.dt).strftime(format = '%A %I:%M %p')))-1) %} +╭─ {{ datetime.fromtimestamp(weather.dt).strftime(format = '%A %I:%M %p')}} {{ '─'*w }}╮ +│ {%if color %}{{ weather.play_condition.ansi_color }}{%endif%}{{ weather.play_condition.message | safe }}{%if color%}\033[0m{%endif%} │ +╰─{{ '─'* len(weather.play_condition.message) }}─╯ +{{ weather.name }} +Feels Like: {{ weather.main.feels_like }} +Air Quality: {{ air_quality.list.0.main.aqi }} +Sunset: {{ datetime.fromtimestamp(sunset).strftime(format = '%I:%M %p') }} +Visibility: {{ weather.visibility }} +{% if weather.wind.speed > 5 -%} +Wind Speed: {{ weather.wind.speed }} +{% endif -%} diff --git a/templates/includestyles.html b/templates/includestyles.html new file mode 100644 index 0000000..345bfd7 --- /dev/null +++ b/templates/includestyles.html @@ -0,0 +1,6 @@ +
    +
    +
    +
    +
    +
    diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ceb2a30 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block body %} +

    Currently

    +
      + {% include "card.html" %} +
    +

    5 Day Forecast

    +
      + {% for weather in forecast %} + {% include "card.html" %} + {% endfor %} +
    +{% endblock %} diff --git a/templates/index.txt b/templates/index.txt new file mode 100644 index 0000000..8e009e0 --- /dev/null +++ b/templates/index.txt @@ -0,0 +1,4 @@ +{% include "card.txt" %} +{% for weather in forecast %} + {% include "card.txt" %} +{% endfor %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a2123ed --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT