
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:
@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-xInstall 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-polyfillStyling 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;
}Set up Google OAuth#
// 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#
// 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#
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>
);
}
