@crescender/calendar
Version:
A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.
473 lines (396 loc) • 12.6 kB
Markdown
# @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](./MIGRATION.md) 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
```bash
npm install @crescender/calendar
```
## Quick Start
### Server-Side (Node.js/API Routes)
```typescript
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)
```typescript
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
```typescript
import { EVENT_TYPES, PAYMENT_STATUS } from '@crescender/calendar';
// OR
import { EVENT_TYPES, PAYMENT_STATUS } from '@crescender/calendar/shared';
```
## Server-Side Usage (Node.js)
```typescript
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
```typescript
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: 'sarah@jazzcorner.com.au',
contactPhone: '+61 3 9876 5432'
});
// Create a promoter contact
const promoter = await createContact({
name: 'Mike Smith',
email: 'mike@promotions.com.au',
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
```typescript
import { createContact, createEvent, addEventIncome } from '@crescender/calendar/server';
// Create a student contact
const student = await createContact({
name: 'Emma Wilson',
email: 'emma.wilson@email.com',
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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