React Native Expo Event Management

1

Part 1: Pengenalan & Arsitektur

2

Part 2: Instalasi di Mac, Windows, Ubuntu

3

Part 3: Project Setup

4

Part 4: Autentikasi Admin

5

Part 5: Manajemen Event

6

Part 6: QR Scanner Kehadiran

7

Part 7: Polish & Deploy

Artikel ini tersedia dalam Bahasa Inggris

🇬🇧 Read in English

21 Januari 2026

🇮🇩 Bahasa Indonesia

React Native Expo Event Management Part 5: Manajemen Event

Menampilkan daftar event dengan pull-to-refresh, membuat komponen EventCard, halaman detail event, dan daftar peserta terdaftar.

7 min read


📖 Overview

Di bagian ini, kita akan membangun fitur manajemen event:

  • ✅ Event list dengan pull-to-refresh
  • ✅ Event card component yang reusable
  • ✅ Event detail screen dengan tab (Info & Peserta)
  • ✅ Participant list dengan status kehadiran

🃏 Step 1: Buat EventCard Component

Komponen reusable untuk menampilkan event di list.

File: components/EventCard.tsx

import React from 'react';
import { View, Text, TouchableOpacity, Image } from 'react-native';
import { Calendar, MapPin, Users, CheckCircle } from 'lucide-react-native';
import { router } from 'expo-router';
import { Event } from '@/services/events';

interface EventCardProps {
  event: Event;
}

export function EventCard({ event }: EventCardProps) {
  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('id-ID', {
      weekday: 'long',
      day: 'numeric',
      month: 'long',
      year: 'numeric',
    });
  };

  const handlePress = () => {
    router.push(`/event/${event.id}`);
  };

  return (
    <TouchableOpacity
      className="bg-gray-800 rounded-2xl overflow-hidden mb-4"
      onPress={handlePress}
      activeOpacity={0.8}
    >
      {/* Event Image */}
      {event.imageUrl ? (
        <Image
          source={{ uri: event.imageUrl }}
          className="w-full h-40"
          resizeMode="cover"
        />
      ) : (
        <View className="w-full h-40 bg-gray-700 items-center justify-center">
          <Text className="text-gray-500 text-lg">No Image</Text>
        </View>
      )}

      {/* Content */}
      <View className="p-4">
        {/* Title */}
        <Text className="text-white text-lg font-bold mb-2" numberOfLines={2}>
          {event.title}
        </Text>

        {/* Date */}
        <View className="flex-row items-center mb-2">
          <Calendar size={16} color="#f0880a" />
          <Text className="text-gray-400 ml-2 text-sm">
            {formatDate(event.date)}
          </Text>
        </View>

        {/* Location */}
        <View className="flex-row items-center mb-3">
          <MapPin size={16} color="#f0880a" />
          <Text className="text-gray-400 ml-2 text-sm" numberOfLines={1}>
            {event.location}
          </Text>
        </View>

        {/* Stats */}
        <View className="flex-row justify-between pt-3 border-t border-gray-700">
          {/* Registrations */}
          <View className="flex-row items-center">
            <Users size={16} color="#9BA1A6" />
            <Text className="text-gray-400 ml-2 text-sm">
              {event.registrationCount} terdaftar
            </Text>
          </View>

          {/* Attendance */}
          <View className="flex-row items-center">
            <CheckCircle size={16} color="#22c55e" />
            <Text className="text-green-500 ml-2 text-sm">
              {event.attendanceCount} hadir
            </Text>
          </View>
        </View>
      </View>
    </TouchableOpacity>
  );
}

📋 Step 2: Buat 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 { getAdminEvents, Event } from '@/services/events';
import { EventCard } from '@/components/EventCard';
import { useAuthStore } from '@/store/auth';

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

  const fetchEvents = async (showRefreshIndicator = false) => {
    if (showRefreshIndicator) {
      setIsRefreshing(true);
    } else {
      setIsLoading(true);
    }
    setError(null);

    try {
      const response = await getAdminEvents();
      if (response.success) {
        setEvents(response.data.events);
      } else {
        setError('Gagal mengambil data events');
      }
    } catch (err: any) {
      console.error('Error fetching events:', err);
      setError(err.message || 'Terjadi kesalahan');
    } finally {
      setIsLoading(false);
      setIsRefreshing(false);
    }
  };

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

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

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

  // Error state
  if (error) {
    return (
      <View className="flex-1 items-center justify-center bg-secondary px-4">
        <Text className="text-red-500 text-center mb-4">{error}</Text>
        <Text
          className="text-primary underline"
          onPress={() => fetchEvents()}
        >
          Coba lagi
        </Text>
      </View>
    );
  }

  return (
    <View className="flex-1 bg-secondary">
      {/* Header */}
      <View className="px-4 pt-4 pb-2">
        <Text className="text-white text-2xl font-bold">
          Halo, {user?.name?.split(' ')[0]}! 👋
        </Text>
        <Text className="text-gray-400 mt-1">
          {events.length} event tersedia
        </Text>
      </View>

      {/* Event List */}
      <FlatList
        data={events}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => <EventCard event={item} />}
        contentContainerStyle={{ padding: 16 }}
        refreshControl={
          <RefreshControl
            refreshing={isRefreshing}
            onRefresh={onRefresh}
            tintColor="#f0880a"
            colors={['#f0880a']}
          />
        }
        ListEmptyComponent={
          <View className="items-center py-20">
            <Text className="text-gray-500 text-lg">Belum ada event</Text>
          </View>
        }
      />
    </View>
  );
}

📄 Step 3: Buat Event Detail Screen

File: app/event/[id].tsx

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

interface Participant {
  id: string;
  name: string;
  email: string;
  checkedInAt: string | null;
}

interface EventDetail {
  id: string;
  title: string;
  description: string;
  date: string;
  time: string;
  location: string;
  imageUrl?: string;
  registrationCount: number;
  attendanceCount: number;
  participants: Participant[];
}

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

  useEffect(() => {
    const fetchDetail = async () => {
      try {
        const response = await getEventDetail(id);
        if (response.success) {
          setEvent(response.data.event);
        }
      } catch (error) {
        console.error('Error fetching event detail:', error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchDetail();
  }, [id]);

  const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString('id-ID', {
      weekday: 'long',
      day: 'numeric',
      month: 'long',
      year: 'numeric',
    });
  };

  const formatTime = (dateString: string) => {
    return new Date(dateString).toLocaleTimeString('id-ID', {
      hour: '2-digit',
      minute: '2-digit',
    });
  };

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

  if (!event) {
    return (
      <View className="flex-1 items-center justify-center bg-secondary">
        <Text className="text-gray-400">Event tidak ditemukan</Text>
      </View>
    );
  }

  return (
    <>
      <Stack.Screen
        options={{
          title: event.title,
          headerStyle: { backgroundColor: '#1a1a1a' },
          headerTintColor: '#fff',
        }}
      />

      <View className="flex-1 bg-secondary">
        {/* Image */}
        {event.imageUrl && (
          <Image
            source={{ uri: event.imageUrl }}
            className="w-full h-48"
            resizeMode="cover"
          />
        )}

        {/* Tabs */}
        <View className="flex-row border-b border-gray-700">
          <TouchableOpacity
            className={`flex-1 py-4 ${
              activeTab === 'info' ? 'border-b-2 border-primary' : ''
            }`}
            onPress={() => setActiveTab('info')}
          >
            <Text
              className={`text-center font-semibold ${
                activeTab === 'info' ? 'text-primary' : 'text-gray-400'
              }`}
            >
              Info Event
            </Text>
          </TouchableOpacity>
          <TouchableOpacity
            className={`flex-1 py-4 ${
              activeTab === 'participants' ? 'border-b-2 border-primary' : ''
            }`}
            onPress={() => setActiveTab('participants')}
          >
            <Text
              className={`text-center font-semibold ${
                activeTab === 'participants' ? 'text-primary' : 'text-gray-400'
              }`}
            >
              Peserta ({event.participants?.length || 0})
            </Text>
          </TouchableOpacity>
        </View>

        {activeTab === 'info' ? (
          <ScrollView className="flex-1 px-4 py-4">
            {/* Title */}
            <Text className="text-white text-2xl font-bold mb-4">
              {event.title}
            </Text>

            {/* Stats */}
            <View className="flex-row mb-6">
              <View className="flex-row items-center mr-6">
                <Users size={18} color="#9BA1A6" />
                <Text className="text-gray-400 ml-2">
                  {event.registrationCount} terdaftar
                </Text>
              </View>
              <View className="flex-row items-center">
                <CheckCircle size={18} color="#22c55e" />
                <Text className="text-green-500 ml-2">
                  {event.attendanceCount} hadir
                </Text>
              </View>
            </View>

            {/* Date & Time */}
            <View className="bg-gray-800 rounded-xl p-4 mb-4">
              <View className="flex-row items-center mb-3">
                <Calendar size={20} color="#f0880a" />
                <Text className="text-white ml-3">
                  {formatDate(event.date)}
                </Text>
              </View>
              <View className="flex-row items-center mb-3">
                <Clock size={20} color="#f0880a" />
                <Text className="text-white ml-3">{event.time} WIB</Text>
              </View>
              <View className="flex-row items-center">
                <MapPin size={20} color="#f0880a" />
                <Text className="text-white ml-3">{event.location}</Text>
              </View>
            </View>

            {/* Description */}
            <Text className="text-gray-300 leading-6">
              {event.description}
            </Text>
          </ScrollView>
        ) : (
          <ParticipantList participants={event.participants || []} />
        )}
      </View>
    </>
  );
}

// Participant List Component
function ParticipantList({ participants }: { participants: Participant[] }) {
  return (
    <ScrollView className="flex-1 px-4 py-4">
      {participants.length === 0 ? (
        <View className="items-center py-20">
          <Text className="text-gray-500">Belum ada peserta terdaftar</Text>
        </View>
      ) : (
        participants.map((participant) => (
          <View
            key={participant.id}
            className="flex-row items-center bg-gray-800 rounded-xl p-4 mb-3"
          >
            {/* Avatar */}
            <View className="w-12 h-12 rounded-full bg-gray-700 items-center justify-center">
              <Text className="text-white text-lg font-bold">
                {participant.name.charAt(0).toUpperCase()}
              </Text>
            </View>

            {/* Info */}
            <View className="flex-1 ml-3">
              <Text className="text-white font-semibold">
                {participant.name}
              </Text>
              <Text className="text-gray-400 text-sm" numberOfLines={1}>
                {participant.email}
              </Text>
            </View>

            {/* Status */}
            {participant.checkedInAt ? (
              <View className="flex-row items-center">
                <CheckCircle size={20} color="#22c55e" />
                <Text className="text-green-500 text-xs ml-1">
                  {new Date(participant.checkedInAt).toLocaleTimeString('id-ID', {
                    hour: '2-digit',
                    minute: '2-digit',
                  })}
                </Text>
              </View>
            ) : (
              <View className="bg-gray-700 px-3 py-1 rounded-full">
                <Text className="text-gray-400 text-xs">Belum hadir</Text>
              </View>
            )}
          </View>
        ))
      )}
    </ScrollView>
  );
}

🔄 Step 4: Update Event Service untuk Detail

Tambahkan response type untuk detail event.

Update: services/events.ts

// Tambahkan di akhir file

export interface Participant {
  id: string;
  name: string;
  email: string;
  checkedInAt: string | null;
}

export interface EventDetailResponse {
  success: boolean;
  data: {
    event: {
      id: string;
      title: string;
      description: string;
      date: string;
      time: string;
      location: string;
      imageUrl?: string;
      registrationCount: number;
      attendanceCount: number;
      participants: Participant[];
    };
  };
}

export const getEventDetail = async (id: string): Promise<EventDetailResponse> => {
  const response = await api.get(`/admin/events/${id}`);
  return response.data;
};

🧩 Step 5: Buat Loading Spinner Component

File: components/LoadingSpinner.tsx

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

interface LoadingSpinnerProps {
  message?: string;
}

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

✅ Checklist

  • EventCard component dibuat
  • Home screen dengan event list dibuat
  • Event detail screen dengan tabs dibuat
  • Participant list dengan status dibuat
  • Pull-to-refresh berfungsi

🚀 Selanjutnya

Di Part 6, kita akan membuat fitur QR Scanner untuk mencatat kehadiran peserta dengan feedback animasi dan error handling.


← Part 4: Autentikasi | Part 6: QR Scanner →


Series ini dibuat untuk mendukung materi Casual Meetup #15 di LampungDev.

Lanjut Membaca

Previous article thumbnail

← Artikel Sebelumnya

React Native Expo Event Management Part 4: Autentikasi Admin

Artikel Selanjutnya →

React Native Expo Event Management Part 6: QR Scanner Kehadiran

Next article thumbnail