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
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
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.
1Create a Supabase schema for a multi-calendar event app.23Tables:4- calendars: id (uuid pk), user_id (references auth.users), name (text), color (text, hex code), is_default (bool default false), created_at5- 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_at6- 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_at78RLS 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)1314Create 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.
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.
1Build the main calendar page at src/pages/Calendar.tsx.23Requirements: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 mode74. 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 component107. Add a month navigation header with left/right arrows and the current month/year118. Add a 'New Event' Button in the top right that opens the EventCreateDialog129. Add a sidebar showing the user's calendars as a list with colored circle indicators and checkboxes to toggle calendar visibilityPro 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.
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.
1// src/components/EventCreateDialog.tsx2// Build a shadcn/ui Dialog for creating events.3// Props: open, onOpenChange, defaultDate?: Date4//5// Form fields (react-hook-form + zod):6// - title: required string7// - calendarId: Select populated from user's calendars, defaults to default calendar8// - startDate + startTime: combined into start_at timestamptz9// - endDate + endTime: combined into end_at, must be after start_at10// - isAllDay: Switch — when true, hide time pickers11// - description: Textarea (optional)12// - location: Input (optional)13// - attendeeEmails: multi-value Input — user types an email and presses Enter to add it14// Render added emails as dismissible Badges below the input15//16// On submit:17// 1. Insert into events table18// 2. For each attendee email, look up the user by email in a Supabase RPC function19// 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 grid22//23// Add a DatePicker using shadcn/ui Popover + Calendar for date selection24// Add time pickers as Select components with 30-minute intervalsPro 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.
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.
1Build EventDetailSheet at src/components/EventDetailSheet.tsx.23Props: eventId: string | null, open, onOpenChange45When open and eventId is set:61. Fetch event + calendar name/color + attendees list with user email and rsvp_status72. Display in a shadcn/ui Sheet (side='right', width 480px):8 - Calendar color bar at the top of the sheet header9 - Event title (large), calendar name with colored dot10 - Start/end time formatted: 'Monday, June 9 · 2:00 PM – 3:30 PM'11 - Location if present (with map pin icon)12 - Description if present13 - Attendees section:14 - AvatarGroup showing first 5 attendees15 - Full list below showing each attendee's email, rsvp_status as Badge16 - 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-filled20 - 'Delete' button with AlertDialog confirmation2122RSVP button click:23- Update event_attendees SET rsvp_status = 'accepted' WHERE event_id = ? AND user_id = auth.uid()24- Show toast confirmation25- Re-fetch attendees to reflect updated statusesPro 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.
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.
1Update the calendar sidebar component.23Requirements:4- List all user calendars from the calendars table5- Each row: colored checkbox (checked = visible on grid), calendar name, three-dot menu6- 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 + Save8- The checkbox state is stored in React state as a Set<string> of hidden calendar IDs9- Pass the hidden calendar IDs to the calendar grid, which filters events before rendering dots10- Default calendars (is_default = true) show a 'Default' Badge and cannot be deleted1112Color 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
1import { format } from 'date-fns'23type CalendarEvent = {4 id: string5 title: string6 start_at: string7 calendar_color: string8 calendar_id: string9}1011type EventDayCellProps = {12 date: Date13 events: CalendarEvent[]14 hiddenCalendarIds: Set<string>15 onClick: (date: Date) => void16}1718const MAX_DOTS = 31920export function EventDayCell({ date, events, hiddenCalendarIds, onClick }: EventDayCellProps) {21 const visibleEvents = events.filter(22 (e) => !hiddenCalendarIds.has(e.calendar_id)23 )2425 const uniqueColors = Array.from(26 new Map(visibleEvents.map((e) => [e.calendar_color, e.calendar_color])).values()27 )2829 const dotsToShow = uniqueColors.slice(0, MAX_DOTS)30 const overflow = uniqueColors.length - MAX_DOTS3132 return (33 <button34 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 <span42 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.
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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation