389 lines
13 KiB
TypeScript
389 lines
13 KiB
TypeScript
import React, { useMemo, useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
Image,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import { ChevronRight } from "lucide-react-native";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Input } from "~/components/ui/input";
|
|
import { Icons } from "~/assets/icons";
|
|
import TopBar from "~/components/ui/topBar";
|
|
import BottomSheet from "~/components/ui/bottomSheet";
|
|
import { router } from "expo-router";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useEvents } from "~/lib/hooks/useEvents";
|
|
import Skeleton from "~/components/ui/skeleton";
|
|
|
|
export default function EventsScreen() {
|
|
const { t } = useTranslation();
|
|
const {
|
|
data: events,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
} = useEvents({
|
|
status: "ACTIVE",
|
|
limit: 50,
|
|
});
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const [filterVisible, setFilterVisible] = useState(false);
|
|
const [filterName, setFilterName] = useState("");
|
|
const [filterLocation, setFilterLocation] = useState("");
|
|
const [dateFilter, setDateFilter] = useState<"all" | "today" | "this_week">(
|
|
"all"
|
|
);
|
|
|
|
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
|
|
const normalizedFilterName = filterName.trim().toLowerCase();
|
|
const normalizedFilterLocation = filterLocation.trim().toLowerCase();
|
|
|
|
const filteredEvents = useMemo(() => {
|
|
if (!events) return [];
|
|
|
|
return events.filter((event) => {
|
|
const name = (event as any).name ?? "";
|
|
const venue = (event as any).venue ?? "";
|
|
const haystack = `${name} ${venue}`.toLowerCase();
|
|
|
|
if (normalizedQuery && !haystack.includes(normalizedQuery)) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
normalizedFilterName &&
|
|
!name.toLowerCase().includes(normalizedFilterName)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
normalizedFilterLocation &&
|
|
!venue.toLowerCase().includes(normalizedFilterLocation)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (dateFilter !== "all" && (event as any).startDate) {
|
|
const start = new Date((event as any).startDate);
|
|
const now = new Date();
|
|
|
|
const startDay = new Date(
|
|
start.getFullYear(),
|
|
start.getMonth(),
|
|
start.getDate()
|
|
);
|
|
const today = new Date(
|
|
now.getFullYear(),
|
|
now.getMonth(),
|
|
now.getDate()
|
|
);
|
|
|
|
if (dateFilter === "today") {
|
|
if (startDay.getTime() !== today.getTime()) return false;
|
|
} else if (dateFilter === "this_week") {
|
|
const endOfWeek = new Date(today);
|
|
endOfWeek.setDate(today.getDate() + 7);
|
|
if (startDay < today || startDay > endOfWeek) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [
|
|
events,
|
|
normalizedQuery,
|
|
normalizedFilterName,
|
|
normalizedFilterLocation,
|
|
dateFilter,
|
|
]);
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<View className="flex-1 bg-white">
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 32 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<TopBar />
|
|
|
|
<View className="px-5 pt-6">
|
|
<Text className="text-lg font-dmsans-medium text-[#0F7B4A] mb-1">
|
|
{t("events.title")}
|
|
</Text>
|
|
<Text className="text-base font-dmsans text-gray-500 mb-6">
|
|
{t("events.subtitle")}
|
|
</Text>
|
|
|
|
<View className="mb-4">
|
|
<Input
|
|
placeholderText={t("events.searchPlaceholder")}
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
placeholderColor="#9CA3AF"
|
|
textClassName="text-[#111827] text-sm"
|
|
value={searchQuery}
|
|
onChangeText={setSearchQuery}
|
|
/>
|
|
</View>
|
|
|
|
<View className="flex flex-col gap-4 mb-10">
|
|
<Button
|
|
className="h-11 rounded-[4px] bg-[#0F7B4A]"
|
|
onPress={() => setFilterVisible(true)}
|
|
>
|
|
<View className="flex-row items-center justify-center space-x-2">
|
|
<Image
|
|
source={Icons.filterBar}
|
|
style={{ width: 18, height: 18 }}
|
|
resizeMode="contain"
|
|
/>
|
|
<Text className="text-white ml-2 font-dmsans-medium text-sm">
|
|
{t("events.filterButton")}
|
|
</Text>
|
|
</View>
|
|
</Button>
|
|
|
|
<Button
|
|
className="h-11 rounded-[4px] bg-[#FFB668]"
|
|
onPress={() => router.push(ROUTES.MY_TICKETS)}
|
|
>
|
|
<View className="flex-row items-center justify-center space-x-2">
|
|
<Image
|
|
source={Icons.ticketIcon}
|
|
style={{ width: 18, height: 18 }}
|
|
resizeMode="contain"
|
|
/>
|
|
<Text className="text-white ml-2 font-dmsans-medium text-sm">
|
|
{t("events.myTicketsButton")}
|
|
</Text>
|
|
</View>
|
|
</Button>
|
|
</View>
|
|
|
|
<Text className="text-lg font-dmsans-medium text-[#0F7B4A] mb-4">
|
|
{t("events.featuredTitle")}
|
|
</Text>
|
|
|
|
{loading && (
|
|
<View className="space-y-4 mb-6">
|
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
<View
|
|
key={index}
|
|
className="bg-[#E9F9F0] rounded-[4px] p-6 mb-2"
|
|
>
|
|
<View className="w-full mb-4">
|
|
<Skeleton width="100%" height={176} radius={4} />
|
|
</View>
|
|
<View className="space-y-2">
|
|
<Skeleton width="60%" height={14} radius={4} />
|
|
<Skeleton width="40%" height={12} radius={4} />
|
|
</View>
|
|
<View className="mt-4 flex-row items-center justify-between">
|
|
<Skeleton width="55%" height={10} radius={4} />
|
|
<Skeleton width={26} height={26} radius={13} />
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{!loading && error && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<Text className="text-red-500 font-dmsans text-sm mb-2">
|
|
Failed to load events
|
|
</Text>
|
|
<Button
|
|
className="h-9 px-4 bg-primary rounded-full"
|
|
onPress={() => refetch()}
|
|
>
|
|
<Text className="text-white font-dmsans text-xs">Retry</Text>
|
|
</Button>
|
|
</View>
|
|
)}
|
|
|
|
{!loading && !error && events && events.length === 0 && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<Text className="text-gray-500 font-dmsans text-sm">
|
|
No events found.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading &&
|
|
!error &&
|
|
events &&
|
|
events.length > 0 &&
|
|
filteredEvents.length === 0 && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<Text className="text-gray-500 font-dmsans text-sm">
|
|
No events match your search.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading &&
|
|
!error &&
|
|
filteredEvents &&
|
|
filteredEvents.length > 0 && (
|
|
<View className="space-y-4 mb-6">
|
|
{filteredEvents.map((event) => {
|
|
const heroImage =
|
|
event.images && event.images.length > 0
|
|
? event.images[0]
|
|
: "https://images.pexels.com/photos/1190297/pexels-photo-1190297.jpeg?auto=compress&cs=tinysrgb&w=800";
|
|
const startDate = new Date(event.startDate);
|
|
const formattedDate = startDate.toLocaleDateString();
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={event.id}
|
|
activeOpacity={0.9}
|
|
onPress={() =>
|
|
router.push({
|
|
pathname: ROUTES.EVENT_DETAIL,
|
|
params: { id: event.id },
|
|
})
|
|
}
|
|
>
|
|
<View className="bg-[#E9F9F0] rounded-[4px] p-6 mb-2">
|
|
<View className="w-full h-44 rounded-[4px] overflow-hidden mb-4 bg-gray-300">
|
|
<Image
|
|
source={{ uri: heroImage }}
|
|
style={{ width: "100%", height: "100%" }}
|
|
resizeMode="cover"
|
|
/>
|
|
</View>
|
|
|
|
<Text className="text-sm font-dmsans-bold text-[#FFB668] mb-1">
|
|
{event.name}
|
|
</Text>
|
|
<Text className="text-sm font-dmsans text-[#105D38] mb-4">
|
|
{event.venue}
|
|
</Text>
|
|
|
|
<View className="flex-row items-center justify-between">
|
|
<Text className="text-xs font-dmsans text-[#111827]">
|
|
<Text className="font-dmsans-bold">
|
|
{t("events.ticketCountPrefix")}
|
|
</Text>
|
|
{event.organizer?.name || ""} -
|
|
<Text className="italic"> {formattedDate}</Text>
|
|
</Text>
|
|
<ChevronRight size={26} color="#FFB668" />
|
|
</View>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
<BottomSheet
|
|
visible={filterVisible}
|
|
onClose={() => setFilterVisible(false)}
|
|
maxHeightRatio={0.7}
|
|
>
|
|
<Text className="text-lg font-dmsans-bold text-black mb-1">
|
|
Filter events
|
|
</Text>
|
|
<Text className="text-xs font-dmsans text-gray-500 mb-4">
|
|
Filter by date, name and location
|
|
</Text>
|
|
|
|
<Text className="text-base font-dmsans-medium text-black mb-2">
|
|
Date
|
|
</Text>
|
|
<View className="flex-row mb-4">
|
|
{[
|
|
{ key: "all", label: "All dates" },
|
|
{ key: "today", label: "Today" },
|
|
{ key: "this_week", label: "This week" },
|
|
].map((option) => (
|
|
<TouchableOpacity
|
|
key={option.key}
|
|
onPress={() =>
|
|
setDateFilter(option.key as "all" | "today" | "this_week")
|
|
}
|
|
className={`px-3 py-1 rounded-full mr-2 border ${
|
|
dateFilter === option.key
|
|
? "bg-[#0F7B4A] border-[#0F7B4A]"
|
|
: "bg-white border-gray-300"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`text-xs font-dmsans ${
|
|
dateFilter === option.key ? "text-white" : "text-gray-700"
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
<Text className="text-base font-dmsans-medium text-black mb-2">
|
|
Event name
|
|
</Text>
|
|
<View className="mb-3">
|
|
<Input
|
|
value={filterName}
|
|
onChangeText={setFilterName}
|
|
placeholderText="Search by event name"
|
|
placeholderColor="#9CA3AF"
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
textClassName="text-[#111827] text-sm"
|
|
/>
|
|
</View>
|
|
|
|
<Text className="text-base font-dmsans-medium text-black mb-2 mt-2">
|
|
Location
|
|
</Text>
|
|
<View className="mb-4">
|
|
<Input
|
|
value={filterLocation}
|
|
onChangeText={setFilterLocation}
|
|
placeholderText="Search by location"
|
|
placeholderColor="#9CA3AF"
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
textClassName="text-[#111827] text-sm"
|
|
/>
|
|
</View>
|
|
|
|
<View className="flex-row justify-between mt-4">
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setFilterName("");
|
|
setFilterLocation("");
|
|
setDateFilter("all");
|
|
}}
|
|
>
|
|
<Text className="text-sm font-dmsans text-primary">Clear</Text>
|
|
</TouchableOpacity>
|
|
<Button
|
|
className="h-9 px-4 rounded-full bg-[#0F7B4A]"
|
|
onPress={() => setFilterVisible(false)}
|
|
>
|
|
<Text className="text-xs font-dmsans text-white">
|
|
Apply filters
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
</BottomSheet>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|