mirror of
https://github.com/hanzoai/cal.git
synced 2026-10-16 20:43:01 +00:00
A booking link fills in the attendee, and the Booker ships as a site
/{owner}/{slug}?name=&email=¬es= starts the details step filled in,
as a Cal.com link does: hanzo.ai's sales page sends its visitors here
with the answers they gave. The page reads them once and takes them off
the address before anything reports it.
That needs @hanzo/booker 8.3.6's prefill, so the app moves to the 8.x
line: @hanzo/gui 8.3.5 and React 19. The committed lockfile no longer
matched package.json and is regenerated.
cal.hanzo.ai is served from the Sites plane slug cal-web, and nothing
published there: the image lane pinned a values file universe deleted,
and the forge copy runs no workflows. hanzo.yml gains the site lane and
.github/workflows calls hanzoai/ci; the Dockerfile and the dead lanes go.
Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
# Keep the build context lean and let the image build its own deps/bundle.
|
||||
# node_modules is re-installed in-container (host symlinks/arch would not port);
|
||||
# dist is rebuilt by `pnpm build` in stage 1.
|
||||
**/node_modules
|
||||
**/dist
|
||||
*.tsbuildinfo
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
deploy
|
||||
LLM.md
|
||||
@@ -0,0 +1,19 @@
|
||||
name: CI/CD
|
||||
|
||||
# The caller and nothing else: hanzo.yml says how this repo gates and ships,
|
||||
# and hanzoai/ci reads it. The hanzoai org's runners serve github.com, so this
|
||||
# is the plane that builds cal.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cicd:
|
||||
uses: hanzoai/ci/.github/workflows/build.yml@v1
|
||||
secrets: inherit
|
||||
@@ -1,15 +0,0 @@
|
||||
# Canonical caller — every knob lives in /hanzo.yml, none here.
|
||||
# Runs on the git-runner fleet at git.hanzo.ai, the only pool serving these
|
||||
# labels. github.com resolves only .github/workflows and has no runner for
|
||||
# them, so a caller placed there is a gate that cannot be scheduled.
|
||||
name: CI/CD
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
cicd:
|
||||
uses: hanzoai/ci/.github/workflows/build.yml@v1
|
||||
secrets: inherit
|
||||
@@ -1,172 +0,0 @@
|
||||
# Build the image, prove it pulls, and pin it. This is the whole lane.
|
||||
#
|
||||
# The number universe pins is DERIVED, never a sha: a CR that names a commit
|
||||
# cannot be read as "which release is live", and pin.sh in universe refuses a
|
||||
# sha outright — so a sha-only build (what this used to be) produced images no
|
||||
# reviewed pin could ever consume. imgver gives the one number every builder on
|
||||
# this fleet uses: max(declared app/package.json, published ghcr) + a patch.
|
||||
#
|
||||
# cal's tag carries a leading v — its git tags and its ghcr tags are v0.2.x, and
|
||||
# pin.sh honours whatever convention the values file already holds. So imgver's
|
||||
# bare number gets a v at the two places a human reads it (the image tag and the
|
||||
# git tag); universe's pin.sh re-adds it from cal-web.yaml's own spelling.
|
||||
#
|
||||
# The pin is cal-web, NOT cal: values/hanzo/cal.yaml is the booking BACKEND on a
|
||||
# different image and is frozen by a ratchet. cal-web.yaml is this web image.
|
||||
name: deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-cal-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Must name a label a runner ACTUALLY advertises. The git-runners offer
|
||||
# linux-amd64; a label nothing offers does not fail, it queues
|
||||
# until the 24h timeout — silence, not an error.
|
||||
runs-on: [linux-amd64]
|
||||
steps:
|
||||
# Full history + tags: this job TAGS the release, so it needs the tag
|
||||
# namespace to push into and a non-shallow clone to describe from.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
# One implementation (hanzoai/ci bin/imgver), every builder. cal declares
|
||||
# its floor in app/, so the published tag at ghcr carries the series.
|
||||
- id: ver
|
||||
uses: hanzoai/ci/.github/actions/imgver@v1
|
||||
with: { repo: ghcr.io/hanzoai/cal }
|
||||
env: { GH_PAT: '${{ secrets.GH_PAT }}' }
|
||||
|
||||
# Stamp before the build so the tree that gets tagged is byte-identical to
|
||||
# the tree that was built. cal's version lives in app/, not the repo root.
|
||||
- name: Stamp app/package.json with the release version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
sed -i.bak -E "s/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/\1${v}\2/" app/package.json && rm -f app/package.json.bak
|
||||
echo "app/package.json -> $(grep -m1 '\"version\"' app/package.json)"
|
||||
|
||||
# Kept alongside the semver, for forensics only. Nothing pins it.
|
||||
- id: meta
|
||||
run: echo "tag=sha-$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with: { driver: docker-container, driver-opts: network=host }
|
||||
|
||||
# Without this the push is unauthenticated and 403s at the registry.
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USER }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
provenance: false
|
||||
tags: |
|
||||
ghcr.io/hanzoai/cal:v${{ steps.ver.outputs.version }}
|
||||
ghcr.io/hanzoai/cal:${{ steps.meta.outputs.tag }}
|
||||
secrets: |
|
||||
GIT_AUTH_TOKEN=${{ secrets.GH_PAT }}
|
||||
|
||||
# build-push-action can exit 0 before the manifest resolves, so a green run
|
||||
# could still mean a future ImagePullBackOff. Prove it pulls BEFORE the pin
|
||||
# moves — pinning an image the registry cannot serve has no rollback path.
|
||||
- name: Verify the pushed image resolves
|
||||
run: |
|
||||
set -euo pipefail
|
||||
img="ghcr.io/hanzoai/cal:v${{ steps.ver.outputs.version }}"
|
||||
for i in 1 2 3 4 5 6; do
|
||||
docker buildx imagetools inspect "$img" >/dev/null 2>&1 && { echo "resolved $img"; exit 0; }
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::pushed image never resolved: $img"; exit 1
|
||||
|
||||
# THE STEP THAT SHIPS. Everything above publishes an image; cd.hanzo.ai
|
||||
# reconciles from universe's values files and never reads the registry, so
|
||||
# an image nothing points at is just bytes in ghcr. pin.sh resolves tag AND
|
||||
# digest from ONE lookup and writes them together, so the pod cannot report
|
||||
# a new version while serving old bytes.
|
||||
- name: Ship it
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://api.hanzo.ai' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
# pin.sh probes the registry before it moves anything; these let it read
|
||||
# cal's private manifest instead of falling back to anonymous.
|
||||
GHCR_USER: ${{ secrets.GHCR_USER }}
|
||||
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.ver.outputs.version }}"
|
||||
|
||||
# Secrets come from KMS, never from a file or a repo variable. The org
|
||||
# is carried by the credential, not by the path.
|
||||
KMS_TOKEN=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
[ -n "$KMS_TOKEN" ] || { echo "::error::KMS login failed at ${KMS_ENDPOINT} — image v${VERSION} is pushed but NOT deployed"; exit 1; }
|
||||
|
||||
PIN_TOKEN=$(curl -fsS \
|
||||
"${KMS_ENDPOINT}/v1/kms/secrets/deploy/UNIVERSE_PIN_TOKEN?env=${KMS_SECRET_ENV}" \
|
||||
-H "Authorization: Bearer ${KMS_TOKEN}" | jq -r '.value // empty')
|
||||
[ -n "$PIN_TOKEN" ] || { echo "::error::UNIVERSE_PIN_TOKEN missing in KMS (path deploy, env ${KMS_SECRET_ENV}) — image v${VERSION} is pushed but NOT deployed"; exit 1; }
|
||||
echo "::add-mask::${PIN_TOKEN}"
|
||||
|
||||
# cd reads THIS copy. universe also exists under hanzoai/ and is a
|
||||
# different, staler commit — pinning there deploys nothing, silently.
|
||||
git clone --quiet --depth 1 \
|
||||
"https://x-access-token:${PIN_TOKEN}@git.hanzo.ai/hanzo/universe" /tmp/universe
|
||||
|
||||
# Another service can land its own pin between our clone and our push.
|
||||
# A shallow clone has no common history to rebase onto, so RE-APPLY
|
||||
# rather than merge: pinning is idempotent and derives everything from
|
||||
# the file it finds, so the retry re-checks the monotonic guard too.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
/tmp/universe/charts/app/pin.sh cal-web "$VERSION"
|
||||
|
||||
if git -C /tmp/universe push --quiet origin HEAD:main 2>/dev/null; then
|
||||
echo "pinned cal-web to v${VERSION} — cd.hanzo.ai rolls it out"; exit 0
|
||||
fi
|
||||
|
||||
echo "universe moved under us (attempt ${attempt}/5) — re-applying on the new tip"
|
||||
git -C /tmp/universe fetch --quiet --depth 1 origin main
|
||||
git -C /tmp/universe reset --hard --quiet FETCH_HEAD
|
||||
done
|
||||
echo "::error::image v${VERSION} shipped but the pin never landed — cal is published and unreachable"; exit 1
|
||||
|
||||
# ONLY after the image is proven pullable and pinned, so a git tag can never
|
||||
# name a version the registry lacks. Bookkeeping is last so it cannot
|
||||
# un-ship a good build. universe pins the image, not this tag.
|
||||
- name: Tag the version this image ships
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
git config user.name 'hanzo-ci'
|
||||
git config user.email '[email protected]'
|
||||
# Idempotent: a run that shipped its image and then died leaves the tag
|
||||
# already there, and `git tag` on an existing name is fatal.
|
||||
if git rev-parse -q --verify "refs/tags/v${v}" >/dev/null; then
|
||||
echo "tag v${v} already exists"
|
||||
else
|
||||
git tag "v${v}"
|
||||
git push origin "v${v}"
|
||||
fi
|
||||
echo "v${v} tagged"
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# cal.hanzo.ai — Cal's <Booker> (debranded as @hanzo/cal) as a static SPA,
|
||||
# served by hanzoai/spa (Go static server, :3000). Self-contained multi-stage:
|
||||
# stage 1 compiles the Vite bundle with the pinned pnpm; stage 2 serves it. A
|
||||
# fresh `docker build .` needs no pre-built dist on the host (app/dist is
|
||||
# gitignored), so CI and local builds are identical.
|
||||
|
||||
# --- stage 1: build app/dist ---
|
||||
FROM node:22-slim AS build
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
# Manifests first: the install layer caches independently of source edits.
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY app/package.json app/package.json
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
# Sources, then build → app/dist. @hanzo/cal is workspace TS source that
|
||||
# Vite transpiles; @calcom/atoms is consumed as its pre-built ESM bundle.
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# --- stage 2: serve the bundle with the Hanzo static SPA server ---
|
||||
# hanzoai/spa falls back to index.html for any client-routed path so the SPA
|
||||
# router resolves /{owner}/{slug} and /booking/{uid} on the client.
|
||||
FROM ghcr.io/hanzoai/spa:sha-b9d3a13-amd64
|
||||
COPY --from=build /app/app/dist /public
|
||||
# Ship the fork attribution alongside the served bundle (MIT @calcom/atoms).
|
||||
COPY NOTICE /NOTICE
|
||||
ENV PORT=3000
|
||||
ENV ROOT=/public
|
||||
# Allow same-origin + inline bootstrap (React hydration) and connect-src https:
|
||||
# so the browser can reach the Base booking API at cal.hanzo.ai/v1/calendar.
|
||||
# The variable is SPA_CSP: that is the name hanzoai/spa reads, and the
|
||||
# HANZO_STATIC_CSP spelled here before was read by nothing.
|
||||
#
|
||||
# It is still read by nothing. hanzoai/spa sets Content-Security-Policy only on
|
||||
# the branch where framing is refused, so with ALLOW_FRAMING=true it sends no
|
||||
# policy at all — the whole header, on every path, not just frame-ancestors.
|
||||
# Confirmed against ghcr.io/hanzoai/spa:sha-b9d3a13: `curl -sI /` returns no CSP
|
||||
# and no X-Frame-Options. Making this value ship takes a change to that shared
|
||||
# image — set the CSP unconditionally and let framing choose frame-ancestors
|
||||
# rather than choose whether there is a policy — and the value below is written
|
||||
# to be correct the day it does.
|
||||
ENV SPA_CSP="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-src 'self'; media-src 'self' https:; frame-ancestors *"
|
||||
# /embed is meant to be framed by other people's pages, which hanzoai/spa
|
||||
# otherwise refuses with X-Frame-Options: DENY + frame-ancestors 'none'. The
|
||||
# allowance is per-server, so it covers /booking/<uid> too — and a uid IS a
|
||||
# credential: it reads the attendee and cancels the meeting. What framing could
|
||||
# steal there is a click on Cancel, which is why cancelling takes two.
|
||||
ENV ALLOW_FRAMING=true
|
||||
EXPOSE 3000
|
||||
@@ -8,27 +8,27 @@ booking is created in Base.
|
||||
- `app` — the Vite + React SPA (`@hanzo/cal-app`). Routes: `/` landing,
|
||||
`/{owner}/{slug}` the Booker, `/booking/{uid}` confirmation + cancel, `/embed`
|
||||
the framed Booker a third-party page loads.
|
||||
- `Dockerfile` — self-contained multi-stage: stage 1 builds the Vite bundle with
|
||||
the pinned pnpm; stage 2 is `FROM ghcr.io/hanzoai/spa` and copies the bundle to
|
||||
`/public` (SPA fallback). A fresh `docker build .` needs no pre-built `app/dist`
|
||||
(it is gitignored), so CI and local builds are identical.
|
||||
- `.hanzo/workflows/deploy.yml` — native CI on the self-hosted pool
|
||||
`hanzo-build-linux-amd64`; builds and pushes `ghcr.io/hanzoai/cal:sha-<short>`.
|
||||
It never deploys: the rollout is a reviewed tag pin on the `cal` App CR in
|
||||
`hanzoai/universe`, and cd.hanzo.ai's selfHeal would undo anything CI patched.
|
||||
- `hanzo.yml` — the gate (frozen install + build) and the site lane, which
|
||||
publishes `app/dist` to the Sites plane slug `cal-web`. universe serves that
|
||||
prefix at cal.hanzo.ai (`cal-hanzo-ai-static`) with the response headers in
|
||||
`cal-hanzo-ai-headers`, and routes `/v1/calendar` to the Base backend. The
|
||||
caller is `.github/workflows/cicd.yml`. There is no image.
|
||||
- `deploy/mirror-to-forge.sh` — server-side mirror-IN resync (idempotent).
|
||||
|
||||
## The UI package
|
||||
`@hanzo/booker` (published from `hanzoai/gui/pkgs/calendar`) — a universal
|
||||
(web + native) Booker built on hanzogui primitives, pinned at `^0.1.0`. It is
|
||||
NOT Cal's atoms; it re-implements the flow and speaks Cal's API-v2 shapes.
|
||||
(web + native) Booker built on hanzogui primitives, at `^8.3.6` on `@hanzo/gui`
|
||||
8.3.5 and React 19. It is NOT Cal's atoms; it re-implements the flow and speaks
|
||||
Cal's API-v2 shapes. 8.3.6 added `prefill`, the name, email and notes the
|
||||
details step starts with.
|
||||
|
||||
Two build facts follow from it and are easy to trip over:
|
||||
- `@hanzo/booker` imports its peer under the name `hanzogui`. Vite aliases that
|
||||
to `@hanzo/gui`; `app/tsconfig.json` repeats the alias in `paths`, or the type
|
||||
checker cannot resolve it. Change one, change both.
|
||||
- Do not chase `@hanzo/booker@8.x`: it peer-requires `@hanzo/gui >= 8`, and the
|
||||
app is pinned to the 7.3.0 line. 8.1.1 adds no props 0.1.0 lacks.
|
||||
- `/{owner}/{slug}?name=&email=¬es=` fills the details step, as a Cal.com
|
||||
link does; hanzo.ai's sales page sends its visitors here that way. The page
|
||||
reads the three once and takes them off the address before anything reports
|
||||
it, because `@hanzo/event` stamps the URL on every event.
|
||||
- The app's own `gui.ts` and `main.tsx` import `hanzogui`, which Vite aliases to
|
||||
`@hanzo/gui`; `app/tsconfig.json` repeats the alias in `paths`. Change one,
|
||||
change both.
|
||||
|
||||
## Backend contract
|
||||
`app/src/api.ts` resolves the base (`VITE_CAL_API`, default `/v1/calendar`), and
|
||||
@@ -69,18 +69,10 @@ and cancels the booking, so it never leaves the booking page.
|
||||
full-viewport overlay with no script at all; the bar is drawn in the loader's
|
||||
own DOM under `all:initial !important`, which a scriptless injection cannot
|
||||
remove, and it says whose dialog it is.
|
||||
- `ALLOW_FRAMING=true` is set in the Dockerfile because `ghcr.io/hanzoai/spa`
|
||||
otherwise sends `X-Frame-Options: DENY` + `frame-ancestors 'none'` and the
|
||||
embed silently refuses to render. **It also means cal serves no CSP at all.**
|
||||
That server sets the header only on the branch where framing is refused, so
|
||||
with framing allowed the whole policy is dropped on every path — measured
|
||||
against `sha-b9d3a13`: no CSP and no X-Frame-Options on `/` or `/booking/x`.
|
||||
The value is spelled `SPA_CSP`, the name the server actually reads
|
||||
(`HANZO_STATIC_CSP` was read by nothing), and it ships the day that image sets
|
||||
the header unconditionally and lets framing pick `frame-ancestors` instead of
|
||||
picking whether there is a policy at all. `SPA_*` is also the runtime-config
|
||||
namespace there, so the value is additionally templated into a public
|
||||
`/config.json` — public either way, but worth knowing.
|
||||
- Framing is decided by the edge now, not by this repo: the Sites plane
|
||||
answers with universe's `cal-hanzo-ai-headers`, which today send
|
||||
`X-Frame-Options: DENY`, so a third-party page cannot frame `/embed` until
|
||||
that middleware lets it.
|
||||
- Framing reaches `/booking/<uid>` too, since the allowance is per-server. The
|
||||
uid is the capability that reads the attendee and cancels the meeting, so
|
||||
cancelling takes two clicks, and `Keep it` is the one that sits where the first
|
||||
@@ -136,6 +128,6 @@ from `useBooker`'s `selectSlot` / `submit` / the failed-submit branch — not a
|
||||
fork of the UI here and not a wrapper around `fetch`.
|
||||
|
||||
## Homing
|
||||
Canonical origin is the forge, `git.hanzo.ai/hanzoai/cal`, which is also the CI
|
||||
executor. (The repo is `cal`, not `calendar`, because `hanzoai/calendar` is a
|
||||
GitHub (`github.com/hanzoai/cal`) is home and builds it; the forge copy is a
|
||||
mirror. (The repo is `cal`, not `calendar`, because `hanzoai/calendar` is a
|
||||
full fork of `calcom/cal.diy` — a different artifact.)
|
||||
|
||||
+8
-8
@@ -10,19 +10,19 @@
|
||||
"preview": "vite preview --host --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/booker": "^0.1.0",
|
||||
"@hanzo/booker": "^8.3.6",
|
||||
"@hanzo/event": "^0.3.21",
|
||||
"@hanzo/font": "^1.8.11",
|
||||
"@hanzo/gui": "7.3.0",
|
||||
"@hanzogui/config": "7.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native-web": "^0.20.0",
|
||||
"@hanzo/gui": "8.3.5",
|
||||
"@hanzogui/config": "8.3.5",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"react-native-web": "^0.21.2",
|
||||
"react-router-dom": "6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "5.4.11"
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
import { Booker } from "@hanzo/booker";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { calendarApi } from "../api";
|
||||
import { booked, useShown } from "../analytics";
|
||||
|
||||
/** The query names a link may fill the details step with, as a Cal.com link does. */
|
||||
const PREFILL = ["name", "email", "notes"] as const;
|
||||
|
||||
// The public booking page: the native Hanzo <Booker> (universal Tamagui, on
|
||||
// @hanzo/booker) for /{owner}/{slug}, pointed at the native /v1/calendar backend.
|
||||
// On success we route to the confirmation view by the new booking's uid.
|
||||
//
|
||||
// `?name=`, `?email=` and `?notes=` fill in the details step: hanzo.ai's sales
|
||||
// page sends its visitor here with the answers they already gave. They are read
|
||||
// once and taken off the address before anything reports the page, because
|
||||
// @hanzo/event stamps the URL on every event and an attendee is not the
|
||||
// warehouse's to keep.
|
||||
export function Booking() {
|
||||
const { owner = "", slug = "" } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const target = { host: owner, eventSlug: slug };
|
||||
const [prefill] = useState(() => {
|
||||
const q = new URLSearchParams(window.location.search);
|
||||
return Object.fromEntries(PREFILL.map((k) => [k, q.get(k) ?? undefined]));
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (!PREFILL.some((k) => url.searchParams.has(k))) return;
|
||||
for (const k of PREFILL) url.searchParams.delete(k);
|
||||
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
||||
}, []);
|
||||
|
||||
useShown("page", target);
|
||||
|
||||
@@ -19,6 +40,7 @@ export function Booking() {
|
||||
host={owner}
|
||||
eventSlug={slug}
|
||||
apiUrl={calendarApi}
|
||||
prefill={prefill}
|
||||
onBooked={(booking) => {
|
||||
booked("page", target);
|
||||
if (booking?.uid) navigate(`/booking/${booking.uid}`);
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
# cal.hanzo.ai — the Booker SPA over Hanzo Base /v1/calendar.
|
||||
#
|
||||
# Gate only. The image for this repo is built and pushed by the native builder
|
||||
# already in this repo (.hanzo/workflows/deploy.yml → ghcr.io/hanzoai/cal:<sha>);
|
||||
# declaring `images:` here would push a second, differently-tagged copy of the
|
||||
# same commit. What was missing is the assertion, so that is what this adds:
|
||||
# the workspace installs against the committed lockfile and every package builds.
|
||||
# One gate and one lane. The gate installs against the committed lockfile and
|
||||
# builds; the site lane publishes app/dist to the Sites plane slug `cal-web`,
|
||||
# which universe serves at cal.hanzo.ai (static-sites.yaml, cal-hanzo-ai-static)
|
||||
# beside /v1/calendar on the Base backend. There is no image: nothing in the
|
||||
# cluster runs this SPA.
|
||||
test:
|
||||
- name: build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
|
||||
site:
|
||||
slug: cal-web
|
||||
dir: app/dist
|
||||
build: corepack enable && pnpm install --frozen-lockfile && pnpm build
|
||||
on: [main]
|
||||
|
||||
Generated
+1866
-5979
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user