npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@doany-ai/calendar

v0.1.0

Published

Booking the business's services, on a Doany site: calendar hooks and default components (service list, date and time picker, booking flow, class sessions, booking status)

Readme

@doany-ai/calendar

Booking the business's services on a Doany site: the services a visitor can book, a date and time picker, the booking flow, class sessions with seats, and the page a booking's link opens. Hooks read and book through the SDK client the site gives <DoanyProvider>; the components are defaults the site restyles through the theme variables, adjusts through props (every word through labels), or replaces while keeping the hooks.

The business sets its services up in its Catalog (type, options with duration and price, how clients book, cancellation rules, questions). The site only reads them and books. Nothing here creates services.

import { ServiceList, BookingFlow, SessionList, BookingStatus } from "@doany-ai/calendar";
import { Link, useParams } from "react-router-dom";

// /book — what can be booked; each card leads to its booking page
<ServiceList hrefFor={(s) => `/book/${s.id}`} renderLink={({ href, className, children }) => <Link to={href} className={className}>{children}</Link>} />

// /book/:serviceSettingId — pick a time, say who, done
const { serviceSettingId } = useParams();
<BookingFlow serviceSettingId={serviceSettingId} />

// /classes — upcoming sessions with seats; Register opens the form in a dialog
<SessionList />

// /bookings/:registrationId — the page a booking's link opens; change the time or cancel within the policy
const { registrationId } = useParams();
<BookingStatus registrationId={registrationId} />

Keep AuthProvider in src/App.jsx: BookingFlow and SessionList read the signed-in account to prefill the form. List the pages in doany/features.jsonc under booking.

Records

What the hooks return, as the platform gives them (the fields this package reads; src/types.ts has them all).

  • BookableService (services.list / useBookableServices): id (what slots and book take), item_name, booking_mode (appointment | class), online_booking_enabled (false: the business takes this service's bookings itself; the site shows no booking entry), variants[] (id, name, duration_minutes, price_amount, currency, is_default), payment_requirement (none | full_online), location_type (business_address | customer_address | online) + address, phone_required, booking_fields[] (the questions: key, type — text, textarea, email, phone, url, number, date, select, multiselect, checkbox — label, required, options), allow_customer_cancel, cancellation_cutoff_minutes, allow_customer_reschedule, reschedule_cutoff_minutes, cancellation_policy_text, booking_horizon_days, seat_capacity.
  • Slot (useSlots): start_at, end_at, slot_token (good for five minutes; holds nothing), token_expires_at.
  • Booking (useSessions): an appointment or a class session: id, version, kind, status, item_name, variant_name, start_at, end_at, timezone, price_amount, currency, payment_requirement, location_type, address, booking_fields_snapshot, allowed_actions (create_registration while seats are open; on the registration's own read of an appointment, reschedule while its visitor may move it); a session adds capacity, remaining_seats, allowed_variants[] (the tickets).
  • Registration (useRegistration, in every booking result): one person's place: id, booking_id, version, status (pending_payment | confirmed | canceled | expired), order_id (when paid online), hold_expires_at, allowed_actions (cancel when the policy allows).
  • BookingResult (useBook, useRegister): { booking, registration, access_token, order_access_token }. The SDK keeps the tokens for the browser session, so the calls that follow need none.

Times are ISO instants; the components show them in the visitor's timezone (timezone prop, default the browser's). Prices are in the currency's smallest unit (12000 = $120.00).

Components

ServiceList

The bookable services as cards (name, price, length, a Book button), in the catalog's name order (the catalog is read with sort: "name"), with "Load more" while there are more.

| prop | | | --- | --- | | params | { booking_mode?: "appointment" \| "class", limit? }. A menu is the whole list: it reads pages of at least 50 whatever limit says, so a business with up to 50 services sees no "Load more" | | hrefFor | (service) => string: each card's link | | renderLink | ({ href, className, children }) => node: render links with the site's router | | onSelect | (service) => void: instead of a link | | renderItem | (service) => node: replaces the card | | empty | node shown when nothing can be booked | | footer | node inside each card, under its text | | variant | "grid" (default): tiles. "menu": the services grouped by their catalog category, with a category bar and a search box, each a row — a service menu | | showDetails | read each service's description, category and image from the catalog (doany.catalog.list({ type: "service" })); default true. A catalog that cannot be read leaves the cards without them | | locale, labels, className | |

ServiceCard

One service: service, href / onSelect, renderLink, footer, locale, labels, className.

BookingFlow

Book an appointment: the variant (when the service has several), a day and a time, who it is for, then the confirmation.

| prop | | | --- | --- | | serviceSettingId | required: the service (BookableService.id) | | catalogItemVariantId | skip the variant choice | | timezone | the timezone times are shown in; default the browser's | | address | for a service at the customer's address | | layout | "split" (default): the service's summary (image, description, length, price, place, chosen time, timezone) in a column beside the steps. "stacked": one column, summary on top | | order | "when-first" (default): day and time, then the details. "who-first": the details first, then the time; the chosen time books at once | | image, showDetails | the summary's image (the catalog item's by default) and whether to read the catalog's description and image at all (default true) | | renderPayment | (result) => node: for a service paid online, what to render while the payment is due — the site's checkout for result.registration.order_id (@doany-ai/commerce's OrderCheckout) | | onBooked | (result) => void | | children | (result) => node: under the confirmation, links onward | | locale, labels, className | |

A slot taken by someone else meanwhile (SLOT_UNAVAILABLE) sends the visitor back to pick another; the times are re-read.

On a preview address, the flow shows a notice before the booking action. Visitors can choose a time, fill in their details and run local validation; the form keeps their input and never creates a booking, calls onBooked, renders payment, or shows a booking confirmation. This also applies to class registration (SessionList, SessionCalendar, RegisterDialog), prepaid services and package funding. Use the published website for a real booking. The backend determines preview status through calendar.context(); if that read fails, viewing and filling remain available while the final action waits for Try again. A backend INVALID_STATE with details.reason="preview_only" keeps the input and shows the same notice.

DateTimePicker

The calendar and the day's times, with the timezone under it. For a site that draws its own flow around it. Today is picked to begin with and its times shown; a day without times hands over to the next, for two weeks at most and never past the horizon, until the visitor picks a day.

locale formats the calendar's month, weekday headings and day numbers as well as the chosen day and times. Weekday headings keep the locale's short names when they fit a four-character abbreviation; longer names use its narrow form when the resulting seven headings stay distinct, with the full weekday name kept for screen readers. Dates use the Gregorian calendar to match the picker's grid and month navigation, including locales that default to another calendar. Pass labels.previousMonth / labels.nextMonth for its navigation buttons' accessible names. BookingFlow passes these through; SessionCalendar uses the same calendar localization.

| prop | | | --- | --- | | service, variant | required | | timezone, onTimezoneChange | | | value, onChange | the chosen Slot | | onConfirm, confirmLabel | the button that appears beside the chosen slot (selectTime label by default) | | reschedule | { bookingId, accessToken? }: the times an existing appointment can move to, instead of a new booking's | | layout | "wide" (default): the times beside the calendar. "narrow": under it | | locale, labels, className | |

AttendeeForm

Name, email, phone and the service's questions. fields (the service's booking_fields), phoneRequired, defaultValues, onSubmit(attendee), submitLabel, busy, disabled (disable the final action while preserving the inputs), error, secondaryAction, locale, labels, className. The flow adds the timezone before booking. A field that may be left blank is marked Optional at the end of its label row; required fields carry nothing. The form checks what the platform will check and says so under the field once it was left or the form submitted, in place of the browser's own bubbles: a name that is not blank, an email or a phone (the phone when phoneRequired), the email an address, every required question answered, and an email, phone, url, number or date question given an answer of its kind: an email address; a phone typed like the contact phone and sent in E.164; a web address (example.com or a full URL); a number, sent as a number; a date, sent as YYYY-MM-DD. A text question is not held to any of these. The phone is typed as a local number behind a country, shown as its flag and calling code: the chip opens a searchable list of every country (the United States first, then by name; a name, a code or a calling code finds one), the United States to start with; a number typed with its own + or 00 moves the flag to that country and keeps the rest; the trunk prefix is dropped where the country dials one — and is sent in E.164. These fields are @doany-ai/app-core/form's, shared with the inquiry form. A site changes those messages through labels (nameRequired, contactRequired, emailFormat, phoneRequired, phoneFormat, answerRequired, choiceRequired, urlFormat, numberFormat, dateFormat, optional, country, searchCountry, noCountry); it does not wrap the form to check them again.

SessionCalendar

A month of class sessions: the days with sessions are marked on the calendar; the chosen day's sessions, with their seats, are listed beside it, each with Register (the same dialog as SessionList).

| prop | | | --- | --- | | serviceSettingId | only this class | | timezone, renderPayment, onRegistered, locale, labels, className | as in SessionList |

PackageList, PackageCard

The packages of uses a visitor can buy (doany.catalog.list({ type: "plan", purchasable: true })), as pricing cards: name, price, description, and the way to buy — which the site provides, because buying is @doany-ai/commerce's job:

<PackageList
  highlight={(item) => item.name === "10-Class Pack"}
  renderBuy={(item, variant) => <BuyButton items={[{ catalog_item_variant_id: variant.id, quantity: 1 }]} successPath={(id) => `/orders/${id}`} />}
/>

| prop | | | --- | --- | | renderBuy | (item, variant) => node: the buying control. Left out, a plain Buy button calls onBuy | | onBuy | (item, variant) => void | | highlight | (item) => boolean: the card to mark out | | footerFor | (item) => node: under each description | | category, limit, renderItem, empty, locale, labels, className | |

SessionList

Upcoming class sessions grouped by day, each with its seats and a Register button that opens the form in a dialog.

| prop | | | --- | --- | | params | { service_setting_id?, from?, to?, limit? } | | timezone | default the browser's | | renderPayment | as in BookingFlow | | onRegistered | (result) => void | | empty | node when there are no sessions | | locale, labels, className | |

BookingStatus

The page a booking's link opens (/bookings/<registration id>#t=<token>): status, what, when, where, and Change time and Cancel within the policy. Change time shows while the platform lists reschedule on the booking: the picker over the booking's own free times, then the booking moves and the page reads it again.

| prop | | | --- | --- | | registrationId | required | | accessToken | default: the link's #t=, else this browser session's | | timezone | default the browser's | | allowCancel | default true | | allowReschedule | default true | | children | (status, detail) => node | | locale, labels, className | |

Hooks

| hook | returns | | useServiceDetails() | query of the catalog's service items, plus byItem: a Map from catalog item id to { description, category, image_url, … } | | usePackages({ limit?, category? }) | query of the packages on sale, plus packages | | --- | --- | | useCalendarContext() | query of { preview }: whether the backend identifies this website address as a preview; custom booking UIs should show the notice and stop before mutation when true | | useBookableServices({ booking_mode?, limit? }) | react-query infinite query + services (every page so far) + timezone (the business's) | | useBookableService(id) | query of one BookableService | | useSlots(serviceSettingId, { catalogItemVariantId, date: "YYYY-MM-DD", timezone }) | infinite query + slots | | useBook() | mutation: mutate({ service_setting_id, catalog_item_variant_id, start_at, slot_token, attendee: { name, timezone, email?, phone?, answers? }, address? }) → BookingResult | | useSessions({ service_setting_id?, from?, to?, limit? }) | infinite query + sessions + timezone | | useSession(bookingId) | query of one Booking | | useRegister() | mutation: mutate({ bookingId, attendee, catalog_item_variant_id?, expected: { booking_version, payment_requirement, item_version?, variant_version? } }) → BookingResult | | useRegistration(registrationId, { accessToken? }) | query of { registration, booking }; polls while pending_payment | | useCancelRegistration() | mutation: mutate({ registrationId, registration_version, booking_version, reason?, accessToken? }) | | useRescheduleSlots(bookingId, { date: "YYYY-MM-DD", timezone, accessToken? }) | infinite query + slots: the times this appointment can move to | | useReschedule() | mutation: mutate({ bookingId, registrationId, registration_version, booking_version, start_at, slot_token, reason?, accessToken? }) → the moved Booking; reads the registration again | | readBookingLinkToken(registrationId) | the token in the page's link, for a page that calls the SDK itself |

Helpers: formatDuration(minutes), formatTime(iso, timezone, locale), formatDay, formatDateTime, formatTimezone, localDate(date, timezone), browserTimezone(), servicePriceLabel(service, locale), variantPriceLabel(variant, locale), defaultVariant(service).

Labels

Every word the components show is in labels (Partial<CalendarLabels>; the rest keep their defaults). A site in another language passes its words once to the outer component; it passes them on.

free, fromPrice(price), duration(minutes), book, askForQuote, noServices, loadError, tryAgain, loadMore, loadingMore, notAvailable, selectDateTime, timezone, noSlots, previousMonth, nextMonth, options, yourDetails, name, nameRequired, email, phone, phoneFormat, optional, contactRequired, emailFormat, phoneRequired, country, answerRequired, choiceRequired, urlFormat, confirmBooking, booking, back, slotTaken, bookingFailed, previewOnly, previewChecked, rateLimited, detailsRejected, bookedTitle, bookedBody(when), payToConfirmTitle, payToConfirmBody(until), payNow, seatsLeft(n), full, register, registered, noSessions, spotsOf(left, total), onlyLeft(n), thisWeek(n), previousWeek, nextWeek, noSessionsThisWeek, selectTime, searchServices, allServices, noMatches, classSeats(n), buy, noPackages, pickDay, noSessionsOnDay, statusConfirmed, statusPendingPayment, statusCanceled, statusExpired, cancelBooking, cancelConfirm, keepBooking, canceling, cancelFailed, cancelProcessing, changeTime, moveHere, keepTime, rescheduling, rescheduled, rescheduleFailed, bookingNotFound, bookingCheckFailed, cancellationPolicy, location, online, atYourAddress, joinMeeting, yourAddress, addressRequired.

Failure wording: the site's label (loadError, bookingFailed, cancelFailed, rescheduleFailed) when set, else the platform's own message (with or without an error code), else the default. Known failures keep their specific labels (slotTaken, rateLimited, detailsRejected for VALIDATION_ERROR); an expired slot quote uses an explicit slotTaken, then bookingFailed, then the platform's message. Translating the message does not change recovery: expired quotes still reload the times, and changed service details still reload the service.

Customizing

Stop at the first level that does what was asked:

  1. Theme: the variables in the site's src/index.css (--primary, --radius, --font-heading, …) style every component here.
  2. Props: className, labels, renderLink, renderItem, empty, renderPayment, children.
  3. Hooks: build the part yourself on useBookableServices, useSlots, useBook, useSessions, useRegister, useRegistration, useCancelRegistration, useRescheduleSlots, useReschedule.

The package ships no CSS: the site's Tailwind build scans node_modules/@doany-ai/calendar/dist (the general and service scaffolds do).

Develop

npm install
npm run typecheck   # includes tests/sdk-contract.types.ts: the real SDK client satisfies CalendarClient
npm test
npm run build