Skip to main content
RapidDev - Software Development Agency

How to Build a Event Calendar App with Lovable

Build a fully interactive event calendar in Lovable with custom day cell rendering, per-calendar color coding, and a Dialog-driven event creation flow. Attendees can RSVP directly from the event detail view, and all data lives in Supabase with RLS so users only see calendars they own or are invited to. Build time is roughly 2.5 hours.

What you'll build

  • Monthly/weekly calendar grid with shadcn/ui Calendar component and custom day cell rendering
  • Multiple named calendars per user with color labels (work, personal, team)
  • Event creation Dialog with title, description, start/end datetime pickers, calendar selector, and location
  • RSVP system where invited attendees can accept, decline, or mark tentative
  • Attendee management panel showing acceptance status per event with avatar group
  • Supabase RLS policies that isolate each user's calendars and share events only with invited attendees
  • Event detail Sheet that slides in from the right showing full event info, attendee list, and RSVP buttons
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a fully interactive event calendar in Lovable with custom day cell rendering, per-calendar color coding, and a Dialog-driven event creation flow. Attendees can RSVP directly from the event detail view, and all data lives in Supabase with RLS so users only see calendars they own or are invited to. Build time is roughly 2.5 hours.

What you're building

The calendar grid uses the shadcn/ui Calendar component as a base but overrides the day rendering to show colored event dots below each date number. Each dot corresponds to a calendar's color, so a Monday with three events from two different calendars shows two dots. Clicking a day opens a popover listing that day's events; clicking an event opens the detail Sheet.

The data model has three core tables: calendars (owned by a user, has a color and name), events (belongs to a calendar, has start_at and end_at timestamps), and event_attendees (join table between events and users with an rsvp_status column). RLS on events allows SELECT if the user owns the calendar or exists in event_attendees for that event.

The RSVP flow works entirely from the event detail Sheet. When an event is opened, the attendee list loads with each person's name, avatar, and current status shown as a Badge (accepted=green, declined=red, tentative=yellow, pending=gray). The current user sees Accept, Decline, and Tentative buttons if their own row shows pending status.

Final result

A multi-calendar event app with RSVP tracking, color-coded day cells, and a polished Sheet-based event detail view — all backed by Supabase with proper access control.

Tech stack

LovableFrontend app builder
SupabaseDatabase and Auth
shadcn/uiCalendar, Dialog, Sheet, Badge, Avatar components
date-fnsDate arithmetic and formatting
react-hook-form + zodEvent creation form validation
Supabase AuthUser identity and attendee lookup

Prerequisites

  • Lovable Pro account for Edge Function and multi-file generation
  • Supabase project with Auth enabled and email confirmations turned off for testing
  • Supabase URL and anon key saved in Cloud tab → Secrets as VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
  • Basic understanding of how Supabase Row Level Security works
  • Two test user accounts in Supabase Auth to test the RSVP invite flow

Build steps

1

Create the calendar and event schema in Supabase

Start by prompting Lovable to generate the full database schema. Three tables are needed plus RLS policies that handle shared access. This step also seeds two default calendars for new users via a database trigger.

prompt.txt
1Create a Supabase schema for a multi-calendar event app.
2
3Tables:
4- calendars: id (uuid pk), user_id (references auth.users), name (text), color (text, hex code), is_default (bool default false), created_at
5- events: id (uuid pk), calendar_id (references calendars), title (text), description (text), location (text), start_at (timestamptz), end_at (timestamptz), is_all_day (bool default false), created_by (references auth.users), created_at
6- event_attendees: id (uuid pk), event_id (references events), user_id (references auth.users), email (text, for inviting non-users), rsvp_status (text check: pending|accepted|declined|tentative, default 'pending'), responded_at, invited_at
7
8RLS policies:
9- calendars: SELECT/INSERT/UPDATE/DELETE where user_id = auth.uid()
10- events: SELECT where calendar_id IN (SELECT id FROM calendars WHERE user_id = auth.uid()) OR id IN (SELECT event_id FROM event_attendees WHERE user_id = auth.uid())
11- events: INSERT/UPDATE/DELETE where calendar_id IN (SELECT id FROM calendars WHERE user_id = auth.uid())
12- event_attendees: SELECT where event_id IN (SELECT id from events visible to user via events RLS); UPDATE where user_id = auth.uid() (for RSVP updates)
13
14Create a trigger function that runs after a new user signs up (auth.users INSERT) and creates two default calendars for them: 'Personal' (color #3b82f6) and 'Work' (color #10b981), with 'Personal' set as is_default = true.

Pro tip: Ask Lovable to generate TypeScript types from the schema right after creation: 'Generate database.types.ts from my current Supabase schema and update all existing queries to use these types.' This prevents type errors when you add the calendar grid later.

Expected result: Three tables exist in Supabase. RLS is enabled on all three. Two default calendars are auto-created when a new user signs up. TypeScript types file is present.

2

Build the calendar grid with event dot rendering

Ask Lovable to build the main calendar view. The shadcn/ui Calendar component accepts a custom day render prop. Use it to show colored dots below each day number for events on that date.

prompt.txt
1Build the main calendar page at src/pages/Calendar.tsx.
2
3Requirements:
41. Fetch all events for the current month where the user has access (uses events RLS automatically)
52. Group events by date: Map<string, Event[]> where the key is 'YYYY-MM-DD'
63. Render a shadcn/ui Calendar component in month mode
74. Use the 'components' prop on Calendar to override the DayContent renderer. For each day, render the day number plus a row of colored dots below it. Each dot color maps to the calendar's color. Show max 3 dots and a '+N' text if more.
85. On day click, open a Popover listing that day's events as clickable rows (title + time range)
96. On event row click, close the Popover and open the EventDetailSheet component
107. Add a month navigation header with left/right arrows and the current month/year
118. Add a 'New Event' Button in the top right that opens the EventCreateDialog
129. Add a sidebar showing the user's calendars as a list with colored circle indicators and checkboxes to toggle calendar visibility

Pro tip: Tell Lovable: 'When fetching events for the calendar grid, also fetch the calendar color via a join so dot colors are available without a second query.' This reduces the number of Supabase calls on each month navigation.

Expected result: The calendar grid renders the current month. Days with events show colored dots. Clicking a day opens a Popover with that day's events. The sidebar lists calendars with toggle checkboxes.

3

Build the event creation Dialog

Ask Lovable to build the EventCreateDialog component with all required fields. The Dialog should pre-fill the start time with the clicked day if opened from a day click.

prompt.txt
1// src/components/EventCreateDialog.tsx
2// Build a shadcn/ui Dialog for creating events.
3// Props: open, onOpenChange, defaultDate?: Date
4//
5// Form fields (react-hook-form + zod):
6// - title: required string
7// - calendarId: Select populated from user's calendars, defaults to default calendar
8// - startDate + startTime: combined into start_at timestamptz
9// - endDate + endTime: combined into end_at, must be after start_at
10// - isAllDay: Switch — when true, hide time pickers
11// - description: Textarea (optional)
12// - location: Input (optional)
13// - attendeeEmails: multi-value Input — user types an email and presses Enter to add it
14// Render added emails as dismissible Badges below the input
15//
16// On submit:
17// 1. Insert into events table
18// 2. For each attendee email, look up the user by email in a Supabase RPC function
19// get_user_id_by_email(email) that queries auth.users (SECURITY DEFINER)
20// 3. Insert rows into event_attendees for found users (status: pending)
21// 4. Close dialog, show a toast 'Event created', refresh calendar grid
22//
23// Add a DatePicker using shadcn/ui Popover + Calendar for date selection
24// Add time pickers as Select components with 30-minute intervals

Pro tip: The get_user_id_by_email RPC must be SECURITY DEFINER because regular users cannot query auth.users directly. Ask Lovable: 'Create a SECURITY DEFINER Supabase function get_user_id_by_email(p_email text) that returns uuid by looking up auth.users, callable by authenticated users only.'

Expected result: The New Event Dialog opens with all fields. Adding attendee emails shows dismissible Badges. Submitting inserts the event and attendees, closes the dialog, and updates the calendar grid.

4

Build the event detail Sheet with RSVP controls

Ask Lovable to build the EventDetailSheet that slides in from the right. It shows full event info and lets attendees RSVP. The sheet also lets the event owner edit or delete the event.

prompt.txt
1Build EventDetailSheet at src/components/EventDetailSheet.tsx.
2
3Props: eventId: string | null, open, onOpenChange
4
5When open and eventId is set:
61. Fetch event + calendar name/color + attendees list with user email and rsvp_status
72. Display in a shadcn/ui Sheet (side='right', width 480px):
8 - Calendar color bar at the top of the sheet header
9 - Event title (large), calendar name with colored dot
10 - Start/end time formatted: 'Monday, June 9 · 2:00 PM – 3:30 PM'
11 - Location if present (with map pin icon)
12 - Description if present
13 - Attendees section:
14 - AvatarGroup showing first 5 attendees
15 - Full list below showing each attendee's email, rsvp_status as Badge
16 - If current user is an attendee with pending/tentative/declined status, show RSVP buttons:
17 - 'Accept' (green), 'Decline' (destructive), 'Tentative' (outline)
18 - If current user is the event owner (created_by = auth.uid()):
19 - 'Edit' button opens EventCreateDialog pre-filled
20 - 'Delete' button with AlertDialog confirmation
21
22RSVP button click:
23- Update event_attendees SET rsvp_status = 'accepted' WHERE event_id = ? AND user_id = auth.uid()
24- Show toast confirmation
25- Re-fetch attendees to reflect updated statuses

Pro tip: Ask Lovable to add a real-time subscription on event_attendees for the open event: 'Subscribe to Supabase realtime on event_attendees WHERE event_id = currentEventId. When a row changes, refetch the attendees list.' This makes RSVP updates appear instantly when multiple users have the sheet open.

Expected result: The detail Sheet slides in showing complete event info. RSVP buttons appear for invited attendees. Clicking Accept updates the status badge in real time. The owner sees Edit and Delete controls.

5

Add the calendar sidebar and visibility toggles

Polish the sidebar so users can create new calendars, change colors, and toggle individual calendar visibility on the grid. Calendar visibility is local state — no database writes needed for the toggle.

prompt.txt
1Update the calendar sidebar component.
2
3Requirements:
4- List all user calendars from the calendars table
5- Each row: colored checkbox (checked = visible on grid), calendar name, three-dot menu
6- Three-dot menu options: Rename, Change Color (opens a color picker Popover with 8 preset hex colors), Delete (with AlertDialog: deleting a calendar deletes all its events)
7- At the bottom: 'Add Calendar' button that opens a small inline form: name input + color picker + Save
8- The checkbox state is stored in React state as a Set<string> of hidden calendar IDs
9- Pass the hidden calendar IDs to the calendar grid, which filters events before rendering dots
10- Default calendars (is_default = true) show a 'Default' Badge and cannot be deleted
11
12Color options: #3b82f6 (blue), #10b981 (green), #f59e0b (amber), #ef4444 (red), #8b5cf6 (purple), #ec4899 (pink), #14b8a6 (teal), #f97316 (orange)

Expected result: The sidebar shows all calendars with toggleable checkboxes. Unchecking a calendar hides its event dots from the grid instantly. The Add Calendar form creates a new calendar and it appears in the sidebar.

Complete code

src/components/EventDayCell.tsx
1import { format } from 'date-fns'
2
3type CalendarEvent = {
4 id: string
5 title: string
6 start_at: string
7 calendar_color: string
8 calendar_id: string
9}
10
11type EventDayCellProps = {
12 date: Date
13 events: CalendarEvent[]
14 hiddenCalendarIds: Set<string>
15 onClick: (date: Date) => void
16}
17
18const MAX_DOTS = 3
19
20export function EventDayCell({ date, events, hiddenCalendarIds, onClick }: EventDayCellProps) {
21 const visibleEvents = events.filter(
22 (e) => !hiddenCalendarIds.has(e.calendar_id)
23 )
24
25 const uniqueColors = Array.from(
26 new Map(visibleEvents.map((e) => [e.calendar_color, e.calendar_color])).values()
27 )
28
29 const dotsToShow = uniqueColors.slice(0, MAX_DOTS)
30 const overflow = uniqueColors.length - MAX_DOTS
31
32 return (
33 <button
34 onClick={() => onClick(date)}
35 className="flex flex-col items-center w-full h-full min-h-[40px] py-1 hover:bg-accent rounded-md transition-colors"
36 >
37 <span className="text-sm tabular-nums">{format(date, 'd')}</span>
38 {visibleEvents.length > 0 && (
39 <div className="flex items-center gap-0.5 mt-0.5">
40 {dotsToShow.map((color) => (
41 <span
42 key={color}
43 className="w-1.5 h-1.5 rounded-full flex-shrink-0"
44 style={{ backgroundColor: color }}
45 />
46 ))}
47 {overflow > 0 && (
48 <span className="text-[10px] text-muted-foreground leading-none ml-0.5">
49 +{overflow}
50 </span>
51 )}
52 </div>
53 )}
54 </button>
55 )
56}

Customization ideas

Recurring events

Add a recurrence_rule column (text, iCal RRULE format) to the events table. On the calendar grid query, expand recurring events into occurrences client-side using the rrule npm package. Ask Lovable to add a recurrence selector in the creation Dialog with options: Does not repeat, Daily, Weekly, Monthly, Custom.

Google Calendar sync

Add a Supabase Edge Function that accepts a Google OAuth token, reads the user's Google Calendar events via the Calendar API, and upserts them into your events table with a source='google' column. Add a 'Sync from Google Calendar' button in the sidebar that triggers this Edge Function.

Week and day views

Add two additional view modes alongside the month view. The week view renders 7 columns with hour rows showing event blocks positioned with CSS top/height based on start/end times. The day view shows a single column. Add a ToggleGroup in the header to switch between Month, Week, and Day views.

Event reminders via email

Add a reminders table storing event_id, user_id, and minutes_before (e.g. 10, 30, 60, 1440). Create a Supabase Edge Function triggered on a cron schedule every minute that queries for upcoming reminders and sends emails via Resend. The event creation Dialog gets a reminder Select.

Public calendar sharing

Add a share_token column (uuid, unique) to calendars. A 'Share' button generates the token and produces a read-only public URL like /calendar/[share_token]. Create a public page that fetches events for that token using a Supabase function with SECURITY DEFINER bypassing the user-based RLS.

Common pitfalls

Pitfall: Querying all events instead of filtering by month

How to avoid: Filter the Supabase query by date range: .gte('start_at', startOfMonth.toISOString()).lte('start_at', endOfMonth.toISOString()). Refetch when the user navigates months. Ask Lovable to pass the current month's start and end dates as query parameters.

Pitfall: Skipping RLS on event_attendees update

How to avoid: The UPDATE policy on event_attendees must be: USING (user_id = auth.uid()). This ensures users can only change their own RSVP row regardless of what event_id they send.

Pitfall: Using local timezone for start_at/end_at storage

How to avoid: Always store timestamps as UTC timestamptz. In the creation form, convert the selected date and time to UTC before inserting: new Date(localDateTimeString).toISOString(). Display times back in the user's local timezone using date-fns format with the user's locale.

Pitfall: Not debouncing the attendee email lookup on input

How to avoid: Only call the lookup function when the user finishes typing an email (on Enter key or comma press), not on every change event. The multi-value badge input pattern naturally handles this — add to the list on Enter, then resolve user IDs in batch on form submit.

Best practices

  • Always store event timestamps as UTC timestamptz in Supabase and convert to local time only at render time. This prevents cross-timezone bugs when users travel or share events with people in other regions.
  • Use Supabase Realtime subscriptions on event_attendees when the detail Sheet is open. This lets RSVP updates from other users appear immediately without polling.
  • Add a unique constraint on event_attendees(event_id, user_id) to prevent duplicate attendee rows from double-clicking the invite button.
  • Scope calendar visibility toggles to local React state only — do not write hidden calendar preferences to the database on every toggle. Persist them to localStorage or a user_preferences table only when the user explicitly saves settings.
  • Use a SECURITY DEFINER Supabase function for any query that needs to access auth.users (like email-to-user-ID lookup). Never bypass RLS with the service role key from the frontend.
  • When deleting a calendar, use CASCADE on the foreign key from events to calendars so all associated events and attendee rows are deleted atomically rather than doing multiple delete calls from the client.
  • Load event data for the previous and next month in the background when the user views the current month. This makes month navigation feel instant — the data is pre-fetched before the user clicks the arrow.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a multi-user event calendar in React with Supabase. I have a calendars table, an events table with start_at and end_at as timestamptz, and an event_attendees join table. Help me write a React hook useCalendarEvents(year, month) that fetches all events the current user can see (either owns or is invited to) for a given month, grouped by date as a Map<string, Event[]> where the key is 'YYYY-MM-DD'. Show me how to handle the UTC-to-local timezone conversion when grouping by date so events near midnight don't appear on the wrong day.

Lovable Prompt

Add an iCal export button to the EventDetailSheet. When clicked, it should generate a .ics file download for that single event. The .ics should include DTSTART, DTEND, SUMMARY, DESCRIPTION, LOCATION, and ORGANIZER fields formatted per RFC 5545. Use a Blob and URL.createObjectURL to trigger the browser download without a server call. Also add an 'Export Calendar' option in the calendar sidebar three-dot menu that exports all visible events for a calendar as a single .ics file.

Build Prompt

In Supabase, write a SQL function get_events_for_month(p_year int, p_month int) that returns all events the calling user can see (either as calendar owner or as an attendee) within that calendar month. Join with calendars to include calendar_name and calendar_color. Order by start_at ASC. Make the function SECURITY INVOKER so it respects RLS automatically. Return columns: id, title, start_at, end_at, is_all_day, calendar_id, calendar_name, calendar_color, rsvp_status (from event_attendees for the calling user, null if owner).

Frequently asked questions

Can the shadcn/ui Calendar component handle custom day rendering?

Yes. The shadcn/ui Calendar component accepts a components prop that lets you override the DayContent renderer. You pass a React component that receives the date and can return any JSX — in this case the day number plus colored event dots. Ask Lovable to implement this with: components={{ DayContent: ({ date }) => EventDayCell component that receives the date }}.

How do I prevent one user from seeing another user's private calendars?

Supabase Row Level Security handles this automatically. The RLS policy on calendars restricts SELECT to rows where user_id = auth.uid(). For shared events, the events SELECT policy additionally allows access if the user exists in event_attendees for that event. As long as RLS is enabled on both tables and no SECURITY DEFINER bypass exists on the client, cross-user data leakage is prevented at the database layer.

What is the best way to handle events that span multiple days on the calendar grid?

For multi-day events, compute all the dates between start_at and end_at and add the event to each date's bucket in your grouping map. In the DayContent renderer, check if an event starts on this day, ends on this day, or spans through it and apply different dot styles or a continuous bar style accordingly. Ask Lovable to handle this in the grouping hook.

Can I invite users who do not have an account yet?

Yes, with extra work. The event_attendees table includes an email column for non-user invites. Store the invited email with a null user_id. When that email signs up (detected via a trigger on auth.users), match their new user_id to any pending attendee rows by email and update them. For now, invited non-users won't see the event until they sign up.

How do I show events in the correct timezone for each user?

Store all timestamps as UTC in Supabase. When rendering, use the JavaScript Intl.DateTimeFormat API or date-fns with the user's detected timezone (Intl.DateTimeFormat().resolvedOptions().timeZone). Ask Lovable to store the user's preferred timezone in a user_preferences table and use it for display throughout the app.

How many events can the calendar handle before it gets slow?

The main performance concern is the initial month fetch. A query returning 500 events with a joined calendar color is well within Supabase's response time budget. For users with very large calendars (1,000+ events per month), add a database index on events(start_at) and events(calendar_id) to keep the date range query fast.

Can I get help building a more complex calendar with recurring events and integrations?

RapidDev specializes in Lovable builds including complex scheduling and calendar features. If you need recurring event support, Google Calendar sync, or attendee notification emails, reach out for a scoped build.

How do I test the RSVP flow without multiple real accounts?

Create two test users in Supabase Auth (Authentication → Users → Invite User) with different email addresses. Open your Lovable app in two different browser profiles (or use an incognito window), sign in as each user, and create an event as user one with user two's email as an attendee. Switch to user two's session to see the RSVP buttons appear in the event detail Sheet.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.