@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.
- package/README.md +78 -0
- package/dist/cjs/Booker.cjs +153 -0
- package/dist/cjs/Booker.native.js +158 -0
- package/dist/cjs/Booker.native.js.map +1 -0
- package/dist/cjs/BookingForm.cjs +197 -0
- package/dist/cjs/BookingForm.native.js +203 -0
- package/dist/cjs/BookingForm.native.js.map +1 -0
- package/dist/cjs/CalendarEmbed.cjs +121 -0
- package/dist/cjs/CalendarEmbed.native.js +125 -0
- package/dist/cjs/CalendarEmbed.native.js.map +1 -0
- package/dist/cjs/Confirmation.cjs +177 -0
- package/dist/cjs/Confirmation.native.js +185 -0
- package/dist/cjs/Confirmation.native.js.map +1 -0
- package/dist/cjs/EventHeader.cjs +112 -0
- package/dist/cjs/EventHeader.native.js +118 -0
- package/dist/cjs/EventHeader.native.js.map +1 -0
- package/dist/cjs/MonthCalendar.cjs +177 -0
- package/dist/cjs/MonthCalendar.native.js +191 -0
- package/dist/cjs/MonthCalendar.native.js.map +1 -0
- package/dist/cjs/TimeSlots.cjs +119 -0
- package/dist/cjs/TimeSlots.native.js +126 -0
- package/dist/cjs/TimeSlots.native.js.map +1 -0
- package/dist/cjs/client.cjs +136 -0
- package/dist/cjs/client.native.js +267 -0
- package/dist/cjs/client.native.js.map +1 -0
- package/dist/cjs/hooks.cjs +193 -0
- package/dist/cjs/hooks.native.js +221 -0
- package/dist/cjs/hooks.native.js.map +1 -0
- package/dist/cjs/index.cjs +71 -0
- package/dist/cjs/index.native.js +74 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/time.cjs +145 -0
- package/dist/cjs/time.native.js +150 -0
- package/dist/cjs/time.native.js.map +1 -0
- package/dist/cjs/useBooker.cjs +151 -0
- package/dist/cjs/useBooker.native.js +173 -0
- package/dist/cjs/useBooker.native.js.map +1 -0
- package/dist/esm/Booker.mjs +128 -0
- package/dist/esm/Booker.mjs.map +1 -0
- package/dist/esm/Booker.native.js +130 -0
- package/dist/esm/Booker.native.js.map +1 -0
- package/dist/esm/BookingForm.mjs +172 -0
- package/dist/esm/BookingForm.mjs.map +1 -0
- package/dist/esm/BookingForm.native.js +175 -0
- package/dist/esm/BookingForm.native.js.map +1 -0
- package/dist/esm/CalendarEmbed.mjs +96 -0
- package/dist/esm/CalendarEmbed.mjs.map +1 -0
- package/dist/esm/CalendarEmbed.native.js +97 -0
- package/dist/esm/CalendarEmbed.native.js.map +1 -0
- package/dist/esm/Confirmation.mjs +152 -0
- package/dist/esm/Confirmation.mjs.map +1 -0
- package/dist/esm/Confirmation.native.js +157 -0
- package/dist/esm/Confirmation.native.js.map +1 -0
- package/dist/esm/EventHeader.mjs +87 -0
- package/dist/esm/EventHeader.mjs.map +1 -0
- package/dist/esm/EventHeader.native.js +90 -0
- package/dist/esm/EventHeader.native.js.map +1 -0
- package/dist/esm/MonthCalendar.mjs +152 -0
- package/dist/esm/MonthCalendar.mjs.map +1 -0
- package/dist/esm/MonthCalendar.native.js +163 -0
- package/dist/esm/MonthCalendar.native.js.map +1 -0
- package/dist/esm/TimeSlots.mjs +94 -0
- package/dist/esm/TimeSlots.mjs.map +1 -0
- package/dist/esm/TimeSlots.native.js +98 -0
- package/dist/esm/TimeSlots.native.js.map +1 -0
- package/dist/esm/client.mjs +104 -0
- package/dist/esm/client.mjs.map +1 -0
- package/dist/esm/client.native.js +232 -0
- package/dist/esm/client.native.js.map +1 -0
- package/dist/esm/hooks.mjs +165 -0
- package/dist/esm/hooks.mjs.map +1 -0
- package/dist/esm/hooks.native.js +190 -0
- package/dist/esm/hooks.native.js.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +13 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +13 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/time.mjs +107 -0
- package/dist/esm/time.mjs.map +1 -0
- package/dist/esm/time.native.js +109 -0
- package/dist/esm/time.native.js.map +1 -0
- package/dist/esm/useBooker.mjs +126 -0
- package/dist/esm/useBooker.mjs.map +1 -0
- package/dist/esm/useBooker.native.js +145 -0
- package/dist/esm/useBooker.native.js.map +1 -0
- package/package.json +61 -0
- package/src/Booker.tsx +152 -0
- package/src/BookingForm.tsx +180 -0
- package/src/CalendarEmbed.tsx +111 -0
- package/src/Confirmation.tsx +145 -0
- package/src/EventHeader.tsx +85 -0
- package/src/MonthCalendar.tsx +179 -0
- package/src/TimeSlots.tsx +102 -0
- package/src/client.ts +218 -0
- package/src/hooks.ts +180 -0
- package/src/index.ts +72 -0
- package/src/time.ts +131 -0
- package/src/useBooker.ts +187 -0
- package/types/Booker.d.ts +16 -0
- package/types/Booker.d.ts.map +1 -0
- package/types/BookingForm.d.ts +13 -0
- package/types/BookingForm.d.ts.map +1 -0
- package/types/CalendarEmbed.d.ts +15 -0
- package/types/CalendarEmbed.d.ts.map +1 -0
- package/types/Confirmation.d.ts +12 -0
- package/types/Confirmation.d.ts.map +1 -0
- package/types/EventHeader.d.ts +10 -0
- package/types/EventHeader.d.ts.map +1 -0
- package/types/MonthCalendar.d.ts +14 -0
- package/types/MonthCalendar.d.ts.map +1 -0
- package/types/TimeSlots.d.ts +15 -0
- package/types/TimeSlots.d.ts.map +1 -0
- package/types/client.d.ts +77 -0
- package/types/client.d.ts.map +1 -0
- package/types/hooks.d.ts +43 -0
- package/types/hooks.d.ts.map +1 -0
- package/types/index.d.ts +12 -0
- package/types/index.d.ts.map +1 -0
- package/types/time.d.ts +38 -0
- package/types/time.d.ts.map +1 -0
- package/types/useBooker.d.ts +41 -0
- 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
|
+
}
|