@crescender/calendar
v0.3.1
Published
A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.
Maintainers
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/calendarQuick 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.00Creating 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, showslesson: Music teaching sessionsaudition: Auditions for bands, orchestras, etc.practice: Personal practice sessionsrehearsal: Band or ensemble rehearsalsrecording: Studio recording sessionsmeeting: Business meetings, planning sessions
Custom Fields
Each event can include musician-specific fields:
genre: Jazz, Classical, Rock, Pop, etc.instrument: Primary instrument for the eventdifficulty: Beginner, Intermediate, Advanced, Professionalrepertoire: Songs or pieces to be performed/practicedsetList: JSON array of songs in performance orderequipmentNeeded: JSON array of required equipmentdresscode: Performance attire requirementssoundcheckTime&loadInTime: For gigspaymentStatus&paymentDueDate: Financial trackingstudentLevel&lessonFocus: For teachingauditionPiece&auditionRequirements: For auditionspracticeGoals&rehearsalNotes: For practice/rehearsal sessions
API Reference
Core Functions
initDb(dataSource)- Initialize database connectioncreateEvent(calendarId, eventData)- Create new eventupdateEvent(eventId, updates)- Update existing eventdeleteEvent(eventId)- Delete eventgetEventsByCalendar(calendarId)- Get all events for calendargetEventsByType(calendarId, type)- Get events by type
Financial Functions
addEventIncome(eventId, income)- Add income to eventaddEventExpense(eventId, expense)- Add expense to eventcalculateEventProfit(eventId)- Calculate net profitgetFinancialSummary(eventIds)- Generate financial report
Venue & Contact Functions
createVenue(venueData)- Create venuecreateContact(contactData)- Create contactgetUpcomingGigs(calendarId, limit)- Get upcoming performancesgetStudentLessons(calendarId, studentId)- Get lessons for student
Export Functions
generateIcs(calendar, calendarId)- Generate ICS calendar feed
License
MIT
