My App
CourseKitExamples

React Schedule App

Frontend example with TimetableGrid, hooks, and conflict checking

Overview

This example builds a complete schedule viewer with event creation, conflict checking, and availability display using @hfu.digital/coursekit-react.

Provider Setup

// App.tsx
import { CourseKitProvider } from '@hfu.digital/coursekit-react';

function App() {
    const token = useAuthToken();

    return (
        <CourseKitProvider
            apiUrl="/api/coursekit"
            fetch={(url, init) => fetch(url, {
                ...init,
                headers: { ...init?.headers, Authorization: `Bearer ${token}` },
            })}
        >
            <SchedulePage />
        </CourseKitProvider>
    );
}

Week Schedule View

// SchedulePage.tsx
import { useState } from 'react';
import {
    useTimetable,
    useAvailability,
    TimetableGrid,
    EventCard,
    AvailabilityOverlay,
} from '@hfu.digital/coursekit-react';

function SchedulePage() {
    const [weekStart, setWeekStart] = useState('2026-03-02');
    const weekEnd = getWeekEnd(weekStart); // helper to add 5 days

    const { data: occurrences, loading } = useTimetable({
        dateRange: { start: weekStart, end: weekEnd },
    });

    const { slots } = useAvailability({
        entityType: 'instructor',
        entityId: 'current-user',
        dateRange: { start: weekStart, end: weekEnd },
    });

    if (loading) return <div>Loading schedule...</div>;

    // Map occurrences to grid events
    const gridEvents = occurrences.map(occ => ({
        id: `${occ.eventId}-${occ.occurrenceDate}`,
        title: occ.originalEvent.title,
        startTime: occ.startTime,
        durationMin: occ.durationMin,
        dayIndex: getDayIndex(occ.startTime, weekStart), // 0=Mon, 4=Fri
    }));

    // Map availability to overlay blocks
    const availabilityBlocks = slots
        .filter(s => s.type === 'blocked')
        .map(s => ({
            startTime: s.startTime,
            endTime: s.endTime,
            dayIndex: getDayIndex(s.startTime, weekStart),
            type: s.type as 'blocked',
            hardness: s.hardness as 'hard' | 'soft',
        }));

    return (
        <div>
            <WeekNav weekStart={weekStart} onChange={setWeekStart} />

            <div style={{ position: 'relative' }}>
                <TimetableGrid
                    weekStart={weekStart}
                    startHour={8}
                    endHour={18}
                    events={gridEvents}
                    renderEvent={(event) => {
                        const occ = occurrences.find(
                            o => `${o.eventId}-${o.occurrenceDate}` === event.id,
                        );
                        return (
                            <EventCard
                                title={event.title}
                                startTime={event.startTime}
                                durationMin={event.durationMin}
                                isException={occ?.isException}
                                exceptionType={occ?.exceptionType}
                            />
                        );
                    }}
                />
                <AvailabilityOverlay blocks={availabilityBlocks} startHour={8} />
            </div>
        </div>
    );
}

Event Creator with Conflict Preview

// EventCreator.tsx
import { useState } from 'react';
import { useConflictCheck, useMutation, ConflictBadge } from '@hfu.digital/coursekit-react';

function EventCreator() {
    const [title, setTitle] = useState('');
    const [startTime, setStartTime] = useState('');
    const [durationMin, setDurationMin] = useState(90);
    const [roomId, setRoomId] = useState('');

    const { result: conflicts, loading: checking, check } = useConflictCheck();
    const { createEvent, loading: saving } = useMutation({
        onSuccess: () => alert('Event created!'),
    });

    const handlePreview = async () => {
        await check(
            { title, startTime, durationMin, roomId: roomId || null },
            { start: startTime, end: addMonths(startTime, 4) },
        );
    };

    const handleCreate = async () => {
        await createEvent({
            title,
            startTime,
            durationMin,
            roomId: roomId || null,
        });
    };

    return (
        <form onSubmit={(e) => { e.preventDefault(); handleCreate(); }}>
            <input value={title} onChange={e => setTitle(e.target.value)} placeholder="Event title" />
            <input type="datetime-local" value={startTime} onChange={e => setStartTime(e.target.value)} />
            <input type="number" value={durationMin} onChange={e => setDurationMin(+e.target.value)} />
            <input value={roomId} onChange={e => setRoomId(e.target.value)} placeholder="Room ID" />

            <button type="button" onClick={handlePreview} disabled={checking}>
                Check Conflicts
            </button>

            {conflicts && (
                <div>
                    {conflicts.conflicts.map(c => (
                        <ConflictBadge
                            key={c.id}
                            severity={c.severity}
                            message={c.message}
                            type={c.type}
                        />
                    ))}
                    {conflicts.conflicts.length === 0 && <p>No conflicts detected</p>}
                </div>
            )}

            <button type="submit" disabled={saving || conflicts?.hasErrors}>
                Create Event
            </button>
        </form>
    );
}

Room Finder

// RoomFinder.tsx
import { useRoomSearch } from '@hfu.digital/coursekit-react';

function RoomFinder({ startTime, endTime }: { startTime: string; endTime: string }) {
    const { rooms, loading, search } = useRoomSearch();

    return (
        <div>
            <button onClick={() => search({
                minCapacity: 30,
                availableAt: { start: startTime, end: endTime },
            })}>
                Find Available Rooms
            </button>

            {loading && <p>Searching...</p>}

            {rooms.map(room => (
                <div key={room.id}>
                    <strong>{room.name}</strong>
                    {room.building && <span> — {room.building}</span>}
                    <span> (capacity: {room.capacity})</span>
                </div>
            ))}
        </div>
    );
}

Helper Functions

function getWeekEnd(weekStart: string): string {
    const date = new Date(weekStart);
    date.setDate(date.getDate() + 5);
    return date.toISOString().split('T')[0];
}

function getDayIndex(dateStr: string, weekStart: string): number {
    const date = new Date(dateStr);
    const start = new Date(weekStart);
    return Math.floor((date.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
}

function addMonths(dateStr: string, months: number): string {
    const date = new Date(dateStr);
    date.setMonth(date.getMonth() + months);
    return date.toISOString();
}

On this page