BuildCalendar

Full Stack Calendar Setup (Interactive)

This guide shows the interactive setup: users can sign in with Google, view calendars/events, and create/edit/delete events via the Schedule-X interactive event modal.

If you only need read-only viewing, see the free version: Full Stack Calendar Setup (Free Tier).

Getting an API Key#

Get an API key from your dashboard at buildcalendar.com and keep it in an env var (don’t commit it).

You will need a paid plan to access the interactive features. All paid plans at buildcalendar.com include an auth token for the Schedule-X premium packages.

Configure Premium Access#

Set up your .npmrc file at the root of your project with your SX_PREMIUM_TOKEN:

.npmrc
@sx-premium:registry=https://gitlab.schedule-x.com/api/v4/packages/npm/
//gitlab.schedule-x.com/api/v4/packages/npm/:_authToken=${SX_PREMIUM_TOKEN}

Installing Dependencies#

Install the SDK + adapter:

npm install @buildcalendar/sdk @buildcalendar/schedule-x

Install Schedule-X packages (including premium):

npm install @schedule-x/react @schedule-x/calendar @schedule-x/theme-default @schedule-x/event-recurrence @sx-premium/interactive-event-modal temporal-polyfill

Styling the Calendar#

Add the following CSS rule to your global CSS file (e.g., globals.css, index.css, or your main stylesheet) to set appropriate dimensions for the calendar wrapper:

index.css
.sx-react-calendar-wrapper {
  width: 1200px;
  max-width: 100vw;
  height: 800px;
  max-height: 90vh;
}

Set up Google OAuth#

hooks/useGoogleOAuth.ts
// Small hook that starts Google OAuth and reads the callback params back into app state.
import { useCallback, useEffect, useState } from "react";
import type { createClient } from "@buildcalendar/sdk";

export function useGoogleOAuth({
  client,
  callbackPath,
}: {
  client: ReturnType<typeof createClient>;
  callbackPath: string;
}) {
  const [externalUserId, setExternalUserId] = useState<string | null>(null);
  const [isSigningIn, setIsSigningIn] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const startGoogleSignIn = useCallback(async () => {
    const userId = "user_" + Math.random().toString(36).substring(7);
    setIsSigningIn(true);
    setError(null);

    try {
      const { url } = await client.google.getAuthUrl({
        externalUserId: userId,
        callbackUrl: window.location.origin + callbackPath,
      });

      window.location.href = url;
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setIsSigningIn(false);
    }
  }, [client, callbackPath]);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const success = params.get("success");
    const oauthError = params.get("error");
    const userId = params.get("external_user_id");

    if (success === "true" && userId) {
      setExternalUserId(userId);
      window.history.replaceState({}, "", window.location.pathname);
      setIsSigningIn(false);
      return;
    }

    if (oauthError) {
      setError(`Google sign-in failed: ${oauthError}`);
      setIsSigningIn(false);
    }
  }, []);

  return { externalUserId, isSigningIn, error, startGoogleSignIn };
}

Create a calendar picker component#

components/CalendarPicker.tsx
// Minimal calendar selector UI component.
import type { CalendarWithEvents } from "@buildcalendar/sdk";

export function CalendarPicker({
  calendars,
  selectedCalendarId,
  isLoading,
  onChange,
}: {
  calendars: CalendarWithEvents[];
  selectedCalendarId: string | null;
  isLoading: boolean;
  onChange: (calendarId: string) => void;
}) {
  return (
    <select
      value={selectedCalendarId ?? ""}
      onChange={(e) => onChange(e.target.value)}
      disabled={isLoading || calendars.length === 0}
      style={{ padding: "0.5rem", fontSize: "1rem", flex: 1 }}
    >
      {calendars.length === 0 ? (
        <option value="">
          {isLoading ? "Loading calendars..." : "No calendars"}
        </option>
      ) : (
        calendars.map((c) => (
          <option key={c.id} value={c.id}>
            {c.name} ({c.timezone})
          </option>
        ))
      )}
    </select>
  );
}

Full example#

src/pages/index.tsx
import { useEffect, useMemo, useState } from "react";
import { createClient, type CalendarWithEvents } from "@buildcalendar/sdk";
import { useCalendarApp, createEventHandlers } from "@buildcalendar/schedule-x";
import { ScheduleXCalendar } from "@schedule-x/react";
import {
  createViewDay,
  createViewMonthGrid,
  createViewWeek,
} from "@schedule-x/calendar";
import {
  createEventsServicePlugin,
  createEventRecurrencePlugin,
} from "@schedule-x/event-recurrence";
import { createInteractiveEventModal } from "@sx-premium/interactive-event-modal";
import "temporal-polyfill/global";
import "@schedule-x/theme-default/dist/index.css";
import "@sx-premium/interactive-event-modal/index.css";

import { useGoogleOAuth } from "./hooks/useGoogleOAuth";
import { CalendarPicker } from "./components/CalendarPicker";

export default function FullStackCalendarWithInteractive() {
  const apiKey = process.env.NEXT_PUBLIC_BUILDCALENDAR_API_KEY!;
  const baseUrl = undefined; // e.g. "http://localhost:5173/api/v1"

  const client = useMemo(
    () => createClient({ apiKey, baseUrl }),
    [apiKey, baseUrl],
  );

  const { externalUserId, isSigningIn, error, startGoogleSignIn } =
    useGoogleOAuth({ client, callbackPath: "/google/callback" });

  const [calendars, setCalendars] = useState<CalendarWithEvents[]>([]);
  const [selectedCalendarId, setSelectedCalendarId] = useState<string | null>(
    null,
  );
  const [isLoadingCalendars, setIsLoadingCalendars] = useState(false);
  const [calendarError, setCalendarError] = useState<string | null>(null);

  useEffect(() => {
    if (!externalUserId) return;

    let cancelled = false;

    async function run() {
      setIsLoadingCalendars(true);
      setCalendarError(null);
      try {
        const data = await client.calendars.byUser(externalUserId, {
          sync: true,
        });
        if (cancelled) return;

        setCalendars(data);
        setSelectedCalendarId((prev) => prev ?? data[0]?.id ?? null);
      } catch (e) {
        if (cancelled) return;
        setCalendarError(e instanceof Error ? e.message : String(e));
      } finally {
        if (!cancelled) setIsLoadingCalendars(false);
      }
    }

    run();
    return () => {
      cancelled = true;
    };
  }, [client, externalUserId]);

  const selectedCalendar =
    calendars.find((c) => c.id === selectedCalendarId) ?? null;

  const eventsService = useMemo(() => createEventsServicePlugin(), []);
  const eventRecurrence = useMemo(() => createEventRecurrencePlugin(), []);

  const eventHandlers = useMemo(() => {
    if (!selectedCalendar) return null;
    return createEventHandlers(
      client,
      selectedCalendar.id,
      selectedCalendar.timezone,
    );
  }, [client, selectedCalendar]);

  const interactiveModal = useMemo(() => {
    if (!selectedCalendar || !eventHandlers) return null;

    return createInteractiveEventModal({
      eventsService,
      onAddEvent: eventHandlers.onAddEvent,
      onDeleteEvent: eventHandlers.onDeleteEvent,
    });
  }, [selectedCalendar, eventHandlers, eventsService]);

  const { calendarApp } = useCalendarApp({
    buildcalendarConfig: {
      apiKey,
      baseUrl,
      calendarId: selectedCalendar?.id ?? "cal_placeholder",
      enabled: Boolean(selectedCalendar),
      interactiveEventModal: interactiveModal ?? undefined,
    },
    timezone: selectedCalendar?.timezone ?? "UTC",
    defaultView: "month-grid",
    views: [createViewDay(), createViewWeek(), createViewMonthGrid()],
    plugins: interactiveModal
      ? [eventsService, eventRecurrence, interactiveModal]
      : [],
    callbacks: eventHandlers
      ? { onEventUpdate: eventHandlers.onEventUpdate }
      : undefined,
  });

  if (!externalUserId) {
    return (
      <div style={{ padding: "2rem", textAlign: "center" }}>
        <h1>Connect your Google Calendar</h1>
        <p style={{ marginBottom: "1.5rem" }}>
          Sign in with Google to view and manage your calendars
        </p>
        <button
          onClick={startGoogleSignIn}
          disabled={isSigningIn}
          style={{
            padding: "0.75rem 1.5rem",
            fontSize: "1rem",
            background: "#4285f4",
            color: "white",
            border: "none",
            borderRadius: "0.5rem",
            cursor: isSigningIn ? "not-allowed" : "pointer",
          }}
        >
          {isSigningIn ? "Signing in..." : "Sign in with Google"}
        </button>
        {error && <p style={{ color: "red", marginTop: "1rem" }}>{error}</p>}
      </div>
    );
  }

  return (
    <div style={{ padding: "1rem" }}>
      <div style={{ marginBottom: "1rem", display: "flex", gap: "1rem" }}>
        <CalendarPicker
          calendars={calendars}
          selectedCalendarId={selectedCalendarId}
          isLoading={isLoadingCalendars}
          onChange={(id) => setSelectedCalendarId(id)}
        />
        <p style={{ margin: 0, color: "#059669", fontWeight: "bold" }}>
          Interactive
        </p>
      </div>

      {calendarError && <p style={{ color: "red" }}>{calendarError}</p>}

      {!selectedCalendar && (
        <p>
          {isLoadingCalendars
            ? "Loading calendars..."
            : "Choose a calendar to render it."}
        </p>
      )}

      {selectedCalendar && !calendarApp && <p>Loading calendar...</p>}

      {calendarApp && (
        <div>
          <p
            style={{
              marginBottom: "1rem",
              color: "#6b7280",
              fontSize: "0.875rem",
            }}
          >
            Double-click on any date/time to create an event. Click events to
            edit/delete.
          </p>
          <ScheduleXCalendar calendarApp={calendarApp} />
        </div>
      )}
    </div>
  );
}