Getting started with @zaptime/core
Installation
sh
$ npm install @zaptime/coresh
$ pnpm add @zaptime/coresh
$ yarn add @zaptime/corePeer dependency: vue ^3.4. The package is ESM only.
Initialization sequence
Before the calendar can show anything, the remote configuration of the event type has to be fetched and pushed into the composables. The order matters: init() needs the config and the date-fns locale to be set first.
fetchRemoteConfiguration(token, apiBaseUrl?, reservationUuid?)- Push the remote data into core: locations, Stripe config, booking form fields, max guests, reservation (when rescheduling).
- Merge the remote configuration under your local config.
loadDateFnsConfig(locale.preset)setConfig(mergedConfig)useCalendar().init()
The composable below wraps that sequence and exposes a status you can render against. Copy it into your app.
ts
// useZaptimeInit.ts
import { ref, shallowRef } from "vue";
import {
fetchRemoteConfiguration,
mergeObjects,
useBookingForm,
useCalendar,
useConfig,
useDateFormatters,
useGuests,
useLocations,
useReservationReschedule,
useStripeConfig,
type ZaptimeConfig,
} from "@zaptime/core";
import { isSameDay } from "date-fns";
export type ZaptimeInitStatus = "idle" | "loading" | "ready" | "disabled" | "error";
export function useZaptimeInit(localConfig: ZaptimeConfig, calendarId?: string) {
const status = ref<ZaptimeInitStatus>("idle");
const error = shallowRef<unknown>(null);
const eventTypeName = ref("");
const { setConfig } = useConfig(calendarId);
const { setLocations } = useLocations(calendarId);
const { setStripeConfig } = useStripeConfig(calendarId);
const { setBookingForm } = useBookingForm(calendarId);
const { setMaxGuests } = useGuests(calendarId);
const { setSelectedReservation } = useReservationReschedule(calendarId);
const { loadDateFnsConfig } = useDateFormatters();
const { init: initCalendar, dayClicked, state } = useCalendar(calendarId);
async function init() {
if (!localConfig?.token) {
status.value = "error";
error.value = new Error("Zaptime: config.token is required");
return;
}
status.value = "loading";
const result = await fetchRemoteConfiguration(
localConfig.token,
localConfig.apiBaseUrl,
localConfig.reservationUuid,
);
if (result.isErr()) {
status.value = "error";
error.value = result.error; // "invalidToken" (also on network failure)
return;
}
const remote = result.value;
eventTypeName.value = remote.eventTypeName;
if (remote.disabled) {
status.value = "disabled";
return;
}
if (remote.reservation) setSelectedReservation(remote.reservation);
if (remote.locations) setLocations(remote.locations);
if (remote.stripeConfig) setStripeConfig(remote.stripeConfig);
if (remote.customFields) setBookingForm(remote.customFields);
setMaxGuests(remote.maxGuests ?? null);
// Remote configuration is the base; local keys override it.
const merged = mergeObjects({ ...remote.configuration }, { ...localConfig }) as ZaptimeConfig;
await loadDateFnsConfig(merged.locale?.preset || "en");
setConfig(merged);
await initCalendar();
// Rescheduling: preselect the day of the existing reservation.
if (remote.reservation) {
const current = new Date(remote.reservation.start);
const day = state.days.find((d) => d.date && isSameDay(d.date, current));
if (day) dayClicked(day);
}
status.value = "ready";
}
return { status, error, eventTypeName, init };
}Minimal calendar
vue
<template>
<p v-if="status !== 'ready'">{{ status }}</p>
<template v-else>
<header>
<button :disabled="prevDisabled" @click="prev">‹</button>
<span>{{ monthName }} {{ currentYear }}</span>
<button :disabled="nextDisabled" @click="next">›</button>
</header>
<div class="grid">
<span v-for="h in state.headers" :key="h">{{ h }}</span>
<button
v-for="(day, i) in state.days"
:key="i"
:disabled="!day.date || day.isPast || !dayHasTimeSlot(day)"
:aria-selected="isSelectedDay(day)"
@click="dayClicked(day)"
>
{{ day.label }}
</button>
</div>
<ul>
<li v-for="slot in state.timeSlots" :key="slot.start">
<button :aria-pressed="isSelected(slot)" @click="selectTimeSlot(slot)">
{{ getFormattedTime(slot.start) }}
</button>
</li>
</ul>
<button v-if="selectedTimeSlot" @click="submit">Book</button>
</template>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { book, useCalendar, useDateFormatters, useSelectedTimeSlot } from "@zaptime/core";
import { useZaptimeInit } from "./useZaptimeInit";
const { status, init } = useZaptimeInit({ token: "<API_TOKEN>" });
const {
state, prev, next, prevDisabled, nextDisabled, monthName, currentYear,
dayClicked, dayHasTimeSlot, isSelectedDay, selectTimeSlot, isSelected,
} = useCalendar();
const { selectedTimeSlot } = useSelectedTimeSlot();
const { getFormattedTime } = useDateFormatters();
onMounted(init);
async function submit() {
await book({ email: "john@doe.test" });
}
</script>For the booking form, guests, rescheduling, error handling and timezone switching see the full example.
Things core does not tell you
init()auto-advances to the next month when the current one has no slots, sostate.datecan differ from today right after init.useDateFormattersformats in English untilloadDateFnsConfig(preset)resolves. Await it before rendering.- Changing the timezone with
setTimezonedoes not refetch; callgetDays()afterwards because slots can move across days. fetchRemoteConfigurationreturns ats-results-esResult. Branch onisOk()/isErr()and read.value/.error.- Two calendars without distinct
calendarIds share selection and config. - Only one reservation created with
reservecan be in flight per page; its refresh interval is global.
