Vue Calendar
A complete calendar implementation using Vue 3 and useTemporal.
Full Calendar Component
vue
<template>
<div class="calendar-app">
<CalendarHeader
:temporal="temporal"
:currentPeriod="currentPeriod"
@view-change="changeView"
/>
<component
:is="viewComponent"
:temporal="temporal"
:period="currentPeriod"
@date-select="handleDateSelect"
/>
<CalendarSidebar
:temporal="temporal"
:selectedDate="selectedDate"
:events="events"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { createTemporal, usePeriod } from 'usetemporal'
import CalendarHeader from './CalendarHeader.vue'
import CalendarSidebar from './CalendarSidebar.vue'
import MonthView from './MonthView.vue'
import WeekView from './WeekView.vue'
import DayView from './DayView.vue'
// Initialize temporal
const temporal = createTemporal({
date: new Date(),
now: { interval: 60000 } // Update "now" every minute
})
// State
const currentView = ref('month')
const selectedDate = ref(null)
const events = ref([])
// Computed
const currentPeriod = computed(() =>
usePeriod(temporal, currentView.value).value
)
const viewComponent = computed(() => ({
month: MonthView,
week: WeekView,
day: DayView
}[currentView.value]))
// Methods
const changeView = (view) => {
currentView.value = view
}
const handleDateSelect = (date) => {
selectedDate.value = date
// Navigate to day view on date selection
if (currentView.value !== 'day') {
temporal.browsing.value = toPeriod(temporal, date, 'day')
currentView.value = 'day'
}
}
// Load events for current period
watch(currentPeriod, async (period) => {
events.value = await loadEventsForPeriod(period)
})
// Keyboard navigation
onMounted(() => {
document.addEventListener('keydown', handleKeyboard)
})
const handleKeyboard = (e) => {
if (e.key === 'ArrowLeft') {
temporal.browsing.value = previous(temporal, currentPeriod.value)
} else if (e.key === 'ArrowRight') {
temporal.browsing.value = next(temporal, currentPeriod.value)
}
}
</script>
<style scoped>
.calendar-app {
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: auto 1fr;
height: 100vh;
background: #f5f5f5;
}
</style>Calendar Header Component
vue
<template>
<header class="calendar-header">
<div class="nav-controls">
<button @click="goToday">Today</button>
<button @click="goPrevious"><</button>
<button @click="goNext">></button>
</div>
<h1 class="period-title">{{ periodTitle }}</h1>
<div class="view-controls">
<button
v-for="view in views"
:key="view"
:class="{ active: currentView === view }"
@click="$emit('view-change', view)"
>
{{ view }}
</button>
</div>
</header>
</template>
<script setup>
import { computed } from 'vue'
import { next, previous, toPeriod } from 'usetemporal'
const props = defineProps({
temporal: Object,
currentPeriod: Object
})
const emit = defineEmits(['view-change'])
const views = ['month', 'week', 'day']
const currentView = computed(() => props.currentPeriod.type)
const periodTitle = computed(() => {
const date = props.currentPeriod.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[currentView.value]()
})
const goToday = () => {
props.temporal.browsing.value = toPeriod(props.temporal, new Date(), currentView.value)
}
const goPrevious = () => {
props.temporal.browsing.value = previous(props.temporal, props.currentPeriod)
}
const goNext = () => {
props.temporal.browsing.value = next(props.temporal, props.currentPeriod)
}
</script>
<style scoped>
.calendar-header {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.nav-controls {
display: flex;
gap: 0.5rem;
}
.view-controls {
display: flex;
gap: 0.25rem;
}
button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background: #f5f5f5;
}
button.active {
background: #2196f3;
color: white;
border-color: #2196f3;
}
.period-title {
font-size: 1.5rem;
font-weight: 600;
}
</style>Month View Component
vue
<template>
<div class="month-view">
<div class="weekdays">
<div v-for="day in weekDays" :key="day" class="weekday">
{{ day }}
</div>
</div>
<div class="days-grid">
<div
v-for="day in calendarDays"
:key="day.date.toISOString()"
class="day-cell"
:class="getDayClasses(day)"
@click="$emit('date-select', day.date)"
>
<div class="day-number">{{ day.date.getDate() }}</div>
<div class="day-events">
<div
v-for="event in getEventsForDay(day)"
:key="event.id"
class="event-dot"
:style="{ background: event.color }"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { divide, period, isSame, isToday, isWeekend } from 'usetemporal'
const props = defineProps({
temporal: Object,
period: Object,
events: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['date-select'])
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
// Generate 6-week calendar grid
const calendarDays = computed(() => {
const month = props.period
const weeks = divide(props.temporal, month, 'week')
// Get all weeks that touch this month
const firstWeek = weeks[0]
const prevWeek = previous(props.temporal, firstWeek)
const allWeeks = [prevWeek, ...weeks]
// Ensure 6 weeks total
while (allWeeks.length < 6) {
const lastWeek = allWeeks[allWeeks.length - 1]
allWeeks.push(next(props.temporal, lastWeek))
}
// Get all days from the weeks
return allWeeks.flatMap(week => divide(props.temporal, week, 'day'))
})
const getDayClasses = (day) => ({
'other-month': !isSame(props.temporal, day, props.period, 'month'),
'today': isToday(day, props.temporal),
'weekend': isWeekend(day),
'has-events': getEventsForDay(day).length > 0
})
const getEventsForDay = (day) => {
return props.events.filter(event =>
isSame(props.temporal,
toPeriod(props.temporal, event.date, 'day'),
day,
'day'
)
)
}
</script>
<style scoped>
.month-view {
padding: 1rem;
background: white;
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
margin-bottom: 0.5rem;
}
.weekday {
text-align: center;
font-weight: 600;
color: #666;
padding: 0.5rem;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #e0e0e0;
}
.day-cell {
background: white;
min-height: 100px;
padding: 0.5rem;
cursor: pointer;
transition: background-color 0.2s;
}
.day-cell:hover {
background-color: #f5f5f5;
}
.day-cell.other-month {
color: #999;
background: #fafafa;
}
.day-cell.today {
background-color: #e3f2fd;
}
.day-cell.weekend {
background-color: #f5f5f5;
}
.day-number {
font-weight: 500;
margin-bottom: 0.25rem;
}
.day-events {
display: flex;
gap: 2px;
flex-wrap: wrap;
}
.event-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
</style>Using Composition API
vue
<script setup>
import { ref, computed, watch } from 'vue'
import { createTemporal, usePeriod, divide, next, previous } from 'usetemporal'
// Composable for calendar logic
function useCalendar(initialDate = new Date()) {
const temporal = createTemporal({ date: initialDate })
const view = ref('month')
// Reactive period based on current view
const period = computed(() =>
usePeriod(temporal, view.value).value
)
// Navigation methods
const navigate = {
next: () => temporal.browsing.value = next(temporal, period.value),
previous: () => temporal.browsing.value = previous(temporal, period.value),
today: () => temporal.browsing.value = toPeriod(temporal, new Date(), view.value),
toDate: (date) => temporal.browsing.value = toPeriod(temporal, date, view.value)
}
// View methods
const changeView = (newView) => {
view.value = newView
}
// Get visible periods
const visiblePeriods = computed(() => {
if (view.value === 'month') {
const month = period.value
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.value, 'day')
})
return {
temporal,
view,
period,
visiblePeriods,
navigate,
changeView
}
}
// Use in component
const {
temporal,
view,
period,
visiblePeriods,
navigate,
changeView
} = useCalendar()
</script>Integration with Pinia
javascript
// stores/calendar.js
import { defineStore } from 'pinia'
import { createTemporal, usePeriod, next, previous, toPeriod } from 'usetemporal'
export const useCalendarStore = defineStore('calendar', {
state: () => ({
temporal: createTemporal({ date: new Date() }),
view: 'month',
selectedDate: null,
events: []
}),
getters: {
currentPeriod() {
return usePeriod(this.temporal, this.view).value
},
periodTitle() {
const date = this.currentPeriod.date
const formats = {
year: { year: 'numeric' },
month: { month: 'long', year: 'numeric' },
week: { month: 'short', day: 'numeric' },
day: { weekday: 'long', month: 'long', day: 'numeric' }
}
return date.toLocaleDateString('en', formats[this.view])
}
},
actions: {
navigateNext() {
this.temporal.browsing.value = next(this.temporal, this.currentPeriod)
},
navigatePrevious() {
this.temporal.browsing.value = previous(this.temporal, this.currentPeriod)
},
navigateToday() {
this.temporal.browsing.value = toPeriod(
this.temporal,
new Date(),
this.view
)
},
changeView(view) {
this.view = view
},
selectDate(date) {
this.selectedDate = date
},
async loadEvents(period) {
// Load events for period
this.events = await api.getEvents({
start: period.start,
end: period.end
})
}
}
})See Also
- Month Calendar - Month view implementation
- Reactive Time Units - Vue reactivity integration
- Framework Agnostic - Core concepts
- React Calendar - React implementation