@happyvertical/smrt-events

Infinite-nesting event hierarchy with series, participant tracking, and recurrence patterns.

v0.20.44Events1 Component

Overview

smrt-events provides infinite-nesting event hierarchy with series, participant tracking, and recurrence patterns. Events can model anything from conferences with sessions to sports games with periods and goals.

Installation

bash
npm install @happyvertical/smrt-events

Quick Start

typescript
import { EventCollection, EventSeriesCollection, EventParticipantCollection } from '@happyvertical/smrt-events';

// Initialize
const events = await EventCollection.create({ db: {...} });
const series = await EventSeriesCollection.create({ db: {...} });
const participants = await EventParticipantCollection.create({ db: {...} });

// Create event series
const playoffs = await series.create({
  name: 'NBA Playoffs 2025',
  typeId: tournamentTypeId,
  organizerId: nbaProfileId,
  startDate: new Date('2025-04-15'),
  endDate: new Date('2025-06-20')
});
await playoffs.save();

// Create game event
const game = await events.create({
  name: 'Lakers vs Warriors',
  seriesId: playoffs.id,
  typeId: gameTypeId,
  placeId: arenaId,
  startDate: new Date('2025-04-20T19:00:00'),
  endDate: new Date('2025-04-20T21:30:00'),
  status: 'scheduled',
  round: 1
});
await game.save();

// Add participants
const lakers = await participants.create({
  eventId: game.id,
  profileId: lakersProfileId,
  role: 'home',
  placement: 0
});
await lakers.save();

const warriors = await participants.create({
  eventId: game.id,
  profileId: warriorsProfileId,
  role: 'away',
  placement: 1
});
await warriors.save();

Core Models

Event (Hierarchical)

typescript
class Event extends SmrtObject {
  name: string
  seriesId?: string         // FK to EventSeries
  parentEventId?: string    // Self-referencing hierarchy
  typeId: string            // FK to EventType
  placeId?: string          // FK to Place
  description?: string
  startDate?: Date
  endDate?: Date
  status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'postponed'
  round?: number            // Sequence in series
  metadata?: Record<string, any>

  // Hierarchy navigation
  async getParent(): Promise<Event | null>
  async getChildren(): Promise<Event[]>
  async getAncestors(): Promise<Event[]>
  async getDescendants(): Promise<Event[]>
  async getRootEvent(): Promise<Event>
  async getHierarchy(): Promise<Event[]>

  // Participants
  async getParticipants(): Promise<EventParticipant[]>

  // Related data
  async getSeries(): Promise<EventSeries | null>
  async getType(): Promise<EventType | null>
  async getPlace(): Promise<Place | null>

  // Status
  updateStatus(newStatus: EventStatus): void
  isInProgress(): boolean
}

EventSeries (Recurring)

typescript
class EventSeries extends SmrtObject {
  name: string
  typeId: string
  organizerId: string       // FK to Profile
  description?: string
  startDate?: Date
  endDate?: Date
  recurrence?: RecurrencePattern
  metadata?: Record<string, any>

  async getOrganizer(): Promise<Profile | null>
  async getEvents(): Promise<Event[]>
}

interface RecurrencePattern {
  frequency: 'daily' | 'weekly' | 'monthly' | 'yearly'
  interval?: number          // Every N periods
  count?: number             // Total occurrences
  until?: Date               // End date
  byDay?: string[]           // ['MO', 'WE', 'FR']
  byMonthDay?: number[]      // [1, 15]
  byMonth?: number[]         // [1-12]
}

EventParticipant

typescript
// conflictColumns: ['event_id', 'profile_id', 'role']
class EventParticipant extends SmrtObject {
  eventId: string           // FK to Event (plain string, cross-package)
  profileId: string         // FK to Profile (plain string, cross-package)
  role: string              // 'home' | 'away' | 'speaker' | 'panelist' | etc.
  placement?: number        // Numeric: 0=home, 1=away, or ranking position
  groupId?: string          // Logical grouping (e.g., team members in a game)
  metadata?: Record<string, any>

  isHome(): boolean         // placement === 0
  isAway(): boolean         // placement === 1
  async getEvent(): Promise<Event | null>
  async getProfile(): Promise<Profile | null>
  async getGroupParticipants(): Promise<EventParticipant[]>
}

Hierarchical Events

typescript
// Create hierarchical game structure
const game = await events.create({
  name: 'Lakers vs Warriors',
  startDate: new Date('2025-01-20T19:00:00'),
  status: 'scheduled'
});
await game.save();

// Create quarters
const q1 = await events.create({
  name: '1st Quarter',
  parentEventId: game.id,
  startDate: new Date('2025-01-20T19:00:00'),
  endDate: new Date('2025-01-20T19:12:00')
});
await q1.save();

// Create goal event
const goal = await events.create({
  name: 'LeBron 3-pointer',
  parentEventId: q1.id,
  startDate: new Date('2025-01-20T19:05:23'),
  metadata: { points: 3, player: 'LeBron James' }
});
await goal.save();

// Navigate hierarchy
const children = await game.getChildren(); // [q1, q2, q3, q4]
const ancestors = await goal.getAncestors(); // [q1, game]
const root = await goal.getRootEvent(); // game

Calendar Integration

Utility Functions

typescript
import {
  checkSchedulingConflict,
  calculateNextOccurrence,
  formatEventDateRange,
  generateEventSlug,
  calculateDuration,
  formatDuration,
  isEventNow,
  getEventStatusFromDates,
  sortEventsByDate,
  validateEventStatus,
  parseRecurrencePattern
} from '@happyvertical/smrt-events';

// Check scheduling conflict
const hasConflict = checkSchedulingConflict(
  event1Start, event1End,
  event2Start, event2End
);

// Duration and formatting
const ms = calculateDuration(startDate, endDate);
const readable = formatDuration(ms); // "2h 30m"
const dateStr = formatEventDateRange(startDate, endDate);

// Status helpers
const status = getEventStatusFromDates(startDate, endDate);
const valid = validateEventStatus('scheduled', 'in_progress');

Recurrence Patterns

typescript
// Weekly meeting series
const weeklySeries = await series.create({
  name: 'Team Standup',
  slug: 'team-standup-2025',
  recurrence: {
    frequency: 'weekly',
    interval: 1,
    byDay: ['MO', 'WE', 'FR'],
    until: new Date('2025-12-31')
  }
});

// Monthly board meeting
const monthlySeries = await series.create({
  name: 'Board Meeting',
  slug: 'board-meeting-2025',
  recurrence: {
    frequency: 'monthly',
    byMonthDay: [15],  // 15th of each month
    count: 12           // 12 occurrences
  }
});

// Calculate next occurrence
import { calculateNextOccurrence } from '@happyvertical/smrt-events';
const nextDate = calculateNextOccurrence(pattern, new Date());

UI Components

The @happyvertical/smrt-events package includes Svelte 5 components for displaying event and meeting information.

Available Components

Usage Example

svelte
<script>
  import { MeetingView } from '@happyvertical/smrt-events/svelte';

  const meeting = {
    id: 'meeting-123',
    name: 'Town Council Meeting',
    startDate: '2025-01-15T19:00:00',
    status: 'scheduled',
    agendaUrl: '/docs/agenda.pdf',
    minutesUrl: '/docs/minutes.pdf',
    videoUrl: 'https://youtube.com/watch?v=...'
  };
</script>

<MeetingView {meeting} calendarUrl="/calendar" />

View all event components →

Best Practices

DOs

  • Use event series for recurring events
  • Check for conflicts before scheduling
  • Store performance data in participant metadata
  • Initialize default event types with initializeDefaults()
  • Use placement field for home/away or speaker order

DON'Ts

  • Don't create circular hierarchies (parent references child)
  • Don't transition completed events to other states
  • Don't skip validation when changing event status
  • Don't delete parent events without handling children
  • Don't store large binary data in metadata (use smrt-assets)

Related Modules