# @getmicdrop/venue-calendar

A beautiful, customizable calendar component built with Svelte for displaying comedy events. Perfect for comedy clubs, venues, and event organizers who want to showcase their upcoming shows.

## Features

✨ **Three View Modes**: List, Gallery, and Calendar views
🎨 **Beautiful UI**: Modern, responsive design built with Tailwind CSS
📱 **Mobile-Friendly**: Swipe gestures, touch-optimized, responsive design
🔌 **Easy Integration**: Works with React, Vue, vanilla JS, and more
🌐 **One script tag**: Self-hosted bundle — paste one `<script>`, no build step
🎨 **Styles included**: The bundle injects its own CSS — no separate stylesheet to link
⚡ **Auto-Mount**: Automatically finds and mounts to designated containers
🎯 **Customizable**: Configure views, navigation, and more
🌙 **Dark Mode**: Built-in light, dark, and high-contrast themes
♿ **Accessible**: ARIA labels, keyboard navigation, screen reader support
🎫 **Event Status**: Visual badges for "On Sale", "Selling Fast", "Sold Out"

## Installation

Add one `<script>` tag pointing at the Micdrop-hosted bundle. The package is
private (not on npm/jsDelivr) — it is delivered from `get-micdrop.com`:

```html
<script
  defer
  src="https://get-micdrop.com/embed/venue-calendar.iife.js"
></script>
```

- **No stylesheet to link.** The bundle injects its own CSS at runtime, so a
  plain page with just this `<script>` renders fully styled.
- **Use `defer`** (or `async`) so the bundle never blocks page parse. It is
  ~310 KB gzipped.
- **Serve your page over HTTPS.** Checkout stores a `Secure` cart cookie, which
  Safari drops on non-secure (`http://`) pages.

> **Heads-up:** the exact hosted URL is being finalized (Micdrop ticket
> MIC-1130). Confirm the address with Micdrop before going live.

> The `import { ... } from '@getmicdrop/venue-calendar'` examples further down
> are for **Micdrop-internal apps** that build with a bundler and have registry
> access to the private package. Public sites (comedy clubs, etc.) use the
> `<script>` tag above — not `npm install`.

## Quick Start

### Method 1: Auto-Mount (Easiest)

Simply add a div with the class `micdrop-calendar-container` and the calendar will automatically mount:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>My Comedy Club</title>
    <!-- Preload the bundle while the page parses. defer keeps the
       <script> non-blocking; preload starts the fetch earlier. -->
    <link
      rel="preload"
      as="script"
      href="https://get-micdrop.com/embed/venue-calendar.iife.js"
    />
    <script
      defer
      src="https://get-micdrop.com/embed/venue-calendar.iife.js"
    ></script>
  </head>
  <body>
    <!-- Calendar auto-mounts here. Use data-organization-id to show all of an
       organization's shows, or data-venue-id for a single venue. -->
    <div
      class="micdrop-calendar-container"
      data-organization-id="your-organization-id"
      data-view="calendar"
      data-show-view-options="true"
      data-show-month-switcher="true"
      data-locale="en-US"
    ></div>
  </body>
</html>
```

### Method 2: Web Component

Use the custom `<micdrop-calendar>` element:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>My Comedy Club</title>
    <script
      defer
      src="https://get-micdrop.com/embed/venue-calendar.iife.js"
    ></script>
  </head>
  <body>
    <!-- Web Component -->
    <micdrop-calendar
      venue-id="your-venue-id"
      view="calendar"
      show-view-options="true"
      show-month-switcher="true"
      locale="en-US"
    >
    </micdrop-calendar>
  </body>
</html>
```

### Method 3: JavaScript API

For more control, use the JavaScript API:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>My Comedy Club</title>
  </head>
  <body>
    <div id="my-calendar"></div>

    <!-- Load the bundle, then call the global it exposes. -->
    <script
      defer
      src="https://get-micdrop.com/embed/venue-calendar.iife.js"
    ></script>
    <script>
      window.addEventListener('load', function () {
        window.VenueCalendar.initVenueCalendar({
          target: '#my-calendar',
          organizationId: 'your-organization-id',
          view: 'calendar',
          showViewOptions: true,
          showMonthSwitcher: true,
        });
      });
    </script>
  </body>
</html>
```

## Framework Integration

### React

```jsx
import React, { useEffect, useRef } from 'react';
import { initVenueCalendar, unmount } from '@getmicdrop/venue-calendar';

function VenueCalendarComponent({ venueId, view = 'calendar' }) {
  const calendarRef = useRef(null);
  const instanceRef = useRef(null);

  useEffect(() => {
    if (!calendarRef.current) return;
    instanceRef.current = initVenueCalendar({
      target: calendarRef.current,
      venueId,
      view,
      events: [],
      showViewOptions: true,
      showMonthSwitcher: true,
    });

    return () => {
      // Svelte 5 — components mounted via `mount()` are destroyed via
      // the `unmount` helper, not `.$destroy()` (that was Svelte 4).
      if (instanceRef.current) {
        try {
          unmount(instanceRef.current);
        } catch {}
        instanceRef.current = null;
      }
    };
  }, [venueId, view]);

  return <div ref={calendarRef}></div>;
}

export default VenueCalendarComponent;
```

### Error reporting and runtime config

Wire any errors caught by the widget into your existing monitoring:

```js
import { configureVenueCalendar } from '@getmicdrop/venue-calendar';

configureVenueCalendar({
  onError: (err, { source }) => {
    Sentry.captureException(err, { tags: { source, micdrop: true } });
  },
  // Optional overrides for self-hosted backends or regional failover.
  // apiBaseUrl: 'https://api.eu.micdrop.com',
  // apiTimeout: 15000,
  // apiRetries: 2,
});
```

For ad-hoc support diagnostics, the widget exposes its version on
`window`:

```js
window.__MICDROP_CALENDAR__.version; // e.g. "3.6.23"
```

**Venue pixels and visitor consent:**

If your organization saved a Meta Pixel or Google Analytics 4 ID, the calendar
loads it only after the visitor agrees. With no consent signal on the page it
sends no Meta or Google request at all. Tell it the visitor said yes in any one
of these ways:

```html
<!-- your consent tool already said yes before the calendar loads -->
<div data-consent-granted="true" ...></div>
```

```js
// from your cookie banner's "Accept" callback
window.__MICDROP_CALENDAR__.setConsentGranted(true);
```

A consent tool that speaks the IAB TCF API (`__tcfapi`) is picked up
automatically. `data-consent-required="false"` tells the calendar your site does
not need consent for these pixels; that is your own legal call, and a visitor
whose browser sends Global Privacy Control still gets no pixels.

**Usage in React App:**

```jsx
import React, { useState } from 'react';
import VenueCalendarComponent from './VenueCalendarComponent';

function App() {
  const [venueId, setVenueId] = useState('comedy-club-123');
  const [view, setView] = useState('calendar');

  return (
    <div style={{ padding: '20px' }}>
      <h1>Event Viewer</h1>

      <div style={{ marginBottom: '20px' }}>
        <label>Venue ID:</label>
        <input
          type="text"
          value={venueId}
          onChange={e => setVenueId(e.target.value)}
        />
      </div>

      <div style={{ marginBottom: '20px' }}>
        <label>Select View:</label>
        <label>
          <input
            type="radio"
            value="list"
            checked={view === 'list'}
            onChange={e => setView(e.target.value)}
          />
          List
        </label>
        <label>
          <input
            type="radio"
            value="gallery"
            checked={view === 'gallery'}
            onChange={e => setView(e.target.value)}
          />
          Gallery
        </label>
        <label>
          <input
            type="radio"
            value="calendar"
            checked={view === 'calendar'}
            onChange={e => setView(e.target.value)}
          />
          Calendar
        </label>
      </div>

      <VenueCalendarComponent venueId={venueId} view={view} />
    </div>
  );
}

export default App;
```

### Vue 3

```vue
<template>
  <div ref="calendarContainer"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { initVenueCalendar } from '@getmicdrop/venue-calendar';

const props = defineProps({
  venueId: String,
  view: {
    type: String,
    default: 'calendar',
  },
});

const calendarContainer = ref(null);
let calendarInstance = null;

onMounted(() => {
  calendarInstance = initVenueCalendar({
    target: calendarContainer.value,
    venueId: props.venueId,
    view: props.view,
    events: [],
    showViewOptions: true,
    showMonthSwitcher: true,
  });
});

onUnmounted(() => {
  if (calendarInstance && calendarInstance.$destroy) {
    calendarInstance.$destroy();
  }
});

watch(
  () => props.venueId,
  newId => {
    if (calendarInstance) {
      calendarInstance.$destroy();
      calendarInstance = initVenueCalendar({
        target: calendarContainer.value,
        venueId: newId,
        view: props.view,
        events: [],
        showViewOptions: true,
        showMonthSwitcher: true,
      });
    }
  }
);
</script>
```

### Svelte

```svelte
<script>
  import { VenueCalendar } from '@getmicdrop/venue-calendar';
  import { Calendar, Grid, List } from 'carbon-icons-svelte';
  import { writable } from 'svelte/store';

  let venueId = 'your-venue-id';
  let currentMonth = writable(new Date().getUTCMonth());
  let currentYear = writable(new Date().getUTCFullYear());

  function handleNext() {
    currentMonth.update(m => m + 1);
  }

  function handlePrev() {
    currentMonth.update(m => m - 1);
  }
</script>

<VenueCalendar
  showViewOptions={[
    { id: 0, text: 'List view', icon: List },
    { id: 1, text: 'Gallery view', icon: Grid },
    { id: 2, text: 'Calendar view', icon: Calendar },
  ]}
  showMonthSwitcher={true}
  events={[]}
  {currentMonth}
  {currentYear}
  {handleNext}
  {handlePrev}
  on:eventClick={e => console.log('Event clicked:', e.detail)}
/>
```

Host-page props — for a page that already owns part of what the widget draws.
Every one defaults to on, so embeds are unchanged:

| Prop                | Type    | Default | Description                                                                                                                                                                                                                                                |
| ------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkoutProbe`     | boolean | `true`  | Ask whether this page can sell a ticket and overlay the "Online ticket sales are unavailable here" alert when it cannot. `false` skips the probe and never draws the alert — for a host where no ticket is bought, such as a site builder's design canvas. |
| `showGiftCardsLink` | boolean | `true`  | The footer's "Gift cards" link. `false` for a host page whose own footer already carries it.                                                                                                                                                               |
| `showPoweredBy`     | boolean | `true`  | The footer's "Powered by Micdrop" mark. `false` for a host page whose own footer already carries it.                                                                                                                                                       |

### Angular

```typescript
import {
  Component,
  OnInit,
  OnDestroy,
  ElementRef,
  ViewChild,
} from '@angular/core';
import { initVenueCalendar } from '@getmicdrop/venue-calendar';

@Component({
  selector: 'app-venue-calendar',
  template: '<div #calendarContainer></div>',
})
export class VenueCalendarComponent implements OnInit, OnDestroy {
  @ViewChild('calendarContainer', { static: true })
  calendarContainer!: ElementRef;
  private calendarInstance: any;

  ngOnInit() {
    this.calendarInstance = initVenueCalendar({
      target: this.calendarContainer.nativeElement,
      venueId: 'your-venue-id',
      view: 'calendar',
      events: [],
      showViewOptions: true,
      showMonthSwitcher: true,
    });
  }

  ngOnDestroy() {
    if (this.calendarInstance && this.calendarInstance.$destroy) {
      this.calendarInstance.$destroy();
    }
  }
}
```

## Configuration Options

### Data Attributes (for auto-mount)

| Attribute                  | Type    | Default      | Description                                          |
| -------------------------- | ------- | ------------ | ---------------------------------------------------- |
| `data-venue-id`            | string  | `''`         | The venue ID to fetch events for                     |
| `data-view`                | string  | `'calendar'` | Initial view: `'list'`, `'gallery'`, or `'calendar'` |
| `data-show-view-options`   | boolean | `true`       | Show view switcher buttons                           |
| `data-show-month-switcher` | boolean | `true`       | Show month navigation controls                       |

### JavaScript API Options

```javascript
initVenueCalendar({
  target: '.my-calendar', // CSS selector or HTMLElement (required)
  venueId: 'venue-123', // Venue ID (optional)
  view: 'calendar', // 'list', 'gallery', or 'calendar' (default: 'calendar')
  events: [], // Array of event objects (default: [])
  showViewOptions: true, // Show view switcher (default: true)
  showMonthSwitcher: true, // Show month navigation (default: true)
});
```

### Event Object Structure

```javascript
{
  id: 'event-123',
  name: 'Comedy Night',
  date: '2024-10-25T20:00:00Z',
  image: 'https://example.com/image.jpg',
  status: 'On Sale',
  timeline: '8:00 PM - 10:00 PM',
  // ... other fields
}
```

## Views

### Calendar View

The default view showing events in a monthly calendar grid. Perfect for venues with regular shows.

### List View

A vertical list layout showing all upcoming events with details. Great for mobile experiences.

### Gallery View

A grid layout displaying event posters in a gallery format. Ideal for showcasing event imagery.

## WordPress Integration

For WordPress sites, you can add this to your page/post HTML:

```html
<div
  class="micdrop-calendar-container"
  data-venue-id="your-venue-id"
  data-view="calendar"
></div>

<script src="https://get-micdrop.com/embed/venue-calendar.iife.js"></script>
```

Or add the script to your theme's footer and use the div anywhere in your content.

## Styling

The calendar comes with built-in styles using Tailwind CSS. If you need to customize the appearance, you can override the CSS classes or add your own styles.

```css
/* Example: Custom styling */
.micdrop-calendar-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}
```

**Note:** The mount target must not be a shrink-to-fit box (`display: inline-block`, `float`, `position: absolute`, `display: table-cell`, or an auto-width item in a flex row). The calendar sizes its typography to its own container, which requires the mount target to get its width from its parent — a normal block element (as in every example above) is exactly right. If the calendar detects a shrink-to-fit mount target, it falls back to page-width type scaling and logs a console warning.

## Theming

The calendar supports comprehensive theming via CSS custom properties and JavaScript utilities.

### Using CSS Custom Properties

Override the default theme by setting CSS custom properties:

```css
/* Custom brand colors */
.micdrop-calendar-container {
  --Brand-Primary: 270 76% 60%; /* Purple */
  --Text-Primary: 0 0% 10%;
  --BG-Primary: 0 0% 100%;
}

/* Dark mode */
.dark .micdrop-calendar-container,
[data-theme='dark'] .micdrop-calendar-container {
  --Brand-Primary: 270 76% 70%;
  --Text-Primary: 0 0% 95%;
  --BG-Primary: 0 0% 10%;
}
```

### Available CSS Variables

| Variable               | Description             | Default (Light)      |
| ---------------------- | ----------------------- | -------------------- |
| `--Brand-Primary`      | Primary brand color     | `217 91% 60%` (Blue) |
| `--Text-Primary`       | Main text color         | `0 0% 0%`            |
| `--Text-Secondary`     | Secondary text          | `0 0% 40%`           |
| `--BG-Primary`         | Main background         | `0 0% 100%`          |
| `--BG-Secondary`       | Secondary background    | `0 0% 98%`           |
| `--Stroke-Primary`     | Border colors           | `0 0% 80%`           |
| `--Status-OnSale`      | "On Sale" badge         | `217 91% 60%`        |
| `--Status-SellingFast` | "Selling Fast" badge    | `38 92% 50%`         |
| `--Status-SoldOut`     | "Sold Out" badge        | `0 84% 60%`          |
| `--Today-BG`           | Today's date background | `217 91% 97%`        |
| `--Focus-Ring`         | Keyboard focus ring     | `217 91% 60%`        |

### Using JavaScript Theme Utilities

```javascript
import {
  applyTheme,
  themes,
  generateThemeCSS,
} from '@getmicdrop/venue-calendar';

// Apply a preset theme
applyTheme(themes.dark);

// Apply to a specific container
const container = document.querySelector('.micdrop-calendar-container');
applyTheme(themes.dark, container);

// Create a custom theme
const myTheme = {
  brandPrimary: '270 76% 60%', // Purple
  textPrimary: '0 0% 10%',
  bgPrimary: '0 0% 100%',
  statusOnSale: '142 71% 45%', // Green for on sale
};
applyTheme(myTheme);

// Generate CSS string for embedding
const cssString = generateThemeCSS(myTheme);
console.log(cssString);
// Output: :root { --Brand-Primary: 270 76% 60%; ... }
```

### Preset Themes

Three themes are included out of the box:

```javascript
import { themes } from '@getmicdrop/venue-calendar';

// Light theme (default)
applyTheme(themes.light);

// Dark theme
applyTheme(themes.dark);

// High contrast (accessibility)
applyTheme(themes.highContrast);
```

### Automatic Dark Mode

The calendar automatically respects the user's system preference:

```css
/* Automatically applied when user prefers dark mode */
@media (prefers-color-scheme: dark) {
  /* Dark theme variables are applied */
}
```

You can also manually toggle dark mode:

```html
<!-- Add 'dark' class to enable dark theme -->
<div class="dark">
  <div class="micdrop-calendar-container" data-venue-id="123"></div>
</div>

<!-- Or use data-theme attribute -->
<div data-theme="dark">
  <div class="micdrop-calendar-container" data-venue-id="123"></div>
</div>
```

## Localization and the CDN bundle

The calendar and the checkout speak thirteen languages. English is built into the
bundle; the other twelve are **fetched at runtime**, one file, only for the
locale actually in use — so a buyer never downloads twelve languages to read one.
This is what keeps the CDN bundle at ~594 KB gzipped instead of ~686 KB.

For the npm package nothing is required: your bundler code-splits the catalogues
and addresses them itself.

For the `<script>` embed, the bundle works out where to fetch from by looking at
the URL it was itself served from, and loads a **version-pinned sibling**:

```
https://get-micdrop.com/embed/venue-calendar.iife.js
https://get-micdrop.com/embed/locales/<version>/main/es.js   ← calendar copy
https://get-micdrop.com/embed/locales/<version>/flow/es.js   ← checkout copy
```

If you self-host the bundle, **copy the `dist/locales/` directory alongside it**.
Version-pinned paths mean new releases only ever add files; they never overwrite
the catalogue an older pinned bundle is using.

### When the fetch cannot happen

If the locale file cannot be loaded — a Content-Security-Policy that allowlists
the exact script file rather than the origin, an offline CDN, or a
`<script type="module">` embed where the bundle cannot see its own URL — the
calendar simply stays in English. Nothing errors and nothing breaks; the widget
renders and sells tickets exactly as it does for an English buyer.

Two ways to point it at the right place when it cannot work it out:

```html
<!-- Option A: tell it before the bundle loads -->
<script>
  window.__MICDROP_CALENDAR_ASSET_BASE__ = 'https://get-micdrop.com/embed/';
</script>
<script
  defer
  src="https://get-micdrop.com/embed/venue-calendar.iife.js"
></script>

<!-- Option B: put it on the tag -->
<script
  defer
  src="https://get-micdrop.com/embed/venue-calendar.iife.js"
  data-vc-base="https://get-micdrop.com/embed/"
></script>
```

`defer` and `async` need no override — a classic script still knows its own URL.

### First paint is always English

A non-English buyer sees English for a moment on a cold load and the page swaps
to their language when the catalogue arrives. This is deliberate: the calendar
paints immediately rather than waiting on a translation file.

## Browser Support

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)

## Development

### Building the Package

```bash
# Install dependencies
npm install

# Build the library
npm run build:lib

# Development mode (SvelteKit app)
npm run dev

# Preview production build
npm run preview
```

`build:lib` is three passes and the order matters: the IIFE pass runs first and
owns the `dist/` wipe, the ES/UMD pass appends to it, and
`vite.config.locales.js` appends the runtime locale catalogues
(`dist/locales/<version>/{main,flow}/<tag>.js`) last. `npm run size` fails if the
bundle and its catalogues do not both come out of that.

### Project Structure

```
venue-calendar/
├── src/
│   ├── components/         # Svelte components
│   │   ├── Calendar/
│   │   ├── CalendarContainer/
│   │   └── Button/
│   ├── lib/               # Library entry points
│   │   ├── VenueCalendar.js
│   │   └── web-component.js
│   └── routes/            # SvelteKit routes (for dev)
├── dist/                  # Built package (generated)
│   └── locales/<version>/ # Runtime locale catalogues for the CDN bundle
├── package.json
├── vite.config.lib.js     # Library build config (ES/UMD + IIFE passes)
├── vite.config.locales.js # Emits the runtime locale catalogues
└── README.md
```

### Lockfile policy

`package-lock.json` is the single canonical lockfile — CI and the publish
workflow install with `npm ci`. Do not commit `yarn.lock` or
`pnpm-lock.yaml` (both gitignored). Local dev machines may use pnpm for the
svelte-components symlink workflow, but dependency changes must land in
`package-lock.json` via npm.

## API Reference

### `initVenueCalendar(options)`

Initialize a calendar instance.

**Parameters:**

- `options` (Object): Configuration options

**Returns:** Svelte component instance

**Example:**

```javascript
const calendar = initVenueCalendar({
  target: '#calendar',
  venueId: 'venue-123',
  view: 'calendar',
});
```

### `autoMount()`

Automatically mount calendars to all elements with class `micdrop-calendar-container`.

**Example:**

```javascript
import { autoMount } from '@getmicdrop/venue-calendar';
autoMount();
```

### Component Events

The calendar component emits events that you can listen to:

```javascript
const calendar = initVenueCalendar({
  target: '#calendar',
  // ... other options
});

// Listen to component events (if using Svelte component directly)
calendar.$on('eventClick', event => {
  console.log('Event clicked:', event.detail);
});
```

## Troubleshooting

### Calendar not appearing

1. **Check the script is loaded**: Open browser console and verify no errors
2. **Verify container exists**: Make sure the target element exists in the DOM
3. **Check data attributes**: Ensure attributes are correctly formatted with `data-` prefix

### Styles not applying

1. **CSS not loaded**: The styles are bundled in the JS file and auto-injected — no separate stylesheet needed
2. **CSS conflicts**: Check if other styles are overriding the calendar styles
3. **Bundle didn't load**: Confirm the `<script src>` points at the Micdrop-hosted bundle and returns 200 (not 404)

### Events not showing

1. **Check event data format**: Ensure events match the expected structure
2. **Date format**: Use ISO 8601 format for dates (`YYYY-MM-DDTHH:mm:ssZ`)
3. **Venue ID**: Verify the venue ID is correct

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT © MicDrop

## Support

For issues, questions, or feature requests, please visit:
https://github.com/get-micdrop/venue-calendar/issues

---

Made with ❤️ by the MicDrop team
