import React, { useCallback, useEffect, useRef, useState } from 'react'; import { View, Text, Image, TouchableOpacity, LayoutAnimation, Modal } from 'react-native'; import { FlashList } from '@shopify/flash-list'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { popupStyles, styles } from './styles'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, interpolate, Extrapolation } from 'react-native-reanimated'; import { FilterImage } from 'react-native-svg/filter-image'; import { NAVIGATION_PAGES } from 'src/types'; import { StoreType, storage } from 'src/storage'; import { Header, Input, PageWrapper } from 'src/components'; import { Colors } from 'src/theme'; import SearchIcon from 'assets/icons/search.svg'; import CalendarIcon from 'assets/icons/events/calendar-solid.svg'; import EarthIcon from 'assets/icons/travels-section/earth.svg'; import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg'; import CalendarPlusIcon from 'assets/icons/events/calendar-plus.svg'; import ShoppingCartIcon from 'assets/icons/events/shopping-cart.svg'; import StarIcon from 'assets/icons/events/star.svg'; import { PostGetEventsListReturn, SingleEvent, useGetCanAddEventQuery, useGetEventsListQuery } from '@api/events'; import moment from 'moment'; import { API_HOST } from 'src/constants'; import { renderSpotsText } from './utils'; import ChevronIcon from 'assets/icons/chevron-left.svg'; import Tooltip from 'react-native-walkthrough-tooltip'; import InfoIcon from 'assets/icons/info-solid.svg'; import { SafeAreaView } from 'react-native-safe-area-context'; import TabViewWrapper from 'src/components/TabViewWrapper'; function TabViewDelayed({ children, waitBeforeShow = 0 }: { children: React.ReactNode; waitBeforeShow?: number; }) { const [isShown, setIsShown] = useState(false); useEffect(() => { const timer = setTimeout(() => { setIsShown(true); }, waitBeforeShow); return () => clearTimeout(timer); }, [waitBeforeShow]); return isShown ? children : null; } const EventsScreen = () => { const token = (storage.get('token', StoreType.STRING) as string) ?? null; const { data, refetch } = useGetEventsListQuery(token, 0, true); const { data: pastData } = useGetEventsListQuery(token, 1, true); const { data: canAddEvent } = useGetCanAddEventQuery(token, true); const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(''); const [events, setEvents] = useState({ local_meetings: [], nm: [], shared_trips: [] } as never); const [pastEvents, setPastEvents] = useState({ local_meetings: [], nm: [], shared_tripsv: [] } as never); const [filteredEvents, setFilteredEvents] = useState({ local_meetings: [], nm: [], shared_trips: [] } as never); const [filteredPastEvents, setFilteredPastEvents] = useState({ local_meetings: [], nm: [], shared_trips: [] } as never); const [tooltipStates, setTooltipStates] = useState>({}); const date = new Date(); const [expandedStates, setExpandedStates] = useState>({ nm: false, shared_trips: false, local_meetings: false }); const scrollViewRefs = useRef | null>>({ nm: null, shared_trips: null, local_meetings: null }); const sectionRef = useRef(null); const [showPopup, setShowPopup] = useState(false); const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 }); const [toolTipVisible, setToolTipVisible] = useState(false); const buttonRef = useRef(null); const [index, setIndex] = useState(0); const [routes] = useState<{ key: 'nm' | 'shared_trips' | 'local_meetings'; title: string }[]>([ { key: 'nm', title: 'NomadMania Events' }, { key: 'shared_trips', title: 'Shared Trips' }, { key: 'local_meetings', title: 'Local Meetings' } ]); const SEARCH_CONTAINER_HEIGHT = 44; const searchContainerHeight = useSharedValue(SEARCH_CONTAINER_HEIGHT); const lastScrollY = useRef(0); const isSearchVisible = useRef(true); const hideSearchContainer = useCallback(() => { 'worklet'; if (isSearchVisible.current) { isSearchVisible.current = false; searchContainerHeight.value = withTiming(0, { duration: 150 }); } }, []); const showSearchContainer = useCallback(() => { 'worklet'; if (!isSearchVisible.current) { isSearchVisible.current = true; searchContainerHeight.value = withTiming(SEARCH_CONTAINER_HEIGHT, { duration: 150 }); } }, []); const handleScroll = useCallback( (event: any) => { const currentScrollY = event.nativeEvent.contentOffset.y; const diff = currentScrollY - lastScrollY.current; if (diff > 3 && currentScrollY > 20 && isSearchVisible.current) { runOnJS(hideSearchContainer)(); } else if (currentScrollY <= 5 && !isSearchVisible.current) { runOnJS(showSearchContainer)(); } lastScrollY.current = currentScrollY; }, [hideSearchContainer, showSearchContainer] ); const searchContainerAnimatedStyle = useAnimatedStyle(() => { return { height: searchContainerHeight.value, overflow: 'hidden', opacity: interpolate( searchContainerHeight.value, [0, SEARCH_CONTAINER_HEIGHT], [0, 1], Extrapolation.CLAMP ) }; }); const handleAddButtonPress = () => { if (buttonRef.current) { buttonRef.current.measure((x, y, width, height, pageX, pageY) => { setPopupPosition({ x: pageX - 120, y: pageY + height + 5 }); setShowPopup(true); }); } }; const handlePopupOption = (option: 'meeting' | 'trip') => { setShowPopup(false); if (option === 'meeting') { navigation.navigate(NAVIGATION_PAGES.CREATE_EVENT as never); } else if (option === 'trip') { navigation.navigate(NAVIGATION_PAGES.CREATE_SHARED_TRIP as never); } }; const toggleExpand = (tabKey: string) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut, () => { if (!expandedStates[tabKey] && sectionRef.current && scrollViewRefs.current[tabKey]) { scrollViewRefs.current[tabKey]?.scrollToEnd({ animated: true }); } }); setExpandedStates((prev) => ({ ...prev, [tabKey]: !prev[tabKey] })); }; useEffect(() => { if (data && data.nm) { setEvents(data); setFilteredEvents(data); } }, [data]); useEffect(() => { if (pastData && pastData.nm) { setPastEvents(pastData); setFilteredPastEvents(pastData); } }, [pastData]); useFocusEffect( useCallback(() => { refetch(); }, [navigation]) ); const handleSearch = (text: string) => { if (text) { const searchData = (index === 0 ? events.nm : index === 1 ? events.shared_trips : events.local_meetings ).filter((item: any) => { const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase(); const textData = text.toLowerCase(); return itemData.indexOf(textData) > -1; }) ?? []; setFilteredEvents( index === 0 ? { ...events, nm: searchData } : index === 1 ? { ...events, shared_trips: searchData } : { ...events, local_meetings: searchData } ); const searchPastData = (index === 0 ? pastEvents.nm : index === 1 ? pastEvents.shared_trips : pastEvents.local_meetings ).filter((item: any) => { const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase(); const textData = text.toLowerCase(); return itemData.indexOf(textData) > -1; }) ?? []; setFilteredPastEvents( index === 0 ? { ...events, nm: searchPastData } : index === 1 ? { ...events, shared_trips: searchPastData } : { ...events, local_meetings: searchPastData } ); setSearchQuery(text); } else { setFilteredEvents(events); setFilteredPastEvents(pastEvents); setSearchQuery(text); } }; const formatEventDate = (event: SingleEvent) => { if (event.date_from && event.date_to) { if (event.date_tentative) { const dateFrom = moment(event.date_from, 'YYYY-MM').format('MMM YYYY'); const dateTo = moment(event.date_to, 'YYYY-MM').format('MMM YYYY'); if (dateFrom === dateTo) { return dateFrom; } return `${dateFrom} - ${dateTo}`; } return `${moment(event.date_from, 'YYYY-MM-DD').format('DD MMM YYYY')} - ${moment(event.date_to, 'YYYY-MM-DD').format('DD MMM YYYY')}`; } else { if (event.date_tentative) { return `${moment(event.date, 'YYYY-MM').format('MMM YYYY')}`; } return moment(event.date, 'YYYY-MM-DD').format('DD MMMM YYYY'); } }; const renderEventCard = ({ item }: { item: SingleEvent }) => { let staticImgUrl = '/static/img/events/meeting.webp'; let badgeColor = Colors.DARK_BLUE; let badgeText = ''; if (item.full) { badgeColor = Colors.LIGHT_GRAY; badgeText = 'FULL'; } else if (item.closed) { badgeColor = Colors.LIGHT_GRAY; badgeText = 'CLOSED'; } else if (item.type === 2) { badgeColor = Colors.ORANGE; badgeText = 'TOUR'; staticImgUrl = '/static/img/events/trip.webp'; } else if (item.type === 3) { badgeColor = Colors.DARK_BLUE; badgeText = 'CONF'; staticImgUrl = '/static/img/events/conference.webp'; } const photo = item.photo ? API_HOST + '/webapi/events/get-square-photo/' + item.id : API_HOST + staticImgUrl; return ( navigation.navigate(...([NAVIGATION_PAGES.EVENT, { url: item.url }] as never)) } disabled={item.active === 0} > {item.active === 0 ? ( ) : ( )} {item.star === 1 && ( )} {item.joined && token ? ( Joined ) : null} {item.name} {item.type === 1 && item?.flag ? ( {item.country}} contentStyle={{ backgroundColor: Colors.FILL_LIGHT }} tooltipStyle={{ position: 'absolute', zIndex: 1000 }} arrowStyle={{ width: 16, height: 8 }} placement="top" onClose={() => setTooltipStates((prev) => ({ ...prev, [item.id]: false }))} backgroundColor="transparent" allowChildInteraction={false} > setTooltipStates((prev) => ({ ...prev, [item.id]: true }))} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > ) : ( )} {item.address1} {formatEventDate(item)} {item.registrations_info !== 1 && ( {renderSpotsText(item)} )} {item.type === 2 || item.type === 3 || item.full || item.closed ? ( {badgeText} ) : null} ); }; const renderScene = ({ route }: { route: { key: 'nm' | 'shared_trips' | 'local_meetings'; title: string }; }) => { const isCurrentTabExpanded = expandedStates[route.key]; return ( <> { scrollViewRefs.current[route.key] = ref; }} data={filteredEvents[route.key] || []} ListHeaderComponent={ route.key === 'shared_trips' ? ( setToolTipVisible(true)} style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 12 }} > Disclaimer: All trips listed here are shared by members of our travel community. NomadMania is not the organizer of these trips, and we do not verify their details, safety, or suitability. Participation is at your own discretion and risk. We do not take any responsibility for arrangements, agreements, or outcomes related to these community-shared trips. } contentStyle={{ backgroundColor: Colors.WHITE }} placement="bottom" onClose={() => setToolTipVisible(false)} backgroundColor="transparent" allowChildInteraction={false} > setToolTipVisible(true)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > Disclaimer [read more] ) : null } scrollEnabled={true} keyExtractor={(item) => `${route.key}-${item.id}`} renderItem={renderEventCard} estimatedItemSize={120} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={16} ListFooterComponent={ filteredPastEvents[route.key] && filteredPastEvents[route.key].length ? ( toggleExpand(route.key)} style={styles.header}> Past Events {isCurrentTabExpanded ? ( item.id.toString()} renderItem={renderEventCard} estimatedItemSize={100} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} /> ) : null} ) : null } /> ); }; const handleIndexChange = useCallback( (newIndex: number) => { setSearchQuery(''); if (newIndex >= 0 && newIndex < routes.length) { setIndex(newIndex); } }, [routes.length] ); return (
) : null } /> handleSearch(text)} value={searchQuery} icon={} height={38} /> setShowPopup(false)} > setShowPopup(false)} activeOpacity={1} > handlePopupOption('meeting')} > Add meeting handlePopupOption('trip')} > Add shared trip ); }; export default EventsScreen;