// Calendar Event Shape - Individual event cards spawned on the canvas // Similar to FathomNoteShape but for calendar events import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, createShapeId, } from "tldraw" import React from "react" import type { DecryptedCalendarEvent } from "@/hooks/useCalendarEvents" type ICalendarEventShape = TLBaseShape< "CalendarEvent", { w: number h: number // Event data eventId: string calendarId: string summary: string description: string | null location: string | null startTime: number // timestamp endTime: number // timestamp isAllDay: boolean timezone: string meetingLink: string | null // Visual primaryColor: string tags: string[] } > export class CalendarEventShape extends BaseBoxShapeUtil { static override type = "CalendarEvent" as const // Calendar theme color: Green static readonly PRIMARY_COLOR = "#22c55e" getDefaultProps(): ICalendarEventShape["props"] { return { w: 350, h: 250, eventId: "", calendarId: "", summary: "Untitled Event", description: null, location: null, startTime: Date.now(), endTime: Date.now() + 3600000, // 1 hour later isAllDay: false, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, meetingLink: null, primaryColor: CalendarEventShape.PRIMARY_COLOR, tags: ["calendar", "event"], } } override canResize() { return true } // Factory method to create from DecryptedCalendarEvent static createFromEvent( event: DecryptedCalendarEvent, x: number, y: number, primaryColor: string = CalendarEventShape.PRIMARY_COLOR ) { return { id: createShapeId(`calendar-event-${event.id}`), type: "CalendarEvent" as const, x, y, props: { w: 350, h: 250, eventId: event.id, calendarId: event.calendarId, summary: event.summary, description: event.description, location: event.location, startTime: event.startTime.getTime(), endTime: event.endTime.getTime(), isAllDay: event.isAllDay, timezone: event.timezone, meetingLink: event.meetingLink, primaryColor, tags: ["calendar", "event"], }, } } component(shape: ICalendarEventShape) { const { props } = shape const { w, h } = props const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) // Detect dark mode const isDarkMode = typeof document !== "undefined" && document.documentElement.classList.contains("dark") // Format date and time const formatDateTime = (timestamp: number, isAllDay: boolean) => { const date = new Date(timestamp) const dateStr = date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", }) if (isAllDay) { return dateStr } const timeStr = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }) return `${dateStr} at ${timeStr}` } // Format time range const formatTimeRange = () => { if (props.isAllDay) { const startDate = new Date(props.startTime) const endDate = new Date(props.endTime) // Check if same day if (startDate.toDateString() === endDate.toDateString()) { return `All day - ${startDate.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", })}` } return `${startDate.toLocaleDateString("en-US", { month: "short", day: "numeric", })} - ${endDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })}` } const startDate = new Date(props.startTime) const endDate = new Date(props.endTime) const startTimeStr = startDate.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }) const endTimeStr = endDate.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }) const dateStr = startDate.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric", }) return `${dateStr}\n${startTimeStr} - ${endTimeStr}` } // Calculate duration const getDuration = () => { const durationMs = props.endTime - props.startTime const hours = Math.floor(durationMs / (1000 * 60 * 60)) const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)) if (hours === 0) { return `${minutes}m` } if (minutes === 0) { return `${hours}h` } return `${hours}h ${minutes}m` } const colors = isDarkMode ? { bg: "#1a1a1a", headerBg: props.primaryColor, text: "#e4e4e7", textMuted: "#a1a1aa", border: "#404040", linkBg: "#3b82f6", } : { bg: "#ffffff", headerBg: props.primaryColor, text: "#1f2937", textMuted: "#6b7280", border: "#e5e7eb", linkBg: "#3b82f6", } const handleMeetingLinkClick = (e: React.MouseEvent) => { e.stopPropagation() if (props.meetingLink) { window.open(props.meetingLink, "_blank", "noopener,noreferrer") } } return (
{/* Header */}
{props.isAllDay ? "📅" : "🕐"}
{props.summary}
{props.isAllDay ? "All Day" : getDuration()}
{/* Content */}
{/* Time */}
When
{formatTimeRange()}
{/* Location */} {props.location && (
Location
📍 {props.location}
)} {/* Description */} {props.description && (
Description
{props.description}
)} {/* Meeting Link */} {props.meetingLink && ( )}
{/* Footer with tags */}
{props.tags.map((tag, i) => ( {tag} ))}
) } indicator(shape: ICalendarEventShape) { return ( ) } }