npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@crescender/calendar

v0.3.1

Published

A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.

Readme

@crescender/calendar

A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.

🚨 Version 0.3.0 Breaking Changes

This version introduces client/server separation. If you're upgrading from v0.2.x, please see the Migration Guide for detailed upgrade instructions.

Features

  • Client/Server Architecture: Clean separation between browser-safe client code and Node.js server operations
  • Full CRUD operations for events and calendars
  • Musician-specific event types: gigs, lessons, auditions, practices, rehearsals, recordings
  • Financial tracking: Income and expense management per event
  • Venue management: Store and associate venues with events
  • Contact management: Track students, band members, promoters, etc.
  • ICS export for calendar compatibility
  • Recurrence support with RFC 5545 RRULE
  • React components for rapid UI development
  • Enhanced client utilities for event processing and validation
  • PostgreSQL backend for robust data storage

Installation

npm install @crescender/calendar

Quick Start

Server-Side (Node.js/API Routes)

import { DataSource } from 'typeorm';
import { 
  initDb, 
  createEvent, 
  Event, 
  Calendar, 
  addEventIncome 
} from '@crescender/calendar/server';

// Initialize database
const dataSource = new DataSource({
  type: 'postgres',
  // ... your config
  entities: [Event, Calendar, /* other entities */],
});

await dataSource.initialize();
initDb(dataSource);

// Create an event
const event = await createEvent('calendar-id', {
  summary: 'Jazz Gig',
  start: new Date('2025-02-15T20:00:00+11:00'),
  end: new Date('2025-02-15T23:00:00+11:00'),
  type: 'gig'
});

Client-Side (React/Browser)

import { 
  EventCard, 
  validateEvent, 
  enhanceClientEvent,
  formatDateAustralian 
} from '@crescender/calendar/client';

function EventList({ events }) {
  const enhancedEvents = events.map(enhanceClientEvent);
  
  return (
    <div>
      {enhancedEvents.map(event => (
        <EventCard key={event.id} event={event} />
      ))}
    </div>
  );
}

// Form validation
const validation = validateEvent(formData);
if (validation.isValid) {
  // Submit form
}

Shared Types & Constants

import { EVENT_TYPES, PAYMENT_STATUS } from '@crescender/calendar';
// OR
import { EVENT_TYPES, PAYMENT_STATUS } from '@crescender/calendar/shared';

Server-Side Usage (Node.js)

import { DataSource } from 'typeorm';
import { 
  initDb, 
  createEvent, 
  Event, 
  Calendar, 
  Venue, 
  Contact,
  EventIncome,
  EventExpense,
  addEventIncome,
  addEventExpense,
  calculateEventProfit 
} from '@crescender/calendar/server';

// Initialize database connection
const dataSource = new DataSource({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'your_username',
  password: 'your_password',
  database: 'your_database',
  entities: [Event, Calendar, Venue, Contact, EventIncome, EventExpense],
  synchronize: true, // Don't use in production
});

await dataSource.initialize();
initDb(dataSource);

Musician-Specific Features

Creating a Gig with Financial Tracking

import { 
  createVenue, 
  createContact, 
  createEvent,
  addEventIncome,
  addEventExpense,
  calculateEventProfit 
} from '@crescender/calendar/server';

// Create a venue
const venue = await createVenue({
  name: 'The Jazz Corner',
  address: '123 Music St',
  city: 'Melbourne',
  state: 'VIC',
  country: 'Australia',
  contactName: 'Sarah Johnson',
  contactEmail: '[email protected]',
  contactPhone: '+61 3 9876 5432'
});

// Create a promoter contact
const promoter = await createContact({
  name: 'Mike Smith',
  email: '[email protected]',
  phone: '+61 4 1234 5678',
  role: 'promoter'
});

// Create a gig event
const gig = await createEvent('calendar-id', {
  summary: 'Jazz Quartet Performance',
  description: 'Evening jazz performance featuring original compositions',
  start: new Date('2025-02-15T20:00:00+11:00'),
  end: new Date('2025-02-15T23:00:00+11:00'),
  type: 'gig',
  genre: 'Jazz',
  instrument: 'Piano',
  difficulty: 'Professional',
  repertoire: 'Original compositions and jazz standards',
  setList: JSON.stringify([
    'Take Five',
    'Blue Rondo à la Turk',
    'Original Composition #1',
    'Autumn Leaves'
  ]),
  equipmentNeeded: JSON.stringify(['Piano', 'Microphone', 'Music stand']),
  dresscode: 'Smart casual',
  soundcheckTime: new Date('2025-02-15T19:00:00+11:00'),
  loadInTime: new Date('2025-02-15T18:30:00+11:00'),
  paymentStatus: 'Confirmed',
  status: 'Confirmed',
  venue,
  primaryContact: promoter
});

// Add income streams
await addEventIncome(gig.id, {
  description: 'Performance fee',
  amount: 800.00,
  currency: 'AUD',
  notes: 'Flat rate for 3-hour performance'
});

await addEventIncome(gig.id, {
  description: 'Merchandise sales',
  amount: 150.00,
  currency: 'AUD',
  notes: 'CDs and t-shirts sold during interval'
});

// Add expenses
await addEventExpense(gig.id, {
  description: 'Travel costs',
  amount: 45.00,
  currency: 'AUD',
  notes: 'Petrol and parking'
});

await addEventExpense(gig.id, {
  description: 'Equipment hire',
  amount: 120.00,
  currency: 'AUD',
  notes: 'Piano tuning and microphone rental'
});

// Calculate profit
const profit = await calculateEventProfit(gig.id);
console.log(`Net profit: $${profit.toFixed(2)}`); // Net profit: $785.00

Creating Music Lessons

import { createContact, createEvent, addEventIncome } from '@crescender/calendar/server';

// Create a student contact
const student = await createContact({
  name: 'Emma Wilson',
  email: '[email protected]',
  phone: '+61 4 9876 5432',
  role: 'student',
  notes: 'Grade 6 piano, preparing for AMEB exam'
});

// Create recurring weekly lessons
const lesson = await createEvent('calendar-id', {
  summary: 'Piano Lesson - Emma Wilson',
  description: 'Grade 6 piano lesson focusing on exam preparation',
  start: new Date('2025-02-10T16:00:00+11:00'),
  end: new Date('2025-02-10T17:00:00+11:00'),
  type: 'lesson',
  recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO;COUNT=12', // 12 weekly lessons
  instrument: 'Piano',
  studentLevel: 'Grade 6',
  lessonFocus: 'AMEB exam preparation',
  repertoire: 'Bach Invention No. 4, Chopin Waltz in A minor',
  paymentStatus: 'Paid',
  status: 'Confirmed',
  primaryContact: student
});

// Add lesson income
await addEventIncome(lesson.id, {
  description: 'Lesson fee',
  amount: 65.00,
  currency: 'AUD',
  notes: '1-hour private lesson'
});

Financial Reporting

import { getEventsByType, getFinancialSummary, getUpcomingGigs } from '@crescender/calendar/server';

// Get all gigs for the month
const gigs = await getEventsByType('calendar-id', 'gig');
const gigIds = gigs.map(g => g.id);

// Generate financial summary
const summary = await getFinancialSummary(gigIds);
console.log(`
Monthly Gig Summary:
- Total Income: $${summary.totalIncome.toFixed(2)}
- Total Expenses: $${summary.totalExpenses.toFixed(2)}
- Net Profit: $${summary.netProfit.toFixed(2)}
- Number of Gigs: ${summary.eventCount}
- Average Profit per Gig: $${summary.averageProfitPerEvent.toFixed(2)}
`);

// Get upcoming gigs
const upcomingGigs = await getUpcomingGigs('calendar-id', 5);
upcomingGigs.forEach(gig => {
  console.log(`${gig.summary} - ${gig.start.toLocaleDateString()} at ${gig.venue?.name}`);
});

Client-Side Usage (React/Browser)

Event Processing and Validation

import { 
  validateEvent, 
  validateIncome, 
  validateExpense,
  enhanceClientEvent,
  formatDateAustralian,
  formatCurrency,
  calculateFinancials 
} from '@crescender/calendar/client';

// Form validation
const eventValidation = validateEvent({
  title: 'Jazz Gig',
  startDate: '15/Feb/2025',
  startTime: '20:00',
  endDate: '15/Feb/2025', 
  endTime: '23:00',
  eventType: 'gig'
});

if (eventValidation.isValid) {
  // Form is valid, submit to server
  console.log('Event data is valid');
} else {
  // Show validation errors
  console.log('Validation errors:', eventValidation.errors);
}

// Enhance events with computed properties
const rawEvent = await fetch('/api/events/123').then(r => r.json());
const enhancedEvent = enhanceClientEvent(rawEvent);

console.log(enhancedEvent.duration); // "3 hours"
console.log(enhancedEvent.profit); // 650.00
console.log(enhancedEvent.formattedDate); // "15/Feb/2025"
console.log(enhancedEvent.formattedTime); // "8:00 PM - 11:00 PM"

React Components

import { EventCard, CalendarView } from '@crescender/calendar/client';
import type { IEvent } from '@crescender/calendar/shared';

interface EventListProps {
  events: IEvent[];
  onEdit: (event: IEvent) => void;
  onDelete: (eventId: string) => void;
}

function EventList({ events, onEdit, onDelete }: EventListProps) {
  return (
    <div className="event-list">
      {events.map(event => (
        <EventCard
          key={event.id}
          event={event}
          onEdit={() => onEdit(event)}
          onDelete={() => onDelete(event.id)}
          showFinancials={event.type === 'gig'}
        />
      ))}
    </div>
  );
}

// Calendar view component
function MyCalendar({ events }: { events: IEvent[] }) {
  return (
    <CalendarView
      events={events}
      onEventClick={handleEventClick}
      onDateClick={handleDateClick}
      view="month"
    />
  );
}

Advanced Event Processing

import { 
  filterEvents, 
  sortEvents, 
  groupEventsByDate,
  expandRecurrence 
} from '@crescender/calendar/client';

// Filter events
const upcomingGigs = filterEvents(events, { 
  type: 'gig', 
  status: 'confirmed',
  dateRange: { start: new Date(), end: addDays(new Date(), 30) }
});

// Sort events
const sortedEvents = sortEvents(events, 'start', 'asc');

// Group events by date for calendar display
const groupedEvents = groupEventsByDate(events);
console.log(groupedEvents['2025-02-15']); // Array of events on that date

// Expand recurring events
const recurringEvent = events.find(e => e.recurrenceRule);
if (recurringEvent) {
  const occurrences = expandRecurrence(
    recurringEvent, 
    new Date('2025-01-01'), 
    new Date('2025-12-31')
  );
  console.log(`${occurrences.length} occurrences this year`);
}

Event Types

The library supports various musician-specific event types:

  • gig: Performances, concerts, shows
  • lesson: Music teaching sessions
  • audition: Auditions for bands, orchestras, etc.
  • practice: Personal practice sessions
  • rehearsal: Band or ensemble rehearsals
  • recording: Studio recording sessions
  • meeting: Business meetings, planning sessions

Custom Fields

Each event can include musician-specific fields:

  • genre: Jazz, Classical, Rock, Pop, etc.
  • instrument: Primary instrument for the event
  • difficulty: Beginner, Intermediate, Advanced, Professional
  • repertoire: Songs or pieces to be performed/practiced
  • setList: JSON array of songs in performance order
  • equipmentNeeded: JSON array of required equipment
  • dresscode: Performance attire requirements
  • soundcheckTime & loadInTime: For gigs
  • paymentStatus & paymentDueDate: Financial tracking
  • studentLevel & lessonFocus: For teaching
  • auditionPiece & auditionRequirements: For auditions
  • practiceGoals & rehearsalNotes: For practice/rehearsal sessions

API Reference

Core Functions

  • initDb(dataSource) - Initialize database connection
  • createEvent(calendarId, eventData) - Create new event
  • updateEvent(eventId, updates) - Update existing event
  • deleteEvent(eventId) - Delete event
  • getEventsByCalendar(calendarId) - Get all events for calendar
  • getEventsByType(calendarId, type) - Get events by type

Financial Functions

  • addEventIncome(eventId, income) - Add income to event
  • addEventExpense(eventId, expense) - Add expense to event
  • calculateEventProfit(eventId) - Calculate net profit
  • getFinancialSummary(eventIds) - Generate financial report

Venue & Contact Functions

  • createVenue(venueData) - Create venue
  • createContact(contactData) - Create contact
  • getUpcomingGigs(calendarId, limit) - Get upcoming performances
  • getStudentLessons(calendarId, studentId) - Get lessons for student

Export Functions

  • generateIcs(calendar, calendarId) - Generate ICS calendar feed

License

MIT