diff --git a/packages/app/components/card/LargeCard.tsx b/packages/app/components/card/LargeCard.tsx
index b6d1d8b72..5db0d9630 100644
--- a/packages/app/components/card/LargeCard.tsx
+++ b/packages/app/components/card/LargeCard.tsx
@@ -126,10 +126,14 @@ const loadStyles = (theme: any) => {
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
- padding: currentTheme.size.cardPadding,
+ padding:
+ Platform.OS === 'web'
+ ? currentTheme.size.cardPadding
+ : currentTheme.size.mobilePadding,
paddingHorizontal: currentTheme.padding.paddingInside,
marginBottom: 20,
height: Platform.OS === 'web' ? 650 : '23%',
+ minHeight: 350,
overflow: 'hidden',
},
};
diff --git a/packages/app/components/destination/index.tsx b/packages/app/components/destination/index.tsx
index 8d4d33190..1f477f335 100644
--- a/packages/app/components/destination/index.tsx
+++ b/packages/app/components/destination/index.tsx
@@ -132,69 +132,80 @@ export const DestinationPage = () => {
const map = () => ;
return (
-
- {isLoading && (
- Loading...
- )}
- {!isLoading && !isError && (
-
-
+
+ {isLoading && (
+
- {Platform.OS === 'web' ? (
-
- ) : (
-
+ )}
+
+ {!isLoading && !isError && (
+ <>
+ {
- router.push('/search');
+ zIndex: 1,
+ width: '100%',
+ ...styles.headerContainer,
}}
>
-
-
- Search by park, city, or trail
-
-
- )}
-
-
-
- (
-
+ ) : (
+ {
+ router.push('/search');
+ }}
+ >
+
+
+ Search by park, city, or trail
+
+
+ )}
+
+
+
+ (
+
+ )}
+ ContentComponent={map}
+ contentProps={{ shape }}
+ type="map"
/>
- )}
- ContentComponent={map}
- contentProps={{ shape }}
- type="map"
- />
-
+
+ >
+ )}
- )}
-
+
+
);
};
@@ -208,7 +219,6 @@ const loadStyles = (theme) => {
paddingBottom: 12,
paddingLeft: 16,
width: '100%',
- marginBottom: 40,
backgroundColor: currentTheme.colors.background,
},
headerContainer: {
@@ -246,6 +256,8 @@ const loadStyles = (theme) => {
justifyContent: 'space-between',
marginBottom: 20,
width: '100%',
+ backgroundColor: isDark ? '#2D2D2D' : currentTheme.colors.white,
+ padding: 25,
},
};
};
diff --git a/packages/app/components/map/Map.native.tsx b/packages/app/components/map/Map.native.tsx
index 5af0e88dd..06d72f517 100644
--- a/packages/app/components/map/Map.native.tsx
+++ b/packages/app/components/map/Map.native.tsx
@@ -14,6 +14,7 @@ import {
RButton as OriginalRButton,
RInput as OriginalRInput,
RStack,
+ RText,
} from '@packrat/ui';
import { MAPBOX_ACCESS_TOKEN } from '@packrat/config';
@@ -37,6 +38,8 @@ import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system';
import { DOMParser } from 'xmldom';
import { MapProps } from './models';
+import { useUserQuery } from 'app/auth/hooks';
+import { useUpdateUser } from 'app/hooks/user/useUpdateUser';
interface GeoJsonProperties {
name?: string;
@@ -66,9 +69,13 @@ Mapbox.setAccessToken(MAPBOX_ACCESS_TOKEN);
const NativeMap: React.FC = ({
shape: shapeProp,
onExitFullScreen,
+ mapName: predefinedMapName,
forceFullScreen = false,
- downloadable = true,
+ shouldEnableDownload = true,
}) => {
+ const { user, refetch } = useUserQuery();
+ console.log({ user });
+ const updateUser = useUpdateUser();
const styles = useCustomStyles(loadStyles);
const {
camera,
@@ -85,7 +92,7 @@ const NativeMap: React.FC = ({
setShowMapNameInputDialog,
shape,
setShape,
- mapName,
+ mapName: rawMapName,
setMapName,
trailCenterPoint,
setTrailCenterPoint,
@@ -99,6 +106,13 @@ const NativeMap: React.FC = ({
// For some reason not setting default state value not working from hook
const isFullScreenMode = forceFullScreen || mapFullscreen;
+ const mapName = rawMapName?.trim();
+ const mapNameErrorMessage = !mapName
+ ? 'The map name must not be empty'
+ : user?.offlineMaps?.[mapName?.toLowerCase()] != null
+ ? 'A map with the same name already exist'
+ : '';
+
const handleShapeUpload = async () => {
try {
const result: any = await DocumentPicker.getDocumentAsync({
@@ -116,6 +130,36 @@ const NativeMap: React.FC = ({
}
};
+ const handleDownloadMap = async () => {
+ const bounds = mapViewRef.current
+ ? await mapViewRef.current.getVisibleBounds()
+ : null;
+ const downloadOptions = {
+ name: mapName,
+ styleURL: 'mapbox://styles/mapbox/outdoors-v11',
+ bounds,
+ minZoom: 0,
+ maxZoom: 8,
+ metadata: {
+ shape: JSON.stringify(shape),
+ },
+ };
+
+ // Save the map under user profile.
+ updateUser({
+ id: user.id,
+ offlineMaps: {
+ ...(user.offlineMaps || {}),
+ [mapName.toLowerCase()]: downloadOptions,
+ },
+ })
+ .then(() => refetch())
+ .then(() => {
+ onDownload(downloadOptions);
+ })
+ .catch(() => {});
+ };
+
function CircleCapComp() {
return (
= ({
});
}}
styles={styles}
- downloadable={downloadable || isShapeDownloadable(shape)}
+ downloadable={shouldEnableDownload && isShapeDownloadable(shape)}
downloading={downloading}
shape={shape}
onDownload={() => {
- setShowMapNameInputDialog(true);
+ if (predefinedMapName) {
+ handleDownloadMap();
+ } else {
+ setShowMapNameInputDialog(true);
+ }
}}
- handleGpxUpload={handleShapeUpload}
+ handleGpxUpload={shouldEnableDownload && handleShapeUpload}
progress={progress}
/>
@@ -334,11 +382,14 @@ const NativeMap: React.FC = ({
onChangeText={(text) => {
setMapName(text);
}}
- value={mapName}
+ value={rawMapName}
mx="3"
placeholder="map name"
w="100%"
/>
+
+ {mapNameErrorMessage}
+
= ({
{
- setShowMapNameInputDialog(false);
- const bounds = mapViewRef.current
- ? await mapViewRef.current.getVisibleBounds()
- : null;
- const downloadOptions = {
- name: mapName,
- styleURL: 'mapbox://styles/mapbox/outdoors-v11',
- bounds,
- minZoom: 0,
- maxZoom: 8,
- metadata: {
- shape: JSON.stringify(shape),
- },
- };
-
- onDownload(downloadOptions);
+ disabled={!!mapNameErrorMessage}
+ onPress={() => {
setShowMapNameInputDialog(false);
+ handleDownloadMap();
}}
>
OK
@@ -439,6 +476,11 @@ const loadStyles = () => ({
justifyContent: 'center',
alignItems: 'center',
},
+ mapNameFieldErrorMessage: {
+ color: theme.colors.error,
+ fontStyle: 'italic',
+ fontSize: 12,
+ },
});
export default NativeMap;
diff --git a/packages/app/components/map/MapPreview.tsx b/packages/app/components/map/MapPreview.tsx
index 1d7215bd8..c47578eb9 100644
--- a/packages/app/components/map/MapPreview.tsx
+++ b/packages/app/components/map/MapPreview.tsx
@@ -1,7 +1,9 @@
import { RImage } from '@packrat/ui';
import { useProcessedShape, useMapPreviewData } from './useMapPreview';
+import { useAuthUserToken } from 'app/auth/hooks';
export default function MapPreview({ shape }) {
const processedShape = useProcessedShape(shape);
+ const { token } = useAuthUserToken();
const mapPreviewData: any = useMapPreviewData(shape, processedShape);
if (!mapPreviewData) return null;
@@ -14,6 +16,9 @@ export default function MapPreview({ shape }) {
}}
source={{
uri: mapPreviewData.uri,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
}}
/>
);
diff --git a/packages/app/components/map/models.ts b/packages/app/components/map/models.ts
index 793c1d935..adb61cd02 100644
--- a/packages/app/components/map/models.ts
+++ b/packages/app/components/map/models.ts
@@ -2,5 +2,6 @@ export interface MapProps {
shape: any;
onExitFullScreen?: () => void;
forceFullScreen?: boolean;
- downloadable?: boolean;
+ shouldEnableDownload?: boolean;
+ mapName?: string;
}
diff --git a/packages/app/hooks/user/useUpdateUser.ts b/packages/app/hooks/user/useUpdateUser.ts
index 527abc6ed..74ab06937 100644
--- a/packages/app/hooks/user/useUpdateUser.ts
+++ b/packages/app/hooks/user/useUpdateUser.ts
@@ -13,7 +13,7 @@ export const useUpdateUser = () => {
setUser(updatedUser);
return updatedUser; // Return the updated user or a value to indicate success
} catch (error) {
- console.error('Failed to update the user');
+ console.error('Failed to update the user', error);
throw error;
}
};
diff --git a/packages/app/screens/maps/index.tsx b/packages/app/screens/maps/index.tsx
index 4bc5f24e8..82714f9eb 100644
--- a/packages/app/screens/maps/index.tsx
+++ b/packages/app/screens/maps/index.tsx
@@ -1,29 +1,40 @@
-import { Modal, Text, View, Image, Dimensions } from 'react-native';
-import Mapbox, {
- offlineManager,
- OfflineCreatePackOptions,
-} from '@rnmapbox/maps';
+import { Modal, Text, View, Image } from 'react-native';
+import { offlineManager } from '@rnmapbox/maps';
import { useState, useCallback } from 'react';
import { useFocusEffect } from 'expo-router';
import { TouchableOpacity } from 'react-native-gesture-handler';
-import MapButtonsOverlay from 'app/components/map/MapButtonsOverlay';
-import { theme } from 'app/theme';
import useTheme from 'app/hooks/useTheme';
-import { StyleSheet } from 'react-native';
-import {
- calculateZoomLevel,
- getShapeSourceBounds,
-} from 'app/utils/mapFunctions';
import { api } from 'app/constants/api';
-import { RScrollView, RStack } from '@packrat/ui';
+import { RButton, RScrollView, RStack } from '@packrat/ui';
import useCustomStyles from 'app/hooks/useCustomStyles';
import { Map } from 'app/components/map';
+import { useAuthUserToken, useUserQuery } from 'app/auth/hooks';
+import type OfflinePack from '@rnmapbox/maps/lib/typescript/src/modules/offline/OfflinePack';
+import { ZStack } from 'tamagui';
-interface Pack {
- bounds: number[][];
- metadata: string;
+interface OfflineMap {
+ name: string;
+ styleURL: string;
+ bounds: [number[], number[]];
+ minZoom: number;
+ maxZoom: number;
+ downloaded: boolean;
+ metadata: {
+ shape: unknown;
+ };
}
+const getCenterCoordinates = (bounds: [number[], number[]]) => {
+ const [
+ [southWestLongitude, southWestLatitude],
+ [northEastLongitude, northEastLatitude],
+ ] = bounds;
+ const centerLatitude = (northEastLatitude + southWestLatitude) / 2;
+ const centerLongitude = (northEastLongitude + southWestLongitude) / 2;
+
+ return [centerLongitude, centerLatitude];
+};
+
function CircleCapComp() {
const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
useTheme();
@@ -46,33 +57,49 @@ export default function DownloadedMaps() {
const styles = useCustomStyles(loadStyles);
const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
useTheme();
- const [offlinePacks, setOfflinePacks] = useState([]);
- const [showMap, setShowMap] = useState(false);
- const [pack, setPack] = useState(null);
+ const { user } = useUserQuery();
+ const { token } = useAuthUserToken();
+ const [offlineMaps, setOfflineMaps] = useState();
+ const refreshOfflineMapList = async () => {
+ const offlineMaps = Object.values(user.offlineMaps || {});
+ const offlineMapboxPacks: OfflineMap[] = [];
+ for (const map of offlineMaps) {
+ const offlineMap: OfflineMap = {
+ styleURL: `${map.styleURL}`,
+ name: `${map.name}`,
+ minZoom: map.minZoom,
+ maxZoom: map.maxZoom,
+ bounds: map.bounds,
+ metadata: {
+ shape: JSON.parse(map.metadata.shape),
+ },
+ downloaded: false,
+ };
+
+ let offlineMapboxPack: OfflinePack | null;
+ try {
+ offlineMapboxPack = await offlineManager.getPack(map.name);
+ } catch (error) {
+ console.error(error);
+ offlineMapboxPack = null;
+ }
+
+ if (offlineMapboxPack) {
+ offlineMap.downloaded = true;
+ }
+
+ offlineMapboxPacks.push(offlineMap);
+ }
- let shape, zoomLevel;
- if (pack != null) {
- shape = pack && JSON.parse(JSON.parse(pack.metadata).shape);
- const dw = Dimensions.get('screen').width;
- zoomLevel = calculateZoomLevel(pack.bounds, {
- width: dw,
- height: 360,
- });
- }
+ setOfflineMaps(offlineMapboxPacks);
+ };
useFocusEffect(
useCallback(() => {
- offlineManager.getPacks().then((packs) => {
- setOfflinePacks(packs);
- });
- }, []),
+ refreshOfflineMapList();
+ }, [user]),
);
- const handleExitMapFullScreen = () => {
- setShowMap(false);
- setPack(null);
- };
-
return (
Downloaded Maps
- {offlinePacks ? (
+ {offlineMaps ? (
- {offlinePacks.map(({ pack }) => {
- const metadata = JSON.parse(pack.metadata);
+ {offlineMaps.map((offlineMap) => {
+ const center = getCenterCoordinates(offlineMap.bounds);
return (
- {
- setPack(pack);
- setShowMap(true);
- }}
>
- {pack && (
-
- )}
+
- {metadata.name}
+ {offlineMap.name}
-
+
);
})}
@@ -143,33 +160,10 @@ export default function DownloadedMaps() {
)}
- {showMap ? (
-
-
-
- ) : null}
);
}
-const getCenterCoordinates = (bounds: [number, number, number, number]) => {
- const [
- southWestLongitude,
- southWestLatitude,
- northEastLongitude,
- northEastLatitude,
- ] = bounds;
- const centerLatitude = (northEastLatitude + southWestLatitude) / 2;
- const centerLongitude = (northEastLongitude + southWestLongitude) / 2;
-
- return [centerLongitude, centerLatitude];
-};
-
const loadStyles = ({ currentTheme }) => {
return {
lineLayer: {
diff --git a/packages/ui/src/RButton/index.tsx b/packages/ui/src/RButton/index.tsx
index 5471a04e7..fd903327d 100644
--- a/packages/ui/src/RButton/index.tsx
+++ b/packages/ui/src/RButton/index.tsx
@@ -9,6 +9,13 @@ interface HapticRButtonProps extends ButtonProps {
const StyledButton = styled(Button, {
backgroundColor: '#0C66A1', // temp fix, we need to set up proper tamagui theme
color: 'white',
+ variants: {
+ disabled: {
+ true: {
+ backgroundColor: '#5C788A',
+ },
+ },
+ },
} as any);
const RButton = React.forwardRef(