瀏覽代碼

fixing conflicts

Viktoriia 1 年之前
父節點
當前提交
c9778dc8fa

+ 51 - 12
Route.tsx

@@ -1,9 +1,10 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
 import { useFonts } from 'expo-font';
 import * as SplashScreen from 'expo-splash-screen';
+import { Platform } from 'react-native';
 
 import { createStackNavigator } from '@react-navigation/stack';
-import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { BottomTabNavigationProp, createBottomTabNavigator } from '@react-navigation/bottom-tabs';
 
 import WelcomeScreen from './src/screens/WelcomeScreen';
 import LoginScreen from './src/screens/LoginScreen';
@@ -11,9 +12,14 @@ import ResetPasswordScreen from './src/screens/ResetPasswordScreen';
 import ResetPasswordDeepScreen from './src/screens/ResetPasswordDeepScreen';
 import JoinUsScreen from './src/screens/RegisterScreen/JoinUs';
 import EditAccount from './src/screens/RegisterScreen/EditAccount';
+import HomeScreen from './src/screens/HomeScreen';
 
 import { NAVIGATION_PAGES } from './src/types';
 import { storageGet } from './src/storage';
+import { openDatabases } from './src/db';
+
+import TabBarButton from './src/components/TabBarButton';
+import { ParamListBase, RouteProp } from '@react-navigation/native';
 
 const ScreenStack = createStackNavigator();
 const BottomTab = createBottomTabNavigator();
@@ -26,16 +32,32 @@ const Route = () => {
     'redhat-700': require('./assets/fonts/RedHatDisplay-Bold-700.ttf'),
     'redhat-600': require('./assets/fonts/RedHatDisplay-SemiBold-600.ttf')
   });
+  const [dbLoaded, setDbLoaded] = useState(false);
 
   useEffect(() => {
-    const hideSplashScreen = async () => {
-      if (fontsLoaded) {
-        await SplashScreen.hideAsync();
-      }
+    const prepareApp = async () => {
+      await openDatabases();
+      setDbLoaded(true);
     };
+  
+    prepareApp();
+  }, []);
+  
+  useEffect(() => {
+    if (fontsLoaded && dbLoaded) {
+      SplashScreen.hideAsync();
+    }
+  }, [fontsLoaded, dbLoaded]); 
+
+  // useEffect(() => {
+  //   const hideSplashScreen = async () => {
+  //     if (fontsLoaded) {
+  //       await SplashScreen.hideAsync();
+  //     }
+  //   };
 
-    hideSplashScreen();
-  }, [fontsLoaded]);
+  //   hideSplashScreen();
+  // }, [fontsLoaded]);
 
   if (!fontsLoaded) {
     return null;
@@ -43,6 +65,24 @@ const Route = () => {
 
   const token = storageGet('token');
 
+  const screenOptions = ({ route }: { route: RouteProp<ParamListBase, string>; navigation: any; }) => ({
+    headerShown: false,
+    tabBarButton: (props: any) => (
+      <TabBarButton
+        {...props}
+        label={route.name}
+        focused={props.accessibilityState.selected}
+      />
+    ),
+    tabBarStyle: {
+      ...Platform.select({
+        android: {
+          height: 58,
+        },
+      }),
+    },
+  });
+
   return (
     <ScreenStack.Navigator
       screenOptions={{ headerShown: false }}
@@ -60,11 +100,10 @@ const Route = () => {
       <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
         {() => (
           <BottomTab.Navigator
-            screenOptions={() => ({
-              headerShown: false
-            })}
+            screenOptions={screenOptions}
           >
-            <BottomTab.Screen name={NAVIGATION_PAGES.LOCATION_TAB} component={WelcomeScreen} />
+            <BottomTab.Screen name={NAVIGATION_PAGES.MAP_TAB} component={HomeScreen} />
+            <BottomTab.Screen name={NAVIGATION_PAGES.TRAVELLERS_TAB} component={WelcomeScreen} />
             <BottomTab.Screen name={NAVIGATION_PAGES.TRAVELS_TAB} component={WelcomeScreen} />
             <BottomTab.Screen name={NAVIGATION_PAGES.PROFILE_TAB} component={WelcomeScreen} />
           </BottomTab.Navigator>

二進制
assets/db/darePlaces.db


二進制
assets/db/nmRegions.db


文件差異過大導致無法顯示
+ 0 - 0
assets/geojson/mqp.json


文件差異過大導致無法顯示
+ 0 - 0
assets/geojson/nm2022.json


+ 4 - 0
assets/icons/bottom-navigation/map.svg

@@ -0,0 +1,4 @@
+<svg width="19" height="22" viewBox="0 0 19 22" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.2093 4.86047C6.94879 4.86047 5.11628 6.69297 5.11628 8.95349C5.11628 11.214 6.94879 13.0465 9.2093 13.0465C11.4698 13.0465 13.3023 11.214 13.3023 8.95349C13.3023 6.69297 11.4698 4.86047 9.2093 4.86047ZM7.16279 8.95349C7.16279 7.82323 8.07905 6.90698 9.2093 6.90698C10.3396 6.90698 11.2558 7.82323 11.2558 8.95349C11.2558 10.0837 10.3396 11 9.2093 11C8.07905 11 7.16279 10.0837 7.16279 8.95349Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M18.4186 9.2093C18.4186 14.8372 11.2558 22 9.2093 22C7.16279 22 0 14.8372 0 9.2093C0 4.12315 4.12315 0 9.2093 0C14.2955 0 18.4186 4.12315 18.4186 9.2093ZM16.3721 9.2093C16.3721 11.2914 14.9657 14.0204 12.9993 16.417C12.0547 17.5682 11.0699 18.5324 10.2414 19.1883C9.82504 19.5179 9.48419 19.741 9.23696 19.8715L9.2093 19.886L9.18165 19.8715C8.93442 19.741 8.59357 19.5179 8.17725 19.1883C7.34867 18.5324 6.3639 17.5682 5.41931 16.417C3.45291 14.0204 2.04651 11.2914 2.04651 9.2093C2.04651 5.2534 5.2534 2.04651 9.2093 2.04651C13.1652 2.04651 16.3721 5.2534 16.3721 9.2093Z"/>
+</svg>

+ 4 - 0
assets/icons/bottom-navigation/profile.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6449 2.54857C9.53402 2.54857 7.83543 4.25521 7.83543 6.35805C7.83543 8.40528 9.43467 10.0663 11.4538 10.156C11.5762 10.1475 11.7056 10.1471 11.8276 10.1559C13.8448 10.0653 15.445 8.40534 15.4544 6.35647C15.4535 4.25549 13.7461 2.54857 11.6449 2.54857ZM6.28687 6.35805C6.28687 3.40223 8.6765 1 11.6449 1C14.6019 1 17.003 3.40109 17.003 6.35805L17.003 6.36127C16.9909 9.25234 14.7121 11.6087 11.8361 11.7053C11.8017 11.7065 11.7673 11.7054 11.733 11.7019C11.6833 11.697 11.6089 11.6963 11.5395 11.7026C11.5076 11.7055 11.4755 11.7064 11.4434 11.7053C8.56801 11.6087 6.28687 9.25219 6.28687 6.35805Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8229 12.5498C13.8135 12.5498 15.8434 13.0494 17.4122 14.0971C18.8223 15.0357 19.6302 16.3514 19.6302 17.7672C19.6302 19.1829 18.8224 20.5009 17.4129 21.4445L17.4127 21.4446C15.8392 22.4975 13.8067 23.0001 11.8151 23.0001C9.82395 23.0001 7.79184 22.4977 6.2184 21.4452C4.80805 20.5067 4 19.1909 4 17.7749C4 16.3592 4.80778 15.0412 6.21733 14.0976L6.22017 14.0957L6.22018 14.0957C7.7981 13.0495 9.83197 12.5498 11.8229 12.5498ZM7.07735 15.3854C5.98959 16.1141 5.54857 16.9946 5.54857 17.7749C5.54857 18.5551 5.9894 19.4329 7.07693 20.1564L7.07863 20.1576C8.3441 21.0043 10.0591 21.4515 11.8151 21.4515C13.571 21.4515 15.286 21.0043 16.5514 20.1577C17.6403 19.4288 18.0817 18.5479 18.0817 17.7672C18.0817 16.987 17.6408 16.1092 16.5533 15.3857L16.5521 15.3849C15.2922 14.5434 13.5798 14.0984 11.8229 14.0984C10.0669 14.0984 8.34866 14.543 7.07735 15.3854Z"/>
+</svg>

+ 6 - 0
assets/icons/bottom-navigation/travellers.svg

@@ -0,0 +1,6 @@
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.64491 1.54857C5.53402 1.54857 3.83543 3.25521 3.83543 5.35805C3.83543 7.40528 5.43467 9.06634 7.4538 9.15602C7.57625 9.1475 7.70561 9.14708 7.82765 9.15593C9.84479 9.0653 11.445 7.40534 11.4544 5.35647C11.4535 3.25549 9.74609 1.54857 7.64491 1.54857ZM2.28687 5.35805C2.28687 2.40223 4.6765 0 7.64491 0C10.6019 0 13.003 2.40109 13.003 5.35805L13.003 5.36127C12.9909 8.25234 10.7121 10.6087 7.83611 10.7053C7.80172 10.7065 7.76729 10.7054 7.73305 10.7019C7.68327 10.697 7.60887 10.6963 7.53951 10.7026C7.50756 10.7055 7.47546 10.7064 7.4434 10.7053C4.56801 10.6087 2.28687 8.25219 2.28687 5.35805Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5203 2.83898C14.5203 2.41136 14.8669 2.0647 15.2945 2.0647C17.7264 2.0647 19.6822 4.03363 19.6822 6.45231C19.6822 8.82209 17.8021 10.7516 15.4577 10.8394C15.416 10.8409 15.3742 10.8391 15.3327 10.8339C15.3172 10.832 15.2857 10.8308 15.2458 10.8352C14.8208 10.8824 14.438 10.5762 14.3908 10.1511C14.3436 9.72613 14.6498 9.34331 15.0748 9.29609C15.1964 9.28258 15.3238 9.27945 15.4495 9.2896C16.943 9.20773 18.1336 7.96815 18.1336 6.45231C18.1336 4.886 16.8683 3.61327 15.2945 3.61327C14.8669 3.61327 14.5203 3.26661 14.5203 2.83898Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.82285 11.5498C9.81349 11.5498 11.8434 12.0494 13.4122 13.0971C14.8223 14.0357 15.6302 15.3514 15.6302 16.7672C15.6302 18.1829 14.8224 19.5009 13.4129 20.4445L13.4127 20.4446C11.8392 21.4975 9.80666 22.0001 7.81511 22.0001C5.82395 22.0001 3.79184 21.4977 2.2184 20.4452C0.808052 19.5067 0 18.1909 0 16.7749C0 15.3592 0.807777 14.0412 2.21733 13.0976L2.22017 13.0957L2.22018 13.0957C3.7981 12.0495 5.83197 11.5498 7.82285 11.5498ZM3.07735 14.3854C1.98959 15.1141 1.54857 15.9946 1.54857 16.7749C1.54857 17.5551 1.9894 18.4329 3.07693 19.1564L3.07863 19.1576C4.3441 20.0043 6.05912 20.4515 7.81511 20.4515C9.57103 20.4515 11.286 20.0043 12.5514 19.1577C13.6403 18.4288 14.0817 17.5479 14.0817 16.7672C14.0817 15.987 13.6408 15.1092 12.5533 14.3857L12.5521 14.3849C11.2922 13.5434 9.57976 13.0984 7.82285 13.0984C6.06685 13.0984 4.34866 13.543 3.07735 14.3854Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5629 12.9924C16.6569 12.5753 17.0713 12.3133 17.4884 12.4073C18.3034 12.591 19.1033 12.9186 19.778 13.4331C20.7495 14.1628 21.2929 15.1799 21.2929 16.2546C21.2929 17.329 20.7498 18.3458 19.7788 19.0755C19.0959 19.5989 18.2838 19.9402 17.4452 20.1149C17.0265 20.2021 16.6165 19.9335 16.5292 19.5148C16.442 19.0962 16.7107 18.6861 17.1293 18.5989C17.7759 18.4642 18.3665 18.2076 18.8392 17.8446L18.8461 17.8393L18.8461 17.8393C19.4834 17.3613 19.7443 16.7791 19.7443 16.2546C19.7443 15.7301 19.4834 15.1479 18.8461 14.6699L18.8408 14.6659L18.8408 14.6659C18.3805 14.3143 17.7979 14.0644 17.148 13.918C16.7308 13.824 16.4689 13.4096 16.5629 12.9924Z"/>
+</svg>

文件差異過大導致無法顯示
+ 1 - 0
assets/icons/bottom-navigation/travels.svg


+ 2 - 2
assets/icons/close.svg

@@ -1,3 +1,3 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2.41919 0.580712C1.91151 0.0730308 1.08839 0.0730308 0.580712 0.580712C0.0730308 1.08839 0.0730308 1.91151 0.580712 2.41919L6.16147 7.99995L0.580712 13.5807C0.073031 14.0884 0.0730313 14.9115 0.580712 15.4192C1.08839 15.9269 1.91151 15.9269 2.41919 15.4192L7.99995 9.83843L13.5807 15.4192C14.0884 15.9269 14.9115 15.9269 15.4192 15.4192C15.9269 14.9115 15.9269 14.0884 15.4192 13.5807L9.83843 7.99995L15.4192 2.41919C15.9269 1.91151 15.9269 1.08839 15.4192 0.580712C14.9115 0.0730308 14.0884 0.0730308 13.5807 0.580712L7.99995 6.16147L2.41919 0.580712Z" fill="#0F3F4F"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="#0F3F4F" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.41919 0.580712C1.91151 0.0730308 1.08839 0.0730308 0.580712 0.580712C0.0730308 1.08839 0.0730308 1.91151 0.580712 2.41919L6.16147 7.99995L0.580712 13.5807C0.073031 14.0884 0.0730313 14.9115 0.580712 15.4192C1.08839 15.9269 1.91151 15.9269 2.41919 15.4192L7.99995 9.83843L13.5807 15.4192C14.0884 15.9269 14.9115 15.9269 15.4192 15.4192C15.9269 14.9115 15.9269 14.0884 15.4192 13.5807L9.83843 7.99995L15.4192 2.41919C15.9269 1.91151 15.9269 1.08839 15.4192 0.580712C14.9115 0.0730308 14.0884 0.0730308 13.5807 0.580712L7.99995 6.16147L2.41919 0.580712Z"/>
 </svg>

+ 3 - 0
assets/icons/location.svg

@@ -0,0 +1,3 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4997 2.50026L2.66353 7.43727L7.02571 9.42008C7.71463 9.73322 8.26678 10.2854 8.57992 10.9743L10.5627 15.3365L15.4997 2.50026ZM15.0756 0.148301C16.8129 -0.519892 18.5199 1.18711 17.8517 2.92442L12.583 16.6229C11.8991 18.4011 9.41079 18.4751 8.62244 16.7407L6.44288 11.9457C6.36459 11.7734 6.22655 11.6354 6.05432 11.5571L1.25928 9.37756C-0.475078 8.58921 -0.40108 6.10086 1.37705 5.41697L15.0756 0.148301Z" fill="#0F3F4F"/>
+</svg>

+ 3 - 0
assets/icons/menu.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.00005 0.800049C1.33731 0.800049 0.800049 1.33731 0.800049 2.00005C0.800049 2.66279 1.33731 3.20005 2.00005 3.20005H18C18.6628 3.20005 19.2001 2.66279 19.2001 2.00005C19.2001 1.33731 18.6628 0.800049 18 0.800049H2.00005ZM0.800049 8.00005C0.800049 7.33731 1.33731 6.80005 2.00005 6.80005H18C18.6628 6.80005 19.2001 7.33731 19.2001 8.00005C19.2001 8.66279 18.6628 9.20005 18 9.20005H2.00005C1.33731 9.20005 0.800049 8.66279 0.800049 8.00005ZM0.800049 14C0.800049 13.3373 1.33731 12.8 2.00005 12.8H18C18.6628 12.8 19.2001 13.3373 19.2001 14C19.2001 14.6628 18.6628 15.2 18 15.2H2.00005C1.33731 15.2 0.800049 14.6628 0.800049 14Z" fill="#0F3F4F"/>
+</svg>

+ 4 - 0
assets/icons/radar.svg

@@ -0,0 +1,4 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.60066 1.79951C5.10216 0.675628 6.97459 0 9 0C13.9682 0 18 4.03176 18 9C18 13.9682 13.9682 18 9 18C4.03176 18 0 13.9682 0 9C0 7.37257 0.432023 5.84066 1.1981 4.51579C1.43377 4.10823 1.95477 3.9682 2.36301 4.20269L9.42545 8.25931C9.83452 8.49428 9.97566 9.01638 9.74069 9.42545C9.50572 9.83452 8.98362 9.97566 8.57455 9.74069L2.29438 6.13339C1.91613 7.01353 1.70836 7.98185 1.70836 9C1.70836 13.0247 4.97526 16.2916 9 16.2916C13.0247 16.2916 16.2916 13.0247 16.2916 9C16.2916 4.97526 13.0247 1.70836 9 1.70836C7.35977 1.70836 5.84355 2.25461 4.62436 3.16718C4.24669 3.44987 3.71136 3.37287 3.42867 2.9952C3.14598 2.61752 3.22298 2.0822 3.60066 1.79951Z" fill="#0F3F4F"/>
+<path d="M6.55297 3.80689C7.29827 3.44876 8.1337 3.2583 9.00009 3.2583C12.1681 3.2583 14.7418 5.83196 14.7418 8.99997C14.7418 12.168 12.1681 14.7416 9.00009 14.7416C5.83208 14.7416 3.25842 12.168 3.25842 8.99997C3.25842 7.94001 3.54533 6.9421 4.05479 6.08659C4.29616 5.68127 4.70284 5.83964 5.10816 6.08101C5.51349 6.32239 5.76397 6.55536 5.5226 6.96068C5.16861 7.55512 4.96678 8.25155 4.96678 8.99997C4.96678 11.2245 6.77558 13.0333 9.00009 13.0333C11.2246 13.0333 13.0334 11.2245 13.0334 8.99997C13.0334 6.77546 11.2246 4.96666 9.00009 4.96666C8.38395 4.96666 7.80203 5.10204 7.29288 5.3467C6.86767 5.55103 6.35733 5.37196 6.15301 4.94675C5.94869 4.52155 6.12776 4.01121 6.55297 3.80689Z" fill="#0F3F4F"/>
+</svg>

+ 3 - 0
assets/icons/search.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.00005 2.70005C5.52065 2.70005 2.70005 5.52065 2.70005 9.00005C2.70005 12.4794 5.52065 15.3 9.00005 15.3C10.6547 15.3 12.1581 14.6637 13.2831 13.6202C14.5253 12.4681 15.3 10.8254 15.3 9.00005C15.3 5.52065 12.4794 2.70005 9.00005 2.70005ZM0.300049 9.00005C0.300049 4.19517 4.19517 0.300049 9.00005 0.300049C13.8049 0.300049 17.7001 4.19517 17.7001 9.00005C17.7001 11.0721 16.9746 12.976 15.7659 14.4697L18.9476 17.6515C19.4163 18.1201 19.4163 18.8799 18.9476 19.3486C18.479 19.8172 17.7192 19.8172 17.2506 19.3486L14.0132 16.1112C12.5964 17.1115 10.866 17.7001 9.00005 17.7001C4.19517 17.7001 0.300049 13.8049 0.300049 9.00005Z" fill="#0F3F4F"/>
+</svg>

+ 1 - 1
metro.config.js

@@ -11,7 +11,7 @@ module.exports = (() => {
   };
   config.resolver = {
     ...resolver,
-    assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'),
+    assetExts: [...resolver.assetExts.filter((ext) => ext !== 'svg'), 'db'],
     sourceExts: [...resolver.sourceExts, 'svg']
   };
 

文件差異過大導致無法顯示
+ 1878 - 253
package-lock.json


+ 5 - 0
package.json

@@ -12,17 +12,21 @@
   "dependencies": {
     "@react-native-async-storage/async-storage": "1.18.2",
     "@react-native-community/datetimepicker": "7.2.0",
+    "@react-native-community/netinfo": "9.3.10",
     "@react-navigation/bottom-tabs": "^6.5.11",
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native-stack": "^6.9.17",
     "@react-navigation/stack": "^6.3.20",
     "@tanstack/react-query": "^5.8.3",
+    "@turf/turf": "^6.5.0",
     "axios": "^1.6.1",
     "dotenv": "^16.3.1",
     "expo": "~49.0.15",
     "expo-checkbox": "~2.4.0",
     "expo-image-picker": "~14.3.2",
+    "expo-location": "~16.1.0",
     "expo-splash-screen": "~0.20.5",
+    "expo-sqlite": "~11.3.3",
     "expo-status-bar": "~1.6.0",
     "formik": "^2.4.5",
     "moment": "^2.29.4",
@@ -31,6 +35,7 @@
     "react": "18.2.0",
     "react-native": "0.72.6",
     "react-native-calendar-picker": "^7.1.4",
+    "react-native-maps": "1.7.1",
     "react-native-safe-area-context": "4.6.3",
     "react-native-screens": "~3.22.0",
     "react-native-svg": "13.9.0",

+ 188 - 0
src/components/RegionPopup/index.tsx

@@ -0,0 +1,188 @@
+import { useEffect, useRef } from 'react';
+import { Text, TouchableOpacity, View, StyleSheet, Image, Animated } from 'react-native';
+
+interface Region {
+  id: number;
+  name: string;
+  region_photos: string;
+  visitors_count: number;
+}
+
+interface RegionPopupProps {
+  region: Region;
+  userAvatars: string[];
+  onMarkVisited: () => void;
+}
+
+const RegionPopup: React.FC<RegionPopupProps> = ({ region, userAvatars, onMarkVisited }) => {
+  const fadeAnim = useRef(new Animated.Value(0)).current;
+
+  useEffect(() => {
+    Animated.timing(fadeAnim, {
+      toValue: 1,
+      duration: 300,
+      useNativeDriver: true,
+    }).start();
+  }, [fadeAnim]);
+  
+  const splitRegionName = (fullName: string) => {
+    const parts = fullName.split(/ – | - /);
+    return {
+      regionTitle: parts[0],
+      regionSubtitle: parts.length > 1 ? parts[1] : '',
+    };
+  };
+
+  const { regionTitle, regionSubtitle } = splitRegionName(region.name);
+  const regionImg = JSON.parse(region.region_photos)[0];
+
+  function formatNumber(number: number) {
+    if (number >= 1000) {
+      return (number / 1000).toFixed(1) + 'k';
+    }
+    return number.toString();
+  }
+  
+  const formattedCount = formatNumber(region.visitors_count);
+
+  return (
+    <Animated.View style={[styles.popupContainer, {opacity: fadeAnim}]}>
+      <View style={styles.regionInfoContainer}>
+        {regionImg && (
+          <Image source={{ uri: regionImg}} style={styles.regionImage} />
+        )}
+        <View style={styles.regionTextContainer}>
+          <Text style={styles.regionTitle}>{regionTitle}</Text>
+          <Text style={styles.regionSubtitle}>{regionSubtitle}</Text>
+        </View>
+      </View>
+
+      <View style={styles.separator} />
+
+      <View style={styles.bottomContainer}>
+        <View style={styles.userContainer}>
+          <View style={styles.userImageContainer}>
+            {userAvatars?.map((avatar, index) => (
+              <Image key={index} source={{ uri: avatar }} style={styles.userImage} />
+            ))}
+            <View style={styles.userCountContainer}>
+              <Text style={styles.userCount}>{formattedCount}</Text>
+            </View>
+          </View>
+        </View>
+        <TouchableOpacity style={styles.markVisitedButton} onPress={onMarkVisited}>
+          <Text style={styles.markVisitedText}>Mark Visited</Text>
+        </TouchableOpacity>
+      </View>
+    </Animated.View>
+  );
+};
+
+export default RegionPopup;
+
+const styles = StyleSheet.create({
+  popupContainer: {
+    position: 'absolute',
+    bottom: 22,
+    left: 0,
+    right: 0,
+    backgroundColor: 'white',
+    padding: 16,
+    borderRadius: 8,
+    alignItems: 'center',
+    justifyContent: 'center',
+    height: 152,
+    marginHorizontal: 24,
+    shadowColor: 'rgba(33, 37, 41, 0.12)',
+    shadowOffset: { width: 0, height: 4 },
+    shadowRadius: 8,
+    elevation: 5,
+    zIndex: 2,
+  },
+  regionInfoContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'flex-start',
+    width: '100%',
+  },
+  regionImage: {
+    width: 60,
+    height: 60,
+    borderRadius: 6,
+    marginRight: 10,
+  },
+  regionTextContainer: {
+    justifyContent: 'center',
+    flex: 1,
+  },
+  regionTitle: {
+    fontSize: 16,
+    fontWeight: 'bold',
+    color: '#0F3F4F'
+  },
+  regionSubtitle: {
+    fontSize: 13,
+    color: '#3E6471',
+  },
+  separator: {
+    borderBottomWidth: 1,
+    borderBottomColor: '#E5E5E5',
+    width: '100%',
+    marginVertical: 16,
+  },
+  bottomContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    width: '100%',
+  },
+  userContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginLeft: 6,
+  },
+  userImageContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 10,
+  },
+  userImage: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    marginLeft: -6,
+    borderWidth: 1,
+    borderColor: '#E6E6E6',
+    resizeMode: 'cover',
+  },
+  userCountContainer: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: '#E5E5E5',
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: -6,
+  },
+  userCount: {
+    fontSize: 12,
+    color: '#0F3F4F',
+    lineHeight: 24,
+  },
+  markVisitedButton: {
+    backgroundColor: '#ED9334',
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    borderRadius: 6,
+    alignItems: 'center',
+    justifyContent: 'center',
+    height: 32,
+  },
+  markVisitedText: {
+    fontSize: 12,
+    color: 'white',
+    fontWeight: '700',
+    letterSpacing: 0.01,
+    lineHeight: 16,
+  },
+});

+ 0 - 0
src/components/RegionPopup/style.tsx


+ 73 - 0
src/components/TabBarButton/index.tsx

@@ -0,0 +1,73 @@
+import { Text, TouchableOpacity, StyleSheet, View, TouchableWithoutFeedback, Platform } from 'react-native';
+import { SvgProps } from 'react-native-svg';
+
+import { NAVIGATION_PAGES } from '../../types';
+
+import MapIcon from '../../../assets/icons/bottom-navigation/map.svg';
+import TravellersIcon from '../../../assets/icons/bottom-navigation/travellers.svg';
+import TravelsIcon from '../../../assets/icons/bottom-navigation/travels.svg';
+import ProfileIcon from '../../../assets/icons/bottom-navigation/profile.svg';
+import { useState } from 'react';
+
+const getTabIcon = (routeName: string) => {
+  switch (routeName) {
+    case NAVIGATION_PAGES.MAP_TAB:
+      return MapIcon;
+    case NAVIGATION_PAGES.TRAVELLERS_TAB:
+      return TravellersIcon;
+    case NAVIGATION_PAGES.TRAVELS_TAB:
+      return TravelsIcon;
+    case NAVIGATION_PAGES.PROFILE_TAB:
+      return ProfileIcon;
+    default:
+      return null;
+  }
+};
+
+const TabBarButton = ({ label, onPress, focused }: { label: string, onPress: () => void, focused: boolean }) => {
+  const [isPressed, setIsPressed] = useState(false);
+
+  const handlePressIn = () => {
+    setIsPressed(true);
+  };
+
+  const handlePressOut = () => {
+    setIsPressed(false);
+  };
+
+  const styles = StyleSheet.create({
+    buttonStyle: {
+      flex: 1,
+      alignItems: 'center',
+      justifyContent: 'center',
+      overflow: 'hidden',
+    },
+    labelStyle: {
+      color: focused ? '#0F3F4F' : '#C8C8C8',
+      marginTop: 4,
+      fontSize: 10,
+      // lineHeight: 12,
+      // ...Platform.select({
+      //   android: {
+      //     marginBottom: 6,
+      //   },
+      // }),
+    },
+  });
+
+  const IconComponent: React.FC<SvgProps> | null = getTabIcon(label);
+
+  let currentColor = focused ? '#0F3F4F' : '#C8C8C8';
+
+
+  return (
+    <TouchableWithoutFeedback onPress={onPress} onPressIn={handlePressIn} onPressOut={handlePressOut} style={{}}>
+      <View style={styles.buttonStyle}>
+        {IconComponent && <IconComponent width={24} height={24} fill={focused ? '#0F3F4F' : '#C8C8C8'} />}
+        <Text style={styles.labelStyle}>{label}</Text>
+      </View>
+    </TouchableWithoutFeedback>
+  );
+};
+
+export default TabBarButton;

+ 0 - 0
src/components/TabBarButton/style.tsx


+ 59 - 0
src/db/index.ts

@@ -0,0 +1,59 @@
+import * as SQLite from 'expo-sqlite';
+import * as FileSystem from 'expo-file-system';
+import { Asset } from 'expo-asset';
+
+let db1: SQLite.SQLiteDatabase | null = null;
+let db2: SQLite.SQLiteDatabase | null = null;
+
+async function copyDatabaseFile(dbName: string, dbAsset: Asset) {
+  await dbAsset.downloadAsync();
+  await FileSystem.downloadAsync(
+    dbAsset.uri,
+    FileSystem.documentDirectory + "SQLite/" + dbName
+  );
+  const dbUri = FileSystem.documentDirectory + `SQLite/${dbName}`;
+  console.log('localUri:', dbAsset.localUri);
+
+  await FileSystem.copyAsync({
+    from: dbAsset.localUri ?? '',
+    to: dbUri,
+  });
+
+  return dbUri;
+}
+
+export async function openDatabases() {
+  console.log('openDatabase - Start');
+
+  try {
+    const sqlDir = FileSystem.documentDirectory + "SQLite";
+    console.log('openDatabase - Checking SQLite directory');
+    const fileInfo = await FileSystem.getInfoAsync(sqlDir);
+
+    if (!fileInfo.exists) {
+      console.log('openDatabase - Creating SQLite directory');
+      await FileSystem.makeDirectoryAsync(sqlDir, { intermediates: true });
+      console.log('openDatabase - Downloading databases');
+      await copyDatabaseFile('nmRegions.db', Asset.fromModule(require('../../assets/db/nmRegions.db')));
+      await copyDatabaseFile('darePlaces.db', Asset.fromModule(require('../../assets/db/darePlaces.db')));
+
+      console.log('openDatabase - Databases downloaded');
+    } else {
+      console.log('openDatabase - Databases already exist');
+    }
+
+    const openDatabase = (dbName: string) => SQLite.openDatabase(dbName);
+    db1 = openDatabase("nmRegions.db");
+    db2 = openDatabase("darePlaces.db");
+  } catch (error) {
+    console.error('openDatabase - Error:', error);
+  }
+}
+
+export function getFirstDatabase() {
+  return db1;
+}
+
+export function getSecondDatabase() {
+  return db2;
+}

+ 467 - 0
src/screens/HomeScreen/index.tsx

@@ -0,0 +1,467 @@
+import {
+  StyleSheet,
+  View,
+  Platform,
+  TouchableOpacity,
+  Text
+} from 'react-native';
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import MapView, { UrlTile, Geojson } from 'react-native-maps';
+import * as turf from '@turf/turf';
+import * as FileSystem from 'expo-file-system';
+import * as Location from 'expo-location';
+
+import MenuIcon from '../../../assets/icons/menu.svg';
+import SearchIcon from '../../../assets/icons/search.svg';
+import RadarIcon from '../../../assets/icons/radar.svg';
+import LocationIcon from '../../../assets/icons/location.svg';
+import CloseSvg from '../../../assets/icons/close.svg';
+
+import regions from '../../../assets/geojson/nm2022.json'
+import dareRegions from '../../../assets/geojson/mqp.json'
+
+import NetInfo from "@react-native-community/netinfo";
+import { getFirstDatabase, getSecondDatabase } from '../../db';
+import RegionPopup from '../../components/RegionPopup';
+
+import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
+import { SQLiteDatabase } from 'expo-sqlite';
+import { FeatureCollection } from '@turf/turf';
+
+const tilesBaseURL = 'https://maps.nomadmania.com/tiles_osm';
+const localTileDir = `${FileSystem.cacheDirectory}tiles`;
+
+const gridUrl = 'https://maps.nomadmania.com/tiles_nm/grid';
+const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
+
+const visitedTiles = 'https://maps.nomadmania.com/tiles_nm/user_visited/51363';
+const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`;
+
+const dareTiles = 'https://maps.nomadmania.com/tiles_nm/regions_mqp';
+const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
+
+interface RegionData {
+  type?: string;
+  name?: string;
+  crs?: {
+    type: string;
+    properties: { name: string; };
+  };
+  features?: any;
+}
+
+interface Region {
+  id: number;
+  name: string;
+  region_photos: string;
+  visitors_count: number;
+}
+
+interface Feature {
+  geometry: turf.helpers.Geometry;
+  properties: {
+    id: number;
+    fill?: string;
+    stroke?: string;
+  };
+  type: 'Feature';
+}
+
+interface GeojsonRegion {
+  type: 'FeatureCollection';
+  features: Feature[];
+}
+
+type HomeScreenNavigationProp = BottomTabNavigationProp<any>;
+interface HomeScreenProps {
+  navigation: HomeScreenNavigationProp;
+}
+
+const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
+  const mapRef = useRef<MapView>(null);
+
+  const [isConnected, setIsConnected] = useState<boolean | null>(true);
+  const [selectedRegion, setSelectedRegion] = useState(null);
+  const [location, setLocation] = useState(null);
+  const [popupVisible, setPopupVisible] = useState<boolean | null>(false);
+  const [regionData, setRegionData] = useState<Region | null>(null);
+  const [userAvatars, setUserAvatars] = useState<string[]>([]);
+
+  useEffect(() => {
+    navigation.setOptions({
+      tabBarStyle: {
+        display: popupVisible ? 'none' : 'flex',
+        position: 'absolute',
+        ...Platform.select({
+          android: {
+            height: 58,
+          },
+        }),
+      }
+    });
+  }, [popupVisible, navigation]);
+
+  // useEffect(() => {
+  //   (async () => {
+      
+  //     let { status } = await Location.requestForegroundPermissionsAsync();
+  //     if (status !== 'granted') {
+  //       return;
+  //     }
+
+  //     let location = await Location.getCurrentPositionAsync({});
+  //     setLocation({
+  //       latitude: location.coords.latitude,
+  //       longitude: location.coords.longitude,
+  //       latitudeDelta: 5,
+  //       longitudeDelta: 5,
+  //     });
+  //   })();
+  // }, []);
+
+  const getData = async (db: SQLiteDatabase | null, regionId: number, name: string) => {
+    return new Promise<void>((resolve, reject) => {
+      db?.transaction(tx => {
+        tx.executeSql(
+          `SELECT * FROM ${name} WHERE id = ${regionId};`,
+          [],
+          (_, { rows }) => {
+            setRegionData(rows._array[0]);
+  
+            const avatarPromises = JSON.parse(rows._array[0].visitors_avatars)?.map((avatarId: number) => {
+              return new Promise((resolveAvatar, rejectAvatar) => {
+                tx.executeSql(
+                  `SELECT * FROM avatars WHERE id = ${avatarId};`,
+                  [],
+                  (_, { rows }) => {
+                    resolveAvatar(rows._array[0].data);
+                  },
+                  (_, error) => {
+                    console.error('error', error);
+                    reject(error);
+                    return false;
+                  }
+                );
+              });
+            });
+  
+            Promise.all(avatarPromises)
+              .then(avatars => {
+                setUserAvatars(avatars);
+                resolve();
+              })
+              .catch(error => {
+                console.error('Error processing avatars', error);
+                reject(error);
+              });
+          },
+          (_, error) => {
+            console.error('error', error);
+            reject(error);
+            return false;
+          }
+        );
+      });
+    });
+  }
+
+  const handleMapPress = async (event: { nativeEvent: { coordinate: { latitude: any; longitude: any; }; }; }) => {
+    const { latitude, longitude } = event.nativeEvent.coordinate;
+    const point = turf.point([longitude, latitude]);
+    setUserAvatars([]);
+
+    const findRegion = (dataset: RegionData) => {
+      return dataset.features.find((region: any) => {
+        const coordinates = region?.geometry?.coordinates;
+        const type = region?.geometry?.type;
+
+        if (!Array.isArray(coordinates) || coordinates.length === 0) {
+          return false;
+        }
+
+        try {
+          const polygon: any = type === 'Polygon'
+            ? turf.polygon(coordinates)
+            : turf.multiPolygon(coordinates);
+
+          return turf.booleanPointInPolygon(point, polygon);
+        } catch (error) {
+          console.error('Error creating polygon:', error);
+          return false;
+        }
+      });
+    };
+
+    let db = getSecondDatabase();
+    let tableName = 'places';
+
+    let foundRegion = findRegion(dareRegions);
+
+    if (!foundRegion) {
+      foundRegion = findRegion(regions);
+      db = getFirstDatabase();
+      tableName = 'regions';
+    }
+    if (foundRegion) {
+      const id = foundRegion.properties.id;
+
+
+      // console.log('foundRegion', foundRegion)
+  
+      setSelectedRegion({
+        type: 'FeatureCollection',
+        features: [{
+          geometry: foundRegion.geometry,
+          properties: {
+            ...foundRegion.properties,
+            fill: "rgba(57, 115, 172, 0.2)",
+            stroke: "#3973ac",
+          },
+          type: 'Feature',
+        }]
+      });
+
+      await getData(db, id, tableName).then(() => {
+        setPopupVisible(true)
+      });
+
+      const bounds = turf.bbox(foundRegion);
+      const padding = 1;
+
+      const region = {
+        latitude: (bounds[1] + bounds[3]) / 2,
+        longitude: (bounds[0] + bounds[2]) / 2,
+        latitudeDelta: Math.abs(bounds[3] - bounds[1]) + padding,
+        longitudeDelta: Math.abs(bounds[2] - bounds[0]) + padding,
+      };
+  
+      mapRef.current?.animateToRegion(region, 1000);
+    } else {
+      handleClosePopup();
+    }
+  };
+
+  useEffect(() => {
+    const unsubscribe = NetInfo.addEventListener(state => {
+      setIsConnected(state.isConnected);
+    });
+  
+    return () => unsubscribe();
+  }, []);
+
+  const renderLocalTiles = () => {
+    return (
+      <UrlTile
+        urlTemplate={`${tilesBaseURL}/{z}/{x}/{y}`}
+        maximumZ={15}
+        maximumNativeZ={13}
+        tileCachePath={`${localTileDir}`}
+        shouldReplaceMapContent
+        minimumZ={0}
+        offlineMode={!isConnected}
+        opacity={1}
+        zIndex={1}
+      />
+    );
+  };
+
+  const renderGridTiles = () => {
+    return (
+      <UrlTile
+        urlTemplate={`${gridUrl}/{z}/{x}/{y}`}
+        maximumZ={15}
+        maximumNativeZ={13}
+        tileCachePath={`${localGridDir}`}
+        shouldReplaceMapContent
+        minimumZ={0}
+        offlineMode={!isConnected}
+        opacity={1}
+        zIndex={2}
+      />
+    );
+  };
+
+  const renderVisitedTiles = () => {
+    return (
+      <UrlTile
+        urlTemplate={`${visitedTiles}/{z}/{x}/{y}`}
+        maximumZ={15}
+        maximumNativeZ={13}
+        tileCachePath={`${localVisitedDir}`}
+        shouldReplaceMapContent
+        minimumZ={0}
+        offlineMode={!isConnected}
+        opacity={0.5}
+        zIndex={2}
+      />
+    );
+  };
+
+  const renderDareTiles = () => {
+    return (
+      <UrlTile
+        urlTemplate={`${dareTiles}/{z}/{x}/{y}`}
+        maximumZ={15}
+        maximumNativeZ={13}
+        tileCachePath={`${localDareDir}`}
+        shouldReplaceMapContent
+        minimumZ={0}
+        offlineMode={!isConnected}
+        opacity={0.5}
+        zIndex={2}
+      />
+    );
+  };
+
+  function renderGeoJSON() {
+    if (!selectedRegion) return null;
+
+    return (
+        <Geojson
+          geojson={selectedRegion}
+          fillColor="rgba(57, 115, 172, 0.2)"
+          strokeColor="#3973ac"
+          strokeWidth={Platform.OS == 'android' ? 3 : 2}
+          zIndex={3}
+        />
+    );
+  };
+
+  const handleClosePopup = () => {
+    setPopupVisible(false);
+    setSelectedRegion(null);
+  }
+
+  const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]);
+
+  return (
+    <View style={styles.container}>
+      <MapView
+        ref={mapRef}
+        // initialRegion={location}
+        // showsUserLocation={true}
+        showsMyLocationButton={false}
+        showsCompass={false}
+        zoomControlEnabled={false}
+        onPress={handleMapPress}
+        style={styles.map}
+        mapType={Platform.OS == 'android' ? 'none' : 'standard'}
+        offlineMode={!isConnected}
+        maxZoomLevel={15}
+        minZoomLevel={0}
+      >
+        {renderLocalTiles()}
+        {renderedGeoJSON}
+        {renderGridTiles()}
+        {renderVisitedTiles()}
+        {renderDareTiles()}
+      </MapView>
+
+      {!popupVisible ? (
+        <>
+          <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton]}>
+            <MenuIcon />
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]}>
+            <SearchIcon />
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.cornerButton, styles.bottomLeftButton]}>
+            <RadarIcon />
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.cornerButton, styles.bottomRightButton]}>
+            <LocationIcon />
+          </TouchableOpacity>
+        </>
+      ) : (
+        <>
+          <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]} onPress={handleClosePopup}>
+            <CloseSvg fill="white" width={13} height={13} />
+            <Text style={{fontSize: 12, color: 'white', fontWeight: '500', lineHeight: 14}}>Close</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.cornerButton, styles.topRightButton, {width: 42, height: 42, borderRadius: 21,}]}>
+            <LocationIcon />
+          </TouchableOpacity>
+
+          <RegionPopup 
+            region={regionData}
+            userAvatars={userAvatars}
+            onMarkVisited={() => console.log('Mark as visited')} 
+          />
+        </>
+      )}
+    </View>
+  );
+}
+
+export default HomeScreen;
+
+const styles = StyleSheet.create({
+  container: {
+    ...StyleSheet.absoluteFillObject,
+    alignItems: 'center',
+    justifyContent: 'flex-end',
+  },
+  map: {
+    ...StyleSheet.absoluteFillObject,
+  },
+  btn: {
+    marginBottom: 5,
+  },
+  button: {
+    backgroundColor: '#007bff',
+    padding: 10,
+    borderRadius: 5,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  cornerButton: {
+    position: 'absolute',
+    backgroundColor: 'rgba(255, 255, 255, 1)',
+    padding: 12,
+    width: 48,
+    height: 48,
+    borderRadius: 24,
+    alignItems: 'center',
+    justifyContent: 'center',
+    shadowColor: 'rgba(33, 37, 41, 0.12)',
+    shadowOffset: { width: 0, height: 4 },
+    shadowRadius: 8,
+    elevation: 5,
+  },
+  topLeftButton: {
+    top: 44,
+    left: 16,
+  },
+  closeLeftButton: {
+    backgroundColor: 'rgba(33, 37, 41, 0.78)',
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    width: 81,
+    height: 36,
+    borderRadius: 18,
+    flexDirection: 'row',
+    gap: 6,
+  },
+  topRightButton: {
+    top: 44,
+    right: 16,
+  },
+  bottomLeftButton: {
+    bottom: Platform.OS == 'android' ? 80 : 100,
+    left: 16,
+    width: 42,
+    height: 42,
+    borderRadius: 21,
+  },
+  bottomRightButton: {
+    bottom: Platform.OS == 'android' ? 80 : 100,
+    right: 16,
+    width: 42,
+    height: 42,
+    borderRadius: 21,
+  },
+});

+ 0 - 0
src/screens/HomeScreen/style.tsx


+ 6 - 0
src/screens/WelcomeScreen/index.tsx

@@ -37,6 +37,12 @@ const WelcomeScreen: FC<Props> = ({ navigation }) => {
               >
                 Login
               </Button>
+              <Button
+                onPress={() => navigation.navigate(NAVIGATION_PAGES.IN_APP)}
+                variant={ButtonVariants.OPACITY}
+              >
+                Without registration
+              </Button>
             </View>
           </View>
         </SafeAreaView>

+ 2 - 1
src/types/navigation.ts

@@ -6,7 +6,8 @@ export enum NAVIGATION_PAGES {
   RESET_PASSWORD = 'resetPassword',
   RESET_PASSWORD_DEEP = 'resetPasswordDeep',
   IN_APP = 'inAppStack',
-  LOCATION_TAB = 'Location',
+  MAP_TAB = 'Map',
   TRAVELS_TAB = 'Travels',
+  TRAVELLERS_TAB = 'Travellers',
   PROFILE_TAB = 'Profile'
 }

部分文件因文件數量過多而無法顯示