React Calendar
A complete calendar implementation using React and useTemporal.
Full Calendar Component
jsx
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { createTemporal, usePeriod, next, previous, toPeriod, divide, period, isSame, isToday, isWeekend } from 'usetemporal'
import CalendarHeader from './CalendarHeader'
import CalendarSidebar from './CalendarSidebar'
import MonthView from './MonthView'
import WeekView from './WeekView'
import DayView from './DayView'
function Calendar() {
// Initialize temporal
const [temporal] = useState(() =>
createTemporal({
date: new Date(),
now: { interval: 60000 } // Update "now" every minute
})
)
// State
const [view, setView] = useState('month')
const [selectedDate, setSelectedDate] = useState(null)
const [events, setEvents] = useState([])
// Get current period using custom hook
const period = useTemporalPeriod(temporal, view)
// View components map
const viewComponents = {
month: MonthView,
week: WeekView,
day: DayView
}
const ViewComponent = viewComponents[view]
// Navigation handlers
const handleNavigate = useCallback((direction) => {
if (direction === 'next') {
temporal.browsing.value = next(temporal, period)
} else if (direction === 'previous') {
temporal.browsing.value = previous(temporal, period)
} else if (direction === 'today') {
temporal.browsing.value = toPeriod(temporal, new Date(), view)
}
}, [temporal, period, view])
// Date selection
const handleDateSelect = useCallback((date) => {
setSelectedDate(date)
// Navigate to day view on selection
if (view !== 'day') {
temporal.browsing.value = toPeriod(temporal, date, 'day')
setView('day')
}
}, [temporal, view])
// Load events when period changes
useEffect(() => {
loadEventsForPeriod(period).then(setEvents)
}, [period])
// Keyboard navigation
useEffect(() => {
const handleKeyboard = (e) => {
if (e.key === 'ArrowLeft') {
handleNavigate('previous')
} else if (e.key === 'ArrowRight') {
handleNavigate('next')
} else if (e.key === 't') {
handleNavigate('today')
}
}
document.addEventListener('keydown', handleKeyboard)
return () => document.removeEventListener('keydown', handleKeyboard)
}, [handleNavigate])
return (
<div className="calendar-app">
<CalendarHeader
period={period}
view={view}
onViewChange={setView}
onNavigate={handleNavigate}
/>
<ViewComponent
temporal={temporal}
period={period}
events={events}
selectedDate={selectedDate}
onDateSelect={handleDateSelect}
/>
<CalendarSidebar
temporal={temporal}
selectedDate={selectedDate}
events={events}
/>
</div>
)
}
// Custom hook for reactive period
function useTemporalPeriod(temporal, unit) {
const [period, setPeriod] = useState(() =>
usePeriod(temporal, unit).value
)
useEffect(() => {
const unwatch = temporal.browsing.watch(() => {
setPeriod(usePeriod(temporal, unit).value)
})
return unwatch
}, [temporal, unit])
return period
}
export default CalendarCalendar Header Component
jsx
import React from 'react'
function CalendarHeader({ period, view, onViewChange, onNavigate }) {
const views = ['month', 'week', 'day']
const periodTitle = useMemo(() => {
const date = period.date
const formatters = {
month: () => date.toLocaleDateString('en', { month: 'long', year: 'numeric' }),
week: () => `Week of ${date.toLocaleDateString('en', { month: 'short', day: 'numeric' })}`,
day: () => date.toLocaleDateString('en', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })
}
return formatters[view]()
}, [period, view])
return (
<header className="calendar-header">
<div className="nav-controls">
<button onClick={() => onNavigate('today')}>Today</button>
<button onClick={() => onNavigate('previous')}><</button>
<button onClick={() => onNavigate('next')}>></button>
</div>
<h1 className="period-title">{periodTitle}</h1>
<div className="view-controls">
{views.map(v => (
<button
key={v}
className={view === v ? 'active' : ''}
onClick={() => onViewChange(v)}
>
{v}
</button>
))}
</div>
</header>
)
}
export default CalendarHeaderMonth View Component
jsx
import React, { useMemo } from 'react'
import { divide, period, isSame, isToday, isWeekend, next, previous } from 'usetemporal'
function MonthView({ temporal, period, events, selectedDate, onDateSelect }) {
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
// Generate 6-week calendar grid
const days = useMemo(() => {
const month = period
const weeks = divide(temporal, month, 'week')
// Get all weeks that touch this month
const firstWeek = weeks[0]
const prevWeek = previous(temporal, firstWeek)
const allWeeks = [prevWeek, ...weeks]
// Ensure 6 weeks total
while (allWeeks.length < 6) {
const lastWeek = allWeeks[allWeeks.length - 1]
allWeeks.push(next(temporal, lastWeek))
}
// Get all days from the weeks
return allWeeks.flatMap(week => divide(temporal, week, 'day'))
}, [temporal, period])
const getDayClasses = (day) => {
const classes = ['day-cell']
if (!isSame(temporal, day, period, 'month')) {
classes.push('other-month')
}
if (isToday(day, temporal)) {
classes.push('today')
}
if (isWeekend(day)) {
classes.push('weekend')
}
if (selectedDate && isSame(temporal, day, toPeriod(temporal, selectedDate, 'day'), 'day')) {
classes.push('selected')
}
if (getEventsForDay(day).length > 0) {
classes.push('has-events')
}
return classes.join(' ')
}
const getEventsForDay = (day) => {
return events.filter(event =>
isSame(temporal,
toPeriod(temporal, event.date, 'day'),
day,
'day'
)
)
}
return (
<div className="month-view">
<div className="weekdays">
{weekDays.map(day => (
<div key={day} className="weekday">{day}</div>
))}
</div>
<div className="days-grid">
{days.map(day => (
<div
key={day.date.toISOString()}
className={getDayClasses(day)}
onClick={() => onDateSelect(day.date)}
>
<div className="day-number">{day.date.getDate()}</div>
<div className="day-events">
{getEventsForDay(day).map(event => (
<div
key={event.id}
className="event-dot"
style={{ backgroundColor: event.color }}
/>
))}
</div>
</div>
))}
</div>
</div>
)
}
export default MonthViewUsing React Hooks
jsx
import { useState, useEffect, useMemo, useCallback, useSyncExternalStore } from 'react'
import { createTemporal, usePeriod, next, previous, toPeriod } from 'usetemporal'
// Custom hook for temporal reactivity
function useTemporalValue(getValue) {
return useSyncExternalStore(
useCallback((callback) => getValue().watch(callback), [getValue]),
useCallback(() => getValue().value, [getValue])
)
}
// Calendar hook
function useCalendar(initialDate = new Date()) {
const [temporal] = useState(() => createTemporal({ date: initialDate }))
const [view, setView] = useState('month')
// Get reactive period
const period = useTemporalValue(
useCallback(() => usePeriod(temporal, view), [temporal, view])
)
// Navigation methods
const navigate = useMemo(() => ({
next: () => temporal.browsing.value = next(temporal, period),
previous: () => temporal.browsing.value = previous(temporal, period),
today: () => temporal.browsing.value = toPeriod(temporal, new Date(), view),
toDate: (date) => temporal.browsing.value = toPeriod(temporal, date, view)
}), [temporal, period, view])
// Get visible periods
const visiblePeriods = useMemo(() => {
if (view === 'month') {
const month = period
const weeks = divide(temporal, month, 'week')
// Get all weeks that touch this month
const firstWeek = weeks[0]
const prevWeek = previous(temporal, firstWeek)
const allWeeks = [prevWeek, ...weeks]
// Ensure 6 weeks total
while (allWeeks.length < 6) {
const lastWeek = allWeeks[allWeeks.length - 1]
allWeeks.push(next(temporal, lastWeek))
}
// Get all days from the weeks
return allWeeks.flatMap(week => divide(temporal, week, 'day'))
}
return divide(temporal, period, 'day')
}, [temporal, period, view])
return {
temporal,
view,
setView,
period,
visiblePeriods,
navigate
}
}
// Usage
function CalendarApp() {
const {
temporal,
view,
setView,
period,
visiblePeriods,
navigate
} = useCalendar()
return (
<div>
<button onClick={navigate.previous}>Previous</button>
<button onClick={navigate.today}>Today</button>
<button onClick={navigate.next}>Next</button>
<div className="calendar-grid">
{visiblePeriods.map(day => (
<div key={day.date.toISOString()}>
{day.date.getDate()}
</div>
))}
</div>
</div>
)
}Integration with Redux Toolkit
javascript
// store/calendarSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { createTemporal, next, previous, toPeriod } from 'usetemporal'
const temporal = createTemporal({ date: new Date() })
const calendarSlice = createSlice({
name: 'calendar',
initialState: {
view: 'month',
selectedDate: null,
browsingDate: temporal.browsing.value.date.toISOString(),
events: []
},
reducers: {
navigateNext(state) {
const period = usePeriod(temporal, state.view).value
temporal.browsing.value = next(temporal, period)
state.browsingDate = temporal.browsing.value.date.toISOString()
},
navigatePrevious(state) {
const period = usePeriod(temporal, state.view).value
temporal.browsing.value = previous(temporal, period)
state.browsingDate = temporal.browsing.value.date.toISOString()
},
navigateToday(state) {
temporal.browsing.value = toPeriod(temporal, new Date(), state.view)
state.browsingDate = temporal.browsing.value.date.toISOString()
},
changeView(state, action) {
state.view = action.payload
},
selectDate(state, action) {
state.selectedDate = action.payload
},
setEvents(state, action) {
state.events = action.payload
}
}
})
export const {
navigateNext,
navigatePrevious,
navigateToday,
changeView,
selectDate,
setEvents
} = calendarSlice.actions
export default calendarSlice.reducer
// Selectors
export const selectCurrentPeriod = (state) => {
return usePeriod(temporal, state.calendar.view).value
}Server Components (Next.js 13+)
jsx
// app/calendar/page.jsx
import { createTemporal, usePeriod, divide } from 'usetemporal'
import CalendarClient from './CalendarClient'
export default async function CalendarPage() {
// Server-side temporal
const temporal = createTemporal({ date: new Date() })
const month = usePeriod(temporal, 'month').value
const days = divide(temporal, month, 'day')
// Fetch events on server
const events = await fetchEvents({
start: month.start,
end: month.end
})
// Serialize for client
const initialData = {
month: {
start: month.start.toISOString(),
end: month.end.toISOString(),
type: month.type
},
days: days.map(d => ({
date: d.date.toISOString(),
start: d.start.toISOString(),
end: d.end.toISOString()
})),
events
}
return <CalendarClient initialData={initialData} />
}See Also
- Month Calendar - Month view patterns
- Reactive Time Units - React integration
- Framework Agnostic - Core concepts
- Vue Calendar - Vue implementation