Skip to main content
RapidDev - Software Development Agency

How to Build Event calendar app with V0

Build an interactive event calendar app with V0 using Next.js, Supabase for event storage, and shadcn/ui Calendar for date navigation. You'll create month/week/day views, event creation with date-time pickers, RSVP functionality, and category filtering — all in about 1-2 hours without touching a terminal.

What you'll build

  • Interactive calendar with month, week, and day view toggle using shadcn/ui Tabs
  • Event creation form with Calendar date picker, time inputs, and category Select
  • Color-coded event categories with Badge components and Popover previews on hover
  • RSVP system with going/maybe/declined status and Avatar group for attendee previews
  • Event detail page with organizer info, attendee list, and location display
  • Efficient viewport-based event querying with composite index on start_time
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build an interactive event calendar app with V0 using Next.js, Supabase for event storage, and shadcn/ui Calendar for date navigation. You'll create month/week/day views, event creation with date-time pickers, RSVP functionality, and category filtering — all in about 1-2 hours without touching a terminal.

What you're building

Event calendars are essential for communities, teams, and public event listings. Whether you are building a meetup group calendar, a team scheduling tool, or a public event directory, users need to browse events visually and RSVP quickly.

V0 generates the calendar grid layout, event forms, and RSVP logic from prompts. The shadcn/ui Calendar component provides the date picker foundation, and you build the full calendar grid with custom CSS grid layouts. Supabase stores events and RSVPs with efficient date-range queries.

The architecture uses Next.js App Router with a Server Component wrapper that fetches events for the visible date range, a client component for the interactive calendar grid and view switching, Server Actions for RSVP toggling with optimistic UI, and Supabase with a composite index for fast viewport queries.

Final result

A fully functional event calendar with month/week/day views, color-coded categories, event creation, RSVP with attendee counts, and responsive design.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase

Prerequisites

  • A V0 account (Premium plan for multiple component iterations)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • A list of event categories for your use case (e.g., social, workshop, meeting)
  • Basic understanding of date/time handling (events have start and end times)

Build steps

1

Set up the project and event database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Prompt V0 to create the events, RSVPs, and categories schema with a composite index for efficient calendar queries.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an event calendar app. Create a Supabase schema with:
3// 1. event_categories: id (uuid PK), name (text), color (text), slug (text unique)
4// 2. events: id (uuid PK), title (text), description (text), start_time (timestamptz), end_time (timestamptz), location (text), color (text default '#3b82f6'), is_all_day (boolean default false), recurrence_rule (text), organizer_id (uuid FK to auth.users), max_attendees (int), category_id (uuid FK to event_categories), created_at (timestamptz)
5// 3. rsvps: id (uuid PK), event_id (uuid FK to events), user_id (uuid FK to auth.users), status (text default 'going' check in 'going','maybe','declined'), created_at (timestamptz), unique(event_id, user_id)
6// Create a composite index on events(start_time, end_time) for fast range queries.
7// Add RLS: anyone can read events, authenticated users can RSVP and create events.

Pro tip: Use Design Mode (Option+D) to visually adjust the calendar grid colors and event card styling without spending V0 credits.

Expected result: Supabase is connected with events, categories, and RSVPs tables. The composite index on start_time and end_time enables fast viewport queries.

2

Build the calendar grid with month/week/day views

Create the main calendar page with a client component for the interactive grid. Events are fetched based on the currently visible date range and displayed as colored blocks on the calendar.

components/calendar-view.tsx
1'use client'
2
3import { useState, useEffect } from 'react'
4import { createClient } from '@/lib/supabase/client'
5import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
6import { Button } from '@/components/ui/button'
7import { Badge } from '@/components/ui/badge'
8import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
9import { ChevronLeft, ChevronRight } from 'lucide-react'
10
11type Event = {
12 id: string
13 title: string
14 start_time: string
15 end_time: string
16 color: string
17 location: string
18}
19
20export function CalendarView() {
21 const [view, setView] = useState<'month' | 'week' | 'day'>('month')
22 const [currentDate, setCurrentDate] = useState(new Date())
23 const [events, setEvents] = useState<Event[]>([])
24 const supabase = createClient()
25
26 useEffect(() => {
27 const fetchEvents = async () => {
28 const start = getViewStart(currentDate, view)
29 const end = getViewEnd(currentDate, view)
30
31 const { data } = await supabase
32 .from('events')
33 .select('id, title, start_time, end_time, color, location')
34 .gte('start_time', start.toISOString())
35 .lte('start_time', end.toISOString())
36 .order('start_time')
37
38 setEvents(data ?? [])
39 }
40 fetchEvents()
41 }, [currentDate, view, supabase])
42
43 return (
44 <div className="container mx-auto py-4">
45 <div className="flex items-center justify-between mb-4">
46 <div className="flex items-center gap-2">
47 <Button variant="outline" size="icon" onClick={() => navigate(-1)}>
48 <ChevronLeft className="h-4 w-4" />
49 </Button>
50 <h2 className="text-xl font-semibold">
51 {currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
52 </h2>
53 <Button variant="outline" size="icon" onClick={() => navigate(1)}>
54 <ChevronRight className="h-4 w-4" />
55 </Button>
56 </div>
57 <Tabs value={view} onValueChange={(v) => setView(v as typeof view)}>
58 <TabsList>
59 <TabsTrigger value="month">Month</TabsTrigger>
60 <TabsTrigger value="week">Week</TabsTrigger>
61 <TabsTrigger value="day">Day</TabsTrigger>
62 </TabsList>
63 </Tabs>
64 </div>
65 <div className="grid grid-cols-7 gap-px bg-muted rounded-lg overflow-hidden">
66 {getDaysInView(currentDate, view).map((day) => (
67 <div key={day.toISOString()} className="bg-background p-2 min-h-[100px]">
68 <span className="text-sm text-muted-foreground">{day.getDate()}</span>
69 {events
70 .filter((e) => isSameDay(new Date(e.start_time), day))
71 .map((event) => (
72 <Popover key={event.id}>
73 <PopoverTrigger asChild>
74 <button
75 className="w-full text-left text-xs p-1 rounded mt-1 text-white truncate"
76 style={{ backgroundColor: event.color }}
77 >
78 {event.title}
79 </button>
80 </PopoverTrigger>
81 <PopoverContent className="w-64">
82 <p className="font-medium">{event.title}</p>
83 <p className="text-sm text-muted-foreground">{event.location}</p>
84 </PopoverContent>
85 </Popover>
86 ))}
87 </div>
88 ))}
89 </div>
90 </div>
91 )
92}
93
94function getViewStart(date: Date, view: string): Date { return new Date(date.getFullYear(), date.getMonth(), 1) }
95function getViewEnd(date: Date, view: string): Date { return new Date(date.getFullYear(), date.getMonth() + 1, 0) }
96function getDaysInView(date: Date, view: string): Date[] {
97 const days: Date[] = []
98 const start = getViewStart(date, view)
99 const end = getViewEnd(date, view)
100 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) days.push(new Date(d))
101 return days
102}
103function isSameDay(a: Date, b: Date) { return a.toDateString() === b.toDateString() }

Expected result: A calendar grid showing the current month with color-coded events. Clicking an event shows a Popover preview with title and location.

3

Build the event creation form

Create the event creation page with date-time pickers, category selection, and form validation. The form uses Zod validation via a Server Action to insert new events.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an event creation page at app/events/new/page.tsx as a 'use client' component.
3// Requirements:
4// - Input for event title (required)
5// - Textarea for description
6// - Calendar component from shadcn/ui for selecting the start date
7// - Two time Input fields for start time and end time (HH:MM format)
8// - Switch for "All day event" toggle that hides time inputs when enabled
9// - Select for event category (fetched from event_categories table)
10// - Input for location
11// - Input for max attendees (optional number)
12// - Color picker using a row of colored Button circles for quick color selection
13// - Submit Button that calls a Server Action with Zod validation:
14// - title required, 3-100 chars
15// - start_time required, must be in the future
16// - end_time must be after start_time
17// - Show success toast and redirect to the calendar after creation
18// - Use Card to wrap the form with clear section headings

Pro tip: Store all times in UTC in the database and convert to the user's local timezone for display using Intl.DateTimeFormat. This prevents timezone confusion for distributed teams.

Expected result: The event form validates inputs and creates events with proper timezone handling. Success redirects back to the calendar.

4

Add RSVP functionality with optimistic UI

Build the RSVP system using a Server Action with optimistic updates. Users can toggle between going, maybe, and declined states. The attendee count updates instantly before the server confirms.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build RSVP functionality for the event detail page at app/events/[id]/page.tsx.
3// Requirements:
4// - Server Component that fetches the event with organizer info and RSVPs count
5// - Display event details: title as h1, description, date/time formatted, location with map pin icon, category Badge
6// - Show attendee count: "X going, Y maybe" with Avatar group showing first 5 attendee photos
7// - RSVP section with three Button variants:
8// - "Going" (default/primary when selected)
9// - "Maybe" (outline when selected)
10// - "Declined" (ghost when selected)
11// - Use a 'use client' RSVPButtons component that calls a Server Action to upsert the RSVP
12// - Implement optimistic UI: update the button state immediately, revert on error
13// - Show max capacity with a Progress bar if max_attendees is set ("15 of 50 spots filled")
14// - Include a "Share Event" Button that copies the event URL to clipboard
15// - Use Card to wrap the event details and Separator between sections

Expected result: The event page shows full details with RSVP buttons. Clicking a button instantly updates the state (optimistic UI) and syncs with the database.

5

Add category filtering and event search

Enhance the calendar with category filtering using a Select dropdown and text search. Users can show/hide specific event categories and search for events by title.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Add filtering capabilities to the calendar view component.
3// Requirements:
4// - A filter bar above the calendar with:
5// - Select dropdown for event category ("All Categories" + each category from database)
6// - Input for searching events by title
7// - Button to toggle showing past events
8// - Category filter chips showing active filters with colored Badge components and X button to remove
9// - When a category is selected, only events in that category appear on the calendar
10// - Search filters events client-side by title (case-insensitive)
11// - Add a "Today" Button that jumps back to the current date
12// - Add a mini Calendar component in a sidebar (Popover) for quick date jumping
13// - Highlight today's date with a different background color on the calendar grid
14// - Show a "No events" message in empty calendar cells

Expected result: The calendar has category filter Select, search Input, and Today button. Filtering updates the visible events instantly without page reload.

Complete code

app/api/events/route.ts
1import { createClient } from '@/lib/supabase/server'
2import { NextRequest, NextResponse } from 'next/server'
3
4export async function GET(req: NextRequest) {
5 const supabase = await createClient()
6 const { searchParams } = new URL(req.url)
7 const start = searchParams.get('start')
8 const end = searchParams.get('end')
9 const category = searchParams.get('category')
10
11 let query = supabase
12 .from('events')
13 .select(`
14 id, title, description, start_time, end_time,
15 location, color, is_all_day, max_attendees,
16 event_categories(name, color, slug),
17 rsvps(count)
18 `)
19 .order('start_time')
20
21 if (start) query = query.gte('start_time', start)
22 if (end) query = query.lte('start_time', end)
23 if (category) query = query.eq('event_categories.slug', category)
24
25 const { data, error } = await query
26
27 if (error) {
28 return NextResponse.json({ error: error.message }, { status: 500 })
29 }
30
31 return NextResponse.json({ data })
32}
33
34export async function POST(req: NextRequest) {
35 const supabase = await createClient()
36 const body = await req.json()
37 const { data: { user } } = await supabase.auth.getUser()
38
39 if (!user) {
40 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
41 }
42
43 const { data, error } = await supabase
44 .from('events')
45 .insert({
46 ...body,
47 organizer_id: user.id,
48 })
49 .select()
50 .single()
51
52 if (error) {
53 return NextResponse.json({ error: error.message }, { status: 500 })
54 }
55
56 return NextResponse.json({ data }, { status: 201 })
57}

Customization ideas

Add recurring events

Implement iCalendar RRULE support to create weekly, monthly, or custom recurring events that automatically generate instances on the calendar.

Add Google Calendar sync

Use the Google Calendar API to import and export events, keeping your app and Google Calendar in sync for users who use both.

Add email reminders

Use Vercel Cron jobs to check for upcoming events and send email reminders via Resend to RSVPed attendees 24 hours before the event.

Add event check-in with QR codes

Generate QR codes for each RSVP that organizers can scan at the door using the browser camera API to mark attendance.

Common pitfalls

Pitfall: Fetching all events instead of filtering by the visible date range

How to avoid: Pass the viewport's start and end dates as query parameters, and use Supabase gte/lte filters on start_time. The composite index makes these range queries fast.

Pitfall: Storing dates without timezone information

How to avoid: Use timestamptz (timestamp with timezone) in Supabase, store in UTC, and convert to local time on the client using Intl.DateTimeFormat or date-fns-tz.

Pitfall: Not handling the RSVP unique constraint on concurrent clicks

How to avoid: Use Supabase upsert with onConflict: 'event_id,user_id' to handle duplicate RSVP attempts gracefully, updating the status instead of throwing an error.

Best practices

  • Query events by the visible viewport date range using gte/lte filters on start_time with a composite index for fast lookups.
  • Store all times in UTC with timestamptz columns and convert to local time on the client using Intl.DateTimeFormat.
  • Use optimistic UI for RSVP actions — update the button state immediately and revert on error for a responsive feel.
  • Use Design Mode (Option+D) to fine-tune calendar grid colors, event card appearance, and responsive breakpoints for free.
  • Use NEXT_PUBLIC_SUPABASE_ANON_KEY for client-side RSVP operations and SUPABASE_SERVICE_ROLE_KEY in Server Actions only.
  • Add a unique constraint on (event_id, user_id) in the rsvps table to prevent duplicate RSVPs from concurrent requests.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an event calendar with Next.js and Supabase. I need to efficiently query events for the visible calendar viewport (e.g., only events in March 2026) using date range filters. Show me the Supabase query with gte/lte on start_time, the composite index to create, and how to pass the viewport dates from a client component to a Server Component via searchParams.

Build Prompt

Build the RSVP system for an event calendar. Create a 'use client' RSVPButtons component that shows three buttons: Going (primary), Maybe (outline), Declined (ghost). Highlight the current user's RSVP status. On click, call a Server Action that upserts into the rsvps table with onConflict for the unique constraint. Implement optimistic UI with useOptimistic to update the button state before the server responds.

Frequently asked questions

How do I handle timezone differences for events?

Store all event times in UTC using the timestamptz column type in Supabase. On the client, convert to the user's local timezone using Intl.DateTimeFormat or the date-fns-tz library. This ensures events display correctly regardless of the user's location.

Can I add recurring weekly or monthly events?

Yes. Add a recurrence_rule text column to the events table that stores iCalendar RRULE strings. Use a library like rrule.js on the client to generate recurring event instances from the rule when rendering the calendar.

How do I deploy the calendar app?

Click Share in V0's top-right corner, then Publish to Production. Your calendar is live on a Vercel URL in 30-60 seconds. For a custom domain, configure it in the Vercel Dashboard.

What V0 plan do I need?

The Premium plan ($20/month) is recommended since the interactive calendar grid requires multiple component iterations. Free tier users can build the basic layout but may need manual adjustments.

Can I limit how many people can RSVP to an event?

Yes. The max_attendees column enforces capacity limits. Before inserting an RSVP, check the current count of 'going' RSVPs against max_attendees. Show remaining spots with a Progress bar on the event page.

Can RapidDev help build a custom event calendar?

Yes. RapidDev has built 600+ apps including event management platforms with recurring events, Google Calendar sync, and ticket sales. Book a free consultation to scope your project.

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.