Built by Hannah Levinson • more at hrlevinson.com
A personal calendar for making and seeing art in New York.
Live: https://nyc-art-cal.vercel.app
Subscribe from Google Calendar or Apple Calendar:
- All events →
/feed.ics - Just shows to attend →
/feed-attend.ics - Just classes and workshops →
/feed-practice.ics
I wanted a personalized instrument for moving through the artworld and my own art practice in New York.
- A weekly calendar of events across media and venues including theatre, dance, film, music, workshops, readings, and more.
- Two modes: Make (classes and workshops) and Witness (shows and screenings).
- A picks / shortlist system with passphrase-based cross-device sync: star on your phone, see it on your laptop.
- A self-updating ICS feed.
- Groups events by week, one week at a time by default. auto-focuses on the current week; arrows to move; jump-to-week dropdown.
- Every event carries structured metadata: category, mode, start / end time, category-color left border.
- Weekly summary strip: event count, cost range, making %, picked count and cost.
- Past events dim and hide by default with a "show past" toggle.
- Places to make / see things: comprehensive list of venues also provided as an ongoing resource, collapsed below the calendar.
- Cross-device picks sync via Supabase, keyed to a passphrase hash, no account, no email.
- Filterable exportable feeds that GCal auto-refreshes.
- sound: concerts, sound art, opera
- dance: dance classes and shows
- film: screenings, workshops
- tech: hardware, generative visuals, live-coding
- make: book arts, darkroom, printmaking, woodworking
- stage: performance art, theatre
- word: readings, writing groups
- circle: communal meals, meditations, social practice
- Not a listings site. It's a curated personal map, not a scraped aggregator.
- Not a social product. The picks-sync is one person's picks shared across their own devices. No feed, other people, or sharing links.
- Not automated. New events go in by editing a JSON in a GitHub web editor. No scraping cron, no email-parsing pipeline (for now).
The tech build is generic; what's included in the calendar is a reflection of my personal practice and preferences.
Fork the code, rewrite the JSON.
- Vite + React + TypeScript, strict mode, no CSS framework, hand-written stylesheet on CSS custom properties.
- All events in JSON:
src/data/events.json,spaces.json,fall.json. I edit these directly in GitHub's web editor and commit, no admin UI, no forms. - Supabase for picks sync. SHA-256 hash of a user-chosen passphrase is the row key. The passphrase is never sent to the server, only its hash. Nothing else is stored in the backend.
- Build-time ICS generation. A
tsxscript (scripts/generate-feeds.ts) runs after Vite build and emits three.icsfiles intodist/, served withContent-Type: text/calendarfrom Vercel viavercel.json. - Stable event UIDs derived from date + slug of event name, so calendar apps update existing events instead of duplicating them on each refresh.
- Today-aware everywhere. Days-until countdowns on events, current-week highlight, past-event dimming, all keyed to
today()at load. - Deployed on Vercel with continuous deploy from
main. Editing JSON on GitHub triggers a Vercel rebuild within a minute; GCal picks up feed changes on its next refresh (typically 12–24 hours).
A scheduled GitHub Action (scripts/scan-events.ts) sweeps the venue list in scripts/scanner/venues.ts and extracts upcoming events. It tries the cheapest, most reliable source first and only falls back when it must:
- Published iCal feeds (
icsUrlon a venue): parsed directly, no LLM. - JSON-LD structured data on the listing page.
- Detail-page crawl: follows event links to pages that carry JSON-LD even when the listing doesn't.
- Platform parsers: server-rendered Squarespace event lists and WordPress "The Events Calendar" markup.
- LLM extraction: last resort for JS-rendered venues with no structured data.
Every candidate passes gates (real future date present in source, title tokens present, valid category/mode) and a fuzzy dedupe check; dense performance runs collapse to one opening-night entry.
Step 5 uses whichever free API key is present, preferring the largest free tier. Set one as a GitHub Actions secret:
GROQ_API_KEY: Groq free tier (thousands of requests/day; covers every LLM-needing venue each run). Recommended.CEREBRAS_API_KEY(+ optionalCEREBRAS_MODEL, defaultllama-3.3-70b): Cerebras free tier; a strong alternative if Groq signup is down.GOOGLE_API_KEY: Gemini free tier (~20 requests/day; venues rotate across runs viascan-state.json).OPENAI_COMPAT_BASE_URL+OPENAI_COMPAT_API_KEY(+ optionalOPENAI_COMPAT_MODEL): any OpenAI-compatible endpoint (OpenRouter, Together, …).
With no key set, steps 1–4 still run, so feed/JSON-LD/CMS venues keep updating for free.
Any event or venue lives in src/data/. Edit in the GitHub web editor, click Commit, Vercel rebuilds automatically and your subscribed calendar reflects the change on its next refresh cycle.
- App: https://nyc-art-cal.vercel.app
- Author: hrlevinson.com