This guide explains how to use the Event Services React components in your application.
Component Overview
Section titled “Component Overview”The Event Services feature provides three main React components:
- EventServicesManager - Main container with view state management (list/create/edit)
- EventServiceList - Displays list of services with conditional edit links
- EventServiceForm - React Hook Form + Zod validation for creating/editing services
Installation
Section titled “Installation”These components are already available in the project. Import them using path aliases:
import { EventServicesManager } from '#components/react/EventServicesManager'import { EventServiceList } from '#components/react/EventServiceList'import { EventServiceForm } from '#components/react/EventServiceForm'Component APIs
Section titled “Component APIs”EventServicesManager
Section titled “EventServicesManager”The main orchestration component that handles view modes and state management.
export type EventServicesManagerProps = { eventId: string // UUID of the event initialServices: EventService[] // Initial services data from server csrfToken: string // CSRF token for mutation operations canEdit: boolean // User permission flag}Features
Section titled “Features”- View mode management (list/create/edit)
- Automatic service list refresh after mutations
- Loading and error state handling
- Permission-based UI rendering
- Success/cancel callback handlers
Example Usage
Section titled “Example Usage”---import { EventServicesManager } from '#components/react/EventServicesManager'import { generateCsrfToken } from '#utils/csrf'import { getDatabase } from '#libs/database'import { canManageEventServices } from '#utils/event-service-authorization'
const { id } = Astro.paramsconst db = getDatabase()
// Fetch event and servicesconst event = await db.getEventById(id)const services = await db.getEventServices(id)
// Check permissionsconst canEdit = canManageEventServices({ userRole: Astro.locals.userRole, userDbId: Astro.locals.userDbId, event})
// Generate CSRF tokenconst csrfToken = await generateCsrfToken(Astro)---
<EventServicesManager eventId={event.id} initialServices={services} csrfToken={csrfToken} canEdit={canEdit} client:load/>import { EventServicesManager } from '#components/react/EventServicesManager'import type { EventService } from '#libs/database-types'
export function EventDashboard({ eventId, userCanEdit }: Props) { const [services, setServices] = useState<EventService[]>([]) const [csrfToken, setCsrfToken] = useState('')
useEffect(() => { // Fetch services and CSRF token fetchServicesAndToken() }, [eventId])
return ( <EventServicesManager eventId={eventId} initialServices={services} csrfToken={csrfToken} canEdit={userCanEdit} /> )}State Management
Section titled “State Management”The component internally manages three view modes:
list- Default view showing all servicescreate- Form for adding new serviceedit- Form for editing existing service
State Flow:
┌─────────────┐│ List │ ←─────────────────┐│ (default) │ │└─────────────┘ │ │ │ ├─► Add Service ──→ Create Form ──┘ │ │ └─► Edit Service ──→ Edit Form ───┘EventServiceList
Section titled “EventServiceList”Displays services with optional edit functionality.
export type EventServiceListProps = { services: EventService[] // Array of services to display eventId: string // Event ID for edit links canEdit: boolean // Show/hide edit links}Features
Section titled “Features”- Empty state handling
- Status badge rendering (active/inactive)
- Conditional edit links based on permissions
- Accessible list markup
Example Usage
Section titled “Example Usage”import { EventServiceList } from '#components/react/EventServiceList'import type { EventService } from '#libs/database-types'
const services: EventService[] = [ { id: 'service-1', event_id: 'event-123', name: 'Haircuts', description: 'Professional haircuts', capacity: 20, status: 'active', display_order: 0, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', },]
export function ServicesDisplay() { return ( <EventServiceList services={services} eventId="event-123" canEdit={true} /> )}Empty State
Section titled “Empty State”When no services exist, the component displays:
"No services available for this event."EventServiceForm
Section titled “EventServiceForm”React Hook Form component with Zod validation for creating and editing services.
export type EventServiceFormProps = { eventId: string // Required: Event UUID csrfToken: string // Required: CSRF token initialData?: EventServiceFormData // Optional: For edit mode serviceId?: string // Optional: Service UUID (edit mode) onSuccess?: () => void // Optional: Success callback onCancel?: () => void // Optional: Cancel callback}Features
Section titled “Features”- React Hook Form integration
- Zod schema validation (
eventServiceSchema) - @fpkit/acss components (Field, Input, Textarea, Button, Alert)
- Automatic create/edit mode detection
- Duplicate service error handling (409 Conflict)
- Loading state management
- Accessibility attributes (aria-invalid, role=“alert”)
Example Usage
Section titled “Example Usage”import { EventServiceForm } from '#components/react/EventServiceForm'
export function CreateServicePage({ eventId, csrfToken }: Props) { const handleSuccess = () => { // Redirect to services list window.location.href = `/dashboard/events/${eventId}/services` }
return ( <div> <h1>Add New Service</h1> <EventServiceForm eventId={eventId} csrfToken={csrfToken} onSuccess={handleSuccess} /> </div> )}import { EventServiceForm } from '#components/react/EventServiceForm'import type { EventService } from '#libs/database-types'
export function EditServicePage({ eventId, service, csrfToken}: Props) { const handleSuccess = () => { // Refresh service data fetchServices() }
const handleCancel = () => { // Return to list view setViewMode('list') }
return ( <div> <h1>Edit Service</h1> <EventServiceForm eventId={eventId} csrfToken={csrfToken} serviceId={service.id} initialData={service} onSuccess={handleSuccess} onCancel={handleCancel} /> </div> )}Form Fields
Section titled “Form Fields”| Field | Type | Required | Validation |
|---|---|---|---|
| name | text | ✅ | 1-100 chars, trimmed |
| description | textarea | ❌ | Max 500 chars, trimmed |
| capacity | number | ❌ | 1-10,000, positive integer |
| status | select | ✅ | ‘active’ or ‘inactive’ |
| display_order | number | ✅ | Non-negative integer, default 0 |
Validation Schema
Section titled “Validation Schema”import { z } from 'zod'
export const eventServiceSchema = z.object({ name: z.string().min(1).max(100).trim(), description: z.string().max(500).trim().optional().or(z.literal('')), capacity: z.number().int().positive().max(10000).nullable().optional(), status: z.enum(['active', 'inactive']).default('active'), display_order: z.number().int().nonnegative().default(0),})
export type EventServiceFormData = z.infer<typeof eventServiceSchema>Error Handling
Section titled “Error Handling”The form handles errors at multiple levels:
Validation Errors (Client-side):
{errors.name && ( <p role="alert">{errors.name.message}</p>)}API Errors:
- 409 Conflict - Duplicate service name
- 401 Unauthorized - Not authenticated
- 403 Forbidden - Insufficient permissions
- 500 Server Error - Unexpected error
Example Error Display:
{error && <Alert severity="error">{error}</Alert>}Integration Patterns
Section titled “Integration Patterns”Pattern 1: Server-Side Rendering (Astro)
Section titled “Pattern 1: Server-Side Rendering (Astro)”Recommended for dashboard pages with authentication.
---import { EventServicesManager } from '#components/react/EventServicesManager'import { getDatabase } from '#libs/database'import { generateCsrfToken } from '#utils/csrf'import { canManageEventServices } from '#utils/event-service-authorization'
const { id } = Astro.params
// 1. Check authenticationif (!Astro.locals.userId) { return Astro.redirect('/login')}
// 2. Fetch dataconst db = getDatabase()const event = await db.getEventById(id)
if (!event) { return new Response('Event not found', { status: 404 })}
const services = await db.getEventServices(id)
// 3. Check permissionsconst canEdit = canManageEventServices({ userRole: Astro.locals.userRole, userDbId: Astro.locals.userDbId, event,})
// 4. Generate CSRF tokenconst csrfToken = await generateCsrfToken(Astro)---
<Layout title={`Manage Services - ${event.title}`}> <EventServicesManager eventId={event.id} initialServices={services} csrfToken={csrfToken} canEdit={canEdit} client:load /></Layout>Pattern 2: Client-Side Fetching
Section titled “Pattern 2: Client-Side Fetching”For dynamic React applications.
import { useEffect, useState } from 'react'import { EventServicesManager } from '#components/react/EventServicesManager'import type { EventService } from '#libs/database-types'
export function EventServicesPage({ eventId }: Props) { const [services, setServices] = useState<EventService[]>([]) const [csrfToken, setCsrfToken] = useState('') const [canEdit, setCanEdit] = useState(false) const [loading, setLoading] = useState(true)
useEffect(() => { async function loadData() { try { // Fetch services const servicesRes = await fetch(`/api/event-services?event_id=${eventId}`) const servicesData = await servicesRes.json() setServices(servicesData.services || [])
// Fetch CSRF token const tokenRes = await fetch('/api/csrf-token') const tokenData = await tokenRes.json() setCsrfToken(tokenData.token)
// Check permissions const permRes = await fetch(`/api/events/${eventId}/permissions`) const permData = await permRes.json() setCanEdit(permData.canManageServices) } catch (error) { console.error('Failed to load services:', error) } finally { setLoading(false) } }
loadData() }, [eventId])
if (loading) return <div>Loading...</div>
return ( <EventServicesManager eventId={eventId} initialServices={services} csrfToken={csrfToken} canEdit={canEdit} /> )}Pattern 3: Standalone Form Usage
Section titled “Pattern 3: Standalone Form Usage”For custom workflows or modals.
import { EventServiceForm } from '#components/react/EventServiceForm'import { Modal } from '#components/react/Modal'
export function ServiceModal({ isOpen, onClose, eventId, csrfToken}: Props) { const handleSuccess = () => { // Close modal and refresh data onClose() refreshServices() }
return ( <Modal isOpen={isOpen} onClose={onClose}> <h2>Add Service</h2> <EventServiceForm eventId={eventId} csrfToken={csrfToken} onSuccess={handleSuccess} onCancel={onClose} /> </Modal> )}Authorization
Section titled “Authorization”Permission Checking
Section titled “Permission Checking”Use the canManageEventServices utility for authorization:
import { canManageEventServices } from '#utils/event-service-authorization'import type { Event } from '#libs/database-types'
const canEdit = canManageEventServices({ userRole: 'team_admin', // From Astro.locals.userRole userDbId: 'user-uuid', // From Astro.locals.userDbId event: eventObject, // Event from database})
// Returns: booleanRequirements:
- User must have
team_admin,admin, orsuper_adminrole - User must own the event OR be
super_admin
Example:
// Alice (team_admin) owns the eventcanManageEventServices({ userRole: 'team_admin', userDbId: 'alice-id', event: { created_by: 'alice-id', ... }}) // ✅ true
// Bob (team_admin) does NOT own the eventcanManageEventServices({ userRole: 'team_admin', userDbId: 'bob-id', event: { created_by: 'alice-id', ... }}) // ❌ false
// Carol (super_admin) can manage any eventcanManageEventServices({ userRole: 'super_admin', userDbId: 'carol-id', event: { created_by: 'alice-id', ... }}) // ✅ trueAPI Integration
Section titled “API Integration”The components interact with these API endpoints:
GET /api/event-services
Section titled “GET /api/event-services”Fetches services for an event.
Query Parameters:
event_id(required) - Event UUID
Response:
{ "services": [ { "id": "service-uuid", "event_id": "event-uuid", "name": "Haircuts", "description": "Professional haircuts", "capacity": 20, "status": "active", "display_order": 0, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } ]}POST /api/event-services/create
Section titled “POST /api/event-services/create”Creates a new service.
Request:
{ "event_id": "event-uuid", "name": "Haircuts", "description": "Professional haircuts", "capacity": 20, "status": "active", "display_order": 0, "csrf_token": "token"}Response (201):
{ "success": true, "serviceId": "new-service-uuid"}PUT /api/event-services/[id]
Section titled “PUT /api/event-services/[id]”Updates an existing service.
Request: Partial service data + CSRF token
Response (200):
{ "success": true}DELETE /api/event-services/[id]
Section titled “DELETE /api/event-services/[id]”Deletes a service.
Request:
{ "csrf_token": "token"}Response (200):
{ "success": true}TypeScript Types
Section titled “TypeScript Types”EventService
Section titled “EventService”export type EventService = { id: string event_id: string name: string description: string | null capacity: number | null status: 'active' | 'inactive' display_order: number created_at: string updated_at: string}EventServiceFormData
Section titled “EventServiceFormData”export type EventServiceFormData = { name: string description?: string | '' capacity?: number | null status: 'active' | 'inactive' display_order: number}Accessibility
Section titled “Accessibility”All components follow WCAG 2.1 Level AA standards:
EventServiceForm
Section titled “EventServiceForm”- ✅ Form labels with
labelForattribute - ✅ Required fields marked with
requiredattribute - ✅ Error messages with
role="alert" - ✅ Invalid fields marked with
aria-invalid - ✅ Semantic HTML5 form elements
- ✅ Keyboard navigation support
EventServiceList
Section titled “EventServiceList”- ✅ Semantic
<ul>and<li>markup - ✅ Accessible links with context
- ✅ Status badges with clear visual indicators
EventServicesManager
Section titled “EventServicesManager”- ✅ Loading states announced
- ✅ Error alerts with
role="alert" - ✅ Disabled states during loading
Storybook Documentation
Section titled “Storybook Documentation”Interactive component documentation available in Storybook:
npm run storybookVisit: http://localhost:6006
Stories:
Components/EventServicesManager- All manager storiesComponents/EventServiceForm- Form variationsComponents/EventServiceList- List states
Story Examples:
- Default view with services
- Read-only mode (canEdit=false)
- Empty list state
- Single service
- Many services
- Mixed statuses (active/inactive)
Testing
Section titled “Testing”Unit Testing
Section titled “Unit Testing”import { render, screen } from '@testing-library/react'import { EventServiceList } from '#components/react/EventServiceList'
describe('EventServiceList', () => { it('displays empty state when no services', () => { render( <EventServiceList services={[]} eventId="event-123" canEdit={false} /> )
expect(screen.getByText('No services available for this event.')).toBeInTheDocument() })
it('displays services with edit links when canEdit=true', () => { const services = [ { id: 'service-1', event_id: 'event-123', name: 'Haircuts', // ... other fields } ]
render( <EventServiceList services={services} eventId="event-123" canEdit={true} /> )
expect(screen.getByText('Haircuts')).toBeInTheDocument() expect(screen.getByText('Edit')).toBeInTheDocument() })})Integration Testing
Section titled “Integration Testing”Test components with mocked API responses:
import { render, screen, waitFor } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { EventServiceForm } from '#components/react/EventServiceForm'
describe('EventServiceForm', () => { it('submits form with valid data', async () => { const onSuccess = vi.fn()
render( <EventServiceForm eventId="event-123" csrfToken="token" onSuccess={onSuccess} /> )
await userEvent.type(screen.getByLabelText(/service name/i), 'Haircuts') await userEvent.click(screen.getByRole('button', { name: /create service/i }))
await waitFor(() => { expect(onSuccess).toHaveBeenCalled() }) })})Troubleshooting
Section titled “Troubleshooting”Component Not Rendering
Section titled “Component Not Rendering”Problem: Component appears blank or doesn’t load.
Solutions:
- Check Astro client directive:
client:load - Verify all required props are passed
- Check browser console for errors
- Ensure @fpkit/acss is installed
Form Submission Fails
Section titled “Form Submission Fails”Problem: Form shows “Failed to save service” error.
Solutions:
- Verify CSRF token is valid (not expired)
- Check user has required permissions
- Ensure event_id exists in database
- Review API endpoint logs for errors
Services Not Refreshing
Section titled “Services Not Refreshing”Problem: List doesn’t update after create/edit.
Solutions:
- Ensure
onSuccesscallback is implemented - Check
fetchServices()is called after mutation - Verify API endpoint returns updated data
- Clear browser cache if stale data persists
Best Practices
Section titled “Best Practices”1. Always Check Permissions
Section titled “1. Always Check Permissions”// ✅ CORRECTconst canEdit = canManageEventServices({ userRole, userDbId, event })
<EventServicesManager canEdit={canEdit} ... />
// ❌ WRONG - Never hardcode permissions<EventServicesManager canEdit={true} ... />2. Generate CSRF Tokens Server-Side
Section titled “2. Generate CSRF Tokens Server-Side”---// ✅ CORRECT - Generate on serverconst csrfToken = await generateCsrfToken(Astro)---
// ❌ WRONG - Never generate client-side3. Handle Loading States
Section titled “3. Handle Loading States”// ✅ CORRECTif (loading) return <Spinner />
return <EventServicesManager ... />
// ❌ WRONG - No loading indicator4. Implement Success Callbacks
Section titled “4. Implement Success Callbacks”// ✅ CORRECTconst handleSuccess = async () => { await fetchServices() // Refresh data setViewMode('list') // Update UI}
// ❌ WRONG - No data refreshconst handleSuccess = () => { setViewMode('list') // UI updates but data is stale}5. Use TypeScript Types
Section titled “5. Use TypeScript Types”// ✅ CORRECTimport type { EventService } from '#libs/database-types'
const [services, setServices] = useState<EventService[]>([])
// ❌ WRONG - No type safetyconst [services, setServices] = useState([])JSON Services: EventFormRHF
Section titled “JSON Services: EventFormRHF”For the JSON services approach, use the EventFormRHF component with built-in service checkboxes from the predefined catalog.
Component API
Section titled “Component API”import type { Event } from '#libs/database-types'
export type EventFormRHFProps = { csrfToken: string event?: Event apiEndpoint: string}Usage Example
Section titled “Usage Example”---import { EventFormRHF } from '#components/react/EventFormRHF'import { generateCsrfToken } from '#utils/csrf'
const csrfToken = await generateCsrfToken(Astro)---
<EventFormRHF csrfToken={csrfToken} apiEndpoint="/api/events/create" client:load/>Service Checkboxes
Section titled “Service Checkboxes”The component automatically renders checkboxes for all active services from services.config.ts:
// Rendered automatically:// ☐ Haircuts - Professional haircutting services// ☐ Showers - Shower facilities and maintenance// ☐ Clothing - Professional clothing and wardrobe services// ☐ Videography - Professional video recording and editingServices are validated against the predefined catalog using Zod schema:
// Schema validation (automatic)services: z.array(z.string()).refine( ids => validateServiceIds(ids).length === 0, { message: 'One or more service IDs are invalid or inactive' })Displaying Services
Section titled “Displaying Services”Use EventServiceList to display services for an event:
import { EventServiceList } from '#components/react/EventServiceList'import type { Event } from '#libs/database-types'
type Props = { event: Event}
export const EventDetails: React.FC<Props> = ({ event }) => { return ( <div> <h2>{event.name}</h2>
{/* Display services (works with both JSON and M2M) */} <h3>Services Offered</h3> <EventServiceList event={event} /> </div> )}The EventServiceList component automatically detects JSON vs M2M services and renders accordingly:
- JSON services: Resolves service IDs from
services.config.ts - M2M services: Fetches assignments from database
Comparison: JSON vs M2M Components
Section titled “Comparison: JSON vs M2M Components”| Feature | JSON (EventFormRHF) | M2M (EventServicesManager) |
|---|---|---|
| Setup | Zero - uses config file | Requires service DB records |
| Rendering | Automatic checkboxes | Fetch and display services |
| Validation | Zod schema + config | Zod schema + DB lookup |
| Flexibility | Fixed catalog | Dynamic catalog |
| Performance | Excellent | Good |
When to Use Each
Section titled “When to Use Each”Use EventFormRHF (JSON):
- Services are predefined and rarely change
- Simple event categorization
- Minimal setup required
- Maximum performance
Use EventServicesManager (M2M):
- Services are created/managed by admins
- Need rich service metadata (capacity, pricing)
- Dynamic service catalog
- Service analytics required
Learn more: Event Services JSON Guide
Related Documentation
Section titled “Related Documentation”- Managing Event Services (User Guide) - End-user documentation
- Event Services Technical Docs - Database schema and APIs
- Events System Guide - Creating and managing events
- Database API Reference - Complete database API
- CLAUDE-PATTERNS.md - Component development patterns
Need Help?
- Check Storybook for interactive examples:
npm run storybook - Review existing implementations in
src/pages/dashboard/events/ - Consult CLAUDE-PATTERNS.md for coding standards