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(