@hanzo/booker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +78 -0
  2. package/dist/cjs/Booker.cjs +153 -0
  3. package/dist/cjs/Booker.native.js +158 -0
  4. package/dist/cjs/Booker.native.js.map +1 -0
  5. package/dist/cjs/BookingForm.cjs +197 -0
  6. package/dist/cjs/BookingForm.native.js +203 -0
  7. package/dist/cjs/BookingForm.native.js.map +1 -0
  8. package/dist/cjs/CalendarEmbed.cjs +121 -0
  9. package/dist/cjs/CalendarEmbed.native.js +125 -0
  10. package/dist/cjs/CalendarEmbed.native.js.map +1 -0
  11. package/dist/cjs/Confirmation.cjs +177 -0
  12. package/dist/cjs/Confirmation.native.js +185 -0
  13. package/dist/cjs/Confirmation.native.js.map +1 -0
  14. package/dist/cjs/EventHeader.cjs +112 -0
  15. package/dist/cjs/EventHeader.native.js +118 -0
  16. package/dist/cjs/EventHeader.native.js.map +1 -0
  17. package/dist/cjs/MonthCalendar.cjs +177 -0
  18. package/dist/cjs/MonthCalendar.native.js +191 -0
  19. package/dist/cjs/MonthCalendar.native.js.map +1 -0
  20. package/dist/cjs/TimeSlots.cjs +119 -0
  21. package/dist/cjs/TimeSlots.native.js +126 -0
  22. package/dist/cjs/TimeSlots.native.js.map +1 -0
  23. package/dist/cjs/client.cjs +136 -0
  24. package/dist/cjs/client.native.js +267 -0
  25. package/dist/cjs/client.native.js.map +1 -0
  26. package/dist/cjs/hooks.cjs +193 -0
  27. package/dist/cjs/hooks.native.js +221 -0
  28. package/dist/cjs/hooks.native.js.map +1 -0
  29. package/dist/cjs/index.cjs +71 -0
  30. package/dist/cjs/index.native.js +74 -0
  31. package/dist/cjs/index.native.js.map +1 -0
  32. package/dist/cjs/time.cjs +145 -0
  33. package/dist/cjs/time.native.js +150 -0
  34. package/dist/cjs/time.native.js.map +1 -0
  35. package/dist/cjs/useBooker.cjs +151 -0
  36. package/dist/cjs/useBooker.native.js +173 -0
  37. package/dist/cjs/useBooker.native.js.map +1 -0
  38. package/dist/esm/Booker.mjs +128 -0
  39. package/dist/esm/Booker.mjs.map +1 -0
  40. package/dist/esm/Booker.native.js +130 -0
  41. package/dist/esm/Booker.native.js.map +1 -0
  42. package/dist/esm/BookingForm.mjs +172 -0
  43. package/dist/esm/BookingForm.mjs.map +1 -0
  44. package/dist/esm/BookingForm.native.js +175 -0
  45. package/dist/esm/BookingForm.native.js.map +1 -0
  46. package/dist/esm/CalendarEmbed.mjs +96 -0
  47. package/dist/esm/CalendarEmbed.mjs.map +1 -0
  48. package/dist/esm/CalendarEmbed.native.js +97 -0
  49. package/dist/esm/CalendarEmbed.native.js.map +1 -0
  50. package/dist/esm/Confirmation.mjs +152 -0
  51. package/dist/esm/Confirmation.mjs.map +1 -0
  52. package/dist/esm/Confirmation.native.js +157 -0
  53. package/dist/esm/Confirmation.native.js.map +1 -0
  54. package/dist/esm/EventHeader.mjs +87 -0
  55. package/dist/esm/EventHeader.mjs.map +1 -0
  56. package/dist/esm/EventHeader.native.js +90 -0
  57. package/dist/esm/EventHeader.native.js.map +1 -0
  58. package/dist/esm/MonthCalendar.mjs +152 -0
  59. package/dist/esm/MonthCalendar.mjs.map +1 -0
  60. package/dist/esm/MonthCalendar.native.js +163 -0
  61. package/dist/esm/MonthCalendar.native.js.map +1 -0
  62. package/dist/esm/TimeSlots.mjs +94 -0
  63. package/dist/esm/TimeSlots.mjs.map +1 -0
  64. package/dist/esm/TimeSlots.native.js +98 -0
  65. package/dist/esm/TimeSlots.native.js.map +1 -0
  66. package/dist/esm/client.mjs +104 -0
  67. package/dist/esm/client.mjs.map +1 -0
  68. package/dist/esm/client.native.js +232 -0
  69. package/dist/esm/client.native.js.map +1 -0
  70. package/dist/esm/hooks.mjs +165 -0
  71. package/dist/esm/hooks.mjs.map +1 -0
  72. package/dist/esm/hooks.native.js +190 -0
  73. package/dist/esm/hooks.native.js.map +1 -0
  74. package/dist/esm/index.js +13 -0
  75. package/dist/esm/index.js.map +1 -0
  76. package/dist/esm/index.mjs +13 -0
  77. package/dist/esm/index.mjs.map +1 -0
  78. package/dist/esm/index.native.js +13 -0
  79. package/dist/esm/index.native.js.map +1 -0
  80. package/dist/esm/time.mjs +107 -0
  81. package/dist/esm/time.mjs.map +1 -0
  82. package/dist/esm/time.native.js +109 -0
  83. package/dist/esm/time.native.js.map +1 -0
  84. package/dist/esm/useBooker.mjs +126 -0
  85. package/dist/esm/useBooker.mjs.map +1 -0
  86. package/dist/esm/useBooker.native.js +145 -0
  87. package/dist/esm/useBooker.native.js.map +1 -0
  88. package/package.json +61 -0
  89. package/src/Booker.tsx +152 -0
  90. package/src/BookingForm.tsx +180 -0
  91. package/src/CalendarEmbed.tsx +111 -0
  92. package/src/Confirmation.tsx +145 -0
  93. package/src/EventHeader.tsx +85 -0
  94. package/src/MonthCalendar.tsx +179 -0
  95. package/src/TimeSlots.tsx +102 -0
  96. package/src/client.ts +218 -0
  97. package/src/hooks.ts +180 -0
  98. package/src/index.ts +72 -0
  99. package/src/time.ts +131 -0
  100. package/src/useBooker.ts +187 -0
  101. package/types/Booker.d.ts +16 -0
  102. package/types/Booker.d.ts.map +1 -0
  103. package/types/BookingForm.d.ts +13 -0
  104. package/types/BookingForm.d.ts.map +1 -0
  105. package/types/CalendarEmbed.d.ts +15 -0
  106. package/types/CalendarEmbed.d.ts.map +1 -0
  107. package/types/Confirmation.d.ts +12 -0
  108. package/types/Confirmation.d.ts.map +1 -0
  109. package/types/EventHeader.d.ts +10 -0
  110. package/types/EventHeader.d.ts.map +1 -0
  111. package/types/MonthCalendar.d.ts +14 -0
  112. package/types/MonthCalendar.d.ts.map +1 -0
  113. package/types/TimeSlots.d.ts +15 -0
  114. package/types/TimeSlots.d.ts.map +1 -0
  115. package/types/client.d.ts +77 -0
  116. package/types/client.d.ts.map +1 -0
  117. package/types/hooks.d.ts +43 -0
  118. package/types/hooks.d.ts.map +1 -0
  119. package/types/index.d.ts +12 -0
  120. package/types/index.d.ts.map +1 -0
  121. package/types/time.d.ts +38 -0
  122. package/types/time.d.ts.map +1 -0
  123. package/types/useBooker.d.ts +41 -0
  124. package/types/useBooker.d.ts.map +1 -0
@@ -0,0 +1,126 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { DEFAULT_API_URL } from "./client.mjs";
3
+ import { useAvailableSlots, useCreateBooking, useEventType } from "./hooks.mjs";
4
+ import { addMonths, monthRange, startOfMonth, visitorTimeZone } from "./time.mjs";
5
+ function useBooker(opts) {
6
+ const api = opts.apiUrl ?? DEFAULT_API_URL;
7
+ const timeZone = opts.timeZone ?? visitorTimeZone();
8
+ const weekStartsOn = opts.weekStartsOn ?? 0;
9
+ const event = useEventType(api, opts.eventSlug, opts.host);
10
+ const [viewDate, setViewDate] = useState(() => startOfMonth(/* @__PURE__ */new Date()));
11
+ const {
12
+ startTime,
13
+ endTime
14
+ } = useMemo(() => monthRange(viewDate), [viewDate]);
15
+ const slots = useAvailableSlots(api, {
16
+ eventTypeSlug: opts.eventSlug,
17
+ username: opts.host,
18
+ startTime,
19
+ endTime,
20
+ timeZone,
21
+ enabled: !!event.data
22
+ });
23
+ const [selectedDay, setSelectedDay] = useState(null);
24
+ const [selectedSlot, setSelectedSlot] = useState(null);
25
+ const [phase, setPhase] = useState("pick");
26
+ const [booking, setBooking] = useState(null);
27
+ const create = useCreateBooking(api);
28
+ const availableKeys = useMemo(() => {
29
+ const map = slots.data ?? {};
30
+ return new Set(Object.keys(map).filter(k => (map[k]?.length ?? 0) > 0));
31
+ }, [slots.data]);
32
+ const daySlots = useMemo(() => {
33
+ if (!selectedDay) return [];
34
+ return slots.data?.[selectedDay] ?? [];
35
+ }, [slots.data, selectedDay]);
36
+ useEffect(() => {
37
+ if (!opts.autoSelectFirst) return;
38
+ if (selectedDay && availableKeys.has(selectedDay)) return;
39
+ const first = [...availableKeys].sort()[0];
40
+ if (first) {
41
+ setSelectedDay(first);
42
+ setSelectedSlot(null);
43
+ }
44
+ }, [opts.autoSelectFirst, availableKeys, selectedDay]);
45
+ const now = startOfMonth(/* @__PURE__ */new Date());
46
+ const atFirstMonth = viewDate.getFullYear() === now.getFullYear() && viewDate.getMonth() === now.getMonth();
47
+ const goPrevMonth = () => {
48
+ if (atFirstMonth) return;
49
+ setViewDate(d => addMonths(d, -1));
50
+ setSelectedDay(null);
51
+ setSelectedSlot(null);
52
+ };
53
+ const goNextMonth = () => {
54
+ setViewDate(d => addMonths(d, 1));
55
+ setSelectedDay(null);
56
+ setSelectedSlot(null);
57
+ };
58
+ const selectDay = key => {
59
+ setSelectedDay(key);
60
+ setSelectedSlot(null);
61
+ };
62
+ const selectSlot = iso => {
63
+ setSelectedSlot(iso);
64
+ setPhase("form");
65
+ create.reset();
66
+ };
67
+ const back = () => {
68
+ setPhase("pick");
69
+ setSelectedSlot(null);
70
+ create.reset();
71
+ };
72
+ const reset = () => {
73
+ setBooking(null);
74
+ setSelectedSlot(null);
75
+ setSelectedDay(null);
76
+ setPhase("pick");
77
+ create.reset();
78
+ slots.refetch();
79
+ };
80
+ const submit = async responses => {
81
+ if (!selectedSlot) return;
82
+ const result = await create.submit({
83
+ eventTypeSlug: opts.eventSlug,
84
+ username: opts.host,
85
+ start: selectedSlot,
86
+ timeZone,
87
+ responses
88
+ });
89
+ if (result.ok) {
90
+ setBooking(result.booking);
91
+ setPhase("done");
92
+ opts.onBooked?.(result.booking);
93
+ } else if (result.conflict) {
94
+ slots.refetch();
95
+ setSelectedSlot(null);
96
+ setPhase("pick");
97
+ }
98
+ };
99
+ return {
100
+ api,
101
+ timeZone,
102
+ weekStartsOn,
103
+ event,
104
+ slots,
105
+ viewDate,
106
+ availableKeys,
107
+ selectedDay,
108
+ daySlots,
109
+ selectedSlot,
110
+ phase,
111
+ booking,
112
+ submitting: create.submitting,
113
+ error: create.error,
114
+ conflict: create.conflict,
115
+ goPrevMonth,
116
+ goNextMonth,
117
+ atFirstMonth,
118
+ selectDay,
119
+ selectSlot,
120
+ submit,
121
+ back,
122
+ reset
123
+ };
124
+ }
125
+ export { useBooker };
126
+ //# sourceMappingURL=useBooker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useEffect","useMemo","useState","DEFAULT_API_URL","useAvailableSlots","useCreateBooking","useEventType","addMonths","monthRange","startOfMonth","visitorTimeZone","useBooker","opts","api","apiUrl","timeZone","weekStartsOn","event","eventSlug","host","viewDate","setViewDate","Date","startTime","endTime","slots","eventTypeSlug","username","enabled","data","selectedDay","setSelectedDay","selectedSlot","setSelectedSlot","phase","setPhase","booking","setBooking","create","availableKeys","map","Set","Object","keys","filter","k","length","daySlots","autoSelectFirst","has","first","sort","now","atFirstMonth","getFullYear","getMonth","goPrevMonth","d","goNextMonth","selectDay","key","selectSlot","iso","reset","back","refetch","submit","responses","result","start","ok","onBooked","conflict","submitting","error"],"sources":["../../src/useBooker.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,145 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { DEFAULT_API_URL } from "./client.native.js";
3
+ import { useAvailableSlots, useCreateBooking, useEventType } from "./hooks.native.js";
4
+ import { addMonths, monthRange, startOfMonth, visitorTimeZone } from "./time.native.js";
5
+ function useBooker(opts) {
6
+ var _opts_apiUrl;
7
+ var api = (_opts_apiUrl = opts.apiUrl) !== null && _opts_apiUrl !== void 0 ? _opts_apiUrl : DEFAULT_API_URL;
8
+ var _opts_timeZone;
9
+ var timeZone = (_opts_timeZone = opts.timeZone) !== null && _opts_timeZone !== void 0 ? _opts_timeZone : visitorTimeZone();
10
+ var _opts_weekStartsOn;
11
+ var weekStartsOn = (_opts_weekStartsOn = opts.weekStartsOn) !== null && _opts_weekStartsOn !== void 0 ? _opts_weekStartsOn : 0;
12
+ var event = useEventType(api, opts.eventSlug, opts.host);
13
+ var [viewDate, setViewDate] = useState(function () {
14
+ return startOfMonth(/* @__PURE__ */new Date());
15
+ });
16
+ var {
17
+ startTime,
18
+ endTime
19
+ } = useMemo(function () {
20
+ return monthRange(viewDate);
21
+ }, [viewDate]);
22
+ var slots = useAvailableSlots(api, {
23
+ eventTypeSlug: opts.eventSlug,
24
+ username: opts.host,
25
+ startTime,
26
+ endTime,
27
+ timeZone,
28
+ enabled: !!event.data
29
+ });
30
+ var [selectedDay, setSelectedDay] = useState(null);
31
+ var [selectedSlot, setSelectedSlot] = useState(null);
32
+ var [phase, setPhase] = useState("pick");
33
+ var [booking, setBooking] = useState(null);
34
+ var create = useCreateBooking(api);
35
+ var availableKeys = useMemo(function () {
36
+ var _slots_data;
37
+ var map = (_slots_data = slots.data) !== null && _slots_data !== void 0 ? _slots_data : {};
38
+ return new Set(Object.keys(map).filter(function (k) {
39
+ var _map_k;
40
+ var _map_k_length;
41
+ return ((_map_k_length = (_map_k = map[k]) === null || _map_k === void 0 ? void 0 : _map_k.length) !== null && _map_k_length !== void 0 ? _map_k_length : 0) > 0;
42
+ }));
43
+ }, [slots.data]);
44
+ var daySlots = useMemo(function () {
45
+ var _slots_data;
46
+ if (!selectedDay) return [];
47
+ var _slots_data_selectedDay;
48
+ return (_slots_data_selectedDay = (_slots_data = slots.data) === null || _slots_data === void 0 ? void 0 : _slots_data[selectedDay]) !== null && _slots_data_selectedDay !== void 0 ? _slots_data_selectedDay : [];
49
+ }, [slots.data, selectedDay]);
50
+ useEffect(function () {
51
+ if (!opts.autoSelectFirst) return;
52
+ if (selectedDay && availableKeys.has(selectedDay)) return;
53
+ var first = [...availableKeys].sort()[0];
54
+ if (first) {
55
+ setSelectedDay(first);
56
+ setSelectedSlot(null);
57
+ }
58
+ }, [opts.autoSelectFirst, availableKeys, selectedDay]);
59
+ var now = startOfMonth(/* @__PURE__ */new Date());
60
+ var atFirstMonth = viewDate.getFullYear() === now.getFullYear() && viewDate.getMonth() === now.getMonth();
61
+ var goPrevMonth = function () {
62
+ if (atFirstMonth) return;
63
+ setViewDate(function (d) {
64
+ return addMonths(d, -1);
65
+ });
66
+ setSelectedDay(null);
67
+ setSelectedSlot(null);
68
+ };
69
+ var goNextMonth = function () {
70
+ setViewDate(function (d) {
71
+ return addMonths(d, 1);
72
+ });
73
+ setSelectedDay(null);
74
+ setSelectedSlot(null);
75
+ };
76
+ var selectDay = function (key) {
77
+ setSelectedDay(key);
78
+ setSelectedSlot(null);
79
+ };
80
+ var selectSlot = function (iso) {
81
+ setSelectedSlot(iso);
82
+ setPhase("form");
83
+ create.reset();
84
+ };
85
+ var back = function () {
86
+ setPhase("pick");
87
+ setSelectedSlot(null);
88
+ create.reset();
89
+ };
90
+ var reset = function () {
91
+ setBooking(null);
92
+ setSelectedSlot(null);
93
+ setSelectedDay(null);
94
+ setPhase("pick");
95
+ create.reset();
96
+ slots.refetch();
97
+ };
98
+ var submit = async function (responses) {
99
+ if (!selectedSlot) return;
100
+ var result = await create.submit({
101
+ eventTypeSlug: opts.eventSlug,
102
+ username: opts.host,
103
+ start: selectedSlot,
104
+ timeZone,
105
+ responses
106
+ });
107
+ if (result.ok) {
108
+ var _opts_onBooked;
109
+ setBooking(result.booking);
110
+ setPhase("done");
111
+ (_opts_onBooked = opts.onBooked) === null || _opts_onBooked === void 0 ? void 0 : _opts_onBooked.call(opts, result.booking);
112
+ } else if (result.conflict) {
113
+ slots.refetch();
114
+ setSelectedSlot(null);
115
+ setPhase("pick");
116
+ }
117
+ };
118
+ return {
119
+ api,
120
+ timeZone,
121
+ weekStartsOn,
122
+ event,
123
+ slots,
124
+ viewDate,
125
+ availableKeys,
126
+ selectedDay,
127
+ daySlots,
128
+ selectedSlot,
129
+ phase,
130
+ booking,
131
+ submitting: create.submitting,
132
+ error: create.error,
133
+ conflict: create.conflict,
134
+ goPrevMonth,
135
+ goNextMonth,
136
+ atFirstMonth,
137
+ selectDay,
138
+ selectSlot,
139
+ submit,
140
+ back,
141
+ reset
142
+ };
143
+ }
144
+ export { useBooker };
145
+ //# sourceMappingURL=useBooker.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useEffect","useMemo","useState","DEFAULT_API_URL","useAvailableSlots","useCreateBooking","useEventType","addMonths","monthRange","startOfMonth","visitorTimeZone","useBooker","opts","_opts_apiUrl","api","apiUrl","_opts_timeZone","timeZone","_opts_weekStartsOn","weekStartsOn","event","eventSlug","host","viewDate","setViewDate","Date","startTime","endTime","slots","eventTypeSlug","username","enabled","data","selectedDay","setSelectedDay","selectedSlot","setSelectedSlot","phase","setPhase","booking","setBooking","create","availableKeys","_slots_data","map","Set","Object","keys","filter","k","_map_k","_map_k_length","length","daySlots","_slots_data_selectedDay","autoSelectFirst","has","first","sort","now","atFirstMonth","getFullYear","getMonth","goPrevMonth","d","goNextMonth","selectDay","key","selectSlot","iso","reset","back","refetch","submit","responses","result","start","ok","_opts_onBooked","onBooked","call","conflict"],"sources":["../../src/useBooker.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@hanzo/booker",
3
+ "version": "0.1.0",
4
+ "description": "Universal (web + native) scheduling UI for the Hanzo Calendar — a Booker, Confirmation, and compact CalendarEmbed built with hanzogui primitives.",
5
+ "license": "BSD-3-Clause",
6
+ "author": "Hanzo AI <dev@hanzo.ai>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/hanzoai/gui.git",
10
+ "directory": "pkgs/calendar"
11
+ },
12
+ "source": "src/index.ts",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "main": "dist/cjs",
16
+ "module": "dist/esm",
17
+ "types": "./types/index.d.ts",
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ ".": {
21
+ "types": "./types/index.d.ts",
22
+ "react-native": "./dist/esm/index.native.js",
23
+ "browser": "./dist/esm/index.mjs",
24
+ "module": "./dist/esm/index.mjs",
25
+ "import": "./dist/esm/index.mjs",
26
+ "require": "./dist/cjs/index.cjs",
27
+ "default": "./dist/esm/index.mjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "src",
32
+ "types",
33
+ "dist",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "gui-build",
38
+ "watch": "gui-build --watch",
39
+ "clean": "gui-build clean",
40
+ "clean:build": "gui-build clean:build"
41
+ },
42
+ "peerDependencies": {
43
+ "hanzogui": ">=7.0.0",
44
+ "react": ">=18",
45
+ "react-native": "*"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react-native": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@hanzogui/build": "workspace:*",
54
+ "hanzogui": "workspace:*",
55
+ "react": ">=19",
56
+ "react-native": "0.83.2"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }
package/src/Booker.tsx ADDED
@@ -0,0 +1,152 @@
1
+ import type { ReactNode } from 'react'
2
+ import { SizableText, Spinner, XStack, YStack } from 'hanzogui'
3
+ import type { Booking } from './client'
4
+ import { BookingForm } from './BookingForm'
5
+ import { Confirmation } from './Confirmation'
6
+ import { EventHeader } from './EventHeader'
7
+ import { MonthCalendar } from './MonthCalendar'
8
+ import { TimeSlots } from './TimeSlots'
9
+ import { useBooker } from './useBooker'
10
+
11
+ export interface BookerProps {
12
+ /** Host username (the calendar owner), e.g. "hanzo". */
13
+ host: string
14
+ /** Event-type slug, e.g. "intro". */
15
+ eventSlug: string
16
+ /** API base, defaults to https://api.hanzo.ai/v1/calendar */
17
+ apiUrl?: string
18
+ /** Visitor IANA time zone; defaults to the browser/device zone. */
19
+ timeZone?: string
20
+ weekStartsOn?: 0 | 1
21
+ onBooked?: (booking: Booking) => void
22
+ }
23
+
24
+ /** Universal scheduling flow: event → month → times → details → confirmation. */
25
+ export function Booker(props: BookerProps) {
26
+ const m = useBooker(props)
27
+
28
+ return (
29
+ <YStack
30
+ backgroundColor="$background"
31
+ borderColor="$borderColor"
32
+ borderWidth={1}
33
+ borderRadius="$6"
34
+ padding="$4"
35
+ gap="$4"
36
+ width="100%"
37
+ maxWidth={860}
38
+ alignSelf="center"
39
+ >
40
+ {m.event.loading ? (
41
+ <StatePanel>
42
+ <Spinner size="large" color="$color9" />
43
+ <SizableText size="$3" color="$color10">
44
+ Loading…
45
+ </SizableText>
46
+ </StatePanel>
47
+ ) : m.event.error || !m.event.data ? (
48
+ <StatePanel>
49
+ <SizableText size="$5" fontWeight="600" color="$color12">
50
+ Couldn't load this event
51
+ </SizableText>
52
+ <SizableText size="$3" color="$color10" textAlign="center">
53
+ {m.event.error ?? 'The event type was not found.'}
54
+ </SizableText>
55
+ </StatePanel>
56
+ ) : m.phase === 'done' && m.booking ? (
57
+ <Confirmation booking={m.booking} timeZone={m.timeZone} onDone={m.reset} />
58
+ ) : (
59
+ <YStack gap="$4">
60
+ <EventHeader
61
+ event={m.event.data}
62
+ host={props.host}
63
+ timeZone={m.timeZone}
64
+ compact={m.phase === 'form'}
65
+ />
66
+
67
+ {m.phase === 'form' && m.selectedSlot ? (
68
+ <BookingForm
69
+ slotIso={m.selectedSlot}
70
+ timeZone={m.timeZone}
71
+ eventLength={m.event.data.length}
72
+ submitting={m.submitting}
73
+ error={m.error}
74
+ onSubmit={m.submit}
75
+ onBack={m.back}
76
+ />
77
+ ) : (
78
+ <>
79
+ {m.conflict ? <JustTakenBanner /> : null}
80
+ {m.slots.error ? (
81
+ <SizableText size="$2" color="$red10">
82
+ Couldn't load availability: {m.slots.error}
83
+ </SizableText>
84
+ ) : null}
85
+ <XStack
86
+ gap="$5"
87
+ flexDirection="column"
88
+ $sm={{ flexDirection: 'row' }}
89
+ alignItems="flex-start"
90
+ >
91
+ <MonthCalendar
92
+ viewDate={m.viewDate}
93
+ availableKeys={m.availableKeys}
94
+ selectedDay={m.selectedDay}
95
+ onSelectDay={m.selectDay}
96
+ onPrev={m.goPrevMonth}
97
+ onNext={m.goNextMonth}
98
+ atFirstMonth={m.atFirstMonth}
99
+ weekStartsOn={m.weekStartsOn}
100
+ loading={m.slots.loading}
101
+ />
102
+ <TimeSlots
103
+ dayKey={m.selectedDay}
104
+ slots={m.daySlots}
105
+ timeZone={m.timeZone}
106
+ onPick={m.selectSlot}
107
+ selectedIso={m.selectedSlot}
108
+ loading={m.slots.loading && !m.selectedDay}
109
+ />
110
+ </XStack>
111
+ {m.availableKeys.size === 0 && !m.slots.loading && !m.slots.error ? (
112
+ <SizableText size="$3" color="$color10" textAlign="center">
113
+ No availability this month — try the next month.
114
+ </SizableText>
115
+ ) : null}
116
+ </>
117
+ )}
118
+ </YStack>
119
+ )}
120
+ </YStack>
121
+ )
122
+ }
123
+
124
+ function StatePanel({ children }: { children: ReactNode }) {
125
+ return (
126
+ <YStack
127
+ alignItems="center"
128
+ justifyContent="center"
129
+ gap="$3"
130
+ padding="$8"
131
+ minHeight={220}
132
+ >
133
+ {children}
134
+ </YStack>
135
+ )
136
+ }
137
+
138
+ function JustTakenBanner() {
139
+ return (
140
+ <YStack
141
+ backgroundColor="$yellow3"
142
+ borderColor="$yellow6"
143
+ borderWidth={1}
144
+ borderRadius="$4"
145
+ padding="$2.5"
146
+ >
147
+ <SizableText size="$2" color="$yellow11">
148
+ That time was just taken. We refreshed the calendar — please pick another.
149
+ </SizableText>
150
+ </YStack>
151
+ )
152
+ }
@@ -0,0 +1,180 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { Button, Input, SizableText, TextArea, XStack, YStack } from 'hanzogui'
3
+ import type { BookingResponses } from './client'
4
+ import { formatBookingWhen } from './time'
5
+
6
+ export interface BookingFormProps {
7
+ slotIso: string
8
+ timeZone: string
9
+ eventLength: number
10
+ submitting?: boolean
11
+ error?: string | null
12
+ onSubmit: (responses: BookingResponses) => void
13
+ onBack: () => void
14
+ }
15
+
16
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
17
+
18
+ /** Attendee details form for the chosen slot. */
19
+ export function BookingForm({
20
+ slotIso,
21
+ timeZone,
22
+ eventLength,
23
+ submitting,
24
+ error,
25
+ onSubmit,
26
+ onBack,
27
+ }: BookingFormProps) {
28
+ const [name, setName] = useState('')
29
+ const [email, setEmail] = useState('')
30
+ const [notes, setNotes] = useState('')
31
+ const [touched, setTouched] = useState(false)
32
+
33
+ const nameValid = name.trim().length > 0
34
+ const emailValid = EMAIL_RE.test(email.trim())
35
+ const valid = nameValid && emailValid
36
+
37
+ const submit = () => {
38
+ setTouched(true)
39
+ if (!valid || submitting) return
40
+ onSubmit({ name: name.trim(), email: email.trim(), notes: notes.trim() || undefined })
41
+ }
42
+
43
+ return (
44
+ <YStack gap="$3" flex={1} minWidth={240}>
45
+ <YStack gap="$1">
46
+ <XStack alignItems="center" gap="$2">
47
+ <BackLink onPress={onBack} />
48
+ <SizableText size="$5" fontWeight="700" color="$color12">
49
+ Enter details
50
+ </SizableText>
51
+ </XStack>
52
+ <SizableText size="$3" color="$color11">
53
+ {formatBookingWhen(slotIso, timeZone)} · {eventLength} min
54
+ </SizableText>
55
+ </YStack>
56
+
57
+ <Field
58
+ label="Name"
59
+ required
60
+ invalid={touched && !nameValid}
61
+ hint="Please enter your name"
62
+ >
63
+ <Input
64
+ value={name}
65
+ onChangeText={setName}
66
+ placeholder="Ada Lovelace"
67
+ autoComplete="name"
68
+ size="$4"
69
+ />
70
+ </Field>
71
+
72
+ <Field
73
+ label="Email"
74
+ required
75
+ invalid={touched && !emailValid}
76
+ hint="Please enter a valid email"
77
+ >
78
+ <Input
79
+ value={email}
80
+ onChangeText={setEmail}
81
+ placeholder="ada@example.com"
82
+ keyboardType="email-address"
83
+ autoCapitalize="none"
84
+ autoComplete="email"
85
+ size="$4"
86
+ />
87
+ </Field>
88
+
89
+ <Field label="Notes" hint="Optional">
90
+ <TextArea
91
+ value={notes}
92
+ onChangeText={setNotes}
93
+ placeholder="Anything that would help prepare?"
94
+ numberOfLines={3}
95
+ size="$4"
96
+ />
97
+ </Field>
98
+
99
+ {error ? (
100
+ <YStack
101
+ backgroundColor="$red3"
102
+ borderColor="$red6"
103
+ borderWidth={1}
104
+ borderRadius="$4"
105
+ padding="$2.5"
106
+ >
107
+ <SizableText size="$2" color="$red11">
108
+ {error}
109
+ </SizableText>
110
+ </YStack>
111
+ ) : null}
112
+
113
+ <XStack gap="$2" justifyContent="flex-end" marginTop="$1">
114
+ <Button size="$4" chromeless onPress={onBack} disabled={submitting}>
115
+ Back
116
+ </Button>
117
+ <Button
118
+ size="$4"
119
+ theme="blue"
120
+ onPress={submit}
121
+ disabled={submitting || (touched && !valid)}
122
+ opacity={submitting ? 0.7 : 1}
123
+ >
124
+ {submitting ? 'Booking…' : 'Confirm booking'}
125
+ </Button>
126
+ </XStack>
127
+ </YStack>
128
+ )
129
+ }
130
+
131
+ function Field({
132
+ label,
133
+ required,
134
+ invalid,
135
+ hint,
136
+ children,
137
+ }: {
138
+ label: string
139
+ required?: boolean
140
+ invalid?: boolean
141
+ hint?: string
142
+ children: ReactNode
143
+ }) {
144
+ return (
145
+ <YStack gap="$1.5">
146
+ <SizableText size="$2" color="$color11" fontWeight="600">
147
+ {label}
148
+ {required ? <SizableText color="$red10"> *</SizableText> : null}
149
+ </SizableText>
150
+ {children}
151
+ {invalid && hint ? (
152
+ <SizableText size="$1" color="$red10">
153
+ {hint}
154
+ </SizableText>
155
+ ) : null}
156
+ </YStack>
157
+ )
158
+ }
159
+
160
+ function BackLink({ onPress }: { onPress: () => void }) {
161
+ return (
162
+ <YStack
163
+ width={30}
164
+ height={30}
165
+ alignItems="center"
166
+ justifyContent="center"
167
+ borderRadius="$4"
168
+ cursor="pointer"
169
+ hoverStyle={{ backgroundColor: '$color4' }}
170
+ pressStyle={{ backgroundColor: '$color6' }}
171
+ onPress={onPress}
172
+ aria-label="Back"
173
+ role="button"
174
+ >
175
+ <SizableText size="$6" color="$color11">
176
+ {'‹'}
177
+ </SizableText>
178
+ </YStack>
179
+ )
180
+ }