feat: cal.hanzo.ai — Cal's Booker on native Hanzo Base

The public booking SPA: Cal.com's real <Booker> UI powered entirely by the
native Hanzo Base backend at /v1/calendar (no Cal Prisma/Postgres/Node API).

- packages/calendar: @hanzo/calendar, the debranded fork surface — a thin
  Hanzo-namespaced wrapper over published @calcom/atoms (MIT, see NOTICE) that
  re-exports Provider/Booker under Hanzo names for public, no-OAuth booking.
- app: Vite + React SPA. /{owner}/{slug} renders the Booker against Base;
  /booking/{uid} is the confirmation + cancel view.
- Dockerfile: FROM ghcr.io/hanzoai/spa, COPY app/dist /public.

Free-booking Stripe peers are stubbed so the bundle is self-contained/CSP-clean.
Only MIT @calcom code; Cal copyright preserved in NOTICE.
This commit is contained in:
2026-07-22 13:45:14 -07:00
commit 87aba71aaf
24 changed files with 4380 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
app/dist/
*.tsbuildinfo
.DS_Store
.env
.env.local
+13
View File
@@ -0,0 +1,13 @@
# cal.hanzo.ai — Cal's <Booker> (debranded as @hanzo/calendar) as a static SPA,
# served by hanzoai/spa (Go static server, :3000). `pnpm build` writes the Vite
# bundle to app/dist. The booking (/{host}/{event}) and manage (/booking/{uid})
# routes are client-rendered SPA shells; hanzoai/spa falls back to index.html for
# any client-routed path so the router resolves them and the atoms fetch on mount.
FROM ghcr.io/hanzoai/spa:sha-b9d3a13-amd64
COPY app/dist /public
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.
ENV HANZO_STATIC_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:"
EXPOSE 3000
+34
View File
@@ -0,0 +1,34 @@
# cal.hanzo.ai — Cal's Booker on native Hanzo Base
Public booking page: Cal.com's real `<Booker>` UI, powered entirely by the native
Hanzo Base backend at `/v1/calendar`. No Cal Prisma/Postgres, no Cal Node API — the
booking is created in Base.
## Layout
- `packages/calendar` — **`@hanzo/calendar`**, the debranded fork surface. A thin
Hanzo-namespaced wrapper over the published `@calcom/atoms` (MIT — see `NOTICE`):
re-exports `Provider` (Cal's `CalProvider`, defaulted for public no-OAuth booking)
and `Booker` under Hanzo names, and owns the atoms' style side-effect import. The
SPA imports ONLY from here, never `@calcom/atoms` directly.
- `app` — the Vite + React SPA (`@hanzo/cal-app`). Routes: `/` landing,
`/{owner}/{slug}` the Booker, `/booking/{uid}` confirmation + cancel.
- `Dockerfile` — `FROM ghcr.io/hanzoai/spa`, `COPY app/dist /public` (SPA fallback).
## Backend contract
`<Provider apiUrl>` sets `axios.defaults.baseURL`, so every atom request hits our
host. Point it at Base's `/v1/calendar` (default `https://cal.hanzo.ai/v1/calendar`,
override with `VITE_CAL_API`). The backend is the `calendar` plugin in
`github.com/hanzoai/base` (`plugins/calendar`), which speaks Cal's API-v2 shapes.
## Build
```
pnpm install
pnpm build # -> app/dist
```
Public booking is free, so Cal's optional Stripe peers are aliased to local no-op
stubs (`app/src/stripe-*.ts`) — the bundle stays self-contained and CSP-clean.
## Fork discipline
Only MIT `@calcom/atoms` is used; no `ee/` / non-MIT Cal code. Cal's copyright stays
in `NOTICE`, Hanzo's added. Phase-1 milestone is a local/workspace `@hanzo/calendar`;
the immutable npm publish + `github.com/hanzoai/calendar` repo are the follow-on.
+30
View File
@@ -0,0 +1,30 @@
Hanzo Calendar (@hanzo/calendar, cal.hanzo.ai)
Copyright (c) 2020-present Hanzo AI, Inc.
This product includes and derives from Cal.com's open-source scheduling atoms
(@calcom/atoms), used under the MIT License:
MIT License
Copyright (c) 2020-present Cal.com, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Only MIT-licensed Cal.com code is used. No Cal.com enterprise (ee/) or otherwise
non-MIT source is included or derived from.
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Book a time · Hanzo</title>
<!-- Cal's atoms bundle expects Node-ish globals; shim them before any module
runs so the browser build never touches an undefined `process`/`global`. -->
<script>
window.global = window;
window.process = window.process || { env: { NODE_ENV: "production" } };
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@hanzo/cal-app",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --host --port 3000"
},
"dependencies": {
"@hanzo/calendar": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "6.28.0"
},
"devDependencies": {
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.4",
"typescript": "5.6.3",
"vite": "5.4.11"
}
}
+21
View File
@@ -0,0 +1,21 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Booking } from "./pages/Booking";
import { Confirmation } from "./pages/Confirmation";
import { Home } from "./pages/Home";
// Routes:
// / landing
// /booking/:uid booking confirmation / manage (more specific — matched first)
// /:owner/:slug the public Booker for one event type
export function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/booking/:uid" element={<Confirmation />} />
<Route path="/:owner/:slug" element={<Booking />} />
</Routes>
</BrowserRouter>
);
}
+5
View File
@@ -0,0 +1,5 @@
// The native Hanzo Base booking API. Cal's atoms set axios.defaults.baseURL to this
// via <Provider apiUrl>; the confirmation view fetches it directly. Override for
// local development with VITE_CAL_API (e.g. http://localhost:8090/v1/calendar).
export const calendarApi =
import.meta.env.VITE_CAL_API || "https://cal.hanzo.ai/v1/calendar";
+11
View File
@@ -0,0 +1,11 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./styles.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
+26
View File
@@ -0,0 +1,26 @@
import { Booker, Provider } from "@hanzo/calendar";
import { useNavigate, useParams } from "react-router-dom";
import { calendarApi } from "../api";
// The public booking page: Cal's real <Booker> for /{owner}/{slug}, pointed at the
// native Base backend. No access token — public booking needs no managed-user OAuth.
// On success we route to the confirmation view by the new booking's uid.
export function Booking() {
const { owner = "", slug = "" } = useParams();
const navigate = useNavigate();
return (
<Provider apiUrl={calendarApi}>
<Booker
username={owner}
eventSlug={slug}
isTeamEvent={false}
onCreateBookingSuccess={(res: { data?: { uid?: string } }) => {
const uid = res?.data?.uid;
if (uid) navigate(`/booking/${uid}`);
}}
/>
</Provider>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { calendarApi } from "../api";
type Booking = {
uid: string;
title: string;
status: string;
startTime?: string;
start?: string;
location?: string;
attendees?: { name?: string; email?: string }[];
};
// The booking confirmation / manage view for /booking/:uid. The uid is the booking's
// opaque capability, so Base reveals the real meeting location here (never before
// booking). Offers cancellation.
export function Confirmation() {
const { uid = "" } = useParams();
const [booking, setBooking] = useState<Booking | null>(null);
const [error, setError] = useState("");
const [canceling, setCanceling] = useState(false);
useEffect(() => {
fetch(`${calendarApi}/bookings/${encodeURIComponent(uid)}`)
.then((r) => r.json())
.then((res) => {
if (res.status === "success") setBooking(res.data);
else setError(res.error?.message ?? "Booking not found");
})
.catch(() => setError("Booking not found"));
}, [uid]);
const cancel = () => {
setCanceling(true);
fetch(`${calendarApi}/bookings/${encodeURIComponent(uid)}/cancel`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason: "" }),
})
.then((r) => r.json())
.then((res) => {
if (res.status === "success") setBooking(res.data);
else setError(res.error?.message ?? "Could not cancel");
})
.catch(() => setError("Could not cancel"))
.finally(() => setCanceling(false));
};
if (error) {
return (
<main className="shell">
<div className="card">
<p className="muted">{error}</p>
</div>
</main>
);
}
if (!booking) {
return (
<main className="shell">
<div className="card">
<p className="muted">Loading…</p>
</div>
</main>
);
}
const canceled = booking.status === "cancelled";
const when = new Date(booking.startTime ?? booking.start ?? "");
const attendee = booking.attendees?.[0];
return (
<main className="shell">
<div className="card">
<div className={`badge ${canceled ? "badge-off" : "badge-on"}`}>
{canceled ? "Canceled" : "Confirmed"}
</div>
<h1>{booking.title}</h1>
<p className="when">
{when.toLocaleString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
})}
</p>
{booking.location ? <p className="loc">{booking.location}</p> : null}
{attendee ? (
<p className="muted">
{attendee.name}
{attendee.email ? ` · ${attendee.email}` : ""}
</p>
) : null}
{!canceled ? (
<button className="cancel" onClick={cancel} disabled={canceling}>
{canceling ? "Canceling…" : "Cancel booking"}
</button>
) : null}
</div>
</main>
);
}
+15
View File
@@ -0,0 +1,15 @@
// The landing page. Public booking always happens at /{owner}/{slug}; this is just
// a friendly root.
export function Home() {
return (
<main className="shell">
<div className="card">
<h1>Book a time</h1>
<p className="muted">
Open a host's booking link — <code>cal.hanzo.ai/&#123;host&#125;/&#123;event&#125;</code>{" "}
— to pick a time.
</p>
</div>
</main>
);
}
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
// Public booking is free — no Stripe payment UI is ever rendered — so these no-op
// stubs satisfy @calcom/atoms' optional @stripe/react-stripe-js imports (Elements,
// PaymentElement, useStripe, useElements) and keep the bundle self-contained. Paid
// events are out of Phase 1 scope.
export const Elements = ({ children }: { children?: ReactNode }) => <>{children}</>;
export const PaymentElement = (): null => null;
export const useStripe = () => null;
export const useElements = () => null;
+6
View File
@@ -0,0 +1,6 @@
// Public booking is free — no payments — so the Stripe SDK is never loaded. This
// stub satisfies @calcom/atoms' optional `@stripe/stripe-js` import at build time
// and keeps the bundle self-contained (no external SDK, CSP-clean). Paid events are
// out of Phase 1 scope; wiring real Stripe is a later, deliberate step.
export const loadStripe = () => Promise.resolve(null);
export default { loadStripe };
+131
View File
@@ -0,0 +1,131 @@
:root {
--bg: #f6f6f8;
--card: #ffffff;
--ink: #1a1a1a;
--muted: #6b7280;
--line: #e5e7eb;
--accent: #111827;
--on: #047857;
--off: #b91c1c;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
margin: 0;
min-height: 100%;
}
body {
background: var(--bg);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
}
/* The shell hosts our own light pages (landing, confirmation). The Booker itself
ships Cal's Tailwind styling and renders inside its own wrapper. */
.shell {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 14px;
padding: 32px;
max-width: 480px;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.04);
}
.card h1 {
font-size: 22px;
margin: 8px 0 6px;
}
.muted {
color: var(--muted);
font-size: 14px;
}
.when {
font-size: 16px;
font-weight: 600;
margin: 4px 0;
}
.loc {
font-size: 14px;
margin: 4px 0 12px;
word-break: break-word;
}
code {
background: #f1f1f4;
padding: 2px 6px;
border-radius: 6px;
font-size: 13px;
}
.badge {
display: inline-block;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
margin-bottom: 8px;
}
.badge-on {
background: #ecfdf5;
color: var(--on);
}
.badge-off {
background: #fef2f2;
color: var(--off);
}
.cancel {
margin-top: 20px;
background: transparent;
color: var(--off);
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 16px;
font-size: 14px;
cursor: pointer;
}
.cancel:hover {
border-color: var(--off);
}
.cancel:disabled {
opacity: 0.6;
cursor: default;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b0b0d;
--card: #151517;
--ink: #f3f4f6;
--muted: #9ca3af;
--line: #26262b;
}
code {
background: #26262b;
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "../packages/calendar/src"]
}
+32
View File
@@ -0,0 +1,32 @@
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
const stripeStub = fileURLToPath(new URL("./src/stripe-stub.ts", import.meta.url));
const stripeReactStub = fileURLToPath(new URL("./src/stripe-react-stub.tsx", import.meta.url));
// The @hanzo/calendar workspace package is TS source; pnpm symlinks it to its real
// path outside node_modules, so Vite transpiles it. @calcom/atoms is consumed as
// its pre-built ESM bundle. NODE_ENV is pinned so React and the atoms tree-shake to
// their production paths in the browser.
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
// Public booking is free; stub Stripe so the optional peer import resolves
// and the bundle stays self-contained (no external SDK, CSP-clean).
"@stripe/stripe-js/pure": stripeStub,
"@stripe/stripe-js": stripeStub,
"@stripe/react-stripe-js": stripeReactStub,
},
},
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
build: {
outDir: "dist",
sourcemap: false,
chunkSizeWarningLimit: 4000,
},
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "hanzo-calendar",
"private": true,
"description": "cal.hanzo.ai — Cal.com's Booker (debranded as @hanzo/calendar) over the native Hanzo Base /v1/calendar backend.",
"scripts": {
"build": "pnpm --filter @hanzo/cal-app build",
"dev": "pnpm --filter @hanzo/cal-app dev"
},
"packageManager": "[email protected]"
}
+26
View File
@@ -0,0 +1,26 @@
@hanzo/calendar
Copyright (c) 2020-present Hanzo AI, Inc.
Debranded fork surface of Cal.com's @calcom/atoms, used under the MIT License:
MIT License
Copyright (c) 2020-present Cal.com, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@hanzo/calendar",
"version": "0.1.0",
"description": "Hanzo-branded booking components — a debranded fork surface of Cal.com's @calcom/atoms, pointed at the native Hanzo Base /v1/calendar backend.",
"license": "MIT",
"type": "module",
"sideEffects": [
"*.css"
],
"exports": {
".": "./src/index.tsx"
},
"dependencies": {
"@calcom/atoms": "2.11.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
}
+42
View File
@@ -0,0 +1,42 @@
// @hanzo/calendar — Hanzo-branded booking components.
//
// This is the debranded fork surface of Cal.com's @calcom/atoms (MIT — see the
// NOTICE file). The SPA at cal.hanzo.ai imports ONLY from here, never from
// @calcom/atoms directly: the Cal provider and Booker are re-exported under Hanzo
// names and defaulted for public, no-OAuth booking against the native Hanzo Base
// backend at /v1/calendar. Cal's look and feel (its Tailwind styling) ships with
// the atoms and is intentionally preserved — this is Cal's Booker, Hanzo-hosted.
import { CalProvider, Booker as CalBooker } from "@calcom/atoms";
import type { ComponentProps, ReactNode } from "react";
// Cal's atom styles (Tailwind, pre-built). Importing here means a consumer only has
// to import "@hanzo/calendar" to get both the components and their look.
import "@calcom/atoms/globals.min.css";
/** Default Base backend for the booking API. Override with VITE_CAL_API. */
export const DEFAULT_CALENDAR_API = "https://cal.hanzo.ai/v1/calendar";
export type ProviderProps = {
/** Base backend booking API, e.g. https://cal.hanzo.ai/v1/calendar */
apiUrl?: string;
children: ReactNode;
};
/**
* Provider wraps Cal's CalProvider for public booking: no OAuth client, no managed
* user — just an API base pointed at Hanzo Base. Cal's provider sets
* axios.defaults.baseURL = apiUrl, so every Booker request hits our host, and it
* initializes from apiUrl alone (an empty clientId skips the OAuth client fetch).
*/
export function Provider({ apiUrl = DEFAULT_CALENDAR_API, children }: ProviderProps) {
return (
<CalProvider clientId="" options={{ apiUrl }} version="2024-08-13">
{children}
</CalProvider>
);
}
/** Booker is Cal's public Booker, re-exported under the Hanzo surface. */
export const Booker = CalBooker;
export type BookerProps = ComponentProps<typeof CalBooker>;
+3770
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
packages:
- "packages/*"
- "app"