123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- 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<PostGetEventsListReturn>({
- local_meetings: [],
- nm: [],
- shared_trips: []
- } as never);
- const [pastEvents, setPastEvents] = useState<PostGetEventsListReturn>({
- local_meetings: [],
- nm: [],
- shared_tripsv: []
- } as never);
- const [filteredEvents, setFilteredEvents] = useState<PostGetEventsListReturn>({
- local_meetings: [],
- nm: [],
- shared_trips: []
- } as never);
- const [filteredPastEvents, setFilteredPastEvents] = useState<PostGetEventsListReturn>({
- local_meetings: [],
- nm: [],
- shared_trips: []
- } as never);
- const [tooltipStates, setTooltipStates] = useState<Record<number, boolean>>({});
- const date = new Date();
- const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>({
- nm: false,
- shared_trips: false,
- local_meetings: false
- });
- const scrollViewRefs = useRef<Record<string, FlashList<SingleEvent> | null>>({
- nm: null,
- shared_trips: null,
- local_meetings: null
- });
- const sectionRef = useRef<View>(null);
- const [showPopup, setShowPopup] = useState(false);
- const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
- const [toolTipVisible, setToolTipVisible] = useState<boolean>(false);
- const buttonRef = useRef<TouchableOpacity>(null);
- const [index, setIndex] = useState<number>(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 (
- <View>
- <TouchableOpacity
- style={[
- styles.card,
- item.type === 2 || item.type === 3 || item.full || item.closed
- ? { backgroundColor: Colors.FILL_LIGHT }
- : { backgroundColor: Colors.WHITE }
- ]}
- onPress={() =>
- navigation.navigate(...([NAVIGATION_PAGES.EVENT, { url: item.url }] as never))
- }
- disabled={item.active === 0}
- >
- <View style={styles.imageWrapper}>
- {item.active === 0 ? (
- <FilterImage
- source={{ uri: photo }}
- style={[
- styles.image,
- {
- filter: 'grayscale(100%)'
- }
- ]}
- />
- ) : (
- <Image
- source={{ uri: photo, cache: 'reload' }}
- style={styles.image}
- resizeMode="cover"
- />
- )}
- {item.star === 1 && (
- <View style={styles.iconOverlay}>
- <StarIcon fill={Colors.WHITE} width={12} />
- </View>
- )}
- {item.joined && token ? (
- <View style={styles.joinedOverlay}>
- <Text style={{ color: Colors.WHITE, fontSize: 12, fontFamily: 'redhat-700' }}>
- Joined
- </Text>
- </View>
- ) : null}
- </View>
- <View style={styles.info}>
- <Text style={styles.title} numberOfLines={1}>
- {item.name}
- </Text>
- <View style={styles.row}>
- {item.type === 1 && item?.flag ? (
- <Tooltip
- key={item.id}
- isVisible={!!tooltipStates[item.id]}
- content={<Text style={{ color: Colors.BLACK }}>{item.country}</Text>}
- 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}
- >
- <TouchableOpacity
- onPress={() => setTooltipStates((prev) => ({ ...prev, [item.id]: true }))}
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
- >
- <Image
- source={{ uri: API_HOST + item.flag }}
- style={{
- width: 14,
- height: 14,
- borderRadius: 7,
- borderWidth: 0.5,
- borderColor: Colors.DARK_LIGHT
- }}
- />
- </TouchableOpacity>
- </Tooltip>
- ) : (
- <EarthIcon fill={Colors.DARK_BLUE} height={14} width={14} />
- )}
- <Text style={styles.dateAndLocation} numberOfLines={1}>
- {item.address1}
- </Text>
- </View>
- <View style={[styles.row]}>
- <View style={styles.row}>
- <CalendarIcon fill={Colors.DARK_BLUE} height={14} width={14} />
- <Text style={[styles.dateAndLocation, { flex: 0 }]} numberOfLines={1}>
- {formatEventDate(item)}
- </Text>
- </View>
- </View>
- {item.registrations_info !== 1 && (
- <View style={styles.row}>
- <NomadsIcon fill={Colors.DARK_BLUE} height={14} width={14} />
- <Text style={styles.dateAndLocation} numberOfLines={1}>
- {renderSpotsText(item)}
- </Text>
- </View>
- )}
- </View>
- {item.type === 2 || item.type === 3 || item.full || item.closed ? (
- <View
- style={[
- styles.statusBadge,
- { backgroundColor: item.active ? badgeColor : Colors.LIGHT_GRAY }
- ]}
- >
- <View style={styles.rotatedContainer}>
- <Text style={styles.statusText}>{badgeText}</Text>
- </View>
- </View>
- ) : null}
- </TouchableOpacity>
- </View>
- );
- };
- const renderScene = ({
- route
- }: {
- route: { key: 'nm' | 'shared_trips' | 'local_meetings'; title: string };
- }) => {
- const isCurrentTabExpanded = expandedStates[route.key];
- return (
- <>
- <FlashList
- ref={(ref) => {
- scrollViewRefs.current[route.key] = ref;
- }}
- data={filteredEvents[route.key] || []}
- ListHeaderComponent={
- route.key === 'shared_trips' ? (
- <TouchableOpacity
- onPress={() => setToolTipVisible(true)}
- style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 12 }}
- >
- <Tooltip
- isVisible={toolTipVisible}
- content={
- <Text style={{ fontSize: 12, color: Colors.DARK_BLUE }}>
- 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.
- </Text>
- }
- contentStyle={{ backgroundColor: Colors.WHITE }}
- placement="bottom"
- onClose={() => setToolTipVisible(false)}
- backgroundColor="transparent"
- allowChildInteraction={false}
- >
- <TouchableOpacity
- onPress={() => setToolTipVisible(true)}
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
- >
- <InfoIcon fill={Colors.DARK_BLUE} width={16} height={16} />
- </TouchableOpacity>
- </Tooltip>
- <Text style={{ fontSize: 12, color: Colors.DARK_BLUE, fontWeight: '600' }}>
- Disclaimer [read more]
- </Text>
- </TouchableOpacity>
- ) : 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 ? (
- <View ref={sectionRef} style={styles.sectionContainer}>
- <TouchableOpacity onPress={() => toggleExpand(route.key)} style={styles.header}>
- <View style={styles.headerContainer}>
- <Text style={styles.headerText}>Past Events</Text>
- </View>
- <View style={styles.chevronContainer}>
- <ChevronIcon
- fill={Colors.DARK_BLUE}
- style={[styles.headerIcon, isCurrentTabExpanded ? styles.rotate : null]}
- />
- </View>
- </TouchableOpacity>
- {isCurrentTabExpanded ? (
- <FlashList
- data={filteredPastEvents[route.key] || []}
- scrollEnabled={true}
- keyExtractor={(item) => item.id.toString()}
- renderItem={renderEventCard}
- estimatedItemSize={100}
- contentContainerStyle={styles.listContainer}
- showsVerticalScrollIndicator={false}
- />
- ) : null}
- </View>
- ) : null
- }
- />
- </>
- );
- };
- const handleIndexChange = useCallback(
- (newIndex: number) => {
- setSearchQuery('');
- if (newIndex >= 0 && newIndex < routes.length) {
- setIndex(newIndex);
- }
- },
- [routes.length]
- );
- return (
- <SafeAreaView style={{ height: '100%' }} edges={['top']}>
- <View style={{ marginLeft: '5%', marginRight: '5%' }}>
- <Header
- label="Events"
- rightElement={
- canAddEvent?.can ? (
- <TouchableOpacity
- ref={buttonRef}
- onPress={handleAddButtonPress}
- style={{ width: 30 }}
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
- >
- <CalendarPlusIcon fill={Colors.DARK_BLUE} />
- </TouchableOpacity>
- ) : null
- }
- />
- <Animated.View style={[styles.searchContainer, searchContainerAnimatedStyle]}>
- <Input
- inputMode={'search'}
- placeholder={'Search'}
- onChange={(text) => handleSearch(text)}
- value={searchQuery}
- icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
- height={38}
- />
- </Animated.View>
- </View>
- <TabViewWrapper
- routes={routes}
- renderScene={renderScene as never}
- setIndex={setIndex}
- lazy={false}
- selectedIndex={index}
- />
- <Modal
- visible={showPopup}
- transparent={true}
- animationType="fade"
- onRequestClose={() => setShowPopup(false)}
- >
- <TouchableOpacity
- style={{
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.1)'
- }}
- onPress={() => setShowPopup(false)}
- activeOpacity={1}
- >
- <View
- style={[
- popupStyles.popup,
- {
- top: popupPosition.y,
- left: popupPosition.x
- }
- ]}
- >
- <TouchableOpacity
- style={popupStyles.popupOption}
- onPress={() => handlePopupOption('meeting')}
- >
- <Text style={popupStyles.popupText}>Add meeting</Text>
- </TouchableOpacity>
- <TouchableOpacity
- style={[popupStyles.popupOption, popupStyles.popupOptionLast]}
- onPress={() => handlePopupOption('trip')}
- >
- <Text style={popupStyles.popupText}>Add shared trip</Text>
- </TouchableOpacity>
- </View>
- </TouchableOpacity>
- </Modal>
- </SafeAreaView>
- );
- };
- export default EventsScreen;
|