š 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.
