324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import React, { useMemo, useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
Image,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
} from "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 { ChevronRight } from "lucide-react-native";
|
|
import { router } from "expo-router";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import { useTranslation } from "react-i18next";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import { useTickets } from "~/lib/hooks/useTickets";
|
|
import BottomSheet from "~/components/ui/bottomSheet";
|
|
|
|
export default function MyTicketsScreen() {
|
|
const { t } = useTranslation();
|
|
const {
|
|
data: tickets,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
} = useTickets({ status: "ACTIVE", limit: 50, immediate: true });
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [filterVisible, setFilterVisible] = useState(false);
|
|
const [dateFilter, setDateFilter] = useState<"all" | "today" | "this_week">(
|
|
"all"
|
|
);
|
|
|
|
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
|
|
const filteredTickets = useMemo(() => {
|
|
if (!tickets) return [];
|
|
|
|
return tickets.filter((ticket) => {
|
|
const anyTicket = ticket as any;
|
|
const eventName = (anyTicket.event?.name ?? "").toLowerCase();
|
|
|
|
if (normalizedQuery && !eventName.includes(normalizedQuery)) {
|
|
return false;
|
|
}
|
|
|
|
if (dateFilter !== "all") {
|
|
const rawDate = anyTicket.event?.startDate || anyTicket.createdAt;
|
|
if (!rawDate) {
|
|
return true;
|
|
}
|
|
|
|
const date = new Date(rawDate);
|
|
const now = new Date();
|
|
|
|
const ticketDay = new Date(
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate()
|
|
);
|
|
const today = new Date(
|
|
now.getFullYear(),
|
|
now.getMonth(),
|
|
now.getDate()
|
|
);
|
|
|
|
if (dateFilter === "today") {
|
|
if (ticketDay.getTime() !== today.getTime()) return false;
|
|
} else if (dateFilter === "this_week") {
|
|
const endOfWeek = new Date(today);
|
|
endOfWeek.setDate(today.getDate() + 7);
|
|
if (ticketDay < today || ticketDay > endOfWeek) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [tickets, normalizedQuery, dateFilter]);
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<View className="flex-1 bg-white">
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 32 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<View className="">
|
|
<BackButton />
|
|
</View>
|
|
|
|
<View className="px-5">
|
|
<Text className="text-lg font-dmsans-medium text-[#0F7B4A] mb-1">
|
|
{t("mytickets.title")}
|
|
</Text>
|
|
<Text className="text-base font-dmsans text-gray-500 mb-6">
|
|
{t("mytickets.subtitle")}
|
|
</Text>
|
|
|
|
<View className="mb-4">
|
|
<Input
|
|
placeholderText={t("mytickets.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="mb-8">
|
|
<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("mytickets.filterButton")}
|
|
</Text>
|
|
</View>
|
|
</Button>
|
|
<Button
|
|
className="bg-secondary mt-4 rounded-md border border-dashed border-secondary h-11"
|
|
onPress={() => router.back()}
|
|
>
|
|
<Text className="font-dmsans text-white">
|
|
{t("common.back")}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
|
|
<Text className="text-lg font-dmsans-medium text-[#0F7B4A] mb-4">
|
|
{t("mytickets.ticketsTitle")}
|
|
</Text>
|
|
|
|
<View className="flex flex-col gap-3">
|
|
{loading && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<ActivityIndicator size="small" color="#0F7B4A" />
|
|
<Text className="mt-2 text-gray-500 font-dmsans text-sm">
|
|
{t("mytickets.loading")}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading && error && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<Text className="text-red-500 font-dmsans text-sm mb-2">
|
|
{t("mytickets.error")}
|
|
</Text>
|
|
<Button
|
|
className="h-9 px-4 bg-primary rounded-full"
|
|
onPress={() => refetch()}
|
|
>
|
|
<Text className="text-white font-dmsans text-xs">
|
|
{t("common.retry")}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
)}
|
|
|
|
{!loading && !error && tickets && tickets.length === 0 && (
|
|
<View className="flex items-center justify-center py-12">
|
|
<Image
|
|
source={Icons.ticketHome}
|
|
style={{ width: 64, height: 64, marginBottom: 12 }}
|
|
resizeMode="contain"
|
|
/>
|
|
<Text className="text-base font-dmsans-medium text-[#0F7B4A] mb-1">
|
|
No tickets found
|
|
</Text>
|
|
<Text className="text-sm font-dmsans text-gray-500 text-center px-4">
|
|
You don't have any tickets yet.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading &&
|
|
!error &&
|
|
tickets &&
|
|
filteredTickets.length === 0 && (
|
|
<View className="flex items-center justify-center py-8">
|
|
<Text className="text-sm font-dmsans text-gray-500">
|
|
No tickets match your search.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!loading &&
|
|
!error &&
|
|
filteredTickets &&
|
|
filteredTickets.length > 0 && (
|
|
<View className="flex flex-col gap-3">
|
|
{filteredTickets.map((ticket) => {
|
|
const anyTicket = ticket as any;
|
|
const eventName = anyTicket.event?.name || "Ticket";
|
|
const ticketNo = anyTicket.ticketNo || anyTicket.id;
|
|
const qr = anyTicket.qr || ticketNo;
|
|
const qrImage = anyTicket.qrImage as string | undefined;
|
|
|
|
const rawDate =
|
|
anyTicket.event?.startDate || anyTicket.createdAt;
|
|
const formattedDate = rawDate
|
|
? new Date(rawDate).toLocaleDateString()
|
|
: "";
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={anyTicket.id}
|
|
activeOpacity={0.9}
|
|
onPress={() =>
|
|
router.push({
|
|
pathname: ROUTES.EVENT_QR,
|
|
params: {
|
|
code: qr,
|
|
packageName: eventName,
|
|
...(qrImage ? { qrImage } : {}),
|
|
},
|
|
})
|
|
}
|
|
className="flex-row items-center justify-between bg-[#F3FFF7] rounded-[6px] px-4 py-3"
|
|
>
|
|
<View className="flex-row items-center">
|
|
<View className="w-10 h-10 rounded-[4px] bg-[#FFEEDB] items-center justify-center mr-3">
|
|
<Image
|
|
source={Icons.ticketIcon}
|
|
style={{ width: 20, height: 20 }}
|
|
resizeMode="contain"
|
|
/>
|
|
</View>
|
|
<View>
|
|
<Text className="text-sm font-dmsans-medium text-[#FFB668]">
|
|
{eventName}
|
|
</Text>
|
|
<Text className="text-xs font-dmsans text-[#105D38] mt-1">
|
|
{formattedDate}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<ChevronRight size={20} color="#FFB668" />
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
<BottomSheet
|
|
visible={filterVisible}
|
|
onClose={() => setFilterVisible(false)}
|
|
maxHeightRatio={0.5}
|
|
>
|
|
<Text className="text-lg font-dmsans-bold text-black mb-1">
|
|
Filter tickets
|
|
</Text>
|
|
<Text className="text-xs font-dmsans text-gray-500 mb-4">
|
|
Filter by date of event or purchase
|
|
</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>
|
|
|
|
<View className="flex-row justify-between mt-4">
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setSearchQuery("");
|
|
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>
|
|
);
|
|
}
|