Repository: bcye/mapvoyage
Pull request #1: Stack Navigation and Detail Page Rendering
Author: login: bcye
Created: 2025-03-03T12:12:56Z | Merged: 2025-03-03T12:21:48Z

- **split wikitext function, add link component**
- **add single tmux start dev command**
- **move stack into bottom sheet, use card for card ui**
- **display section content as markdown**


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Introduced an interactive map interface with gesture support and a bottom sheet, enhancing navigation and map interactions.
	- Launched a dynamic content view that fetches and renders external article sections with smooth loading indicators.

- **Refactor**
	- Redesigned the information display into a modern, card-based layout for clearer, more intuitive navigation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/app/_layout.tsx b/app/app/_layout.tsx
index 43a3048..4920bfb 100644
--- a/app/app/_layout.tsx
+++ b/app/app/_layout.tsx
@@ -1,7 +1,19 @@
+import { useMapStore } from "@/utils/store";
 import { trpc } from "@/utils/trpc";
+import BottomSheet from "@gorhom/bottom-sheet";
+import {
+  Camera,
+  LocationPuck,
+  MapState,
+  MapView,
+  VectorSource,
+  setAccessToken,
+} from "@rnmapbox/maps";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { httpBatchLink } from "@trpc/client";
 import { Stack } from "expo-router";
+import { StyleSheet } from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
 
 const queryClient = new QueryClient();
 const trpcClient = trpc.createClient({
@@ -12,12 +24,63 @@ const trpcClient = trpc.createClient({
   ],
 });
 
+// The map is inside the layout component, such that the stack is inside the bottom sheet.
+
 export default function RootLayout() {
   return (
     <trpc.Provider client={trpcClient} queryClient={queryClient}>
       <QueryClientProvider client={queryClient}>
-        <Stack />
+        <MapLayout>
+          <Stack screenOptions={{}} />
+        </MapLayout>
       </QueryClientProvider>
     </trpc.Provider>
   );
 }
+
+// set a stub access token as were using maptiler
+setAccessToken?.("adsfwads");
+
+const snapPoints = ["20%", "40%", "100%"];
+
+function MapLayout({ children }: { children: React.ReactNode }) {
+  const { setRegion } = useMapStore();
+
+  function onIdle(state: MapState) {
+    setRegion(state.properties);
+  }
+
+  return (
+    <GestureHandlerRootView style={styles.container}>
+      <MapView
+        style={styles.map}
+        styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+        onMapIdle={onIdle}
+      >
+        <Camera followUserLocation={true} />
+        <LocationPuck visible={true} />
+        <VectorSource
+          id="maptiler"
+          url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+        />
+      </MapView>
+      <BottomSheet
+        index={1}
+        snapPoints={snapPoints}
+        enableDynamicSizing={false}
+      >
+        {children}
+      </BottomSheet>
+    </GestureHandlerRootView>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  map: {
+    width: "100%",
+    height: "100%",
+  },
+});
diff --git a/app/app/index.tsx b/app/app/index.tsx
index 46a62de..78f5f85 100644
--- a/app/app/index.tsx
+++ b/app/app/index.tsx
@@ -1,51 +1,9 @@
 import Infobox from "@/components/infobox";
-import {
-  Camera,
-  LocationPuck,
-  MapState,
-  MapView,
-  VectorSource,
-  setAccessToken,
-} from "@rnmapbox/maps";
-import { useState } from "react";
-import { StyleSheet, View } from "react-native";
-import { GestureHandlerRootView } from "react-native-gesture-handler";
-
-// set a stub access token as were using maptiler
-setAccessToken?.("adsfwads");
+import { useMapStore } from "@/utils/store";
+import { View } from "react-native";
 
 export default function Index() {
-  const [region, setRegion] = useState<MapState["properties"] | null>(null);
-
-  function onIdle(state: MapState) {
-    setRegion(state.properties);
-  }
+  const { region } = useMapStore();
 
-  return (
-    <GestureHandlerRootView style={styles.container}>
-      <MapView
-        style={styles.map}
-        styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
-        onMapIdle={onIdle}
-      >
-        <Camera followUserLocation={true} />
-        <LocationPuck visible={true} />
-        <VectorSource
-          id="maptiler"
-          url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
-        />
-      </MapView>
-      {region && <Infobox region={region} />}
-    </GestureHandlerRootView>
-  );
+  return region ? <Infobox region={region} /> : <View />;
 }
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  map: {
-    width: "100%",
-    height: "100%",
-  },
-});
diff --git a/app/app/page/[pageId]/section/[title].tsx b/app/app/page/[pageId]/section/[title].tsx
new file mode 100644
index 0000000..cb25c29
--- /dev/null
+++ b/app/app/page/[pageId]/section/[title].tsx
@@ -0,0 +1,44 @@
+import { trpc } from "@/utils/trpc";
+import { Stack, useLocalSearchParams } from "expo-router";
+import { SafeAreaView, ScrollView } from "react-native";
+import { View, Text, SkeletonView } from "react-native-ui-lib";
+import Markdown from "react-native-markdown-display";
+import { useMemo } from "react";
+import wtf from "wtf_wikipedia";
+import mdPlugin from "wtf-plugin-markdown";
+import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+
+wtf.extend(mdPlugin);
+
+export default function Section() {
+  const { title, pageId } = useLocalSearchParams();
+  const wikiQuery = trpc.getWikitext.useQuery({ pageId: parseInt(pageId) });
+  const sectionWikitext = useMemo(
+    () => (wikiQuery.data ? wtf(wikiQuery.data).section(title) : null),
+    [wikiQuery.data, title],
+  );
+
+  const displayMarkdown = useMemo<string | null>(() => {
+    if (sectionWikitext) {
+      //@ts-ignore ts files seem to not work
+      const md: string = sectionWikitext.markdown();
+
+      // we dont want to redundantly display the section title
+      const firstNL = md.indexOf("\n");
+      return md.slice(firstNL + 1);
+    } else return null;
+  }, [sectionWikitext]);
+
+  return (
+    <View padding-8 flex>
+      <Stack.Screen options={{ title }} />
+      <SkeletonView
+        template={SkeletonView.templates.TEXT_CONTENT}
+        showContent={wikiQuery.isSuccess}
+        renderContent={() =>
+          !!displayMarkdown && <Markdown>{displayMarkdown}</Markdown>
+        }
+      />
+    </View>
+  );
+}
diff --git a/app/components/infobox.tsx b/app/components/infobox.tsx
index 5205efc..bcb51fa 100644
--- a/app/components/infobox.tsx
+++ b/app/components/infobox.tsx
@@ -1,21 +1,11 @@
 import { trpc } from "@/utils/trpc";
-import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet";
 import { MapState } from "@rnmapbox/maps";
 import { Bbox } from "@server/types/maptiler";
-import React from "react";
-import { useMemo } from "react";
-import {
-  GridList,
-  GridView,
-  Icon,
-  SkeletonView,
-  Text,
-  View,
-} from "react-native-ui-lib";
+import { Link, Stack } from "expo-router";
+import React, { useMemo } from "react";
+import { Card, SkeletonView, View } from "react-native-ui-lib";
 import { useDebounce } from "use-debounce";
 
-const snapPoints = ["20%", "40%", "100%"];
-
 export default function Infobox({
   region,
 }: {
@@ -36,42 +26,46 @@ export default function Infobox({
   );
   const wikiQuery = trpc.getPage.useQuery(query);
 
+  console.log(!!wikiQuery.data, wikiQuery.data);
+
   return (
-    <BottomSheet index={1} snapPoints={snapPoints} enableDynamicSizing={false}>
-      <BottomSheetScrollView>
-        <View padding-8>
-          <Text text40M center>
-            {wikiQuery.data?.title ?? "Loading"}
-          </Text>
-          {false ? (
-            <SkeletonView
-              template={SkeletonView.templates.TEXT_CONTENT}
-              showContent={wikiQuery.isLoading}
-            />
-          ) : (
-            <>
-              <View flex row gap-4 marginT-12>
-                <Infocard title="Understand" />
-                <Infocard title="History" />
+    <View padding-8 flex>
+      <Stack.Screen options={{ title: wikiQuery.data?.title ?? "Loading" }} />
+      <SkeletonView
+        template={SkeletonView.templates.TEXT_CONTENT}
+        showContent={wikiQuery.isSuccess}
+        renderContent={() =>
+          !!wikiQuery.data && (
+            <View flex>
+              <View row gap-8 marginT-12>
+                <Infocard pageId={wikiQuery.data.pageId} title="Understand" />
+                <Infocard pageId={wikiQuery.data.pageId} title="History" />
               </View>
-              <View flex row gap-4 marginT-12>
-                <Infocard title="See" />
-                <Infocard title="Eat" />
-                <Infocard title="Drink" />
+              <View row gap-8 marginT-8>
+                <Infocard pageId={wikiQuery.data.pageId} title="See" />
+                <Infocard pageId={wikiQuery.data.pageId} title="Eat" />
+                <Infocard pageId={wikiQuery.data.pageId} title="Drink" />
               </View>
-            </>
-          )}
-        </View>
-      </BottomSheetScrollView>
-    </BottomSheet>
+            </View>
+          )
+        }
+      />
+    </View>
   );
 }
 
-function Infocard({ title, icon }: { title: string; icon?: string }) {
+function Infocard({ title, pageId }: { title: string; pageId: string }) {
   return (
-    <View bg-$backgroundNeutralMedium padding-6 br30 flex height={50}>
-      {icon && <Icon source={icon} />}
-      <Text text60L>{title}</Text>
-    </View>
+    <Link
+      asChild
+      href={{
+        pathname: "/page/[pageId]/section/[title]",
+        params: { pageId, title },
+      }}
+    >
+      <Card flex padding-12 height={48}>
+        <Card.Section content={[{ text: title, text60: true, grey10: true }]} />
+      </Card>
+    </Link>
   );
 }
diff --git a/app/package-lock.json b/app/package-lock.json
index f23712e..da2f4cc 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -35,13 +35,18 @@
         "react-native": "0.76.3",
         "react-native-gesture-handler": "~2.20.2",
         "react-native-linear-gradient": "^2.8.3",
+        "react-native-markdown-display": "^7.0.2",
         "react-native-reanimated": "~3.16.1",
         "react-native-safe-area-context": "4.12.0",
         "react-native-screens": "~4.1.0",
+        "react-native-shimmer-placeholder": "^2.0.9",
         "react-native-ui-lib": "^7.34.5",
         "react-native-web": "~0.19.13",
         "react-native-webview": "13.12.5",
-        "use-debounce": "^10.0.4"
+        "use-debounce": "^10.0.4",
+        "wtf_wikipedia": "^10.4.0",
+        "wtf-plugin-markdown": "^1.0.1",
+        "zustand": "^5.0.3"
       },
       "devDependencies": {
         "@babel/core": "^7.25.2",
@@ -4457,6 +4462,12 @@
       "version": "7.0.15",
       "license": "MIT"
     },
+    "node_modules/@types/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.10.1",
       "license": "MIT",
@@ -4471,6 +4482,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "license": "MIT"
+    },
     "node_modules/@types/prop-types": {
       "version": "15.7.14",
       "devOptional": true,
@@ -4944,6 +4961,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/asap": {
       "version": "2.0.6",
       "license": "MIT"
@@ -5530,6 +5556,57 @@
         "node": ">=6"
       }
     },
+    "node_modules/camelcase-keys": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz",
+      "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
+      "license": "MIT",
+      "dependencies": {
+        "camelcase": "^6.3.0",
+        "map-obj": "^4.1.0",
+        "quick-lru": "^5.1.1",
+        "type-fest": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/camelcase-keys/node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/camelcase-keys/node_modules/type-fest": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/camelize": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+      "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/caniuse-lite": {
       "version": "1.0.30001687",
       "funding": [
@@ -6019,6 +6096,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/css-color-keywords": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+      "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/css-in-js-utils": {
       "version": "3.1.0",
       "license": "MIT",
@@ -6026,6 +6112,17 @@
         "hyphenate-style-name": "^1.0.3"
       }
     },
+    "node_modules/css-to-react-native": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+      "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "camelize": "^1.0.0",
+        "css-color-keywords": "^1.0.0",
+        "postcss-value-parser": "^4.0.2"
+      }
+    },
     "node_modules/csscolorparser": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
@@ -6115,6 +6212,52 @@
         }
       }
     },
+    "node_modules/decamelize": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
+      "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/decamelize-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+      "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
+      "license": "MIT",
+      "dependencies": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/decamelize-keys/node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/decamelize-keys/node_modules/map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/decimal.js": {
       "version": "10.4.3",
       "dev": true,
@@ -7532,6 +7675,15 @@
       "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
       "license": "ISC"
     },
+    "node_modules/hard-rejection": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
+      "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "license": "MIT",
@@ -8000,6 +8152,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-plain-object": {
       "version": "2.0.4",
       "license": "MIT",
@@ -8077,6 +8238,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/isomorphic-unfetch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+      "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "node-fetch": "^2.6.1",
+        "unfetch": "^4.2.0"
+      }
+    },
     "node_modules/istanbul-lib-coverage": {
       "version": "3.2.2",
       "license": "BSD-3-Clause",
@@ -9047,7 +9218,6 @@
     },
     "node_modules/json-parse-even-better-errors": {
       "version": "2.3.1",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/json-schema-traverse": {
@@ -9186,6 +9356,15 @@
       "version": "1.2.4",
       "license": "MIT"
     },
+    "node_modules/linkify-it": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
+      "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^1.0.1"
+      }
+    },
     "node_modules/loader-runner": {
       "version": "4.3.0",
       "dev": true,
@@ -9329,6 +9508,18 @@
         "tmpl": "1.0.5"
       }
     },
+    "node_modules/map-obj": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+      "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/mapbox-gl": {
       "version": "2.15.0",
       "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz",
@@ -9359,6 +9550,28 @@
         "vt-pbf": "^3.1.3"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
+      "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^1.0.7",
+        "entities": "~2.0.0",
+        "linkify-it": "^2.0.0",
+        "mdurl": "^1.0.1",
+        "uc.micro": "^1.0.5"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.js"
+      }
+    },
+    "node_modules/markdown-it/node_modules/entities": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
+      "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
+      "license": "BSD-2-Clause"
+    },
     "node_modules/marky": {
       "version": "1.2.5",
       "license": "Apache-2.0"
@@ -9385,10 +9598,63 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/mdurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
+      "license": "MIT"
+    },
     "node_modules/memoize-one": {
       "version": "5.2.1",
       "license": "MIT"
     },
+    "node_modules/meow": {
+      "version": "10.1.5",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz",
+      "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/minimist": "^1.2.2",
+        "camelcase-keys": "^7.0.0",
+        "decamelize": "^5.0.0",
+        "decamelize-keys": "^1.1.0",
+        "hard-rejection": "^2.1.0",
+        "minimist-options": "4.1.0",
+        "normalize-package-data": "^3.0.2",
+        "read-pkg-up": "^8.0.0",
+        "redent": "^4.0.0",
+        "trim-newlines": "^4.0.2",
+        "type-fest": "^1.2.2",
+        "yargs-parser": "^20.2.9"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/meow/node_modules/type-fest": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/meow/node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/merge-stream": {
       "version": "2.0.0",
       "license": "MIT"
@@ -9781,6 +10047,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "license": "ISC",
@@ -9798,6 +10073,20 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/minimist-options": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
+      "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+      "license": "MIT",
+      "dependencies": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0",
+        "kind-of": "^6.0.3"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/minipass": {
       "version": "7.1.2",
       "license": "ISC",
@@ -10022,6 +10311,45 @@
       "version": "2.0.18",
       "license": "MIT"
     },
+    "node_modules/normalize-package-data": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+      "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^4.0.1",
+        "is-core-module": "^2.5.0",
+        "semver": "^7.3.4",
+        "validate-npm-package-license": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/hosted-git-info": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+      "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "license": "MIT",
@@ -10361,6 +10689,34 @@
         "node": ">=8"
       }
     },
+    "node_modules/path-exists-cli": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists-cli/-/path-exists-cli-2.0.0.tgz",
+      "integrity": "sha512-qGr0A87KYCznmvabblxyxnzA/MtPZ28wH+4SCMP4tjTFAbzqwvs5xpUZExAYzq5OgHe5vIswzdH5iosCb8YF/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "meow": "^10.1.1",
+        "path-exists": "^5.0.0"
+      },
+      "bin": {
+        "path-exists": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/path-exists-cli/node_modules/path-exists": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+      "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      }
+    },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
       "license": "MIT",
@@ -10773,6 +11129,18 @@
       ],
       "license": "MIT"
     },
+    "node_modules/quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/quickselect": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
@@ -10977,6 +11345,15 @@
         }
       }
     },
+    "node_modules/react-native-fit-image": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
+      "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
+      "license": "Beerware",
+      "dependencies": {
+        "prop-types": "^15.5.10"
+      }
+    },
     "node_modules/react-native-gesture-handler": {
       "version": "2.20.2",
       "license": "MIT",
@@ -11021,6 +11398,22 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-markdown-display": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz",
+      "integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==",
+      "license": "MIT",
+      "dependencies": {
+        "css-to-react-native": "^3.0.0",
+        "markdown-it": "^10.0.0",
+        "prop-types": "^15.7.2",
+        "react-native-fit-image": "^1.5.5"
+      },
+      "peerDependencies": {
+        "react": ">=16.2.0",
+        "react-native": ">=0.50.4"
+      }
+    },
     "node_modules/react-native-reanimated": {
       "version": "3.16.4",
       "license": "MIT",
@@ -11081,6 +11474,16 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-shimmer-placeholder": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/react-native-shimmer-placeholder/-/react-native-shimmer-placeholder-2.0.9.tgz",
+      "integrity": "sha512-s2pfAAO6uaybREYM2KcaxP3c2XlZvLfHfzmhMlGEoOhSKaoM92KpA1Hx0BeC75yilkliJuowqO/hFzvLB0h7hg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "prop-types": ">=15.6.0",
+        "react-native-linear-gradient": ">=2.4.0"
+      }
+    },
     "node_modules/react-native-ui-lib": {
       "version": "7.34.5",
       "resolved": "https://registry.npmjs.org/react-native-ui-lib/-/react-native-ui-lib-7.34.5.tgz",
@@ -11246,6 +11649,83 @@
         "react": "^18.3.1"
       }
     },
+    "node_modules/read-pkg": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz",
+      "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^3.0.2",
+        "parse-json": "^5.2.0",
+        "type-fest": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz",
+      "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^5.0.0",
+        "read-pkg": "^6.0.0",
+        "type-fest": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/type-fest": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/readable-stream": {
       "version": "2.3.8",
       "license": "MIT",
@@ -11276,6 +11756,34 @@
         "node": ">= 4"
       }
     },
+    "node_modules/redent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz",
+      "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==",
+      "license": "MIT",
+      "dependencies": {
+        "indent-string": "^5.0.0",
+        "strip-indent": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/redent/node_modules/indent-string": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+      "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "license": "MIT"
@@ -11808,6 +12316,38 @@
         "source-map": "^0.6.0"
       }
     },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.21",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+      "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+      "license": "CC0-1.0"
+    },
     "node_modules/split": {
       "version": "1.0.1",
       "license": "MIT",
@@ -12017,6 +12557,21 @@
         "node": ">=6"
       }
     },
+    "node_modules/strip-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz",
+      "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==",
+      "license": "MIT",
+      "dependencies": {
+        "min-indent": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/strip-json-comments": {
       "version": "2.0.1",
       "license": "MIT",
@@ -12475,6 +13030,18 @@
       "version": "0.0.3",
       "license": "MIT"
     },
+    "node_modules/trim-newlines": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz",
+      "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/ts-interface-checker": {
       "version": "0.1.13",
       "license": "Apache-2.0"
@@ -12542,6 +13109,12 @@
         "node": "*"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+      "license": "MIT"
+    },
     "node_modules/undici": {
       "version": "6.21.0",
       "license": "MIT",
@@ -12553,6 +13126,12 @@
       "version": "6.20.0",
       "license": "MIT"
     },
+    "node_modules/unfetch": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+      "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==",
+      "license": "MIT"
+    },
     "node_modules/unicode-canonical-property-names-ecmascript": {
       "version": "2.0.1",
       "license": "MIT",
@@ -12749,6 +13328,16 @@
         "node": ">=10.12.0"
       }
     },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
     "node_modules/validate-npm-package-name": {
       "version": "5.0.1",
       "license": "ISC",
@@ -13172,6 +13761,32 @@
         "async-limiter": "~1.0.0"
       }
     },
+    "node_modules/wtf_wikipedia": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/wtf_wikipedia/-/wtf_wikipedia-10.4.0.tgz",
+      "integrity": "sha512-yRxTiBURj2LW5HWAe+T7bCV2x45C/qTqcknUTmInKmB9cmLSxR6Nh44rB9K+nfNiydtjc3HLHwYWxMuHZtpVSQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "isomorphic-unfetch": "^3.1.0",
+        "path-exists-cli": "2.0.0"
+      },
+      "bin": {
+        "wtf_wikipedia": "cli.js"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/wtf-plugin-markdown": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/wtf-plugin-markdown/-/wtf-plugin-markdown-1.0.1.tgz",
+      "integrity": "sha512-OheTSY2DV0T1KcNedFCiq6BsxqNprdbKKzFPaOli29Frc6k+CUuExeGYLxYelKcpeJJYrPzPReq+CCKhYBSXPQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "wtf_wikipedia": ">=9.0.0"
+      }
+    },
     "node_modules/xcode": {
       "version": "3.0.1",
       "license": "Apache-2.0",
@@ -13271,6 +13886,35 @@
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
+    },
+    "node_modules/zustand": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+      "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=18.0.0",
+        "immer": ">=9.0.6",
+        "react": ">=18.0.0",
+        "use-sync-external-store": ">=1.2.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "use-sync-external-store": {
+          "optional": true
+        }
+      }
     }
   }
 }
diff --git a/app/package.json b/app/package.json
index 57ed492..b9d6e0a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -42,13 +42,18 @@
     "react-native": "0.76.3",
     "react-native-gesture-handler": "~2.20.2",
     "react-native-linear-gradient": "^2.8.3",
+    "react-native-markdown-display": "^7.0.2",
     "react-native-reanimated": "~3.16.1",
     "react-native-safe-area-context": "4.12.0",
     "react-native-screens": "~4.1.0",
+    "react-native-shimmer-placeholder": "^2.0.9",
     "react-native-ui-lib": "^7.34.5",
     "react-native-web": "~0.19.13",
     "react-native-webview": "13.12.5",
-    "use-debounce": "^10.0.4"
+    "use-debounce": "^10.0.4",
+    "wtf_wikipedia": "^10.4.0",
+    "wtf-plugin-markdown": "^1.0.1",
+    "zustand": "^5.0.3"
   },
   "devDependencies": {
     "@babel/core": "^7.25.2",
diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml
index bf4d12d..228e68d 100644
--- a/app/pnpm-lock.yaml
+++ b/app/pnpm-lock.yaml
@@ -11,12 +11,18 @@ importers:
       '@expo/vector-icons':
         specifier: ^14.0.2
         version: 14.0.4
+      '@gorhom/bottom-sheet':
+        specifier: ^5.0.6
+        version: 5.1.1(@types/react@18.3.14)(react-native-gesture-handler@2.20.2(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.4(@babel/core@7.26.0)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
       '@react-navigation/bottom-tabs':
         specifier: ^7.0.0
         version: 7.1.3(@react-navigation/native@7.0.13(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native-screens@4.1.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
       '@react-navigation/native':
         specifier: ^7.0.0
         version: 7.0.13(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      '@rnmapbox/maps':
+        specifier: ^10.1.33
+        version: 10.1.37(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(mapbox-gl@2.15.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
       '@tanstack/react-query':
         specifier: ^5.62.3
         version: 5.62.3(react@18.3.1)
@@ -44,6 +50,9 @@ importers:
       expo-linking:
         specifier: ~7.0.3
         version: 7.0.3(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      expo-location:
+        specifier: ~18.0.3
+        version: 18.0.7(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))
       expo-router:
         specifier: ~4.0.11
         version: 4.0.11(szxs4gi25w2igavjhg6qarqcta)
@@ -62,6 +71,9 @@ importers:
       expo-web-browser:
         specifier: ~14.0.1
         version: 14.0.1(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))
+      mapbox-gl:
+        specifier: ^2.15.0
+        version: 2.15.0
       react:
         specifier: 18.3.1
         version: 18.3.1
@@ -74,9 +86,9 @@ importers:
       react-native-gesture-handler:
         specifier: ~2.20.2
         version: 2.20.2(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
-      react-native-maps:
-        specifier: 1.18.0
-        version: 1.18.0(react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      react-native-linear-gradient:
+        specifier: ^2.8.3
+        version: 2.8.3(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
       react-native-reanimated:
         specifier: ~3.16.1
         version: 3.16.4(@babel/core@7.26.0)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
@@ -934,6 +946,27 @@ packages:
     resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}
     hasBin: true
 
+  '@gorhom/bottom-sheet@5.1.1':
+    resolution: {integrity: sha512-Y8FiuRmeZYaP+ZGQ0axDwWrrKqVp4ByYRs1D2fTJTxHMt081MHHRQsqmZ3SK7AFp3cSID+vTqnD8w/KAASpy+w==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-native': '*'
+      react: '*'
+      react-native: '*'
+      react-native-gesture-handler: '>=2.16.1'
+      react-native-reanimated: '>=3.16.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-native':
+        optional: true
+
+  '@gorhom/portal@1.0.14':
+    resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==}
+    peerDependencies:
+      react: '*'
+      react-native: '*'
+
   '@isaacs/cliui@8.0.2':
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
@@ -1041,6 +1074,33 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@mapbox/geojson-rewind@0.5.2':
+    resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==}
+    hasBin: true
+
+  '@mapbox/jsonlint-lines-primitives@2.0.2':
+    resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
+    engines: {node: '>= 0.6'}
+
+  '@mapbox/mapbox-gl-supported@2.0.1':
+    resolution: {integrity: sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==}
+
+  '@mapbox/point-geometry@0.1.0':
+    resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==}
+
+  '@mapbox/tiny-sdf@2.0.6':
+    resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==}
+
+  '@mapbox/unitbezier@0.0.1':
+    resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
+
+  '@mapbox/vector-tile@1.3.1':
+    resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==}
+
+  '@mapbox/whoots-js@3.1.0':
+    resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
+    engines: {node: '>=6.0.0'}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -1224,6 +1284,22 @@ packages:
   '@remix-run/web-stream@1.1.0':
     resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==}
 
+  '@rnmapbox/maps@10.1.37':
+    resolution: {integrity: sha512-9TJ79pjj7v2vQOPJZXO5lJmlYi0xlesRnAPTAwfOjXlHdGPtiucXoxL8+e1xL0PJBVMqVJGOjPLeb1+05kGx2g==}
+    peerDependencies:
+      expo: '>=47.0.0'
+      mapbox-gl: ^2.9.0
+      react: '>=16.6.1'
+      react-dom: '>= 17.0.0'
+      react-native: '>=0.59.9'
+    peerDependenciesMeta:
+      expo:
+        optional: true
+      mapbox-gl:
+        optional: true
+      react-dom:
+        optional: true
+
   '@segment/loosely-validate-event@2.0.0':
     resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==}
 
@@ -1269,6 +1345,48 @@ packages:
     peerDependencies:
       typescript: '>=5.6.2'
 
+  '@turf/along@6.5.0':
+    resolution: {integrity: sha512-LLyWQ0AARqJCmMcIEAXF4GEu8usmd4Kbz3qk1Oy5HoRNpZX47+i5exQtmIWKdqJ1MMhW26fCTXgpsEs5zgJ5gw==}
+
+  '@turf/bbox@7.2.0':
+    resolution: {integrity: sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA==}
+
+  '@turf/bearing@6.5.0':
+    resolution: {integrity: sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A==}
+
+  '@turf/destination@6.5.0':
+    resolution: {integrity: sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ==}
+
+  '@turf/distance@6.5.0':
+    resolution: {integrity: sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==}
+
+  '@turf/helpers@6.5.0':
+    resolution: {integrity: sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==}
+
+  '@turf/helpers@7.2.0':
+    resolution: {integrity: sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==}
+
+  '@turf/invariant@6.5.0':
+    resolution: {integrity: sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==}
+
+  '@turf/length@6.5.0':
+    resolution: {integrity: sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig==}
+
+  '@turf/line-intersect@6.5.0':
+    resolution: {integrity: sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==}
+
+  '@turf/line-segment@6.5.0':
+    resolution: {integrity: sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==}
+
+  '@turf/meta@6.5.0':
+    resolution: {integrity: sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==}
+
+  '@turf/meta@7.2.0':
+    resolution: {integrity: sha512-igzTdHsQc8TV1RhPuOLVo74Px/hyPrVgVOTgjWQZzt3J9BVseCdpfY/0cJBdlSRI4S/yTmmHl7gAqjhpYH5Yaw==}
+
+  '@turf/nearest-point-on-line@6.5.0':
+    resolution: {integrity: sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg==}
+
   '@types/babel__core@7.20.5':
     resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
 
@@ -1296,6 +1414,9 @@ packages:
   '@types/geojson@7946.0.15':
     resolution: {integrity: sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==}
 
+  '@types/geojson@7946.0.8':
+    resolution: {integrity: sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==}
+
   '@types/graceful-fs@4.1.9':
     resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
 
@@ -1958,6 +2079,9 @@ packages:
   css-in-js-utils@3.1.0:
     resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
 
+  csscolorparser@1.0.3:
+    resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==}
+
   cssom@0.3.8:
     resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
 
@@ -1983,6 +2107,9 @@ packages:
     resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
     engines: {node: '>=0.11'}
 
+  debounce@1.2.1:
+    resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
+
   debug@2.6.9:
     resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
     peerDependencies:
@@ -2102,6 +2229,9 @@ packages:
     resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==}
     engines: {node: '>= 0.4'}
 
+  earcut@2.2.4:
+    resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
+
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
@@ -2297,6 +2427,11 @@ packages:
       react: '*'
       react-native: '*'
 
+  expo-location@18.0.7:
+    resolution: {integrity: sha512-EPN5yTtDCH3EMiJpJFCWuD+vJkhVbl1j3tMYo3wkhGJN7xlvZf6naLXj8vYntdXSa5M40KjOgjaRiUUqf6+dXw==}
+    peerDependencies:
+      expo: '*'
+
   expo-modules-autolinking@2.0.3:
     resolution: {integrity: sha512-Q/ALJ54eS7Cr7cmbP+unEDTkHFQivQerWWrqZxuXOrSFYGCYU22+/xAZXaJOpZwseOVsP74zSkoRY/wBimVs7w==}
     hasBin: true
@@ -2513,6 +2648,12 @@ packages:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
 
+  geojson-rbush@3.2.0:
+    resolution: {integrity: sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==}
+
+  geojson-vt@3.2.1:
+    resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==}
+
   get-caller-file@2.0.5:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
@@ -2545,6 +2686,9 @@ packages:
     resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==}
     engines: {node: '>=6'}
 
+  gl-matrix@3.4.3:
+    resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
+
   glob-parent@5.1.2:
     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
     engines: {node: '>= 6'}
@@ -2575,6 +2719,9 @@ packages:
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
+  grid-index@1.1.0:
+    resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==}
+
   has-flag@3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
@@ -3056,6 +3203,9 @@ packages:
   jsonfile@6.1.0:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
 
+  kdbush@4.0.2:
+    resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
+
   kind-of@6.0.3:
     resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
     engines: {node: '>=0.10.0'}
@@ -3191,6 +3341,9 @@ packages:
   makeerror@1.0.12:
     resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
 
+  mapbox-gl@2.15.0:
+    resolution: {integrity: sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A==}
+
   marky@1.2.5:
     resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==}
 
@@ -3359,6 +3512,9 @@ packages:
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
+  murmurhash-js@1.0.0:
+    resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
+
   mz@2.7.0:
     resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
 
@@ -3589,6 +3745,10 @@ packages:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
 
+  pbf@3.3.0:
+    resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==}
+    hasBin: true
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -3635,6 +3795,9 @@ packages:
     resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
     engines: {node: ^10 || ^12 || >=14}
 
+  potpack@2.0.0:
+    resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==}
+
   pretty-bytes@5.6.0:
     resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
     engines: {node: '>=6'}
@@ -3667,6 +3830,9 @@ packages:
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
+  protocol-buffers-schema@3.6.0:
+    resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
+
   psl@1.15.0:
     resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
 
@@ -3700,6 +3866,9 @@ packages:
   queue@6.0.2:
     resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
 
+  quickselect@2.0.0:
+    resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
+
   randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
 
@@ -3707,6 +3876,9 @@ packages:
     resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
     engines: {node: '>= 0.6'}
 
+  rbush@3.0.1:
+    resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
+
   rc@1.2.8:
     resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
     hasBin: true
@@ -3757,16 +3929,11 @@ packages:
       react: '>=18.2.0'
       react-native: '>=0.73.0'
 
-  react-native-maps@1.18.0:
-    resolution: {integrity: sha512-S17nYUqeMptgIPaAZuVRo+eRelPreBBYQWw6jsxU7qQ12p+THSfFaqabcNn7fBmsXhT3T27iIl8ek8v1H8BaGw==}
-    engines: {node: '>=18'}
+  react-native-linear-gradient@2.8.3:
+    resolution: {integrity: sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==}
     peerDependencies:
-      react: '>= 17.0.1'
-      react-native: '>= 0.64.3'
-      react-native-web: '>= 0.11'
-    peerDependenciesMeta:
-      react-native-web:
-        optional: true
+      react: '*'
+      react-native: '*'
 
   react-native-reanimated@3.16.4:
     resolution: {integrity: sha512-dF1Vvu8gG+p0+DmBhKMTx5X9iw/rH1ZF9WaIn2nW0c5rxsVFf00axmDgaAdPxNWblmtLnroaKwrV7SjMUyOx+g==}
@@ -3921,6 +4088,9 @@ packages:
     resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
     engines: {node: '>=8'}
 
+  resolve-protobuf-schema@2.1.0:
+    resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
+
   resolve-workspace-root@2.0.0:
     resolution: {integrity: sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw==}
 
@@ -3956,6 +4126,9 @@ packages:
   run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 
+  rw@1.3.3:
+    resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
   safe-buffer@5.1.2:
     resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
 
@@ -4240,9 +4413,14 @@ packages:
 
   sudo-prompt@8.2.5:
     resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==}
+    deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
 
   sudo-prompt@9.1.1:
     resolution: {integrity: sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==}
+    deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
+
+  supercluster@8.0.1:
+    resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
 
   supports-color@5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
@@ -4342,6 +4520,9 @@ packages:
   tinycolor2@1.6.0:
     resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
 
+  tinyqueue@2.0.3:
+    resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==}
+
   tmp@0.0.33:
     resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
     engines: {node: '>=0.6.0'}
@@ -4526,6 +4707,9 @@ packages:
   vlq@1.0.1:
     resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
 
+  vt-pbf@3.1.3:
+    resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==}
+
   w3c-xmlserializer@4.0.0:
     resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
     engines: {node: '>=14'}
@@ -5934,6 +6118,23 @@ snapshots:
       find-up: 5.0.0
       js-yaml: 4.1.0
 
+  '@gorhom/bottom-sheet@5.1.1(@types/react@18.3.14)(react-native-gesture-handler@2.20.2(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.4(@babel/core@7.26.0)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@gorhom/portal': 1.0.14(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      invariant: 2.2.4
+      react: 18.3.1
+      react-native: 0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)
+      react-native-gesture-handler: 2.20.2(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      react-native-reanimated: 3.16.4(@babel/core@7.26.0)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+    optionalDependencies:
+      '@types/react': 18.3.14
+
+  '@gorhom/portal@1.0.14(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      nanoid: 3.3.8
+      react: 18.3.1
+      react-native: 0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)
+
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
@@ -6143,6 +6344,27 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@mapbox/geojson-rewind@0.5.2':
+    dependencies:
+      get-stream: 6.0.1
+      minimist: 1.2.8
+
+  '@mapbox/jsonlint-lines-primitives@2.0.2': {}
+
+  '@mapbox/mapbox-gl-supported@2.0.1': {}
+
+  '@mapbox/point-geometry@0.1.0': {}
+
+  '@mapbox/tiny-sdf@2.0.6': {}
+
+  '@mapbox/unitbezier@0.0.1': {}
+
+  '@mapbox/vector-tile@1.3.1':
+    dependencies:
+      '@mapbox/point-geometry': 0.1.0
+
+  '@mapbox/whoots-js@3.1.0': {}
+
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -6428,6 +6650,22 @@ snapshots:
     dependencies:
       web-streams-polyfill: 3.3.3
 
+  '@rnmapbox/maps@10.1.37(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(mapbox-gl@2.15.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@turf/along': 6.5.0
+      '@turf/distance': 6.5.0
+      '@turf/helpers': 6.5.0
+      '@turf/length': 6.5.0
+      '@turf/nearest-point-on-line': 6.5.0
+      '@types/geojson': 7946.0.15
+      debounce: 1.2.1
+      react: 18.3.1
+      react-native: 0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)
+    optionalDependencies:
+      expo: 52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+      mapbox-gl: 2.15.0
+      react-dom: 18.3.1(react@18.3.1)
+
   '@segment/loosely-validate-event@2.0.0':
     dependencies:
       component-type: 1.2.2
@@ -6470,6 +6708,86 @@ snapshots:
     dependencies:
       typescript: 5.7.2
 
+  '@turf/along@6.5.0':
+    dependencies:
+      '@turf/bearing': 6.5.0
+      '@turf/destination': 6.5.0
+      '@turf/distance': 6.5.0
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+
+  '@turf/bbox@7.2.0':
+    dependencies:
+      '@turf/helpers': 7.2.0
+      '@turf/meta': 7.2.0
+      '@types/geojson': 7946.0.15
+      tslib: 2.8.1
+
+  '@turf/bearing@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+
+  '@turf/destination@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+
+  '@turf/distance@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+
+  '@turf/helpers@6.5.0': {}
+
+  '@turf/helpers@7.2.0':
+    dependencies:
+      '@types/geojson': 7946.0.15
+      tslib: 2.8.1
+
+  '@turf/invariant@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+
+  '@turf/length@6.5.0':
+    dependencies:
+      '@turf/distance': 6.5.0
+      '@turf/helpers': 6.5.0
+      '@turf/meta': 6.5.0
+
+  '@turf/line-intersect@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+      '@turf/line-segment': 6.5.0
+      '@turf/meta': 6.5.0
+      geojson-rbush: 3.2.0
+
+  '@turf/line-segment@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+      '@turf/meta': 6.5.0
+
+  '@turf/meta@6.5.0':
+    dependencies:
+      '@turf/helpers': 6.5.0
+
+  '@turf/meta@7.2.0':
+    dependencies:
+      '@turf/helpers': 7.2.0
+      '@types/geojson': 7946.0.15
+
+  '@turf/nearest-point-on-line@6.5.0':
+    dependencies:
+      '@turf/bearing': 6.5.0
+      '@turf/destination': 6.5.0
+      '@turf/distance': 6.5.0
+      '@turf/helpers': 6.5.0
+      '@turf/invariant': 6.5.0
+      '@turf/line-intersect': 6.5.0
+      '@turf/meta': 6.5.0
+
   '@types/babel__core@7.20.5':
     dependencies:
       '@babel/parser': 7.26.3
@@ -6507,6 +6825,8 @@ snapshots:
 
   '@types/geojson@7946.0.15': {}
 
+  '@types/geojson@7946.0.8': {}
+
   '@types/graceful-fs@4.1.9':
     dependencies:
       '@types/node': 22.10.1
@@ -7264,6 +7584,8 @@ snapshots:
     dependencies:
       hyphenate-style-name: 1.1.0
 
+  csscolorparser@1.0.3: {}
+
   cssom@0.3.8: {}
 
   cssom@0.5.0: {}
@@ -7286,6 +7608,8 @@ snapshots:
     dependencies:
       '@babel/runtime': 7.26.0
 
+  debounce@1.2.1: {}
+
   debug@2.6.9:
     dependencies:
       ms: 2.0.0
@@ -7376,6 +7700,8 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  earcut@2.2.4: {}
+
   eastasianwidth@0.2.0: {}
 
   ee-first@1.1.1: {}
@@ -7566,6 +7892,10 @@ snapshots:
       - expo
       - supports-color
 
+  expo-location@18.0.7(expo@52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)):
+    dependencies:
+      expo: 52.0.17(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)))(react-native-webview@13.12.5(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1)
+
   expo-modules-autolinking@2.0.3:
     dependencies:
       '@expo/spawn-async': 1.7.2
@@ -7843,6 +8173,16 @@ snapshots:
 
   gensync@1.0.0-beta.2: {}
 
+  geojson-rbush@3.2.0:
+    dependencies:
+      '@turf/bbox': 7.2.0
+      '@turf/helpers': 6.5.0
+      '@turf/meta': 6.5.0
+      '@types/geojson': 7946.0.8
+      rbush: 3.0.1
+
+  geojson-vt@3.2.1: {}
+
   get-caller-file@2.0.5: {}
 
   get-intrinsic@1.2.5:
@@ -7872,6 +8212,8 @@ snapshots:
 
   getenv@1.0.0: {}
 
+  gl-matrix@3.4.3: {}
+
   glob-parent@5.1.2:
     dependencies:
       is-glob: 4.0.3
@@ -7911,6 +8253,8 @@ snapshots:
 
   graceful-fs@4.2.11: {}
 
+  grid-index@1.1.0: {}
+
   has-flag@3.0.0: {}
 
   has-flag@4.0.0: {}
@@ -8631,6 +8975,8 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
+  kdbush@4.0.2: {}
+
   kind-of@6.0.3: {}
 
   kleur@3.0.3: {}
@@ -8741,6 +9087,31 @@ snapshots:
     dependencies:
       tmpl: 1.0.5
 
+  mapbox-gl@2.15.0:
+    dependencies:
+      '@mapbox/geojson-rewind': 0.5.2
+      '@mapbox/jsonlint-lines-primitives': 2.0.2
+      '@mapbox/mapbox-gl-supported': 2.0.1
+      '@mapbox/point-geometry': 0.1.0
+      '@mapbox/tiny-sdf': 2.0.6
+      '@mapbox/unitbezier': 0.0.1
+      '@mapbox/vector-tile': 1.3.1
+      '@mapbox/whoots-js': 3.1.0
+      csscolorparser: 1.0.3
+      earcut: 2.2.4
+      geojson-vt: 3.2.1
+      gl-matrix: 3.4.3
+      grid-index: 1.1.0
+      kdbush: 4.0.2
+      murmurhash-js: 1.0.0
+      pbf: 3.3.0
+      potpack: 2.0.0
+      quickselect: 2.0.0
+      rw: 1.3.3
+      supercluster: 8.0.1
+      tinyqueue: 2.0.3
+      vt-pbf: 3.1.3
+
   marky@1.2.5: {}
 
   md5-file@3.2.3:
@@ -9006,6 +9377,8 @@ snapshots:
 
   ms@2.1.3: {}
 
+  murmurhash-js@1.0.0: {}
+
   mz@2.7.0:
     dependencies:
       any-promise: 1.3.0
@@ -9203,6 +9576,11 @@ snapshots:
 
   path-type@4.0.0: {}
 
+  pbf@3.3.0:
+    dependencies:
+      ieee754: 1.2.1
+      resolve-protobuf-schema: 2.1.0
+
   picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}
@@ -9239,6 +9617,8 @@ snapshots:
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  potpack@2.0.0: {}
+
   pretty-bytes@5.6.0: {}
 
   pretty-format@29.7.0:
@@ -9272,6 +9652,8 @@ snapshots:
       object-assign: 4.1.1
       react-is: 16.13.1
 
+  protocol-buffers-schema@3.6.0: {}
+
   psl@1.15.0:
     dependencies:
       punycode: 2.3.1
@@ -9304,12 +9686,18 @@ snapshots:
     dependencies:
       inherits: 2.0.4
 
+  quickselect@2.0.0: {}
+
   randombytes@2.1.0:
     dependencies:
       safe-buffer: 5.2.1
 
   range-parser@1.2.1: {}
 
+  rbush@3.0.1:
+    dependencies:
+      quickselect: 2.0.0
+
   rc@1.2.8:
     dependencies:
       deep-extend: 0.6.0
@@ -9372,13 +9760,10 @@ snapshots:
       react: 18.3.1
       react-native: 0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)
 
-  react-native-maps@1.18.0(react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1):
+  react-native-linear-gradient@2.8.3(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1):
     dependencies:
-      '@types/geojson': 7946.0.15
       react: 18.3.1
       react-native: 0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1)
-    optionalDependencies:
-      react-native-web: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
 
   react-native-reanimated@3.16.4(@babel/core@7.26.0)(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.14)(react@18.3.1))(react@18.3.1):
     dependencies:
@@ -9616,6 +10001,10 @@ snapshots:
 
   resolve-from@5.0.0: {}
 
+  resolve-protobuf-schema@2.1.0:
+    dependencies:
+      protocol-buffers-schema: 3.6.0
+
   resolve-workspace-root@2.0.0: {}
 
   resolve.exports@2.0.3: {}
@@ -9649,6 +10038,8 @@ snapshots:
     dependencies:
       queue-microtask: 1.2.3
 
+  rw@1.3.3: {}
+
   safe-buffer@5.1.2: {}
 
   safe-buffer@5.2.1: {}
@@ -9938,6 +10329,10 @@ snapshots:
 
   sudo-prompt@9.1.1: {}
 
+  supercluster@8.0.1:
+    dependencies:
+      kdbush: 4.0.2
+
   supports-color@5.5.0:
     dependencies:
       has-flag: 3.0.0
@@ -10037,6 +10432,8 @@ snapshots:
 
   tinycolor2@1.6.0: {}
 
+  tinyqueue@2.0.3: {}
+
   tmp@0.0.33:
     dependencies:
       os-tmpdir: 1.0.2
@@ -10178,6 +10575,12 @@ snapshots:
 
   vlq@1.0.1: {}
 
+  vt-pbf@3.1.3:
+    dependencies:
+      '@mapbox/point-geometry': 0.1.0
+      '@mapbox/vector-tile': 1.3.1
+      pbf: 3.3.0
+
   w3c-xmlserializer@4.0.0:
     dependencies:
       xml-name-validator: 4.0.0
diff --git a/app/utils/store.ts b/app/utils/store.ts
new file mode 100644
index 0000000..82539aa
--- /dev/null
+++ b/app/utils/store.ts
@@ -0,0 +1,14 @@
+import { MapState } from "@rnmapbox/maps";
+import { create } from "zustand";
+
+export type Region = MapState["properties"];
+
+export type Store = {
+  region: Region | null;
+  setRegion: (region: Region) => void;
+};
+
+export const useMapStore = create<Store>((set) => ({
+  region: null,
+  setRegion: (region: Region) => set({ region }),
+}));
diff --git a/app/utils/trpc.ts b/app/utils/trpc.ts
index 2199832..0971359 100644
--- a/app/utils/trpc.ts
+++ b/app/utils/trpc.ts
@@ -1,3 +1,3 @@
-import { createTRPCReact } from "@trpc/react-query";
+import { createTRPCReact, httpBatchLink } from "@trpc/react-query";
 import type { AppRouter } from "@server/index";
 export const trpc = createTRPCReact<AppRouter>();
diff --git a/server/index.ts b/server/index.ts
index 7a5705b..af62e91 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -1,4 +1,5 @@
 import getPage from "./procedures/get-page";
+import getWikitext from "./procedures/get-wikitext";
 import { router } from "./trpc";
 import * as maptiler from "@maptiler/client";
 import { createHTTPServer } from "@trpc/server/adapters/standalone";
@@ -7,6 +8,7 @@ maptiler.config.apiKey = process.env.MAPTILER_API_KEY!;
 
 const appRouter = router({
   getPage,
+  getWikitext,
 });
 
 // Export type router type signature,
diff --git a/server/procedures/get-page.ts b/server/procedures/get-page.ts
index abd9ab4..09f5c7f 100644
--- a/server/procedures/get-page.ts
+++ b/server/procedures/get-page.ts
@@ -1,8 +1,6 @@
 import { geocoding } from "@maptiler/client";
 import { TRPCError } from "@trpc/server";
-import { readFile } from "fs/promises";
 import { RowDataPacket } from "mysql2";
-import path from "path";
 import { z } from "zod";
 import connection from "../clients/db";
 import { publicProcedure } from "../trpc";
@@ -29,11 +27,6 @@ const getPage = publicProcedure
       types: ["country", "region", "locality", "neighbourhood", "municipality"],
     });
     const features = geocodeResult.features as Feature[];
-    console.log(
-      latLng,
-      bbox,
-      features.map((f) => f.place_name),
-    );
 
     // select the feature whose bbox matches the input bbox the most and has a wikidata property
     const feature = features
@@ -72,12 +65,11 @@ const getPage = publicProcedure
 
     if (!rows[0]) throw err;
 
-    const wikitext = await readFile(
-      path.join(process.env.WIKITEXT_DIR!, `${rows[0].wikidata_id}.txt`),
-      "utf-8",
-    );
+    const pageId: string = rows[0].wikidata_id;
+
+    console.log("page", pageId, title);
 
-    return { title, wikitext };
+    return { title, pageId };
   });
 
 export default getPage;
diff --git a/server/procedures/get-wikitext.ts b/server/procedures/get-wikitext.ts
new file mode 100644
index 0000000..25b1fa2
--- /dev/null
+++ b/server/procedures/get-wikitext.ts
@@ -0,0 +1,21 @@
+import z from "zod";
+import { publicProcedure } from "../trpc";
+import path from "path";
+import { readFile } from "fs/promises";
+
+const getWikitext = publicProcedure
+  .input(
+    z.object({
+      pageId: z.number().min(0).max(300000).int(),
+    }),
+  )
+  .query(async function getWikitext(opts) {
+    const wikitext = await readFile(
+      path.join(process.env.WIKITEXT_DIR!, `${opts.input.pageId}.txt`),
+      "utf-8",
+    );
+
+    return wikitext;
+  });
+
+export default getWikitext;
diff --git a/server/trpc.ts b/server/trpc.ts
index 02a8c08..56fa3b0 100644
--- a/server/trpc.ts
+++ b/server/trpc.ts
@@ -1,14 +1,14 @@
-import { initTRPC } from '@trpc/server';
- 
+import { initTRPC } from "@trpc/server";
+
 /**
  * Initialization of tRPC backend
  * Should be done only once per backend!
  */
 const t = initTRPC.create();
- 
+
 /**
  * Export reusable router and procedure helpers
  * that can be used throughout the router
  */
 export const router = t.router;
-export const publicProcedure = t.procedure;
\ No newline at end of file
+export const publicProcedure = t.procedure;
diff --git a/start-dev.sh b/start-dev.sh
new file mode 100755
index 0000000..03e7113
--- /dev/null
+++ b/start-dev.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+tmux new-session -s mapvoyage -n docker 'docker compose up' \; \
+  new-window -n tunnel 'cd server && npm run tunnel' \; \
+  new-window -n server 'sleep 5 && cd server && bun run --watch index.ts' \; \
+  new-window -n app 'cd app && npm run android' \; \
+  select-window -t 0
+
+# see https://ai.roettgers.co/c/20e0db10-5cb0-403c-a679-e0b15f5c2440
\ No newline at end of file


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

<details>
<summary>🧹 Nitpick comments (20)</summary><blockquote>

<details>
<summary>start-dev.sh (3)</summary><blockquote>

`3-5`: **Consider adding error handling and dependency checks.**

The script currently assumes that all required tools (tmux, docker, npm, bun) are installed and doesn't handle potential errors. Consider adding:
1. Dependency checks at the beginning
2. Error handling for service startup failures
3. Support for both Android and iOS development


```diff
#!/bin/bash

+# Check for required dependencies
+for cmd in tmux docker npm bun; do
+  if ! command -v $cmd &> /dev/null; then
+    echo "Error: $cmd is required but not installed. Please install it first."
+    exit 1
+  fi
+done
+
+# Function to handle errors
+handle_error() {
+  echo "Error: $1 failed to start. Check the logs for details."
+  tmux kill-session -t mapvoyage
+  exit 1
+}

tmux new-session -s mapvoyage -n docker 'docker compose up' \; \
  new-window -n tunnel 'cd server && npm run tunnel' \; \
  new-window -n server 'sleep 5 && cd server && bun run --watch index.ts' \; \
  new-window -n app 'cd app && npm run android' \; \
  select-window -t 0

+# Add an option to run iOS instead
+# Usage example: ./start-dev.sh ios
+# if [ "$1" == "ios" ]; then
+#   tmux new-session -s mapvoyage -n docker 'docker compose up' \; \
+#     new-window -n tunnel 'cd server && npm run tunnel' \; \
+#     new-window -n server 'sleep 5 && cd server && bun run --watch index.ts' \; \
+#     new-window -n app 'cd app && npm run ios' \; \
+#     select-window -t 0
+# fi
```

---

`4-4`: **Consider waiting for Docker to fully initialize before starting the server.**

The current 5-second sleep might not be enough if Docker services take longer to initialize. Consider making this more robust with a health check.


```diff
-  new-window -n server 'sleep 5 && cd server && bun run --watch index.ts' \; \
+  new-window -n server 'cd server && echo "Waiting for Docker services..." && until docker compose ps | grep -q "healthy"; do sleep 2; done && echo "Services ready! Starting server..." && bun run --watch index.ts' \; \
```

---

`8-8`: **Remove or expand on the reference comment.**

The comment contains a URL but doesn't explain its relevance. Either remove it or provide more context about what resource it points to and why it's important.


```diff
-# see https://ai.roettgers.co/c/20e0db10-5cb0-403c-a679-e0b15f5c2440
+# Reference for tmux setup pattern: https://ai.roettgers.co/c/20e0db10-5cb0-403c-a679-e0b15f5c2440
```

</blockquote></details>
<details>
<summary>app/utils/store.ts (3)</summary><blockquote>

`4-4`: **Add documentation for the Region type.**

The `Region` type is defined as `MapState["properties"]`, but it's not clear what properties it contains. Consider adding JSDoc comments to provide clarity.


```diff
+/**
+ * Represents a map region with properties from MapState
+ * @typedef {Object} Region
+ * @property {number} latitude - The center latitude of the region
+ * @property {number} longitude - The center longitude of the region
+ * @property {number} [zoom] - The zoom level of the region
+ * @property {number} [pitch] - The pitch angle of the region in degrees
+ * @property {number} [heading] - The heading of the region in degrees
+ * @property {number} [animationDuration] - Duration of transition animations
+ */
export type Region = MapState["properties"];
```

---

`6-9`: **Add input validation for setRegion method.**

The `setRegion` method updates the region state directly without any validation. Consider adding validation to ensure the received region data is valid before updating the state.


```diff
export type Store = {
  region: Region | null;
-  setRegion: (region: Region) => void;
+  setRegion: (region: Region | null) => void;
+  // Additional utility methods could be added here
};
```

---

`11-14`: **Consider adding utility methods to the store.**

The store currently only allows setting the entire region at once. Consider adding utility methods for updating individual properties of the region.


```diff
export const useMapStore = create<Store>((set) => ({
  region: null,
-  setRegion: (region: Region) => set({ region }),
+  setRegion: (region: Region | null) => set({ region }),
+  // Utility methods for common operations
+  updateCenter: (latitude: number, longitude: number) => set((state) => ({
+    region: state.region ? { ...state.region, latitude, longitude } : null
+  })),
+  updateZoom: (zoom: number) => set((state) => ({
+    region: state.region ? { ...state.region, zoom } : null
+  })),
+  resetRegion: () => set({ region: null }),
}));
```

</blockquote></details>
<details>
<summary>server/index.ts (2)</summary><blockquote>

`9-12`: **Consider implementing a more structured router organization.**

As your application grows with more procedures, consider organizing them into logical groups. This will improve maintainability.


```diff
const appRouter = router({
-  getPage,
-  getWikitext,
+  data: router({
+    getPage,
+    getWikitext,
+  }),
+  // Future endpoints could be grouped like:
+  // auth: router({ ... }),
+  // user: router({ ... }),
});
```

---

`22-25`: **Add graceful shutdown handling for the server.**

The server starts correctly but lacks proper shutdown handling. Consider adding signal handlers to gracefully terminate the server.


```diff
const PORT = process.env.PORT ?? 3000;

-server.listen(PORT);
-console.log(`Listening on port ${PORT}`);
+const httpServer = server.listen(PORT, () => {
+  console.log(`Server listening on port ${PORT}`);
+});
+
+// Handle graceful shutdown
+process.on('SIGTERM', () => {
+  console.log('SIGTERM signal received: closing HTTP server');
+  httpServer.close(() => {
+    console.log('HTTP server closed');
+  });
+});
+
+process.on('SIGINT', () => {
+  console.log('SIGINT signal received: closing HTTP server');
+  httpServer.close(() => {
+    console.log('HTTP server closed');
+  });
+});
```

</blockquote></details>
<details>
<summary>server/procedures/get-wikitext.ts (1)</summary><blockquote>

`13-16`: **Consider implementing caching for frequently accessed wikitext.**

Reading from the file system for every request can be inefficient, especially for frequently accessed pages. Consider implementing a caching mechanism.


```diff
+import NodeCache from 'node-cache';
+
+// Initialize cache with TTL of 1 hour and checking for expired keys every 10 minutes
+const wikiCache = new NodeCache({ stdTTL: 3600, checkperiod: 600 });
+
 const getWikitext = publicProcedure
   .input(
     z.object({
       pageId: z.number().min(0).max(300000).int(),
     }),
   )
   .query(async function getWikitext(opts) {
     try {
+      const cacheKey = `wikitext-${opts.input.pageId}`;
+      
+      // Check if data is in cache
+      const cachedData = wikiCache.get(cacheKey);
+      if (cachedData) {
+        return cachedData as string;
+      }
+      
       // Check if WIKITEXT_DIR is defined
       if (!process.env.WIKITEXT_DIR) {
         throw new Error("WIKITEXT_DIR environment variable is not defined");
       }

       const filePath = path.join(process.env.WIKITEXT_DIR, `${opts.input.pageId}.txt`);
       
       const wikitext = await readFile(filePath, "utf-8");
+      
+      // Store in cache
+      wikiCache.set(cacheKey, wikitext);
+      
       return wikitext;
```

</blockquote></details>
<details>
<summary>server/procedures/get-page.ts (2)</summary><blockquote>

`68-69`: **Consider using a number type for pageId**

The `pageId` is typed as a string, but it's coming from a database ID field which is typically a number. This might cause type inconsistencies if other parts of the application expect a number type.

```diff
-    const pageId: string = rows[0].wikidata_id;
+    const pageId: number = rows[0].wikidata_id;
```

---

`70-70`: **Remove or replace console.log statement**

This console.log statement appears to be for debugging purposes. Consider removing it for production code or replacing it with a more structured logging approach.

```diff
-    console.log("page", pageId, title);
```

</blockquote></details>
<details>
<summary>app/package.json (1)</summary><blockquote>

`54-55`: **Consider adding wtf_wikipedia plugins as optional dependencies**

The Wikipedia parsing plugins might not be needed for all parts of the application. Consider moving them to the `optionalDependencies` section if they're only used in specific parts of the app to reduce initial bundle size.

</blockquote></details>
<details>
<summary>app/app/index.tsx (1)</summary><blockquote>

`8-8`: **Consider adding a better fallback UI when region is not available**

The current implementation renders an empty `<View />` when `region` is not available. This might lead to a poor user experience or a blank screen.

```diff
- return region ? <Infobox region={region} /> : <View />;
+ return region ? <Infobox region={region} /> : (
+   <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
+     <Text>Loading map region...</Text>
+   </View>
+ );
```

</blockquote></details>
<details>
<summary>app/app/page/[pageId]/section/[title].tsx (2)</summary><blockquote>

`11-11`: **Consider moving plugin extension to a central setup file**

The `wtf.extend(mdPlugin)` call should ideally be in a central setup file rather than in a component, as this is a global configuration that only needs to happen once.

---

`3-4`: **Unused imports should be removed**

The component imports `SafeAreaView`, `ScrollView`, `Text`, and `BottomSheetScrollView` but doesn't use them in the component. These should be removed to avoid cluttering the import section.

```diff
- import { SafeAreaView, ScrollView } from "react-native";
- import { View, Text, SkeletonView } from "react-native-ui-lib";
- import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+ import { View, SkeletonView } from "react-native-ui-lib";
```


Also applies to: 9-9

</blockquote></details>
<details>
<summary>app/app/_layout.tsx (3)</summary><blockquote>

`1-17`: **Clean up import organization.**

The imports are not organized in a consistent way. Consider grouping them by source: React/React Native core imports, third-party libraries, and local application imports.

```diff
+// Local imports
 import { useMapStore } from "@/utils/store";
 import { trpc } from "@/utils/trpc";
+
+// Third-party libraries
 import BottomSheet from "@gorhom/bottom-sheet";
 import {
   Camera,
   LocationPuck,
   MapState,
   MapView,
   VectorSource,
   setAccessToken,
 } from "@rnmapbox/maps";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { httpBatchLink } from "@trpc/client";
 import { Stack } from "expo-router";
+
+// React Native core
 import { StyleSheet } from "react-native";
 import { GestureHandlerRootView } from "react-native-gesture-handler";
```

---

`44-44`: **Define static constants outside of the component.**

The `snapPoints` array should be defined outside of the component rendering cycle to prevent unnecessary re-creations on each render.

```diff
-const snapPoints = ["20%", "40%", "100%"];
+// Define constants outside component to prevent recreation on renders
+const BOTTOM_SHEET_SNAP_POINTS = ["20%", "40%", "100%"];
```

---

`67-73`: **Consider adding props for customizing the BottomSheet behavior.**

The BottomSheet component has hardcoded values and limited configuration. Consider making it more flexible by adding props for customization.

```diff
<BottomSheet
-  index={1}
-  snapPoints={snapPoints}
+  index={1} // Default to second snap point (40%)
+  snapPoints={BOTTOM_SHEET_SNAP_POINTS}
  enableDynamicSizing={false}
+  handleComponent={() => <BottomSheet.Handle indicatorStyle={{ width: 40, backgroundColor: '#ccc' }} />}
+  backgroundStyle={{ backgroundColor: 'white' }}
+  handleIndicatorStyle={{ backgroundColor: '#ccc' }}
>
  {children}
</BottomSheet>
```

</blockquote></details>
<details>
<summary>app/components/infobox.tsx (2)</summary><blockquote>

`29-29`: **Remove console.log statement.**

This debug statement should be removed before production deployment.

```diff
-  console.log(!!wikiQuery.data, wikiQuery.data);
```

---

`57-70`: **Make the Infocard component more flexible.**

The current implementation has a fixed height which may not accommodate varying content sizes or different device dimensions.

```diff
function Infocard({ title, pageId }: { title: string; pageId: string }) {
  return (
    <Link
      asChild
      href={{
        pathname: "/page/[pageId]/section/[title]",
        params: { pageId, title },
      }}
    >
-      <Card flex padding-12 height={48}>
+      <Card flex padding-12 minHeight={48} marginB-4>
        <Card.Section content={[{ text: title, text60: true, grey10: true }]} />
+        <Card.Section 
+          content={[{ text: 'Tap to view', text80: true, grey30: true }]} 
+          contentStyle={{ marginTop: 4 }} 
+        />
      </Card>
    </Link>
  );
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 472d904806008f99d436d85d42b0be4382433767 and 526f817eefac760343065b390eed02d4826efa3f.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `app/package-lock.json` is excluded by `!**/package-lock.json`
* `app/pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `app/app/_layout.tsx` (2 hunks)
* `app/app/index.tsx` (1 hunks)
* `app/app/page/[pageId]/section/[title].tsx` (1 hunks)
* `app/components/infobox.tsx` (2 hunks)
* `app/package.json` (1 hunks)
* `app/utils/store.ts` (1 hunks)
* `app/utils/trpc.ts` (1 hunks)
* `server/index.ts` (2 hunks)
* `server/procedures/get-page.ts` (1 hunks)
* `server/procedures/get-wikitext.ts` (1 hunks)
* `server/trpc.ts` (1 hunks)
* `start-dev.sh` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* app/utils/trpc.ts
* server/trpc.ts

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>start-dev.sh (1)</summary>

`1-6`: **Good implementation of a development startup script using tmux.**

This script is a useful addition for streamlining the development workflow by starting all necessary services in a tmux session.

</details>
<details>
<summary>app/utils/store.ts (1)</summary>

`1-14`: **Well-structured Zustand store implementation for map state management.**

The store implementation is clean and follows TypeScript best practices. Using Zustand for state management is a good choice for React Native applications.

</details>
<details>
<summary>server/index.ts (2)</summary>

`2-2`: **Good addition of the wikitext import.**

The import statement is correctly implemented.

---

`11-11`: **Good integration of the wikitext procedure into the router.**

The wikitext procedure is properly integrated into the appRouter.

</details>
<details>
<summary>server/procedures/get-wikitext.ts (3)</summary>

`1-5`: **Good use of imports and dependencies.**

The required packages are correctly imported for file system operations and input validation.

---

`6-11`: **Good input validation with Zod.**

Using Zod for input validation is a good practice, and the constraints on the pageId are appropriate.

---

`21-21`: **Good export practice.**

Default export is appropriate for this single-function module.

</details>
<details>
<summary>server/procedures/get-page.ts (1)</summary>

`72-72`: **LGTM: Properly updated return value**

The return value has been correctly updated to match the new architecture where the wikitext is fetched separately using the pageId.

</details>
<details>
<summary>app/app/index.tsx (1)</summary>

`2-3`: **Good implementation of centralized state management**

The change to use `useMapStore` for accessing the map region state follows good practices for state management and leads to cleaner component code.



Also applies to: 6-6

</details>
<details>
<summary>app/app/page/[pageId]/section/[title].tsx (2)</summary>

`15-15`: **Ensure proper type conversion for pageId**

The pageId is being parsed as an integer when passing to the query, but there's no validation to ensure this conversion is successful. If the pageId is not a valid number, this could cause issues.

```diff
- const wikiQuery = trpc.getWikitext.useQuery({ pageId: parseInt(pageId) });
+ const parsedPageId = parseInt(pageId as string);
+ const wikiQuery = trpc.getWikitext.useQuery(
+   { pageId: parsedPageId },
+   { enabled: !isNaN(parsedPageId) }
+ );
```

---

`16-19`: **LGTM: Good use of useMemo for expensive operations**

The use of `useMemo` for processing the Wikipedia data is a good practice to avoid unnecessary recalculations on re-renders.

</details>
<details>
<summary>app/app/_layout.tsx (2)</summary>

`78-86`: **LGTM!**

The styles are well-structured and use appropriate flex layout properties for responsive design.

---

`41-42`: <details>
<summary>✅ Verification successful</summary>

**Replace stub access token with proper authentication.**

Using a fake access token is not a secure practice, even in development. This should be replaced with a proper token management strategy.


```diff
-// set a stub access token as were using maptiler
-setAccessToken?.("adsfwads");
+// Use environment variable for MapBox token if needed
+// If using MapTiler exclusively, this may not be necessary at all
+const mapboxToken = process.env.EXPO_PUBLIC_MAPBOX_TOKEN;
+if (mapboxToken) {
+  setAccessToken?.(mapboxToken);
+}
```

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if MAPBOX_TOKEN is used elsewhere or if setAccessToken is required
echo "Checking if MAPBOX_TOKEN is used in the codebase..."
rg -l "EXPO_PUBLIC_MAPBOX_TOKEN" | grep -v "_layout.tsx"

echo "Checking if setAccessToken is called elsewhere..."
rg -l "setAccessToken" | grep -v "_layout.tsx"

echo "Checking @rnmapbox documentation mentions..."
rg -l "@rnmapbox" | grep -v "_layout.tsx"
```

Length of output: 648

---



</details>

**Security Update: Replace Stub Access Token with Environment-Provided Value**

The changes in _layout.tsx now use the environment variable (`EXPO_PUBLIC_MAPBOX_TOKEN`) instead of a hard-coded token. Our repository search confirms that the fake token was only used here and that the environment variable isn’t referenced elsewhere. Please ensure that a valid token is provided via your deployment configurations. If MapTiler is used exclusively, double-check whether calling `setAccessToken` is still necessary.

- **Location Impacted:**  
  - `app/app/_layout.tsx` (lines 41–47)

</details>
<details>
<summary>app/components/infobox.tsx (1)</summary>

`39-49`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider implementing section availability checking.**

The current implementation assumes all sections ("Understand", "History", etc.) are available for every page, which might not be the case.

It would be beneficial to check if these sections exist in the Wikipedia data before rendering the cards.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's any section validation logic in the codebase

echo "Checking for section validation in page/section route handlers:"
rg -A 5 "section|title" app/page/ --include "*.tsx" --include "*.ts"

echo "Checking if section data is fetched and validated anywhere:"
rg -A 3 "useQuery.*section" --include "*.tsx" --include "*.ts"

echo "Looking for potential error handling when sections don't exist:"
rg -A 3 "section.*not found|section.*missing" --include "*.tsx" --include "*.ts"
```

Length of output: 1950

---



</details>

**Action Required: Validate Section Availability for Infocard Rendering**

It appears that none of the current routes or query handlers implement logic to verify if the specified sections (e.g., "Understand", "History", "See", etc.) actually exist in the fetched Wikipedia data before rendering their respective Infocard components. This means that—even though the UI unconditionally renders all Infocard components—if a particular section is missing from the data, the user might see an empty or misleading card.

**Recommendations:**

- **Check Data Structure:** Verify if the API response (i.e., `wikiQuery.data`) contains an attribute (such as a list of available sections) that can be used to determine which sections are actually available.
- **Conditional Rendering:** Update the rendering logic in `app/components/infobox.tsx` so that each Infocard is rendered only if its corresponding section exists (e.g., by checking something like `wikiQuery.data.sections.includes("Understand")`).
- **Error Handling:** Consider adding fallback behavior or error messages if the expected section is missing, to improve the user experience.

Please manually verify that the absence of section validation is intentional and, if not, implement the necessary checks.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:16:44Z
user: login: bcye
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-03T12:21:23Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:22:27Z

Review comments:
diff_hunk: @@ -0,0 +1,21 @@
+import z from "zod";
+import { publicProcedure } from "../trpc";
+import path from "path";
+import { readFile } from "fs/promises";
+
+const getWikitext = publicProcedure
+  .input(
+    z.object({
+      pageId: z.number().min(0).max(300000).int(),
+    }),
+  )
+  .query(async function getWikitext(opts) {
+    const wikitext = await readFile(
+      path.join(process.env.WIKITEXT_DIR!, `${opts.input.pageId}.txt`),
+      "utf-8",
+    );
+
+    return wikitext;
+  });
path: server/procedures/get-wikitext.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add error handling for file operations.**

The current implementation doesn't handle potential errors that could occur when reading files, such as if the file doesn't exist. Consider adding proper error handling.


```diff
  .query(async function getWikitext(opts) {
-    const wikitext = await readFile(
-      path.join(process.env.WIKITEXT_DIR!, `${opts.input.pageId}.txt`),
-      "utf-8",
-    );
-
-    return wikitext;
+    try {
+      // Check if WIKITEXT_DIR is defined
+      if (!process.env.WIKITEXT_DIR) {
+        throw new Error("WIKITEXT_DIR environment variable is not defined");
+      }
+
+      const filePath = path.join(process.env.WIKITEXT_DIR, `${opts.input.pageId}.txt`);
+      
+      const wikitext = await readFile(filePath, "utf-8");
+      return wikitext;
+    } catch (error) {
+      // Handle specific errors
+      if (error instanceof Error) {
+        if (error.message.includes('ENOENT')) {
+          throw new Error(`Wikitext file for page ID ${opts.input.pageId} not found`);
+        }
+        throw new Error(`Failed to read wikitext: ${error.message}`);
+      }
+      // Fallback for unknown errors
+      throw new Error('An unknown error occurred while retrieving wikitext');
+    }
  });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  .query(async function getWikitext(opts) {
    try {
      // Check if WIKITEXT_DIR is defined
      if (!process.env.WIKITEXT_DIR) {
        throw new Error("WIKITEXT_DIR environment variable is not defined");
      }

      const filePath = path.join(process.env.WIKITEXT_DIR, `${opts.input.pageId}.txt`);
      
      const wikitext = await readFile(filePath, "utf-8");
      return wikitext;
    } catch (error) {
      // Handle specific errors
      if (error instanceof Error) {
        if (error.message.includes('ENOENT')) {
          throw new Error(`Wikitext file for page ID ${opts.input.pageId} not found`);
        }
        throw new Error(`Failed to read wikitext: ${error.message}`);
      }
      // Fallback for unknown errors
      throw new Error('An unknown error occurred while retrieving wikitext');
    }
  });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+import z from "zod";
+import { publicProcedure } from "../trpc";
+import path from "path";
+import { readFile } from "fs/promises";
+
+const getWikitext = publicProcedure
+  .input(
+    z.object({
+      pageId: z.number().min(0).max(300000).int(),
+    }),
+  )
+  .query(async function getWikitext(opts) {
+    const wikitext = await readFile(
+      path.join(process.env.WIKITEXT_DIR!, `${opts.input.pageId}.txt`),
path: server/procedures/get-wikitext.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Validate environment variable WIKITEXT_DIR at startup.**

The code assumes that WIKITEXT_DIR is set and valid but only checks when handling a request. Consider validating this critical environment variable when the server starts.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if WIKITEXT_DIR environment variable is set in the server environment
if ! grep -q "WIKITEXT_DIR" .env; then
  echo "Warning: WIKITEXT_DIR environment variable not found in .env file"
  # Check if it's set in any environment files
  cd server
  for f in .env* .env.* env.*; do
    if [ -f "$f" ] && grep -q "WIKITEXT_DIR" "$f"; then
      echo "Found WIKITEXT_DIR in $f"
      exit 0
    fi
  done
  echo "WIKITEXT_DIR not found in any environment files - this may cause runtime errors"
  exit 1
fi
```

Length of output: 398

---



</details>

**Action Required: Validate WIKITEXT_DIR on Server Startup**  
The current implementation in `server/procedures/get-wikitext.ts` directly uses `process.env.WIKITEXT_DIR` (line 14) without early verification. Although our environment scan confirms that `WIKITEXT_DIR` is defined in `.env.example`, this file is only a template and does not guarantee that the variable is correctly set in the running environment. To catch configuration issues early and prevent potential runtime errors, please add a validation check during server initialization.

- **File:** `server/procedures/get-wikitext.ts` (line 14) – The dynamic path concatenation assumes a valid `WIKITEXT_DIR`.
- **Recommendation:** Validate `WIKITEXT_DIR` at startup (e.g., in your main server initialization routine) and fail fast with a clear error message if it’s missing or misconfigured.

---

_🛠️ Refactor suggestion_

**Path construction could lead to directory traversal if not properly sanitized.**

Although Zod is validating that pageId is a number, ensure that the path construction is safe against directory traversal attacks.


```diff
-      path.join(process.env.WIKITEXT_DIR!, `${opts.input.pageId}.txt`),
+      // Ensure pageId is sanitized to prevent directory traversal
+      path.join(process.env.WIKITEXT_DIR!, `${Math.abs(parseInt(String(opts.input.pageId), 10))}.txt`),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      // Ensure pageId is sanitized to prevent directory traversal
      path.join(process.env.WIKITEXT_DIR!, `${Math.abs(parseInt(String(opts.input.pageId), 10))}.txt`),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,13 +42,18 @@
     "react-native": "0.76.3",
     "react-native-gesture-handler": "~2.20.2",
     "react-native-linear-gradient": "^2.8.3",
+    "react-native-markdown-display": "^7.0.2",
     "react-native-reanimated": "~3.16.1",
path: app/package.json
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify that all new dependencies are necessary**

Five new dependencies have been added to the project which will increase the bundle size. While they all seem to have clear purposes based on the PR description (markdown display, loading states, Wikipedia parsing, and state management), it's good to verify that all are necessary and being used effectively.




Also applies to: 49-50, 53-56

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how many files are using each of the new dependencies
echo "Checking usage of new dependencies:"
echo "react-native-markdown-display:"
fd -e tsx -e ts -e js | xargs grep -l "react-native-markdown-display" | wc -l
echo "react-native-shimmer-placeholder:"
fd -e tsx -e ts -e js | xargs grep -l "react-native-shimmer-placeholder" | wc -l
echo "wtf_wikipedia:"
fd -e tsx -e ts -e js | xargs grep -l "wtf_wikipedia" | wc -l
echo "wtf-plugin-markdown:"
fd -e tsx -e ts -e js | xargs grep -l "wtf-plugin-markdown" | wc -l
echo "zustand:"
fd -e tsx -e ts -e js | xargs grep -l "zustand" | wc -l
```

Length of output: 690

---



</details>

**Dependency Usage Verification – Action Required**

The verification reveals that while four of the new dependencies are referenced in the codebase (each found in at least one file), **react-native-shimmer-placeholder** is not used in any file:

- **react-native-markdown-display**: Used in 1 file.
- **react-native-shimmer-placeholder**: Not used.
- **wtf_wikipedia**: Used in 1 file.
- **wtf-plugin-markdown**: Used in 1 file.
- **zustand**: Used in 1 file.

Please confirm if **react-native-shimmer-placeholder** is intended for future use. If not, consider removing it to avoid unnecessarily increasing the bundle size.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { trpc } from "@/utils/trpc";
+import { Stack, useLocalSearchParams } from "expo-router";
+import { SafeAreaView, ScrollView } from "react-native";
+import { View, Text, SkeletonView } from "react-native-ui-lib";
+import Markdown from "react-native-markdown-display";
+import { useMemo } from "react";
+import wtf from "wtf_wikipedia";
+import mdPlugin from "wtf-plugin-markdown";
+import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+
+wtf.extend(mdPlugin);
+
+export default function Section() {
+  const { title, pageId } = useLocalSearchParams();
+  const wikiQuery = trpc.getWikitext.useQuery({ pageId: parseInt(pageId) });
+  const sectionWikitext = useMemo(
+    () => (wikiQuery.data ? wtf(wikiQuery.data).section(title) : null),
+    [wikiQuery.data, title],
+  );
+
+  const displayMarkdown = useMemo<string | null>(() => {
+    if (sectionWikitext) {
+      //@ts-ignore ts files seem to not work
+      const md: string = sectionWikitext.markdown();
+
+      // we dont want to redundantly display the section title
+      const firstNL = md.indexOf("\n");
+      return md.slice(firstNL + 1);
+    } else return null;
+  }, [sectionWikitext]);
+
+  return (
+    <View padding-8 flex>
+      <Stack.Screen options={{ title }} />
+      <SkeletonView
+        template={SkeletonView.templates.TEXT_CONTENT}
+        showContent={wikiQuery.isSuccess}
+        renderContent={() =>
+          !!displayMarkdown && <Markdown>{displayMarkdown}</Markdown>
+        }
+      />
+    </View>
+  );
path: app/app/page/[pageId]/section/[title].tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling and ensure content scrollability**

The current implementation doesn't handle errors from the query and doesn't ensure that long content can be scrolled. Add error handling and wrap the content in a ScrollView.

```diff
return (
  <View padding-8 flex>
    <Stack.Screen options={{ title }} />
+   {wikiQuery.isError && (
+     <View center>
+       <Text>Failed to load content. Please try again.</Text>
+     </View>
+   )}
    <SkeletonView
      template={SkeletonView.templates.TEXT_CONTENT}
      showContent={wikiQuery.isSuccess}
      renderContent={() =>
-       !!displayMarkdown && <Markdown>{displayMarkdown}</Markdown>
+       !!displayMarkdown && (
+         <ScrollView>
+           <Markdown>{displayMarkdown}</Markdown>
+         </ScrollView>
+       )
      }
    />
  </View>
);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  return (
    <View padding-8 flex>
      <Stack.Screen options={{ title }} />
      {wikiQuery.isError && (
        <View center>
          <Text>Failed to load content. Please try again.</Text>
        </View>
      )}
      <SkeletonView
        template={SkeletonView.templates.TEXT_CONTENT}
        showContent={wikiQuery.isSuccess}
        renderContent={() =>
          !!displayMarkdown && (
            <ScrollView>
              <Markdown>{displayMarkdown}</Markdown>
            </ScrollView>
          )
        }
      />
    </View>
  );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { trpc } from "@/utils/trpc";
+import { Stack, useLocalSearchParams } from "expo-router";
+import { SafeAreaView, ScrollView } from "react-native";
+import { View, Text, SkeletonView } from "react-native-ui-lib";
+import Markdown from "react-native-markdown-display";
+import { useMemo } from "react";
+import wtf from "wtf_wikipedia";
+import mdPlugin from "wtf-plugin-markdown";
+import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+
+wtf.extend(mdPlugin);
+
+export default function Section() {
+  const { title, pageId } = useLocalSearchParams();
+  const wikiQuery = trpc.getWikitext.useQuery({ pageId: parseInt(pageId) });
+  const sectionWikitext = useMemo(
+    () => (wikiQuery.data ? wtf(wikiQuery.data).section(title) : null),
+    [wikiQuery.data, title],
+  );
+
+  const displayMarkdown = useMemo<string | null>(() => {
+    if (sectionWikitext) {
+      //@ts-ignore ts files seem to not work
+      const md: string = sectionWikitext.markdown();
+
+      // we dont want to redundantly display the section title
+      const firstNL = md.indexOf("\n");
+      return md.slice(firstNL + 1);
+    } else return null;
+  }, [sectionWikitext]);
path: app/app/page/[pageId]/section/[title].tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix TypeScript ignore and add error handling for null values**

There's a `@ts-ignore` comment which indicates an issue with the types. Additionally, the code doesn't handle the case where `sectionWikitext` might have a value but `sectionWikitext.markdown()` returns null.

```diff
const displayMarkdown = useMemo<string | null>(() => {
  if (sectionWikitext) {
-    //@ts-ignore ts files seem to not work
-    const md: string = sectionWikitext.markdown();
+    // Properly type the return value or address the type issue
+    const md: string | null = sectionWikitext.markdown?.() || null;
+    if (!md) return null;

    // we dont want to redundantly display the section title
    const firstNL = md.indexOf("\n");
    return md.slice(firstNL + 1);
  } else return null;
}, [sectionWikitext]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const displayMarkdown = useMemo<string | null>(() => {
    if (sectionWikitext) {
      // Properly type the return value or address the type issue
      const md: string | null = sectionWikitext.markdown?.() || null;
      if (!md) return null;

      // we dont want to redundantly display the section title
      const firstNL = md.indexOf("\n");
      return md.slice(firstNL + 1);
    } else return null;
  }, [sectionWikitext]);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,12 +24,63 @@ const trpcClient = trpc.createClient({
   ],
 });
 
+// The map is inside the layout component, such that the stack is inside the bottom sheet.
+
 export default function RootLayout() {
   return (
     <trpc.Provider client={trpcClient} queryClient={queryClient}>
       <QueryClientProvider client={queryClient}>
-        <Stack />
+        <MapLayout>
+          <Stack screenOptions={{}} />
+        </MapLayout>
       </QueryClientProvider>
     </trpc.Provider>
   );
 }
+
+// set a stub access token as were using maptiler
+setAccessToken?.("adsfwads");
+
+const snapPoints = ["20%", "40%", "100%"];
+
+function MapLayout({ children }: { children: React.ReactNode }) {
+  const { setRegion } = useMapStore();
+
+  function onIdle(state: MapState) {
+    setRegion(state.properties);
+  }
+
path: app/app/_layout.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to the map region update function.**

The `onIdle` function updates the region state without any validation or error handling, which could lead to unexpected behavior if the state properties are undefined.

```diff
function MapLayout({ children }: { children: React.ReactNode }) {
  const { setRegion } = useMapStore();

  function onIdle(state: MapState) {
+    if (!state || !state.properties) {
+      console.warn("Invalid map state received in onIdle");
+      return;
+    }
    setRegion(state.properties);
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
function MapLayout({ children }: { children: React.ReactNode }) {
  const { setRegion } = useMapStore();

  function onIdle(state: MapState) {
    if (!state || !state.properties) {
      console.warn("Invalid map state received in onIdle");
      return;
    }
    setRegion(state.properties);
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,12 +24,63 @@ const trpcClient = trpc.createClient({
   ],
 });
 
+// The map is inside the layout component, such that the stack is inside the bottom sheet.
+
 export default function RootLayout() {
   return (
     <trpc.Provider client={trpcClient} queryClient={queryClient}>
       <QueryClientProvider client={queryClient}>
-        <Stack />
+        <MapLayout>
+          <Stack screenOptions={{}} />
+        </MapLayout>
       </QueryClientProvider>
     </trpc.Provider>
   );
 }
+
+// set a stub access token as were using maptiler
+setAccessToken?.("adsfwads");
+
+const snapPoints = ["20%", "40%", "100%"];
+
+function MapLayout({ children }: { children: React.ReactNode }) {
+  const { setRegion } = useMapStore();
+
+  function onIdle(state: MapState) {
+    setRegion(state.properties);
+  }
+
+  return (
+    <GestureHandlerRootView style={styles.container}>
+      <MapView
+        style={styles.map}
+        styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+        onMapIdle={onIdle}
+      >
+        <Camera followUserLocation={true} />
+        <LocationPuck visible={true} />
+        <VectorSource
+          id="maptiler"
+          url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+        />
path: app/app/_layout.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for map loading and environment variables.**

The MapView component uses environment variables without checking if they're defined, which could cause runtime errors.

```diff
+// Verify environment variables are available
+const MAPTILER_KEY = process.env.EXPO_PUBLIC_MAPTILER_KEY;
+if (!MAPTILER_KEY) {
+  console.error("Missing MAPTILER_KEY environment variable");
+}

<MapView
  style={styles.map}
-  styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+  styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${MAPTILER_KEY}`}
  onMapIdle={onIdle}
+  onError={(error) => console.error("MapView error:", error)}
>
  <Camera followUserLocation={true} />
  <LocationPuck visible={true} />
  <VectorSource
    id="maptiler"
-    url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${process.env.EXPO_PUBLIC_MAPTILER_KEY}`}
+    url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${MAPTILER_KEY}`}
  />
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// Verify environment variables are available
const MAPTILER_KEY = process.env.EXPO_PUBLIC_MAPTILER_KEY;
if (!MAPTILER_KEY) {
  console.error("Missing MAPTILER_KEY environment variable");
}

<MapView
  style={styles.map}
  styleURL={`https://api.maptiler.com/maps/streets-v2/style.json?key=${MAPTILER_KEY}`}
  onMapIdle={onIdle}
  onError={(error) => console.error("MapView error:", error)}
>
  <Camera followUserLocation={true} />
  <LocationPuck visible={true} />
  <VectorSource
    id="maptiler"
    url={`https://api.maptiler.com/tiles/v3/tiles.json?key=${MAPTILER_KEY}`}
  />
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,42 +26,46 @@ export default function Infobox({
   );
   const wikiQuery = trpc.getPage.useQuery(query);
 
+  console.log(!!wikiQuery.data, wikiQuery.data);
+
   return (
-    <BottomSheet index={1} snapPoints={snapPoints} enableDynamicSizing={false}>
-      <BottomSheetScrollView>
-        <View padding-8>
-          <Text text40M center>
-            {wikiQuery.data?.title ?? "Loading"}
-          </Text>
-          {false ? (
-            <SkeletonView
-              template={SkeletonView.templates.TEXT_CONTENT}
-              showContent={wikiQuery.isLoading}
-            />
-          ) : (
-            <>
-              <View flex row gap-4 marginT-12>
-                <Infocard title="Understand" />
-                <Infocard title="History" />
+    <View padding-8 flex>
+      <Stack.Screen options={{ title: wikiQuery.data?.title ?? "Loading" }} />
+      <SkeletonView
+        template={SkeletonView.templates.TEXT_CONTENT}
+        showContent={wikiQuery.isSuccess}
+        renderContent={() =>
+          !!wikiQuery.data && (
+            <View flex>
+              <View row gap-8 marginT-12>
+                <Infocard pageId={wikiQuery.data.pageId} title="Understand" />
+                <Infocard pageId={wikiQuery.data.pageId} title="History" />
               </View>
-              <View flex row gap-4 marginT-12>
-                <Infocard title="See" />
-                <Infocard title="Eat" />
-                <Infocard title="Drink" />
+              <View row gap-8 marginT-8>
+                <Infocard pageId={wikiQuery.data.pageId} title="See" />
+                <Infocard pageId={wikiQuery.data.pageId} title="Eat" />
+                <Infocard pageId={wikiQuery.data.pageId} title="Drink" />
               </View>
-            </>
-          )}
-        </View>
-      </BottomSheetScrollView>
-    </BottomSheet>
+            </View>
+          )
+        }
+      />
+    </View>
path: app/components/infobox.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for data fetching and improve loading states.**

The current implementation does not handle API errors gracefully and provides limited feedback during loading states.

```diff
return (
  <View padding-8 flex>
-    <Stack.Screen options={{ title: wikiQuery.data?.title ?? "Loading" }} />
+    <Stack.Screen 
+      options={{ 
+        title: wikiQuery.data?.title ?? (wikiQuery.isError ? "Error" : "Loading"),
+        headerRight: wikiQuery.isLoading ? () => <ActivityIndicator /> : undefined
+      }} 
+    />
    <SkeletonView
      template={SkeletonView.templates.TEXT_CONTENT}
-      showContent={wikiQuery.isSuccess}
+      showContent={!wikiQuery.isLoading && !wikiQuery.isError}
      renderContent={() =>
-        !!wikiQuery.data && (
+        wikiQuery.isSuccess && !!wikiQuery.data ? (
          <View flex>
            <View row gap-8 marginT-12>
              <Infocard pageId={wikiQuery.data.pageId} title="Understand" />
              <Infocard pageId={wikiQuery.data.pageId} title="History" />
            </View>
            <View row gap-8 marginT-8>
              <Infocard pageId={wikiQuery.data.pageId} title="See" />
              <Infocard pageId={wikiQuery.data.pageId} title="Eat" />
              <Infocard pageId={wikiQuery.data.pageId} title="Drink" />
            </View>
          </View>
+        ) : wikiQuery.isError ? (
+          <View center flex>
+            <Text text70>Error loading data. Please try again.</Text>
+            <Button 
+              marginT-12 
+              label="Retry" 
+              onPress={() => wikiQuery.refetch()} 
+            />
+          </View>
        )
      }
    />
  </View>
);
```

Note: You'll need to import these components:
```javascript
import { ActivityIndicator } from 'react-native';
import { Button, Text } from 'react-native-ui-lib';
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-03T12:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,13 +42,18 @@
     "react-native": "0.76.3",
     "react-native-gesture-handler": "~2.20.2",
     "react-native-linear-gradient": "^2.8.3",
+    "react-native-markdown-display": "^7.0.2",
     "react-native-reanimated": "~3.16.1",
path: app/package.json
user: login: bcye
body: required dep
created_at: 2025-03-03T12:21:23Z
author_association: OWNER
diff_hunk: @@ -42,13 +42,18 @@
     "react-native": "0.76.3",
     "react-native-gesture-handler": "~2.20.2",
     "react-native-linear-gradient": "^2.8.3",
+    "react-native-markdown-display": "^7.0.2",
     "react-native-reanimated": "~3.16.1",
path: app/package.json
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@bcye Thanks for confirming that `react-native-shimmer-placeholder` is a required dependency. I see from searching the codebase that there's a `SkeletonView` component being used in your application, which likely depends on this package internally, even if the direct import isn't visible in my initial scan.

It's common for shimmer placeholder libraries to be used indirectly through wrapper components or used dynamically at runtime. This is a good practice for encapsulating UI components like loading states.

I'll note this for future reference when checking dependency usage patterns in your project.



---

🏁 Script executed:
```
#!/bin/bash
# Check for any reference to 'shimmer' in the codebase
echo "Searching for shimmer references:"
rg -i shimmer

# Check for SkeletonView or similar components
echo -e "\nSearching for skeleton/placeholder components:"
rg -i "skeletonview|placeholder"

# Look at the infobox component mentioned in the AI summary
echo -e "\nChecking infobox.tsx content:"
cat app/components/infobox.tsx || echo "File not found"

# Check all TSX files for skeleton-related patterns
echo -e "\nLooking for files with loading/skeleton implementation:"
find . -name "*.tsx" | xargs grep -l "Skeleton\|shimmer\|placeholder" || echo "No matches found"
```

Length of output: 4715

---



</details>

@bcye Thanks for confirming. I can now see how `react-native-shimmer-placeholder` is being used in your project. The `SkeletonView` component from `react-native-ui-lib` that you're using in both `app/components/infobox.tsx` and `app/page/[pageId]/section/[title].tsx` likely depends on this package internally.

This is a common pattern where UI libraries use specialized packages like shimmer placeholders under the hood without requiring you to import them directly. The `SkeletonView.templates.TEXT_CONTENT` template you're using for loading states is probably leveraging this library's functionality.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: bcye
PR: bcye/mapvoyage#1
File: app/package.json:45-46
Timestamp: 2025-03-03T12:22:26.805Z
Learning: In the MapVoyage project, react-native-shimmer-placeholder is a required dependency even if it's not directly imported in any file, as it's likely used indirectly through wrapper components like SkeletonView.
```

---

```
Learnt from: bcye
PR: bcye/mapvoyage#1
File: app/package.json:45-46
Timestamp: 2025-03-03T12:22:26.805Z
Learning: In the MapVoyage project, react-native-shimmer-placeholder is a required dependency because the SkeletonView component from react-native-ui-lib (used in app/components/infobox.tsx and app/page/[pageId]/section/[title].tsx) likely depends on it internally, even though it's not directly imported in any project files.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-03T12:22:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-03T12:13:03Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
These changes restructure the application’s layout and server logic. A new `MapLayout` component now combines map functionalities and a bottom sheet interface, while the `Index` component is refactored to rely on a centralized map state store. A new `Section` component fetches and processes Wikipedia wikitext data, and the `Infobox` has been updated for a more card-based layout with dynamic routing. Additionally, new dependencies (including markdown display, shimmer placeholders, wikitext parsers, and Zustand) were added along with a new utility store and an enhanced server routing to support the wikitext feature, as well as a development script for multi-service management.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `app/app/_layout.tsx`, `app/app/index.tsx` | Introduces a new `MapLayout` component that integrates `MapView`, `BottomSheet`, and gesture handling using `useMapStore`; refactors `Index` to remove embedded map components and rely on centralized state. |
| `app/app/page/[pageId]/section/[title].tsx` | Adds a new `Section` component that fetches Wikipedia wikitext via `trpc`, processes it with `wtf_wikipedia`, and renders Markdown using a loading state (`SkeletonView`). |
| `app/components/infobox.tsx` | Updates the Infobox UI by replacing the `BottomSheet` scroll view with a card-based layout and revises `Infocard` to use a `pageId` prop for dynamic routing. |
| `app/package.json` | Adds new dependencies: `react-native-markdown-display`, `react-native-shimmer-placeholder`, `wtf_wikipedia`, `wtf-plugin-markdown`, and `zustand`. |
| `app/utils/store.ts` | Creates a new Zustand store with `Region` and `Store` types, and exports the `useMapStore` hook for managing map region state. |
| `app/utils/trpc.ts` | Updates the import statement to include `httpBatchLink` from `@trpc/react-query` alongside `createTRPCReact`. |
| `server/index.ts`, `server/procedures/get-page.ts`, `server/procedures/get-wikitext.ts`, `server/trpc.ts` | Enhances server routing by adding the new `getWikitext` procedure; refactors `get-page.ts` to return `pageId` instead of file content; and applies minor formatting adjustments in `trpc.ts`. |
| `start-dev.sh` | Introduces a new shell script that initializes a `tmux` session with multiple windows to run Docker services, set up a tunneling service, start the server in watch mode, and launch the Android application. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant MapLayout
    participant MapView
    participant useMapStore
    participant BottomSheet

    User->>MapLayout: Render with children components
    MapLayout->>MapView: Render map (with Camera, LocationPuck, etc.)
    MapView-->>MapLayout: Trigger onIdle event
    MapLayout->>useMapStore: Update map region state
    MapLayout->>BottomSheet: Render embedded Stack component
```

```mermaid
sequenceDiagram
    participant User
    participant Section
    participant TRPCClient
    participant Server
    participant FileSystem

    User->>Section: Navigate to Section page
    Section->>TRPCClient: Request wikitext with pageId
    TRPCClient->>Server: Invoke getWikitext procedure
    Server->>FileSystem: Read file from WIKITEXT_DIR
    FileSystem-->>Server: Return file content
    Server-->>TRPCClient: Return wikitext string
    TRPCClient-->>Section: Provide wikitext data
    Section->>Section: Process wikitext (via wtf_wikipedia & useMemo)
    Section->>Section: Render Markdown in SkeletonView
```

## Poem
> I'm just a rabbit with floppy ears,  
> Hopping through code and shifting gears,  
> New maps and sections make me soar,  
> Zustand keeps the state to explore,  
> With wikitext magic and scripts so neat,  
> I celebrate changes with a joyful beat!  
> 🐰✨ Happy coding!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Docstrings were successfully generated. (♻️  Check again to generate again)

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-03T12:21:59Z
body: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/bcye/mapvoyage/pull/2
author_association: CONTRIBUTOR