Amba-Agent-App/app/(root)/(screens)/eventdetail.tsx
2026-01-16 00:22:35 +03:00

501 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState } from "react";
import {
View,
Text,
Image,
ScrollView,
ActivityIndicator,
TouchableOpacity,
Dimensions,
} from "react-native";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import { Button } from "~/components/ui/button";
import { Icons } from "~/assets/icons";
import { Share } from "react-native";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import { router, useLocalSearchParams } from "expo-router";
import BackButton from "~/components/ui/backButton";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { EventService, type EventDto } from "~/lib/services/eventService";
import BottomSheet from "~/components/ui/bottomSheet";
import { ROUTES } from "~/lib/routes";
import { awardPoints } from "~/lib/services/pointsService";
export default function EventDetailScreen() {
const { t } = useTranslation();
const params = useLocalSearchParams<{ id?: string }>();
const { user } = useAuthWithProfile();
const [event, setEvent] = useState<EventDto | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [selectedTierId, setSelectedTierId] = useState<string | null>(null);
const [toastVisible, setToastVisible] = useState(false);
const [toastTitle, setToastTitle] = useState("");
const [toastDescription, setToastDescription] = useState<
string | undefined
>();
const [toastVariant, setToastVariant] = useState<
"success" | "error" | "warning" | "info"
>("info");
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const screenWidth = Dimensions.get("window").width;
const [buySheetVisible, setBuySheetVisible] = useState(false);
const [ticketCount, setTicketCount] = useState(1);
const showToast = (
title: string,
description?: string,
variant: "success" | "error" | "warning" | "info" = "info"
) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
setToastTitle(title);
setToastDescription(description);
setToastVariant(variant);
setToastVisible(true);
toastTimeoutRef.current = setTimeout(() => {
setToastVisible(false);
toastTimeoutRef.current = null;
}, 2500);
};
const handleShare = async () => {
try {
await Share.share({
message: t("eventdetail.shareMessage"),
});
try {
await awardPoints("share_event");
} catch (error) {
console.warn("[EventDetail] Failed to award share event points", error);
}
} catch (error) {
console.log("Error sharing event:", error);
showToast(
t("eventdetail.toastErrorTitle"),
t("eventdetail.toastShareError"),
"error"
);
}
};
useEffect(() => {
let cancelled = false;
const loadEvent = async () => {
if (!user || !params.id) return;
setLoading(true);
setError(null);
try {
const token = await user.getIdToken();
const eventData = await EventService.getEventById(
token,
String(params.id)
);
if (!cancelled) {
setEvent(eventData);
// Debug images and description to verify response
console.log("[EventDetail] event images", eventData?.images);
console.log(
"[EventDetail] event description",
eventData?.description
);
}
} catch (err) {
if (!cancelled) {
setError("Failed to load event details");
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
loadEvent();
return () => {
cancelled = true;
};
}, [user, params.id]);
const fallbackImage =
"https://images.pexels.com/photos/1190297/pexels-photo-1190297.jpeg?auto=compress&cs=tinysrgb&w=800";
const images =
event?.images && event.images.length > 0 ? event.images : [fallbackImage];
const startDate = event?.startDate ? new Date(event.startDate) : null;
const endDate = event?.endDate ? new Date(event.endDate) : null;
const formatEventDateRange = (start: Date, end: Date) => {
// Format like: Sat, Jan 10, 2026 at 6:00 PM 1:00 AM (EAT)
const datePart = start.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
});
const startTime = start.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
const endTime = end.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
return `${datePart} at ${startTime} ${endTime} (EAT)`;
};
const formattedDate =
startDate && endDate
? formatEventDateRange(startDate, endDate)
: t("eventdetail.dateTime");
const handleBuy = () => {
if (!event || !selectedTierId) {
showToast(
"Ticket selection required",
"Please select a ticket tier first",
"error"
);
return;
}
const tier = event.ticketTiers?.find((t: any) => t.id === selectedTierId);
if (!tier) {
showToast(
"Ticket selection required",
"Please select a ticket tier first",
"error"
);
return;
}
console.log("[EventDetail] Buy pressed", {
eventId: event.id,
selectedTierId,
});
setTicketCount(1);
setBuySheetVisible(true);
};
const handleConfirmBuy = () => {
if (!event || !selectedTierId) {
showToast(
"Ticket selection required",
"Please select a ticket tier first",
"error"
);
return;
}
const tier = event.ticketTiers?.find((t: any) => t.id === selectedTierId);
if (!tier) {
showToast(
"Ticket selection required",
"Please select a ticket tier first",
"error"
);
return;
}
const priceNumber = Number(tier.price || 0);
const amountInCents = Math.round(priceNumber * 100 * ticketCount);
setBuySheetVisible(false);
router.push({
pathname: ROUTES.CHECKOUT,
params: {
amount: String(amountInCents),
type: "event_ticket",
ticketTierName: tier.name,
ticketTierPrice: priceNumber.toFixed(2),
eventName: event.name,
eventId: event.id,
ticketTierId: tier.id,
ticketCount: String(ticketCount),
},
});
};
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 bg-white">
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingBottom: 32 }}
showsVerticalScrollIndicator={false}
>
<View className="">
<BackButton />
<Text className="mt-4 text-lg px-4 font-dmsans-bold text-[#0F7B4A]">
{event?.name ?? t("eventdetail.title")}
</Text>
</View>
<View className="px-4 pt-4">
{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">
Loading event details...
</Text>
</View>
)}
{!loading && error && (
<View className="flex items-center justify-center py-8">
<Text className="text-red-500 font-dmsans text-sm mb-2">
{error}
</Text>
</View>
)}
{!loading && !error && (
<>
<Text className="text-sm font-dmsans text-[#4B5563] mb-4 leading-5">
{event?.description ?? t("eventdetail.description")}
</Text>
<Text className="text-sm font-dmsans text-[#111827] mb-1">
{event?.venue ?? t("eventdetail.location")}
</Text>
<Text className="text-sm font-dmsans-bold text-[#0F7B4A] mb-4">
{formattedDate}
</Text>
<View className="w-full mb-4">
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToInterval={screenWidth}
decelerationRate="fast"
onMomentumScrollEnd={(e) => {
const { contentOffset, layoutMeasurement } =
e.nativeEvent;
const index = Math.round(
contentOffset.x / layoutMeasurement.width
);
setCurrentImageIndex(index);
}}
>
{images.map((img, index) => (
<View
key={index}
style={{ width: screenWidth, height: 192 }}
className="rounded-[4px] overflow-hidden bg-gray-300"
>
<Image
source={{ uri: img }}
style={{ width: "100%", height: "100%" }}
resizeMode="cover"
/>
</View>
))}
</ScrollView>
<View className="flex-row justify-center mt-2 space-x-1">
{images.map((_, index) => (
<View
key={index}
className={`w-2 h-2 mr-1 rounded-full ${
index === currentImageIndex
? "bg-[#0F7B4A]"
: "bg-gray-300"
}`}
/>
))}
</View>
</View>
<View className="flex-row items-center mb-6">
<View className="flex-row -space-x-2 mr-3">
{[0, 1, 2, 3, 4].map((i) => (
<View
key={i}
className="w-8 h-8 rounded-full overflow-hidden border border-white -ml-2 first:ml-0"
>
<Image
source={Icons.profileImage}
style={{ width: 28, height: 28 }}
resizeMode="cover"
/>
</View>
))}
</View>
<Text className="text-xs font-dmsans text-[#4B5563]">
{t("eventdetail.peopleComing")}
</Text>
</View>
{event?.ticketTiers && event.ticketTiers.length > 0 && (
<View className="flex flex-col gap-3 mb-4 pb-2">
{event.ticketTiers.map((tier: any) => {
const isSelected = tier.id === selectedTierId;
return (
<TouchableOpacity
key={tier.id}
activeOpacity={0.9}
onPress={() => setSelectedTierId(tier.id)}
>
<View
className={`flex-row items-center justify-between rounded-[8px] px-4 py-4 ${
isSelected ? "bg-[#E9F9F0]" : "bg-[#FFF4E5]"
}`}
>
<View className="flex-row items-center">
<View className="w-5 h-5 rounded-full border border-[#FFB668] items-center justify-center mr-3">
{isSelected && (
<View className="w-2.5 h-2.5 rounded-full bg-[#0F7B4A]" />
)}
</View>
<Text className="text-sm font-dmsans text-[#374151]">
{tier.name}
</Text>
</View>
<Text className="text-sm font-dmsans-medium text-[#0F7B4A]">
${tier.price}
</Text>
</View>
</TouchableOpacity>
);
})}
</View>
)}
</>
)}
</View>
</ScrollView>
</View>
<View className="px-4 pb-4 pt-2 border-t border-gray-100 bg-white">
<View className="flex flex-col gap-3">
<Button
className="h-11 rounded-full bg-[#0F7B4A]"
disabled={!selectedTierId}
onPress={handleBuy}
>
<Text className="text-white font-dmsans-medium text-sm">
{t("eventdetail.buyButton")}
</Text>
</Button>
<Button
className="h-11 rounded-full bg-[#FFB668]"
onPress={handleShare}
>
<Text className="text-white font-dmsans-medium text-sm">
{t("eventdetail.shareButton")}
</Text>
</Button>
</View>
</View>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
<BottomSheet
visible={buySheetVisible}
onClose={() => setBuySheetVisible(false)}
maxHeightRatio={0.5}
>
{event && selectedTierId && (
<View>
<Text className="text-lg font-dmsans-bold text-black mb-1">
Confirm tickets
</Text>
<Text className="text-xs font-dmsans text-gray-500 mb-4">
{event.name}
</Text>
{(() => {
const tier = event.ticketTiers?.find(
(t: any) => t.id === selectedTierId
);
if (!tier) return null;
const priceNumber = Number(tier.price || 0);
const total = priceNumber * ticketCount;
return (
<>
<View className="flex-row justify-between items-center mb-4">
<View>
<Text className="text-base font-dmsans-medium text-black">
{tier.name}
</Text>
<Text className="text-xs font-dmsans text-gray-500 mt-1">
${priceNumber.toFixed(2)} per ticket
</Text>
</View>
<View className="items-end">
<Text className="text-xs font-dmsans text-gray-500 mb-1">
Total
</Text>
<Text className="text-lg font-dmsans-bold text-[#0F7B4A]">
${total.toFixed(2)}
</Text>
</View>
</View>
<View className="flex-row items-center justify-between mb-6">
<Text className="text-sm font-dmsans text-black">
Ticket count
</Text>
<View className="flex-row items-center">
<TouchableOpacity
className="w-8 h-8 rounded-full border border-gray-300 items-center justify-center"
onPress={() =>
setTicketCount((c) => Math.max(1, c - 1))
}
>
<Text className="text-lg font-dmsans text-gray-700">
-
</Text>
</TouchableOpacity>
<Text className="mx-4 text-base font-dmsans text-black">
{ticketCount}
</Text>
<TouchableOpacity
className="w-8 h-8 rounded-full border border-gray-300 items-center justify-center"
onPress={() => setTicketCount((c) => c + 1)}
>
<Text className="text-lg font-dmsans text-gray-700">
+
</Text>
</TouchableOpacity>
</View>
</View>
<Button
className="h-11 rounded-full bg-[#0F7B4A]"
onPress={handleConfirmBuy}
>
<Text className="text-white font-dmsans-medium text-sm">
Confirm purchase
</Text>
</Button>
</>
);
})()}
</View>
)}
</BottomSheet>
</ScreenWrapper>
);
}