Add event location and online meeting details#70
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for optional event location metadata and online meeting details throughout event creation, persistence, snapshots, UI surfaces, notifications, and ICS export.
Changes:
- Extend validation, types, and persistence to support
location,isOnlineMeeting, andmeetingLink. - Propagate the new metadata through snapshots/views and surface it via a shared
EventMetaDetailscomponent. - Update ICS export to reflect online meetings (hide physical location, include meeting link in description) and expand unit/UI tests accordingly.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lib/validators.ts | Adds optional text schema + meeting-link URL validation and drops irrelevant fields via transform. |
| src/lib/validators.test.ts | Adds validation tests for location/online meeting/link URL requirements. |
| src/lib/types.ts | Extends event create input and snapshot types with location/online-meeting fields. |
| src/lib/ics.ts | Adds online-meeting-aware LOCATION/description content and filters optional ICS lines. |
| src/lib/i18n/messages.ts | Adds EN/DE strings for event format/location/meeting link labels and validation messages. |
| src/lib/event-service.ts | Persists online meeting fields and carries them into snapshots. |
| src/lib/event-service.test.ts | Updates event creation tests to include isOnlineMeeting. |
| src/lib/availability.ts | Includes new fields in snapshot building; enforces hiding location for online events. |
| src/lib/availability-notifications.tsx | Ensures notification snapshots include the new fields. |
| src/components/public-event-client.tsx | Renders shared event meta details on the public event page. |
| src/components/public-event-client.test.tsx | Updates snapshot test helpers to include new snapshot fields. |
| src/components/manage-event-client.tsx | Renders shared event meta details on the manage page. |
| src/components/manage-event-client.test.tsx | Updates manage-view test helpers to include new snapshot fields. |
| src/components/full-day-availability.tsx | Renders shared event meta details for full-day view. |
| src/components/event-meta-details.tsx | New component to display location/online status/meeting link consistently. |
| src/components/event-heatmap.tsx | Renders shared event meta details in the heatmap header. |
| src/components/create-event-form.tsx | Adds “event format” selection UI and conditional location/meeting-link fields + payload wiring. |
| src/components/create-event-form.test.tsx | Adds UI tests for event format control and payload behavior for online vs in-person. |
| src/app/api/events/route.test.ts | Updates API tests for default isOnlineMeeting + new online-meeting payload passthrough. |
| src/app/api/events/[slug]/ics/route.ts | Passes snapshot location/online-meeting/link into ICS builder. |
| src/app/api/events/[slug]/ics/route.test.ts | Validates ICS output for online meetings (LOCATION + meeting link in description). |
| prisma/schema.prisma | Adds new columns to the Event model. |
| prisma/migrations/20260427143000_add_event_location_details/migration.sql | Migration to add location, isOnlineMeeting, and meetingLink columns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div className="space-y-2"> | ||
| <Label id={eventFieldIds.isOnlineMeeting}> | ||
| {messages.createEvent.eventFormatLabel} | ||
| </Label> | ||
| <div | ||
| role="radiogroup" | ||
| aria-labelledby={eventFieldIds.isOnlineMeeting} | ||
| className="grid grid-cols-1 gap-2 sm:grid-cols-2" | ||
| > | ||
| <button | ||
| type="button" | ||
| role="radio" | ||
| aria-checked={!isOnlineMeeting} | ||
| className={cn( | ||
| "flex h-10 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", | ||
| !isOnlineMeeting | ||
| ? "border-primary bg-primary/10 text-foreground" | ||
| : "border-input bg-background text-muted-foreground", | ||
| )} | ||
| onClick={() => { | ||
| setIsOnlineMeeting(false); | ||
| setMeetingLink(""); | ||
| clearErrors("isOnlineMeeting", "meetingLink"); | ||
| }} | ||
| > | ||
| <MapPinIcon className="size-4" aria-hidden="true" /> | ||
| {messages.createEvent.inPersonLabel} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| role="radio" | ||
| aria-checked={isOnlineMeeting} | ||
| className={cn( | ||
| "flex h-10 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", | ||
| isOnlineMeeting | ||
| ? "border-primary bg-primary/10 text-foreground" | ||
| : "border-input bg-background text-muted-foreground", | ||
| )} | ||
| onClick={() => { | ||
| setIsOnlineMeeting(true); | ||
| setLocation(""); | ||
| clearErrors("isOnlineMeeting", "location"); | ||
| }} | ||
| > | ||
| <VideoIcon className="size-4" aria-hidden="true" /> | ||
| {messages.createEvent.onlineMeetingLabel} | ||
| </button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The new event-format control uses ARIA role="radiogroup"/role="radio" on <div>/<button> but doesn’t implement the required radio-group keyboard behavior (roving tabIndex, arrow-key navigation, Space/Enter activation). This will be confusing for keyboard and assistive-technology users because both buttons remain in the tab order and arrow keys won’t work as expected. Consider switching to native <input type="radio"> elements (or a shared RadioGroup component) and implementing the standard keyboard interactions.
| <div className="space-y-2"> | |
| <Label id={eventFieldIds.isOnlineMeeting}> | |
| {messages.createEvent.eventFormatLabel} | |
| </Label> | |
| <div | |
| role="radiogroup" | |
| aria-labelledby={eventFieldIds.isOnlineMeeting} | |
| className="grid grid-cols-1 gap-2 sm:grid-cols-2" | |
| > | |
| <button | |
| type="button" | |
| role="radio" | |
| aria-checked={!isOnlineMeeting} | |
| className={cn( | |
| "flex h-10 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", | |
| !isOnlineMeeting | |
| ? "border-primary bg-primary/10 text-foreground" | |
| : "border-input bg-background text-muted-foreground", | |
| )} | |
| onClick={() => { | |
| setIsOnlineMeeting(false); | |
| setMeetingLink(""); | |
| clearErrors("isOnlineMeeting", "meetingLink"); | |
| }} | |
| > | |
| <MapPinIcon className="size-4" aria-hidden="true" /> | |
| {messages.createEvent.inPersonLabel} | |
| </button> | |
| <button | |
| type="button" | |
| role="radio" | |
| aria-checked={isOnlineMeeting} | |
| className={cn( | |
| "flex h-10 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", | |
| isOnlineMeeting | |
| ? "border-primary bg-primary/10 text-foreground" | |
| : "border-input bg-background text-muted-foreground", | |
| )} | |
| onClick={() => { | |
| setIsOnlineMeeting(true); | |
| setLocation(""); | |
| clearErrors("isOnlineMeeting", "location"); | |
| }} | |
| > | |
| <VideoIcon className="size-4" aria-hidden="true" /> | |
| {messages.createEvent.onlineMeetingLabel} | |
| </button> | |
| </div> | |
| </div> | |
| <fieldset className="space-y-2"> | |
| <legend | |
| id={eventFieldIds.isOnlineMeeting} | |
| className="text-sm font-medium leading-none" | |
| > | |
| {messages.createEvent.eventFormatLabel} | |
| </legend> | |
| <div className="grid grid-cols-1 gap-2 sm:grid-cols-2"> | |
| <label | |
| className={cn( | |
| "flex h-10 cursor-pointer items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40", | |
| "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", | |
| !isOnlineMeeting | |
| ? "border-primary bg-primary/10 text-foreground" | |
| : "border-input bg-background text-muted-foreground", | |
| )} | |
| > | |
| <input | |
| type="radio" | |
| name="event-format" | |
| className="sr-only" | |
| checked={!isOnlineMeeting} | |
| onChange={() => { | |
| setIsOnlineMeeting(false); | |
| setMeetingLink(""); | |
| clearErrors("isOnlineMeeting", "meetingLink"); | |
| }} | |
| /> | |
| <MapPinIcon className="size-4" aria-hidden="true" /> | |
| {messages.createEvent.inPersonLabel} | |
| </label> | |
| <label | |
| className={cn( | |
| "flex h-10 cursor-pointer items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors hover:bg-muted/40", | |
| "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", | |
| isOnlineMeeting | |
| ? "border-primary bg-primary/10 text-foreground" | |
| : "border-input bg-background text-muted-foreground", | |
| )} | |
| > | |
| <input | |
| type="radio" | |
| name="event-format" | |
| className="sr-only" | |
| checked={isOnlineMeeting} | |
| onChange={() => { | |
| setIsOnlineMeeting(true); | |
| setLocation(""); | |
| clearErrors("isOnlineMeeting", "location"); | |
| }} | |
| /> | |
| <VideoIcon className="size-4" aria-hidden="true" /> | |
| {messages.createEvent.onlineMeetingLabel} | |
| </label> | |
| </div> | |
| </fieldset> |
Summary
Testing
/newflow to confirm the new event format control and conditional fields render correctly.