b895663c3a
Adds the customer-facing /book/<slug> page: service/slot picker, booking form, and confirmation screen, built on #16's existing booking JSON API. Includes iframe auto-fit height reporting (mirroring deploy/booking/booking_layout.js's eaBookingHeight message), brand-color theming via a ?color= query param, a honeypot field with a fake-success response indistinguishable from a real booking, and a clear "just taken" message on slot-conflict. Caddy per-IP rate limiting is documented in deploy/booking/RATE_LIMIT.md for manual application (no Caddyfile is tracked in this repo). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
42 lines
1.7 KiB
Python
42 lines
1.7 KiB
Python
"""Public booking page blueprint (#17): the customer-facing /book/<slug> page,
|
|
embedded via iframe into a client's landing page. This route only resolves a
|
|
slug to a client and renders the slot-grid/booking-form page around it -- all
|
|
booking mutations still go through booking_api.py's JSON API (#16), which the
|
|
page's own JS calls via fetch.
|
|
|
|
IP rate limiting on these public routes is enforced at the Caddy layer (per
|
|
#17's acceptance criteria), not in Flask -- see deploy/booking/RATE_LIMIT.md
|
|
for the Caddy config to apply by hand on the homelab.
|
|
"""
|
|
from flask import Blueprint, abort, render_template, request
|
|
|
|
import booking_db as bdb
|
|
|
|
bp = Blueprint("public_booking", __name__)
|
|
|
|
DEFAULT_BRAND_COLOR = "#0f8a7e"
|
|
|
|
|
|
@bp.get("/book/<slug>")
|
|
def book_page(slug):
|
|
client = bdb.get_client_by_slug(slug)
|
|
if client is None:
|
|
abort(404)
|
|
resources = bdb.list_active_resources(client["client_id"])
|
|
services = bdb.list_active_services(client["client_id"])
|
|
if not resources or not services:
|
|
# No bookable services/resources configured yet -- nothing to show a
|
|
# customer rather than a broken/empty booking form.
|
|
abort(404)
|
|
return render_template(
|
|
"book.html",
|
|
client_id=client["client_id"],
|
|
business_name=client.get("business_name") or slug,
|
|
resources=[{"resource_id": r["resource_id"], "name": r["name"]} for r in resources],
|
|
services=[{"service_id": s["service_id"], "name": s["name"],
|
|
"duration_minutes": s["duration_minutes"],
|
|
"price": float(s["price"]) if s["price"] is not None else None}
|
|
for s in services],
|
|
brand_color=request.args.get("color") or DEFAULT_BRAND_COLOR,
|
|
)
|