Skip to main content

PublicEventPage Component

PublicEventPage Component

Component: src/features/events/components/PublicEventPage.tsx Route: /events/:orgIdPrefix/:orgSlug/:eventSlug Access: Public (no authentication required)

---

Overview

The PublicEventPage is the public-facing event registration page. It displays event details, ticket options, and a registration form that integrates with Stripe Checkout for paid events.

Recent Updates (January 2026)

Sales Tax Support (Jan 22, 2026)

  • Stripe Tax Integration: When collect_sales_tax is enabled on the event, the checkout flow passes this to the Edge Function
  • Automatic Tax Calculation: Stripe Tax calculates applicable sales tax based on attendee location
  • Tax Code: Event tickets use Stripe Tax code txcd_90010001 (Admission to events)

Features

  • Hero section - Event image or gradient background
  • Event details - Date, time, location/virtual link
  • Ticket selection - Multiple ticket types with quantity controls
  • Promo codes - Apply discounts
  • Registration form - Name, email, phone, custom fields
  • Order summary - Real-time total calculation
  • Stripe integration - Secure payment processing
  • Sold out/waitlist states - Graceful handling of capacity

UI Layout

┌─────────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────────┐ │
│ │                                                     │ │
│ │              [Event Hero Image]                     │ │
│ │                                                     │ │
│ │              Event Name                             │ │
│ │              Organization Name                      │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  📅 December 25, 2025 at 6:00 PM                       │
│  📍 Venue Name, City, State                            │
│                                                         │
│  Event description text goes here...                    │
│                                                         │
├─────────────────────────────────────────────────────────┤
│  Select Tickets                                         │
│  ┌─────────────────────────────────────────────────────┐│
│  │ General Admission          $50    [-] 2 [+]        ││
│  │ VIP Package               $100    [-] 0 [+]        ││
│  └─────────────────────────────────────────────────────┘│
│                                                         │
│  Promo Code: [________] [Apply]                        │
│                                                         │
├─────────────────────────────────────────────────────────┤
│  Your Information                                       │
│  ┌─────────────────────────────────────────────────────┐│
│  │ First Name: [________]  Last Name: [________]      ││
│  │ Email: [________________]                          ││
│  │ Phone: [________________]                          ││
│  │                                                     ││
│  │ [Custom Fields...]                                 ││
│  └─────────────────────────────────────────────────────┘│
│                                                         │
├─────────────────────────────────────────────────────────┤
│  Order Summary                                          │
│  2x General Admission                          $100.00  │
│  Promo: SAVE20                                 -$20.00  │
│  ─────────────────────────────────────────────────────  │
│  Total                                          $80.00  │
│                                                         │
│  [Complete Registration]                                │
└─────────────────────────────────────────────────────────┘

Props

interface PublicEventPageProps {
  config: {
    id: string;
    slug: string;
    organizationId: string;
    organizationSlug: string;
    organizationName: string;
    name: string;
    description?: string;
    event_type: string;
    start_date: string;
    end_date?: string;
    timezone: string;
    location_name?: string;
    location_address?: string;
    is_virtual: boolean;
    virtual_url?: string;
    capacity?: number;
    total_registrations: number;
    waitlist_enabled: boolean;
    image_url?: string;
    status: string;
    ticket_types: TicketType[];
    custom_fields: CustomField[];
    has_checklist: boolean;
    // Tax compliance (added Jan 2026)
    collect_sales_tax?: boolean;
  };
}

State Management

const [ticketSelections, setTicketSelections] = useState<Record<string, number>>({});
const [promoCode, setPromoCode] = useState('');
const [appliedPromo, setAppliedPromo] = useState<PromoCode | null>(null);
const [registrantInfo, setRegistrantInfo] = useState({
  firstName: '',
  lastName: '',
  email: '',
  phone: ''
});
const [customFieldValues, setCustomFieldValues] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);

Checkout Flow

const handleSubmit = async () => {
  setIsSubmitting(true);
  
  try {
    const response = await fetch(`${SUPABASE_URL}/functions/v1/create-event-checkout`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        eventId: config.id,
        eventSlug: config.slug,
        organizationId: config.organizationId,
        tickets: Object.entries(ticketSelections).map(([id, qty]) => ({
          ticketTypeId: id,
          quantity: qty
        })),
        registrantEmail: registrantInfo.email,
        registrantFirstName: registrantInfo.firstName,
        registrantLastName: registrantInfo.lastName,
        registrantPhone: registrantInfo.phone,
        customFields: Object.entries(customFieldValues).map(([id, value]) => ({
          fieldId: id,
          value
        })),
        promoCode: appliedPromo?.code,
        successUrl: `${window.location.origin}/events/${orgIdPrefix}/${orgSlug}/${eventSlug}/success`,
        cancelUrl: window.location.href,
        // Tax compliance
        collectSalesTax: config.collect_sales_tax || false
      })
    });
    
    const data = await response.json();
    
    if (data.free) {
      // Free registration - redirect to success
      window.location.href = data.url;
    } else {
      // Paid registration - redirect to Stripe
      window.location.href = data.url;
    }
  } catch (error) {
    toast.error('Registration failed. Please try again.');
  } finally {
    setIsSubmitting(false);
  }
};

Promo Code Validation

const handleApplyPromo = async () => {
  const response = await fetch(`${SUPABASE_URL}/functions/v1/validate-promo`, {
    method: 'POST',
    body: JSON.stringify({
      eventId: config.id,
      code: promoCode
    })
  });
  
  const data = await response.json();
  
  if (data.valid) {
    setAppliedPromo(data.promo);
    toast.success(`Promo code applied: ${data.discount}`);
  } else {
    toast.error(data.error || 'Invalid promo code');
  }
};

Capacity States

| State | Display |
|-------|---------|
| Available | Normal registration form |
| Sold Out (no waitlist) | "Sold Out" message, no form |
| Sold Out (waitlist enabled) | "Join Waitlist" button |
| Waitlist Full | "Event Full" message |

Related Components

  • PublicEventLoader - URL parsing and data fetching
  • EventRegistrationSuccess - Post-registration confirmation

Related Documentation

  • Main Events Doc: ../06-EVENTS-TICKETING.md
  • Registration Success: ./06-EVENT-REGISTRATION-SUCCESS.md

Synced from IFMmvp-Frontend documentation: pages/marketing/events/05-PUBLIC-EVENT-PAGE.md

Ready to Get Started?

See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.

Questions? Email us at steven@getalignmint.org

Ready to get started?Start Plus Trial