All routes moved under /sx/ prefix: - / redirects to /sx/ - /sx/ serves home page - /sx/<path:expr> is the catch-all for SX expression URLs - Bare /(...) and /~... redirect to /sx/(...) and /sx/~... - All ~600 hrefs, sx-get attrs, defhandler paths, redirect targets, and blueprint routes updated across 44 files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
6.8 KiB
Python
166 lines
6.8 KiB
Python
"""SX docs page routes.
|
|
|
|
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage.
|
|
Example API endpoints are now defined in sx/handlers/examples.sx via defhandler.
|
|
This file contains only SSE and marsh demo endpoints that need Python.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import random
|
|
from datetime import datetime
|
|
|
|
from quart import Blueprint, Response, request
|
|
|
|
|
|
def register(url_prefix: str = "/") -> Blueprint:
|
|
bp = Blueprint("pages", __name__, url_prefix=url_prefix)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Reference API endpoints — remaining Python-only
|
|
#
|
|
# Most reference endpoints migrated to sx/sx/handlers/ref-api.sx.
|
|
# SSE stays in Python — fundamentally different paradigm (async generator).
|
|
# ------------------------------------------------------------------
|
|
|
|
@bp.get("/sx/(geography.(hypermedia.(reference.(api.sse-time))))")
|
|
async def ref_sse_time():
|
|
async def generate():
|
|
for _ in range(30): # stream for 60 seconds max
|
|
now = datetime.now().strftime("%H:%M:%S")
|
|
sx_src = f'(span :class "text-emerald-700 font-mono text-sm" "Server time: {now}")'
|
|
yield f"event: time\ndata: {sx_src}\n\n"
|
|
await asyncio.sleep(2)
|
|
return Response(generate(), content_type="text/event-stream",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
|
|
|
# --- Marsh demos ---
|
|
|
|
_marsh_sale_idx = {"n": 0}
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.flash-sale)))")
|
|
async def api_marsh_flash_sale():
|
|
from shared.sx.helpers import sx_response
|
|
prices = [14.99, 9.99, 24.99, 12.49, 7.99, 29.99, 4.99, 16.50]
|
|
_marsh_sale_idx["n"] = (_marsh_sale_idx["n"] + 1) % len(prices)
|
|
new_price = prices[_marsh_sale_idx["n"]]
|
|
now = datetime.now().strftime("%H:%M:%S")
|
|
sx_src = (
|
|
f'(<>'
|
|
f' (div :class "space-y-2"'
|
|
f' (p :class "text-sm text-emerald-600 font-medium"'
|
|
f' "\u26A1 Flash sale! Price: ${new_price:.2f}")'
|
|
f' (p :class "text-xs text-stone-400" "at {now}"))'
|
|
f' (script :type "text/sx" :data-init ""'
|
|
f' "(reset! (use-store \\"demo-price\\") {new_price})"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
# --- Demo 3: sx-on-settle endpoint ---
|
|
|
|
_settle_counter = {"n": 0}
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.settle-data)))")
|
|
async def api_settle_data():
|
|
from shared.sx.helpers import sx_response
|
|
_settle_counter["n"] += 1
|
|
items = ["Widget", "Gadget", "Sprocket", "Gizmo", "Doohickey"]
|
|
item = items[_settle_counter["n"] % len(items)]
|
|
now = datetime.now().strftime("%H:%M:%S")
|
|
sx_src = (
|
|
f'(div :class "space-y-1"'
|
|
f' (p :class "text-sm font-medium text-stone-700" "Fetched: {item}")'
|
|
f' (p :class "text-xs text-stone-400" "at {now}"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
# --- Demo 4: signal-bound URL endpoints ---
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.search-products)))")
|
|
async def api_search_products():
|
|
from shared.sx.helpers import sx_response
|
|
q = request.args.get("q", "")
|
|
items = ["Artisan Widget", "Premium Gadget", "Handcrafted Sprocket",
|
|
"Bespoke Gizmo", "Organic Doohickey"]
|
|
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
rows = " ".join(
|
|
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
for m in matches[:3]
|
|
)
|
|
sx_src = (
|
|
f'(div :class "space-y-1"'
|
|
f' (p :class "text-xs font-semibold text-violet-600 uppercase" "Products")'
|
|
f' (ul :class "list-disc pl-4" {rows})'
|
|
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.search-events)))")
|
|
async def api_search_events():
|
|
from shared.sx.helpers import sx_response
|
|
q = request.args.get("q", "")
|
|
items = ["Summer Workshop", "Craft Fair", "Open Studio",
|
|
"Artist Talk", "Gallery Opening"]
|
|
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
rows = " ".join(
|
|
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
for m in matches[:3]
|
|
)
|
|
sx_src = (
|
|
f'(div :class "space-y-1"'
|
|
f' (p :class "text-xs font-semibold text-emerald-600 uppercase" "Events")'
|
|
f' (ul :class "list-disc pl-4" {rows})'
|
|
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.search-posts)))")
|
|
async def api_search_posts():
|
|
from shared.sx.helpers import sx_response
|
|
q = request.args.get("q", "")
|
|
items = ["On Craft and Code", "The SX Manifesto", "Islands and Lakes",
|
|
"Reactive Marshes", "Self-Hosting Spec"]
|
|
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
rows = " ".join(
|
|
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
for m in matches[:3]
|
|
)
|
|
sx_src = (
|
|
f'(div :class "space-y-1"'
|
|
f' (p :class "text-xs font-semibold text-amber-600 uppercase" "Posts")'
|
|
f' (ul :class "list-disc pl-4" {rows})'
|
|
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
# --- Demo 5: marsh transform endpoint ---
|
|
|
|
@bp.get("/sx/(geography.(reactive.(api.catalog)))")
|
|
async def api_catalog():
|
|
from shared.sx.helpers import sx_response
|
|
items = [
|
|
("Artisan Widget", "19.99", "Hand-crafted with care"),
|
|
("Premium Gadget", "34.50", "Top-of-the-line quality"),
|
|
("Vintage Sprocket", "12.99", "Classic design"),
|
|
("Custom Gizmo", "27.00", "Made to order"),
|
|
]
|
|
random.shuffle(items)
|
|
now = datetime.now().strftime("%H:%M:%S")
|
|
# Build an SX list literal for the data-init script.
|
|
# Inner quotes must be escaped since the whole expression lives
|
|
# inside an SX string literal (the script tag's text content).
|
|
items_sx = "(list " + " ".join(
|
|
f'(dict \\"name\\" \\"{n}\\" \\"price\\" \\"{p}\\" \\"desc\\" \\"{d}\\")'
|
|
for n, p, d in items
|
|
) + ")"
|
|
sx_src = (
|
|
f'(<>'
|
|
f' (p :class "text-sm text-emerald-600 font-medium"'
|
|
f' "Catalog loaded: {len(items)} items (shuffled at {now})")'
|
|
f' (script :type "text/sx" :data-init ""'
|
|
f' "(reset! (use-store \\"catalog-items\\") {items_sx})"))'
|
|
)
|
|
return sx_response(sx_src)
|
|
|
|
return bp
|