Bladeren bron

add event with time

Viktoriia 2 maanden geleden
bovenliggende
commit
ada452a36c

+ 3 - 3
app.config.ts

@@ -73,7 +73,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     infoPlist: {
       UIBackgroundModes: ['location', 'fetch', 'remote-notification'],
       NSLocationAlwaysUsageDescription:
-        'Turn on location service to allow NomadMania.com find friends nearby.',
+        'NomadMania uses your location in the background so other users see your latest location and regions are marked as visited automatically.',
       NSPhotoLibraryUsageDescription:
         'Enable NomadMania.com to access your photo library to upload your profile picture. Any violence, excess of nudity, stolen picture, or scam is forbidden',
       NSPhotoLibraryAddUsageDescription:
@@ -84,9 +84,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         'Nomadmania app needs access to the documents folder to select files.',
       NSCameraUsageDescription: 'Nomadmania app needs access to the camera to record video.',
       NSLocationWhenInUseUsageDescription:
-        'NomadMania app needs access to your location to show relevant data.',
+        'NomadMania uses your location to show it to other users and to help mark regions as visited.',
       NSLocationAlwaysAndWhenInUseUsageDescription:
-        'NomadMania app needs access to your location to show relevant data.',
+        'NomadMania uses your location in the background so other users see your latest location and regions are marked as visited automatically.',
       LSApplicationQueriesSchemes: ['comgooglemaps']
     },
     privacyManifests: {

+ 10 - 0
assets/icons/travels-screens/clock-solid.svg

@@ -0,0 +1,10 @@
+<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5107_44171)">
+<path d="M10.5 0C13.1522 0 15.6957 1.05357 17.5711 2.92893C19.4464 4.8043 20.5 7.34784 20.5 10C20.5 12.6522 19.4464 15.1957 17.5711 17.0711C15.6957 18.9464 13.1522 20 10.5 20C7.84784 20 5.3043 18.9464 3.42893 17.0711C1.55357 15.1957 0.5 12.6522 0.5 10C0.5 7.34784 1.55357 4.8043 3.42893 2.92893C5.3043 1.05357 7.84784 0 10.5 0ZM9.5625 4.6875V10C9.5625 10.3125 9.71875 10.6055 9.98047 10.7812L13.7305 13.2812C14.1602 13.5703 14.7422 13.4531 15.0312 13.0195C15.3203 12.5859 15.2031 12.0078 14.7695 11.7188L11.4375 9.5V4.6875C11.4375 4.16797 11.0195 3.75 10.5 3.75C9.98047 3.75 9.5625 4.16797 9.5625 4.6875Z"/>
+</g>
+<defs>
+<clipPath id="clip0_5107_44171">
+<rect width="20" height="20" fill="white" transform="translate(0.5)"/>
+</clipPath>
+</defs>
+</svg>

+ 70 - 0
src/components/Calendar/InputTimePicker/index.tsx

@@ -0,0 +1,70 @@
+import React, { FC, useState } from 'react';
+import { View } from 'react-native';
+
+import { Input } from '../../Input';
+import { Modal } from '../../Modal';
+import { Button } from '../../Button';
+import SpinnerTimePicker from '../SpinnerTimePicker';
+
+import ClockIcon from 'assets/icons/travels-screens/clock-solid.svg';
+import { Colors } from 'src/theme';
+
+type Props = {
+  selectedTime?: (date: Date) => void;
+  formikError?: string | boolean;
+  headerTitle?: string;
+  defaultTime?: Date | null;
+};
+
+export const InputTimePicker: FC<Props> = ({
+  selectedTime,
+  formikError,
+  headerTitle,
+  defaultTime
+}) => {
+  const [visible, setVisible] = useState(false);
+  const [spinnerSelectedTime, setSpinnerSelectedTime] = useState<Date | null>(defaultTime ?? null);
+
+  const roundToNearest5Min = (date: Date) => {
+    const ms = 1000 * 60 * 5;
+    return new Date(Math.round(date.getTime() / ms) * ms);
+  };
+
+  const onButtonPress = () => {
+    setVisible(false);
+    if (selectedTime) {
+      selectedTime(spinnerSelectedTime ?? new Date());
+      if (!spinnerSelectedTime) {
+        setSpinnerSelectedTime(roundToNearest5Min(new Date()));
+      }
+    }
+  };
+
+  return (
+    <View>
+      <Input
+        icon={<ClockIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+        header={'Time'}
+        value={
+          defaultTime
+            ? defaultTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+            : ''
+        }
+        isFocused={(b) => setVisible(b)}
+        onBlur={() => {}}
+        inputMode={'none'}
+        placeholder={'Choose a time'}
+        formikError={formikError}
+      />
+      <Modal
+        visibleInPercent={'50%'}
+        onRequestClose={() => setVisible(false)}
+        headerTitle={headerTitle ?? 'Select Time'}
+        visible={visible}
+      >
+        <SpinnerTimePicker selectedTime={(date) => setSpinnerSelectedTime(date)} />
+        <Button onPress={onButtonPress}>Done</Button>
+      </Modal>
+    </View>
+  );
+};

+ 70 - 0
src/components/Calendar/SpinnerTimePicker.tsx

@@ -0,0 +1,70 @@
+import React, { FC, useEffect, useState } from 'react';
+import { Platform, View } from 'react-native';
+import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
+import { Picker } from 'react-native-wheel-pick';
+import { Colors } from '../../theme';
+
+type Props = {
+  selectedTime: (date: Date) => void;
+};
+
+const SpinnerTimePicker: FC<Props> = ({ selectedTime }) => {
+  const now = new Date();
+  const [value, setValue] = useState<Date>(new Date());
+  const [selectedHour, setSelectedHour] = useState(now.getHours().toString().padStart(2, '0'));
+  const [selectedMinute, setSelectedMinute] = useState(
+    (Math.round(now.getMinutes() / 5) * 5).toString().padStart(2, '0')
+  );
+
+  const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
+  const minutes = Array.from({ length: 12 }, (_, i) => (i * 5).toString().padStart(2, '0'));
+
+  useEffect(() => {
+    const updated = new Date();
+    updated.setHours(Number(selectedHour));
+    updated.setMinutes(Number(selectedMinute));
+    updated.setSeconds(0);
+    updated.setMilliseconds(0);
+    setValue(updated);
+    selectedTime(updated);
+  }, [selectedHour, selectedMinute]);
+
+  const onChange = (event: DateTimePickerEvent, selectedSpinnerTime?: Date) => {
+    if (event.type === 'set' && selectedSpinnerTime) {
+      setValue(selectedSpinnerTime);
+      selectedTime(selectedSpinnerTime);
+    }
+  };
+
+  if (Platform.OS === 'ios') {
+    return (
+      <DateTimePicker
+        value={value}
+        mode="time"
+        display="spinner"
+        textColor={Colors.DARK_BLUE}
+        onChange={onChange}
+        minuteInterval={5}
+      />
+    );
+  }
+
+  return (
+    <View style={{ flexDirection: 'row', justifyContent: 'center' }}>
+      <Picker
+        style={{ marginVertical: 16, backgroundColor: 'white', width: '50%', height: 215 }}
+        selectedValue={selectedHour}
+        pickerData={hours}
+        onValueChange={(value: React.SetStateAction<string>) => setSelectedHour(value)}
+      />
+      <Picker
+        style={{ marginVertical: 16, backgroundColor: 'white', width: '50%', height: 215 }}
+        selectedValue={selectedMinute}
+        pickerData={minutes}
+        onValueChange={(value: React.SetStateAction<string>) => setSelectedMinute(value)}
+      />
+    </View>
+  );
+};
+
+export default SpinnerTimePicker;

+ 2 - 79
src/components/Input/index.tsx

@@ -38,52 +38,15 @@ const parseTextWithLinks = (text?: string): React.ReactNode => {
   if (!text) return null;
 
   const urlRegex = /((https?:\/\/[^\s]+)|(?<!\w)(www\.[^\s]+))/g;
-  const eventPageRegex = /Event page\s+(https?:\/\/[^\s]+)\s+([^\n]+)/i;
 
   const result: React.ReactNode[] = [];
   let lastIndex = 0;
 
-  const eventMatch = text.match(eventPageRegex);
-  let handledEvent = false;
-
-  if (eventMatch) {
-    const [fullMatch, eventUrl, locationCandidate] = eventMatch;
-    const eventStart = eventMatch.index ?? 0;
-    const eventEnd = eventStart + fullMatch.length;
-
-    if (eventStart > 0) {
-      result.push(<Text key="text-before-event">{text.slice(0, eventStart)}</Text>);
-    }
-
-    result.push(
-      <Text
-        key="event-url"
-        style={{ color: Colors.ORANGE, textDecorationLine: 'underline' }}
-        onPress={() => Linking.openURL(eventUrl)}
-      >
-        {eventUrl}
-      </Text>
-    );
-
-    result.push(
-      <Text
-        key="event-location"
-        style={{ color: Colors.ORANGE, textDecorationLine: 'none' }}
-        onPress={() => openLocation(locationCandidate)}
-      >
-        {' ' + locationCandidate.trim()}
-      </Text>
-    );
-
-    lastIndex = eventEnd;
-    handledEvent = true;
-  }
-
   Array.from(text.matchAll(urlRegex)).forEach((match, index) => {
     const matchText = match[0];
     const matchIndex = match.index ?? 0;
 
-    if (handledEvent && matchIndex < lastIndex) return;
+    if (matchIndex < lastIndex) return;
 
     if (lastIndex < matchIndex) {
       result.push(<Text key={`text-${index}`}>{text.slice(lastIndex, matchIndex)}</Text>);
@@ -120,7 +83,7 @@ const parseTextWithLinks = (text?: string): React.ReactNode => {
     result.push(
       <Text
         key={`link-${index}`}
-        style={{ color: Colors.ORANGE, textDecorationLine: 'underline' }}
+        style={{ color: Colors.ORANGE, textDecorationLine: 'none' }}
         onPress={handlePress}
       >
         {matchText}
@@ -137,46 +100,6 @@ const parseTextWithLinks = (text?: string): React.ReactNode => {
   return result;
 };
 
-const openLocation = async (address: string) => {
-  const endpoint =
-    `https://maps.googleapis.com/maps/api/place/findplacefromtext/json` +
-    `?input=${encodeURIComponent(address)}` +
-    `&inputtype=textquery` +
-    `&fields=geometry,formatted_address,name` +
-    `&key=${GOOGLE_MAP_PLACES_APIKEY}`;
-
-  try {
-    const response = await fetch(endpoint);
-    const data = await response.json();
-
-    if (data.status === 'OK' && data.candidates?.length > 0) {
-      const place = data.candidates[0];
-      const { lat, lng } = place.geometry.location;
-      const label = place.name || place.formatted_address || address;
-      const encodedLabel = encodeURIComponent(label);
-      const fallbackUrl = `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`;
-
-      let mapsUrl: string;
-
-      if (Platform.OS === 'ios') {
-        const canOpenGoogleMaps = await Linking.canOpenURL('comgooglemaps://');
-        mapsUrl = canOpenGoogleMaps
-          ? `comgooglemaps://?center=${lat},${lng}&q=${encodedLabel}@${lat},${lng}`
-          : `http://maps.apple.com/?ll=${lat},${lng}&q=${encodedLabel}`;
-      } else {
-        mapsUrl = `geo:${lat},${lng}?q=${encodedLabel}`;
-      }
-
-      const supported = await Linking.canOpenURL(mapsUrl);
-      await Linking.openURL(supported ? mapsUrl : fallbackUrl);
-    } else {
-      console.warn('Places API did not return valid results:', data.status);
-    }
-  } catch (error) {
-    console.error('Places API error:', error);
-  }
-};
-
 export const Input: FC<Props> = ({
   onChange,
   placeholder,

+ 40 - 2
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -419,7 +419,6 @@ const FilterModal = ({
       setSettings({ token, sharing: 0 });
       setShowNomads && setShowNomads(false);
       storage.set('showNomads', false);
-      await stopBackgroundLocationUpdates();
     }
   };
 
@@ -562,6 +561,43 @@ const FilterModal = ({
             />
           </View>
         </TouchableOpacity>
+        <View style={{ marginVertical: 12, gap: 6 }}>
+          <Text style={[styles.text, styles.boldText]}>
+            At NomadMania, we respect your privacy.
+          </Text>
+          <Text style={[styles.text]}>
+            If you choose to share your location, it will be used to show your current location to
+            other users who also share theirs.
+          </Text>
+          <Text style={[styles.text]}>You can choose how you want to share your location:</Text>
+          <View style={[styles.bulletItem]}>
+            <Text style={styles.bulletIcon}>{'\u2022'}</Text>
+            <Text style={[styles.text, { flex: 1 }]}>
+              <Text style={styles.boldText}>Only when the app is open</Text> – Your location updates
+              only when you open the app. This uses less battery but may be less accurate.
+            </Text>
+          </View>
+          <View style={styles.bulletItem}>
+            <Text style={styles.bulletIcon}>{'\u2022'}</Text>
+            <Text style={[styles.text, { flex: 1 }]}>
+              <Text style={styles.boldText}>In the background</Text> – Your location stays up to
+              date even when the app is closed. Other users see your latest location.
+            </Text>
+          </View>
+          <Text style={[styles.text]}>
+            You’re always in control, and you can change these settings anytime in the app{' '}
+            <Text
+              style={{ color: Colors.ORANGE }}
+              onPress={() =>
+                Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+              }
+            >
+              Settings
+            </Text>{' '}
+            section.
+          </Text>
+        </View>
+
         <WarningModal
           type={'success'}
           isVisible={askLocationVisible}
@@ -619,7 +655,9 @@ const FilterModal = ({
       statusBarTranslucent={true}
       presentationStyle="overFullScreen"
     >
-      <View style={[styles.modalContainer, { height: 260 }]}>{renderScene(isFilterVisible)}</View>
+      <View style={[styles.modalContainer, { minHeight: 260 }]}>
+        {renderScene(isFilterVisible)}
+      </View>
     </ReactModal>
   );
 };

+ 12 - 0
src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx

@@ -85,6 +85,18 @@ export const styles = StyleSheet.create({
   textContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', marginBottom: 12 },
   textWithIcon: { lineHeight: 26, fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
   text: { fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
+  boldText: { fontWeight: '700' },
+  bulletItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    width: '100%'
+  },
+  bulletIcon: {
+    fontSize: 12,
+    fontWeight: '700',
+    marginHorizontal: 6,
+    alignSelf: 'flex-start'
+  },
   icon: {
     backgroundColor: Colors.WHITE,
     width: 26,

+ 17 - 0
src/screens/InAppScreens/TravelsScreen/CreateEvent/index.tsx

@@ -36,10 +36,12 @@ import { SheetManager } from 'react-native-actions-sheet';
 import PhotosForRegionModal from '../Components/PhotosForRegionModal/PhotosForRegionModal';
 import { API_HOST } from 'src/constants';
 import AddMapPinModal from '../Components/AddMapPinModal';
+import { InputTimePicker } from 'src/components/Calendar/InputTimePicker';
 
 const EventSchema = yup.object({
   event_name: yup.string().required().min(3),
   date: yup.date().nullable().required(),
+  time: yup.date().nullable().optional(),
   capacity: yup.number().optional(),
   region: yup.number().required(),
   photo: yup.number().nullable().optional(),
@@ -93,6 +95,7 @@ const CreateEventScreen = () => {
               initialValues={{
                 event_name: '',
                 date: '',
+                time: null,
                 capacity: '',
                 region: null,
                 photo: null,
@@ -123,6 +126,13 @@ const CreateEventScreen = () => {
                   newEvent.capacity = Number(values.capacity);
                 }
 
+                if (values.time) {
+                  newEvent.time = (values.time as Date).toLocaleTimeString([], {
+                    hour: '2-digit',
+                    minute: '2-digit'
+                  });
+                }
+
                 await addEvent(
                   { token, event: JSON.stringify(newEvent) },
                   {
@@ -161,6 +171,13 @@ const CreateEventScreen = () => {
                     icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
+                  <InputTimePicker
+                    headerTitle={'Select Time'}
+                    defaultTime={props.values.time}
+                    selectedTime={(time) => props.setFieldValue('time', time)}
+                    formikError={props.touched.time && props.errors.time}
+                  />
+
                   <Input
                     header={'Maximum capacity'}
                     placeholder={'Set the maximum of people'}

+ 25 - 14
src/screens/InAppScreens/TravelsScreen/EventScreen/index.tsx

@@ -155,9 +155,11 @@ const EventScreen = ({ route }: { route: any }) => {
     }
   }, [data]);
 
-  useFocusEffect(() => {
-    refetch();
-  });
+  useFocusEffect(
+    useCallback(() => {
+      refetch();
+    }, [navigation])
+  );
 
   const handlePreviewDocument = useCallback(async (url: string, fileName: string) => {
     try {
@@ -346,11 +348,6 @@ const EventScreen = ({ route }: { route: any }) => {
   };
 
   const handleJoinEvent = async () => {
-    if (!event.settings.free) {
-      // TO DO
-      Linking.openURL(API_HOST + event.settings.shop_url);
-      return;
-    }
     await joinEvent(
       { token, id: event.id },
       {
@@ -598,7 +595,7 @@ const EventScreen = ({ route }: { route: any }) => {
         </View>
       </TouchableOpacity>
 
-      <KeyboardAwareScrollView>
+      <KeyboardAwareScrollView showsVerticalScrollIndicator={false}>
         <ScrollView
           ref={scrollViewRef}
           contentContainerStyle={{ minHeight: '100%' }}
@@ -849,7 +846,8 @@ const EventScreen = ({ route }: { route: any }) => {
                               source={{ uri: API_HOST + user.avatar }}
                               style={[
                                 styles.userImage,
-                                filteredParticipants.length > maxVisibleParticipantsWithGap &&
+                                (filteredParticipants.length > maxVisibleParticipantsWithGap ||
+                                  filteredParticipants.length < (event.participants ?? 0)) &&
                                 index !== 0
                                   ? { marginLeft: -10 }
                                   : {}
@@ -858,7 +856,8 @@ const EventScreen = ({ route }: { route: any }) => {
                           </TouchableOpacity>
                         </Tooltip>
                       ))}
-                      {maxVisibleParticipants < filteredParticipants.length ? (
+                      {maxVisibleParticipants < filteredParticipants.length ||
+                      filteredParticipants.length < (event.participants ?? 0) ? (
                         <View style={styles.userCountContainer}>
                           <Text style={styles.userCount}>{event.participants}</Text>
                         </View>
@@ -939,7 +938,7 @@ const EventScreen = ({ route }: { route: any }) => {
                         textTransform: 'uppercase'
                       }}
                     >
-                      Join - {event.settings.price}
+                      Interested
                     </Text>
                   </>
                 )}
@@ -1242,9 +1241,21 @@ const WebDisplay = React.memo(function WebDisplay({ html }: { html: string }) {
   const { width: windowWidth } = useWindowDimensions();
   const contentWidth = windowWidth * 0.9;
 
+  const token = storage.get('token', StoreType.STRING) as string;
+
   const processedHtml = React.useMemo(() => {
-    return html.replace(/src="\/img\//g, `src="${API_HOST}/img/`);
-  }, []);
+    let updatedHtml = html;
+
+    updatedHtml = updatedHtml.replace(/src="\/img\//g, `src="${API_HOST}/img/`);
+
+    updatedHtml = updatedHtml.replace(/href="(?!http)([^"]*)"/g, (match, path) => {
+      const separator = path.includes('?') ? '&' : '?';
+      const fullUrl = `${API_HOST}${path}${separator}token=${encodeURIComponent(token)}`;
+      return `href="${fullUrl}"`;
+    });
+
+    return updatedHtml;
+  }, [html, token]);
 
   return <RenderHtml contentWidth={contentWidth} source={{ html: processedHtml }} />;
 });