React Native Expo Event Management

1

Part 1: Introduction & Architecture

2

Part 2: Installation on Mac, Windows, Ubuntu

3

Part 3: Project Setup

4

Part 4: Admin Authentication

5

Part 5: Event Management

6

Part 6: QR Scanner for Attendance

7

Part 7: Polish & Deploy

This article is available in Indonesian

šŸ‡®šŸ‡© Baca dalam Bahasa Indonesia

January 21, 2026

šŸ‡¬šŸ‡§ English

React Native Expo Event Management Part 5: Event Management

Build event listing with pull-to-refresh, EventCard component, event detail screen with tabs, and participant list with attendance status.

6 min read


šŸ“– Overview

In this part, we'll build:

  • Event list with pull-to-refresh
  • Reusable EventCard component
  • Event detail screen with info/participants tabs
  • Participant list with attendance status

šŸ“¦ Step 1: Create EventCard Component

File: components/EventCard.tsx

import React from 'react';
import { View, Text, Image, TouchableOpacity } from 'react-native';
import { useRouter } from 'expo-router';
import { Calendar, MapPin, Users } from 'lucide-react-native';

interface EventCardProps {
  id: string;
  title: string;
  description: string;
  startDate: string;
  location: string;
  imageUrl?: string;
  status: string;
  registrationCount?: number;
}

export default function EventCard({
  id,
  title,
  description,
  startDate,
  location,
  imageUrl,
  status,
  registrationCount = 0,
}: EventCardProps) {
  const router = useRouter();

  const formatDate = (date: string) => {
    return new Date(date).toLocaleDateString('en-US', {
      weekday: 'short',
      day: 'numeric',
      month: 'short',
      year: 'numeric',
    });
  };

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'UPCOMING': return 'bg-blue-500';
      case 'ONGOING': return 'bg-green-500';
      case 'FINISHED': return 'bg-gray-500';
      default: return 'bg-gray-500';
    }
  };

  return (
    <TouchableOpacity
      className="bg-gray-800 rounded-2xl overflow-hidden mb-4"
      onPress={() => router.push(`/event/${id}`)}
      activeOpacity={0.8}
    >
      {/* Image */}
      {imageUrl ? (
        <Image
          source={{ uri: imageUrl }}
          className="w-full h-40"
          resizeMode="cover"
        />
      ) : (
        <View className="w-full h-40 bg-gray-700 items-center justify-center">
          <Calendar size={48} color="#6b7280" />
        </View>
      )}

      {/* Content */}
      <View className="p-4">
        {/* Status Badge */}
        <View className="flex-row items-center mb-2">
          <View className={`px-3 py-1 rounded-full ${getStatusColor(status)}`}>
            <Text className="text-white text-xs font-medium">
              {status}
            </Text>
          </View>
        </View>

        {/* Title */}
        <Text className="text-white text-lg font-bold mb-2" numberOfLines={2}>
          {title}
        </Text>

        {/* Description */}
        <Text className="text-gray-400 text-sm mb-3" numberOfLines={2}>
          {description}
        </Text>

        {/* Meta Info */}
        <View className="space-y-2">
          <View className="flex-row items-center">
            <Calendar size={14} color="#9ca3af" />
            <Text className="text-gray-400 text-sm ml-2">
              {formatDate(startDate)}
            </Text>
          </View>

          <View className="flex-row items-center">
            <MapPin size={14} color="#9ca3af" />
            <Text className="text-gray-400 text-sm ml-2" numberOfLines={1}>
              {location}
            </Text>
          </View>

          <View className="flex-row items-center">
            <Users size={14} color="#9ca3af" />
            <Text className="text-gray-400 text-sm ml-2">
              {registrationCount} participants
            </Text>
          </View>
        </View>
      </View>
    </TouchableOpacity>
  );
}

šŸ“± Step 2: Create Home Screen (Event List)

File: app/(tabs)/index.tsx

import React, { useEffect, useState, useCallback } from 'react';
import {
  View,
  Text,
  FlatList,
  RefreshControl,
  ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import EventCard from '@/components/EventCard';
import { getAdminEvents, Event } from '@/services/events';

export default function HomeScreen() {
  const [events, setEvents] = useState<Event[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchEvents = async () => {
    try {
      setError(null);
      const data = await getAdminEvents();
      setEvents(data);
    } catch (err) {
      console.error('Error fetching events:', err);
      setError('Failed to load events');
    } finally {
      setIsLoading(false);
      setRefreshing(false);
    }
  };

  useEffect(() => {
    fetchEvents();
  }, []);

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    fetchEvents();
  }, []);

  if (isLoading) {
    return (
      <View className="flex-1 bg-gray-900 items-center justify-center">
        <ActivityIndicator size="large" color="#f0880a" />
        <Text className="text-gray-400 mt-4">Loading events...</Text>
      </View>
    );
  }

  return (
    <SafeAreaView className="flex-1 bg-gray-900">
      {/* Header */}
      <View className="px-4 py-4">
        <Text className="text-white text-2xl font-bold">Events</Text>
        <Text className="text-gray-400 mt-1">
          Manage your community events
        </Text>
      </View>

      {/* Event List */}
      <FlatList
        data={events}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <EventCard
            id={item.id}
            title={item.title}
            description={item.description}
            startDate={item.startDate}
            location={item.location}
            imageUrl={item.imageUrl}
            status={item.status}
            registrationCount={item._count?.registrations}
          />
        )}
        contentContainerStyle={{ padding: 16 }}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            tintColor="#f0880a"
          />
        }
        ListEmptyComponent={
          <View className="items-center justify-center py-20">
            <Text className="text-gray-400 text-lg">No events found</Text>
          </View>
        }
      />
    </SafeAreaView>
  );
}

šŸ“„ Step 3: Create Event Detail Screen

File: app/event/[id].tsx

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  ScrollView,
  Image,
  TouchableOpacity,
  ActivityIndicator,
  FlatList,
} from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { Calendar, MapPin, Users, Clock, CheckCircle, XCircle } from 'lucide-react-native';
import { getEventDetail } from '@/services/events';

interface Participant {
  id: string;
  name: string;
  email: string;
  hasAttended: boolean;
  attendedAt?: string;
}

export default function EventDetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const [event, setEvent] = useState<any>(null);
  const [activeTab, setActiveTab] = useState<'info' | 'participants'>('info');
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (id) {
      fetchEventDetail();
    }
  }, [id]);

  const fetchEventDetail = async () => {
    try {
      const data = await getEventDetail(id!);
      setEvent(data);
    } catch (error) {
      console.error('Error fetching event:', error);
    } finally {
      setIsLoading(false);
    }
  };

  if (isLoading) {
    return (
      <View className="flex-1 bg-gray-900 items-center justify-center">
        <ActivityIndicator size="large" color="#f0880a" />
      </View>
    );
  }

  if (!event) {
    return (
      <View className="flex-1 bg-gray-900 items-center justify-center">
        <Text className="text-white">Event not found</Text>
      </View>
    );
  }

  const formatDateTime = (date: string) => {
    return new Date(date).toLocaleString('en-US', {
      weekday: 'long',
      day: 'numeric',
      month: 'long',
      year: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  };

  return (
    <View className="flex-1 bg-gray-900">
      {/* Tab Navigation */}
      <View className="flex-row border-b border-gray-700">
        <TouchableOpacity
          className={`flex-1 py-4 ${activeTab === 'info' ? 'border-b-2 border-orange-500' : ''}`}
          onPress={() => setActiveTab('info')}
        >
          <Text className={`text-center font-medium ${activeTab === 'info' ? 'text-orange-500' : 'text-gray-400'}`}>
            Information
          </Text>
        </TouchableOpacity>
        <TouchableOpacity
          className={`flex-1 py-4 ${activeTab === 'participants' ? 'border-b-2 border-orange-500' : ''}`}
          onPress={() => setActiveTab('participants')}
        >
          <Text className={`text-center font-medium ${activeTab === 'participants' ? 'text-orange-500' : 'text-gray-400'}`}>
            Participants ({event.registrations?.length || 0})
          </Text>
        </TouchableOpacity>
      </View>

      {activeTab === 'info' ? (
        <ScrollView className="flex-1 p-4">
          {/* Image */}
          {event.imageUrl && (
            <Image
              source={{ uri: event.imageUrl }}
              className="w-full h-48 rounded-xl mb-4"
              resizeMode="cover"
            />
          )}

          {/* Title */}
          <Text className="text-white text-2xl font-bold mb-4">
            {event.title}
          </Text>

          {/* Meta Info */}
          <View className="bg-gray-800 rounded-xl p-4 mb-4 space-y-3">
            <View className="flex-row items-center">
              <Calendar size={20} color="#f0880a" />
              <Text className="text-white ml-3">{formatDateTime(event.startDate)}</Text>
            </View>
            <View className="flex-row items-center">
              <MapPin size={20} color="#f0880a" />
              <Text className="text-white ml-3">{event.location}</Text>
            </View>
            <View className="flex-row items-center">
              <Users size={20} color="#f0880a" />
              <Text className="text-white ml-3">
                {event._count?.registrations || 0} registered, {event._count?.attendances || 0} attended
              </Text>
            </View>
          </View>

          {/* Description */}
          <Text className="text-gray-300 leading-6">{event.description}</Text>
        </ScrollView>
      ) : (
        <FlatList
          data={event.registrations || []}
          keyExtractor={(item) => item.id}
          renderItem={({ item }) => (
            <View className="flex-row items-center p-4 border-b border-gray-800">
              <View className="flex-1">
                <Text className="text-white font-medium">{item.user?.name || 'Unknown'}</Text>
                <Text className="text-gray-400 text-sm">{item.user?.email}</Text>
              </View>
              {item.attendance ? (
                <View className="flex-row items-center">
                  <CheckCircle size={20} color="#22c55e" />
                  <Text className="text-green-500 ml-2 text-sm">Attended</Text>
                </View>
              ) : (
                <View className="flex-row items-center">
                  <XCircle size={20} color="#6b7280" />
                  <Text className="text-gray-500 ml-2 text-sm">Not yet</Text>
                </View>
              )}
            </View>
          )}
          ListEmptyComponent={
            <View className="items-center py-10">
              <Text className="text-gray-400">No participants yet</Text>
            </View>
          }
        />
      )}
    </View>
  );
}

šŸ“Š Step 4: Create LoadingSpinner Component

File: components/LoadingSpinner.tsx

import React from 'react';
import { View, ActivityIndicator, Text } from 'react-native';

interface LoadingSpinnerProps {
  message?: string;
}

export default function LoadingSpinner({ message = 'Loading...' }: LoadingSpinnerProps) {
  return (
    <View className="flex-1 bg-gray-900 items-center justify-center">
      <ActivityIndicator size="large" color="#f0880a" />
      <Text className="text-gray-400 mt-4">{message}</Text>
    </View>
  );
}

āœ… Checklist

  • EventCard component created
  • Home screen with event list
  • Pull-to-refresh functionality
  • Event detail screen with tabs
  • Participant list with attendance status
  • Loading and empty states

šŸš€ Next Steps

In Part 6, we'll implement the QR scanner for attendance check-in with camera permissions and animated feedback.


← Part 4: Authentication | Next: QR Scanner →


This series is created to support the Casual Meetup #15 at LampungDev.

Continue Reading

Previous article

← Previous Article

React Native Expo Event Management Part 4: Autentikasi Admin

Next Article →

React Native Expo Event Management Part 6: QR Scanner Kehadiran

Next article