Event Services Components
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