BuildCalendar

Full Stack Calendar Setup (Free Tier)

This guide shows the free tier setup: users can sign in with Google and view calendars/events in a UI calendar built with Schedule-X.

If you need event creation/editing/deletion, see the interactive version: Full Stack Calendar Setup (Interactive).

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).

Installing Dependencies#

Install the SDK + adapter:

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

Install Schedule-X packages:

npm install @schedule-x/react @schedule-x/calendar @schedule-x/theme-default 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:

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

hooks/useGoogleOAuth.ts#

First we need a hook that starts Google OAuth and reads the callback params back into app state.

// 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 };
}

components/CalendarPicker.tsx#

Then, let's create a minimal calendar selector UI component, in case your user has multiple calendars over at Google.

// 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" }}
    >
      {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#

import { useEffect, useMemo, useState } from "react";
import { createClient, type CalendarWithEvents } from "@buildcalendar/sdk";
import { useCalendarApp } from "@buildcalendar/schedule-x";
import { ScheduleXCalendar } from "@schedule-x/react";
import {
  createViewDay,
  createViewMonthGrid,
  createViewWeek,
} from "@schedule-x/calendar";
import "temporal-polyfill/global";
import "@schedule-x/theme-default/dist/index.css";

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

export default function FullStackCalendar() {
  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 { calendarApp } = useCalendarApp({
    buildcalendarConfig: {
      apiKey,
      baseUrl,
      calendarId: selectedCalendar?.id ?? "cal_placeholder",
      enabled: Boolean(selectedCalendar),
    },
    timezone: selectedCalendar?.timezone ?? "UTC",
    defaultView: "month-grid",
    views: [createViewDay(), createViewWeek(), createViewMonthGrid()],
  });

  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 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" }}>
        <CalendarPicker
          calendars={calendars}
          selectedCalendarId={selectedCalendarId}
          isLoading={isLoadingCalendars}
          onChange={(id) => setSelectedCalendarId(id)}
        />
      </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 && <ScheduleXCalendar calendarApp={calendarApp} />}
    </div>
  );
}