UNPKG

@crescender/calendar

Version:

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

473 lines (396 loc) 12.6 kB
# @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