From ac6c9c47cfd13e2367914d6894686d29bec15b9c Mon Sep 17 00:00:00 2001 From: Richard Tan <30404522+richardhjtan@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:11:51 +0800 Subject: [PATCH] add Calendar Card changes [boxel-content-hash:d42b22308cf1] --- .../29788763-4033-4082-bb12-5e2973eba843.json | 36 + .../63403370-827b-425e-a973-eba8435faf0d.json | 69 + .../9f05a3b4-5caa-4432-b1d2-e6d643f731b8.json | 45 + .../calendar/calendar.gts | 3717 +++++++++++++++++ 4 files changed, 3867 insertions(+) create mode 100644 CalendarCard/29788763-4033-4082-bb12-5e2973eba843.json create mode 100644 CardListing/63403370-827b-425e-a973-eba8435faf0d.json create mode 100644 Spec/9f05a3b4-5caa-4432-b1d2-e6d643f731b8.json create mode 100644 study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar.gts diff --git a/CalendarCard/29788763-4033-4082-bb12-5e2973eba843.json b/CalendarCard/29788763-4033-4082-bb12-5e2973eba843.json new file mode 100644 index 0000000..b388666 --- /dev/null +++ b/CalendarCard/29788763-4033-4082-bb12-5e2973eba843.json @@ -0,0 +1,36 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CalendarCard", + "module": "http://localhost:4201/richardtan/richy-workspace/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar" + } + }, + "type": "card", + "attributes": { + "year": 2026, + "month": 4, + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + }, + "viewMode": "day", + "calendarName": null, + "selectedDate": null + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + }, + "cardInfo.cardThumbnail": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/CardListing/63403370-827b-425e-a973-eba8435faf0d.json b/CardListing/63403370-827b-425e-a973-eba8435faf0d.json new file mode 100644 index 0000000..4016a83 --- /dev/null +++ b/CardListing/63403370-827b-425e-a973-eba8435faf0d.json @@ -0,0 +1,69 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CardListing", + "module": "http://localhost:4201/catalog/catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Calendar Card", + "images": [], + "summary": "The CalendarCard module defines a versatile calendar component that can be embedded or displayed in various formats such as month, week, or day views. It manages calendar state including current month, year, and selected date, and facilitates querying and displaying associated events, including their start/end times, locations, and types. The component supports interactive features like navigating between months, weeks, and days, and offers the ability to add, edit, and view detailed events. Multiple embedded and fitted variations provide compact, stylized summaries of calendars and upcoming events, with customizable display modes. The primary purpose is to serve as an adaptable, integrated calendar interface for scheduling, event management, and visual overview within applications.", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "specs.0": { + "links": { + "self": "../Spec/9f05a3b4-5caa-4432-b1d2-e6d643f731b8" + } + }, + "skills": { + "links": { + "self": null + } + }, + "tags.0": { + "links": { + "self": "http://localhost:4201/catalog/Tag/ed5a1a3f-0dbf-47b5-b2a6-d88b0d2a7642" + } + }, + "license": { + "links": { + "self": "http://localhost:4201/catalog/License/4c5a023b-a72c-4f90-930b-da60a1de5b2d" + } + }, + "publisher": { + "links": { + "self": null + } + }, + "categories": { + "links": { + "self": null + } + }, + "examples.0": { + "links": { + "self": "../CalendarCard/29788763-4033-4082-bb12-5e2973eba843" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + }, + "cardInfo.cardThumbnail": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/Spec/9f05a3b4-5caa-4432-b1d2-e6d643f731b8.json b/Spec/9f05a3b4-5caa-4432-b1d2-e6d643f731b8.json new file mode 100644 index 0000000..a40df5c --- /dev/null +++ b/Spec/9f05a3b4-5caa-4432-b1d2-e6d643f731b8.json @@ -0,0 +1,45 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Spec", + "module": "https://cardstack.com/base/spec" + } + }, + "type": "card", + "attributes": { + "ref": { + "name": "CalendarCard", + "module": "http://localhost:4201/richardtan/richy-workspace/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar" + }, + "readMe": "Here is the README documentation for the CalendarCard spec:\n\n## Summary\nThe CalendarCard is a card definition that represents a calendar view. It allows users to view and manage events for a particular calendar.\n\n## Import\n```javascript\nimport { CalendarCard } from 'http://localhost:4201/richardtan/richy-workspace/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar';\n```\n\n## Usage as a Field\nTo use the CalendarCard as a field within a consuming card or field, you can do the following:\n\n```typescript\n@field calendar = linksTo(CalendarCard);\n```\n\nThis will allow you to associate a CalendarCard with the consuming entity.\n\n## Template Usage\nTo use the CalendarCard within a template, you can do the following:\n\n```hbs\n<@fields.calendar @format=\"embedded\" />\n```\n\nThis will render the CalendarCard in its embedded format within the consuming template.", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + }, + "specType": "card", + "cardTitle": "Calendar", + "cardDescription": null, + "containedExamples": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + }, + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.cardThumbnail": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar.gts b/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar.gts new file mode 100644 index 0000000..83fd138 --- /dev/null +++ b/study-hub-4d8746e6-8dd6-452d-8001-6b574ea1de18/calendar/calendar.gts @@ -0,0 +1,3717 @@ +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + field, + contains, + Component, + realmURL, + linksTo, +} from 'https://cardstack.com/base/card-api'; // ¹ Core imports +import StringField from 'https://cardstack.com/base/string'; +import NumberField from 'https://cardstack.com/base/number'; +import DateField from 'https://cardstack.com/base/date'; +import DatetimeField from 'https://cardstack.com/base/datetime'; +import TextAreaField from 'https://cardstack.com/base/text-area'; +import { Button } from '@cardstack/boxel-ui/components'; // ² UI components +import { fn, concat } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { restartableTask } from 'ember-concurrency'; +import { htmlSafe } from '@ember/template'; +import { eq, lt, gt, subtract } from '@cardstack/boxel-ui/helpers'; +import { cached } from '@glimmer/tracking'; +import CalendarIcon from '@cardstack/boxel-icons/calendar'; +import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; +import type { Query } from '@cardstack/runtime-common'; + +// Simple date formatting helper for calendar +function formatCalendarDate( + date: Date | string | number | null | undefined, + format?: string, +): string { + if (!date) return ''; + + let parsedDate: Date; + + if (typeof date === 'string') { + parsedDate = new Date(date); + } else if (typeof date === 'number') { + parsedDate = new Date(date); + } else if (date instanceof Date) { + parsedDate = date; + } else { + return ''; + } + + if (isNaN(parsedDate.getTime())) { + return ''; + } + + // Simple format options + switch (format) { + case 'short': + return parsedDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + case 'long': + return parsedDate.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + }); + case 'time': + return parsedDate.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + case '24h': + return parsedDate.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + case 'month': + return parsedDate.toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }); + case 'day': + return parsedDate.toLocaleDateString('en-US', { weekday: 'short' }); + default: + return parsedDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + } +} + +// ³ Calendar Event card definition +export class CalendarEvent extends CardDef { + static displayName = 'Calendar Event'; + static icon = CalendarIcon; + + @field title = contains(StringField); // ⁴ Event details + @field description = contains(TextAreaField); + @field startTime = contains(DatetimeField); + @field endTime = contains(DatetimeField); + @field location = contains(StringField); + @field isAllDay = contains(StringField); // "true" or "false" + @field eventType = contains(StringField); // meeting, appointment, reminder, etc. + @field eventColor = contains(StringField); // hex color + @field calendar = linksTo(() => CalendarCard); // ²² Link to parent calendar + + static embedded = class Embedded extends Component { + + }; + + static fitted = class Fitted extends Component { + + }; +} + +class CalendarIsolated extends Component { + // ¹¹ Isolated format with event management + @tracked currentDate = new Date(); + @tracked viewMode = 'month'; + @tracked showEventForm = false; + @tracked editingEvent: any = null; + @tracked showMoreEventsFor: any = null; + @tracked hoveredDate: Date | null = null; + @tracked newEventTitle = ''; + @tracked newEventDescription = ''; + @tracked newEventStartTime = ''; + @tracked newEventEndTime = ''; + @tracked newEventLocation = ''; + @tracked newEventIsAllDay = false; + @tracked newEventType = 'meeting'; + + constructor(owner: unknown, args: any) { + super(owner, args); + // ¹² Initialize from model data + if (this.args.model?.month && this.args.model?.year) { + this.currentDate = new Date( + this.args.model.year, + this.args.model.month - 1, + 1, + ); + } + if (this.args.model?.viewMode) { + this.viewMode = this.args.model.viewMode; + } + } + + get currentMonth() { + return this.currentDate.getMonth(); + } + + get currentYear() { + return this.currentDate.getFullYear(); + } + + get monthName() { + return this.currentDate.toLocaleDateString('en-US', { month: 'long' }); + } + + // ²⁴ Use getCards to query events for this calendar + eventsResult = this.args.context?.getCards( + this, + () => this.args.model?.eventsQuery, + () => this.args.model?.realmHrefs, + { isLive: true }, + ); + + // ²⁵ Dynamic event checking using queried events - cached to prevent infinite renders + @cached + get eventsOnDate() { + const events = (this.eventsResult?.instances as CalendarEvent[]) || []; + const eventMap = new Map(); + + events.forEach((event) => { + if (event?.startTime) { + const eventDate = new Date(event.startTime); + const dateKey = `${eventDate.getFullYear()}-${ + eventDate.getMonth() + 1 + }-${eventDate.getDate()}`; + if (!eventMap.has(dateKey)) { + eventMap.set(dateKey, []); + } + eventMap.get(dateKey).push(event); + } + }); + + return eventMap; + } + + // Helper method for template - returns cached array for specific date + getEventsForDate = (date: Date) => { + const dateKey = `${date.getFullYear()}-${ + date.getMonth() + 1 + }-${date.getDate()}`; + + return this.eventsOnDate.get(dateKey) || []; + }; + + // ¹⁵ Get events for the currently selected day in day view + get todaysEvents() { + return this.getEventsForDate(this.currentDate); + } + + // ¹⁶ Get events for current week (for week view) - improved with proper week calculation + get weekEvents() { + const weekStart = new Date(this.currentDate); + weekStart.setDate(weekStart.getDate() - weekStart.getDay()); + weekStart.setHours(0, 0, 0, 0); + + const events = []; + for (let i = 0; i < 7; i++) { + const date = new Date(weekStart); + date.setDate(date.getDate() + i); + const dayEvents = this.getEventsForDate(date); + + // Add day reference to each event for positioning + dayEvents.forEach((event: any) => { + event._weekDay = i; + event._date = new Date(date); + }); + + events.push(...dayEvents); + } + + return events.sort((a, b) => { + const timeA = a.startTime ? new Date(a.startTime).getTime() : 0; + const timeB = b.startTime ? new Date(b.startTime).getTime() : 0; + return timeA - timeB; + }); + } + + // ³⁶ Get current week days for week view + get currentWeekDays() { + const weekStart = new Date(this.currentDate); + weekStart.setDate(weekStart.getDate() - weekStart.getDay()); + weekStart.setHours(0, 0, 0, 0); + + const days = []; + for (let i = 0; i < 7; i++) { + const date = new Date(weekStart); + date.setDate(date.getDate() + i); + + days.push({ + date: new Date(date), + day: date.getDate(), + dayName: date.toLocaleDateString('en-US', { weekday: 'short' }), + isToday: this.isSameDay(date, new Date()), + events: this.getEventsForDate(date), + }); + } + + return days; + } + + // ³⁷ Generate time slots for week view (24-hour format) + get timeSlots() { + const slots = []; + for (let hour = 0; hour < 24; hour++) { + const time = new Date(); + time.setHours(hour, 0, 0, 0); + + slots.push({ + hour, + timeLabel: time.toLocaleTimeString('en-US', { + hour: 'numeric', + hour12: false, + }), + displayLabel: time.toLocaleTimeString('en-US', { + hour: 'numeric', + hour12: true, + }), + }); + } + return slots; + } + + // ³⁸ Get events for a specific hour across all days + getEventsForHour(hour: number) { + return this.weekEvents.filter((event) => { + if (!event.startTime) return false; + const eventTime = new Date(event.startTime); + return eventTime.getHours() === hour; + }); + } + + // Helper to check if event starts at specific hour + eventStartsAtHour(event: any, hour: number): boolean { + if (!event?.startTime) return false; + const eventTime = new Date(event.startTime); + return eventTime.getHours() === hour; + } + + get realmURL(): URL { + return this.args.model[realmURL]!; + } + + @cached + get calendarDays() { + // ⁷ Calendar day calculation with cached events + const year = this.currentYear; + const month = this.currentMonth; + const firstDay = new Date(year, month, 1); + const startDate = new Date(firstDay); + startDate.setDate(startDate.getDate() - firstDay.getDay()); + + const days = []; + const currentDate = new Date(startDate); + + for (let i = 0; i < 42; i++) { + const dayEvents = this.getEventsForDate(currentDate); + days.push({ + date: new Date(currentDate), + day: currentDate.getDate(), + isCurrentMonth: currentDate.getMonth() === month, + isToday: this.isSameDay(currentDate, new Date()), + + hasEvents: dayEvents.length > 0, + events: dayEvents as CalendarEvent[], // Include events array for each day + }); + currentDate.setDate(currentDate.getDate() + 1); + } + + return days; + } + + isSameDay(date1: Date, date2: Date): boolean { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); + } + + hasEventsOnDate(date: Date): boolean { + // ⁸ Dynamic event detection using real events + return this.getEventsForDate(date).length > 0; + } + + @action + previousMonth() { + this.currentDate = new Date(this.currentYear, this.currentMonth - 1, 1); + this.updateModelState(); // ¹ʰ Persist state changes + } + + @action + nextMonth() { + this.currentDate = new Date(this.currentYear, this.currentMonth + 1, 1); + this.updateModelState(); // ¹ʰ Persist state changes + } + + @action + previousWeek() { + // Move to previous week + const prevWeek = new Date(this.currentDate); + prevWeek.setDate(prevWeek.getDate() - 7); + this.currentDate = prevWeek; + this.updateModelState(); + } + + @action + nextWeek() { + // Move to next week + const nextWeek = new Date(this.currentDate); + nextWeek.setDate(nextWeek.getDate() + 7); + this.currentDate = nextWeek; + this.updateModelState(); + } + + // ³⁹ Week date range display + get weekDateRange() { + const weekStart = new Date(this.currentDate); + weekStart.setDate(weekStart.getDate() - weekStart.getDay()); + + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekEnd.getDate() + 6); + + const startMonth = weekStart.toLocaleDateString('en-US', { + month: 'short', + }); + const endMonth = weekEnd.toLocaleDateString('en-US', { month: 'short' }); + const year = weekStart.getFullYear(); + + if (startMonth === endMonth) { + return `${startMonth} ${weekStart.getDate()}-${weekEnd.getDate()}, ${year}`; + } else { + return `${startMonth} ${weekStart.getDate()} - ${endMonth} ${weekEnd.getDate()}, ${year}`; + } + } + + @action + selectDate(day: any) { + // Just show day view when clicking a date + this.viewMode = 'day'; + // Set the current date to the clicked day for day view + this.currentDate = new Date( + day.date.getFullYear(), + day.date.getMonth(), + day.date.getDate(), + ); + this.updateModelState(); + } + + @action + previousDay() { + const prevDay = new Date(this.currentDate); + prevDay.setDate(prevDay.getDate() - 1); + this.currentDate = prevDay; + this.updateModelState(); + } + + @action + nextDay() { + const nextDay = new Date(this.currentDate); + nextDay.setDate(nextDay.getDate() + 1); + this.currentDate = nextDay; + this.updateModelState(); + } + + @action + setViewMode(mode: string) { + this.viewMode = mode; + this.updateModelState(); // ¹⁷ Persist state changes + } + + private _addEvent = restartableTask(async () => { + const calendarEventSource = { + module: new URL(import.meta.url).href, + name: 'CalendarEvent', + }; + + // Use the current date being viewed, not today's date + const eventDate = new Date(this.currentDate); + eventDate.setHours(9, 0, 0, 0); // Default to 9 AM + const endDate = new Date(eventDate); + endDate.setHours(10, 0, 0, 0); // Default to 10 AM (1 hour duration) + + const doc: LooseSingleCardDocument = { + data: { + type: 'card', + attributes: { + title: 'New Event', + startTime: eventDate.toISOString(), + endTime: endDate.toISOString(), + eventType: 'meeting', + isAllDay: 'false', + }, + relationships: { + calendar: { + links: { + self: this.args.model.id ?? null, + }, + }, + }, + meta: { + adoptsFrom: calendarEventSource, + }, + }, + }; + + try { + await this.args.createCard?.( + calendarEventSource, + new URL(calendarEventSource.module), + { + realmURL: this.realmURL, + doc, + }, + ); + } catch (error) { + console.error('CalendarCard: Error creating event', error); + } + }); + + addEvent = () => { + this._addEvent.perform(); + }; + + @action + editEvent(event: any) { + // Open event card for editing + if (event && this.args.viewCard) { + this.args.viewCard(event, 'edit'); + } + } + + @action + showMoreEvents(day: any) { + this.showMoreEventsFor = day; + } + + @action + closeMoreEvents() { + this.showMoreEventsFor = null; + } + + @action + onDateHover(day: any) { + this.hoveredDate = day.date; + } + + @action + onDateLeave() { + this.hoveredDate = null; + } + + @action + handleEventClick(event: any, clickEvent: Event) { + if (clickEvent) { + clickEvent.stopPropagation(); + clickEvent.preventDefault(); + clickEvent.stopImmediatePropagation(); + } + this.editEvent(event); + } + + @action + handleMoreEventsClick(day: any, clickEvent: Event) { + if (clickEvent) { + clickEvent.stopPropagation(); + clickEvent.preventDefault(); + clickEvent.stopImmediatePropagation(); + } + this.showMoreEvents(day); + } + + resetEventForm() { + this.newEventTitle = ''; + this.newEventDescription = ''; + this.newEventStartTime = ''; + this.newEventEndTime = ''; + this.newEventLocation = ''; + this.newEventIsAllDay = false; + this.newEventType = 'meeting'; + } + + getEventTypeColor(type: string): string { + const colors = { + meeting: '#3b82f6', + appointment: '#10b981', + reminder: '#f59e0b', + task: '#8b5cf6', + personal: '#ef4444', + work: '#06b6d4', + }; + return colors[type as keyof typeof colors] || '#3b82f6'; + } + + // ¹⁸ Update model with current state + updateModelState() { + if (this.args.model) { + try { + this.args.model.month = this.currentDate.getMonth() + 1; + this.args.model.year = this.currentDate.getFullYear(); + this.args.model.viewMode = this.viewMode; + } catch (e) { + console.error('CalendarCard: Error updating model state', e); + } + } + } + + +} + +export class CalendarCard extends CardDef { + // ⁵ Generic Calendar card definition + static displayName = 'Calendar'; + static icon = CalendarIcon; + + @field month = contains(NumberField); // ⁶ Calendar state fields + @field year = contains(NumberField); + @field selectedDate = contains(DateField); + @field viewMode = contains(StringField); // month, week, day + @field calendarName = contains(StringField); // ⁷ Calendar identification + + // ⁹ Computed title + @field title = contains(StringField, { + computeVia: function (this: CalendarCard) { + try { + const name = this.calendarName || 'Calendar'; + const currentDate = new Date(); + const month = this.month || currentDate.getMonth() + 1; + const year = this.year || currentDate.getFullYear(); + return `${name} - ${month}/${year}`; + } catch (e) { + console.error('CalendarCard: Error computing title', e); + return 'Calendar'; + } + }, + }); + + // ²³ Query for events that belong to this calendar + get eventsQuery(): Query { + return { + filter: { + every: [ + { + type: { + module: new URL(import.meta.url).href, + name: 'CalendarEvent', + }, + }, + { + on: { + module: new URL(import.meta.url).href, + name: 'CalendarEvent', + }, + eq: { 'calendar.id': this.id }, + }, + ], + }, + sort: [ + { + by: 'startTime', + on: { + module: new URL(import.meta.url).href, + name: 'CalendarEvent', + }, + direction: 'asc', + }, + ], + }; + } + + get realmURL(): URL { + return this[realmURL]!; + } + + get realmHrefs() { + return [this.realmURL.href]; + } + + static isolated = CalendarIsolated; + get today() { + return new Date(); + } + + static embedded = class Embedded extends Component { + // ²⁰ Embedded format with event querying ²⁶ + get currentDate() { + return new Date(); + } + + // ²⁷ Query events for this calendar in embedded format + eventsResult = this.args.context?.getCards( + this, + () => this.args.model?.eventsQuery, + () => this.args.model?.realmHrefs, + { isLive: true }, + ); + + // ²⁸ Get today's events for embedded display + get todaysEvents() { + const today = new Date(); + const events = (this.eventsResult?.instances as CalendarEvent[]) || []; + return events + .filter((event) => { + if (!event?.startTime) return false; + const eventDate = new Date(event.startTime); + return ( + eventDate.getFullYear() === today.getFullYear() && + eventDate.getMonth() === today.getMonth() && + eventDate.getDate() === today.getDate() + ); + }) + .slice(0, 3); // Show max 3 events in embedded view + } + + + }; + + static fitted = class Fitted extends Component { + // ²¹ Fitted format with dynamic data and event querying ²⁹ + get currentDate() { + // Use model date if available, otherwise current date + if (this.args.model?.year && this.args.model?.month) { + return new Date(this.args.model.year, this.args.model.month - 1, 1); + } + return new Date(); + } + + get currentDayNumber() { + return this.currentDate.getDate(); + } + + // ³⁰ Query events for this calendar in fitted format + eventsResult = this.args.context?.getCards( + this, + () => this.args.model?.eventsQuery, + () => this.args.model?.realmHrefs, + { isLive: true }, + ); + + // ³¹ Get today's events for fitted display + get todaysEvents() { + const today = new Date(); + const events = (this.eventsResult?.instances as CalendarEvent[]) || []; + return events + .filter((event) => { + if (!event?.startTime) return false; + const eventDate = new Date(event.startTime); + return ( + eventDate.getFullYear() === today.getFullYear() && + eventDate.getMonth() === today.getMonth() && + eventDate.getDate() === today.getDate() + ); + }) + .slice(0, 3); // Show max 3 events + } + + // ³² Get total event count for displays + get totalEventCount() { + return this.eventsResult?.instances?.length || 0; + } + + + }; +}