diff --git a/.github/actions/deploy-cloudflare-worker/action.yml b/.github/actions/deploy-cloudflare-worker/action.yml
index 0fd58a68f..41c58aa14 100644
--- a/.github/actions/deploy-cloudflare-worker/action.yml
+++ b/.github/actions/deploy-cloudflare-worker/action.yml
@@ -1,5 +1,5 @@
-name: 'Print Wrangler Logs'
-description: 'Prints Wrangler logs files if exist'
+name: 'Deploy worker'
+description: 'Deploys worker'
inputs:
WORKERS_AI_API_KEY:
description: 'The Workers AI API Key'
@@ -12,7 +12,7 @@ inputs:
required: true
BUCKET_SECRET_KEY:
description: 'The Bucket Secret Key'
- requuired: true
+ required: true
GOOGLE_CLIENT_SECRET:
description: 'The Google client secret'
required: true
@@ -59,7 +59,7 @@ inputs:
runs:
using: 'composite'
steps:
- - name: Deploy database
+ - name: Deploy worker
uses: cloudflare/wrangler-action@v3.3.2
with:
wranglerVersion: '3.75.0'
diff --git a/.github/actions/migrate-d1-database/action.yml b/.github/actions/migrate-d1-database/action.yml
index 3388d50f2..0ed1b06bd 100644
--- a/.github/actions/migrate-d1-database/action.yml
+++ b/.github/actions/migrate-d1-database/action.yml
@@ -1,5 +1,5 @@
-name: 'Print Wrangler Logs'
-description: 'Prints Wrangler logs files if exist'
+name: 'Migrate DB'
+description: 'Applies DB migrations'
inputs:
CLOUDFLARE_API_TOKEN:
description: 'Cloudflare API Token'
diff --git a/.github/actions/setup-wrangler-toml/action.yml b/.github/actions/setup-wrangler-toml-and-dotenv/action.yml
similarity index 70%
rename from .github/actions/setup-wrangler-toml/action.yml
rename to .github/actions/setup-wrangler-toml-and-dotenv/action.yml
index 4b30474c7..65a3daec7 100644
--- a/.github/actions/setup-wrangler-toml/action.yml
+++ b/.github/actions/setup-wrangler-toml-and-dotenv/action.yml
@@ -1,52 +1,53 @@
-name: "Generate Wrangler TOML"
-description: "Generates the wrangler.toml file for Cloudflare Workers"
+name: 'Generate Wrangler TOML and Dotenv File'
+description: 'Generates wrangler.toml and .env files for Cloudflare Workers'
inputs:
APP_URL:
- description: "The Application URL"
+ description: 'The Application URL'
required: true
AWS_SIGN_ALGORITHM:
- description: "The AWS sign algorithm"
+ description: 'The AWS sign algorithm'
required: true
BUCKET_ENDPOINT:
- description: "The bucket endpoint"
+ description: 'The bucket endpoint'
required: true
BUCKET_NAME:
- description: "The bucket name"
+ description: 'The bucket name'
required: true
BUCKET_REGION:
- description: "The bucket region"
+ description: 'The bucket region'
required: true
BUCKET_SERVICE:
- description: "The bucket service"
+ description: 'The bucket service'
required: true
BUCKET_SESSION_TOKEN:
- description: "The bucket session token"
+ description: 'The bucket session token'
required: true
environment:
- description: "The current environment"
+ description: 'The current environment'
required: true
CLOUDFLARE_ACCOUNT_ID:
- description: "Cloudflare Account ID"
+ description: 'Cloudflare Account ID'
required: true
DB_ID:
- description: "The database ID"
+ description: 'The database ID'
required: true
GOOGLE_CLIENT_ID:
- description: "Google Client ID"
+ description: 'Google Client ID'
required: true
STMP_EMAIL:
- description: "SMTP Email"
+ description: 'SMTP Email'
required: true
VECTOR_INDEX_NAME:
- description: "The vector index"
+ description: 'The vector index'
required: true
runs:
- using: "composite"
+ using: 'composite'
steps:
- - name: Generate wrangler.toml
+ - name: Generate wrangler.toml and .env
shell: bash
working-directory: ${{ github.workspace }}/server
run: |
+ echo 'NODE_ENV=development' > .env
echo 'name = "packrat-api"' > wrangler.toml
echo 'main = "src/index.ts"' >> wrangler.toml
echo 'compatibility_date = "2024-03-14"' >> wrangler.toml
@@ -56,11 +57,12 @@ runs:
echo 'binding = "DB"' >> wrangler.toml
echo 'database_name = "${{ inputs.environment }}"' >> wrangler.toml
echo 'database_id = "${{ inputs.DB_ID }}"' >> wrangler.toml
+ echo 'migrations_dir = "${{ inputs.MIGRATIONS_PATH }}"' >> wrangler.toml
+ echo '[[env.${{ inputs.environment }}.r2_buckets]]' >> wrangler.toml
+ echo 'binding = "GEOJSON_BUCKET"' >> wrangler.toml
+ echo 'bucket_name = "packrat-geojson-bucket-${{ inputs.environment }}"' >> wrangler.toml
echo '[env.${{ inputs.environment }}.ai]' >> wrangler.toml
echo 'binding = "AI"' >> wrangler.toml
- echo '[[env.${{ inputs.environment }}.vectorize]]' >> wrangler.toml
- echo 'binding = "VECTOR_INDEX"' >> wrangler.toml
- echo 'index_name = "${{ inputs.VECTOR_INDEX_NAME }}"' >> wrangler.toml
echo '[env.${{ inputs.environment }}.vars]' >> wrangler.toml
echo 'APP_URL = "${{ inputs.APP_URL }}"' >> wrangler.toml
echo 'AWS_SIGN_ALGORITHM = "${{ inputs.AWS_SIGN_ALGORITHM }}"' >> wrangler.toml
diff --git a/.github/workflows/backend-preview.yml b/.github/workflows/backend-preview.yml
index 20b4d99b8..3a95406cd 100644
--- a/.github/workflows/backend-preview.yml
+++ b/.github/workflows/backend-preview.yml
@@ -8,7 +8,6 @@ on:
paths:
- '.github/workflows/backend-preview.yml'
- 'packages/validations/**'
- - 'packages/shared-types/**'
- 'server/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -29,8 +28,8 @@ jobs:
- name: Setup JS Runtime environment
uses: ./.github/actions/setup-js-runtime
- - name: Generate wrangler.toml
- uses: ./.github/actions/setup-wrangler-toml
+ - name: Generate wrangler.toml and .env
+ uses: ./.github/actions/setup-wrangler-toml-and-dotenv
with:
environment: preview
APP_URL: ${{ secrets.PREVIEW_APP_URL }}
@@ -42,6 +41,7 @@ jobs:
BUCKET_SESSION_TOKEN: ${{ secrets.PREVIEW_BUCKET_SESSION_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.PREVIEW_CLOUDFLARE_ACCOUNT_ID }}
DB_ID: ${{ secrets.PREVIEW_DB_ID }}
+ MIGRATIONS_PATH: migrations-preview
GOOGLE_CLIENT_ID: ${{ secrets.PREVIEW_GOOGLE_CLIENT_ID }}
STMP_EMAIL: ${{ secrets.PREVIEW_STMP_EMAIL }}
VECTOR_INDEX_NAME: ${{ secrets.PREVIEW_VECTOR_INDEX }}
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 57dda3934..0e347aa86 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -9,7 +9,6 @@ on:
paths:
- '.github/workflows/backend.yml'
- 'packages/validations/**'
- - 'packages/shared-types/**'
- 'server/**'
concurrency:
@@ -31,8 +30,8 @@ jobs:
- name: Setup JS Runtime environment
uses: ./.github/actions/setup-js-runtime
- - name: Generate wrangler.toml
- uses: ./.github/actions/setup-wrangler-toml
+ - name: Generate wrangler.toml and .env
+ uses: ./.github/actions/setup-wrangler-toml-and-dotenv
with:
environment: production
APP_URL: ${{ secrets.APP_URL }}
@@ -44,6 +43,7 @@ jobs:
BUCKET_SESSION_TOKEN: ${{ secrets.BUCKET_SESSION_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
DB_ID: ${{ secrets.PRODUCTION_DB_ID }}
+ MIGRATIONS_PATH: migrations
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
STMP_EMAIL: ${{ secrets.STMP_EMAIL }}
VECTOR_INDEX_NAME: ${{ secrets.VECTOR_INDEX }}
@@ -75,4 +75,3 @@ jobs:
VECTORIZE_API_KEY: ${{ secrets.VECTORIZE_API_KEY }}
WORKERS_AI_API_KEY: ${{ secrets.WORKERS_AI_API_KEY }}
X_AMZ_SECURITY_TOKEN: ${{ secrets.X_AMZ_SECURITY_TOKEN }}
-
diff --git a/.github/workflows/eas-cloud.yml b/.github/workflows/eas-cloud.yml
index c9a0e63fe..f3030826a 100644
--- a/.github/workflows/eas-cloud.yml
+++ b/.github/workflows/eas-cloud.yml
@@ -11,7 +11,6 @@ on:
- 'packages/app/**'
- 'apps/expo/**'
- 'packages/ui/**'
- - 'packages/shared-types/**'
- 'packages/config/**'
- 'packages/crosspath/**'
diff --git a/.github/workflows/eas-local.yml b/.github/workflows/eas-local.yml
index fe2803618..5a3d535fa 100644
--- a/.github/workflows/eas-local.yml
+++ b/.github/workflows/eas-local.yml
@@ -8,7 +8,6 @@ on:
- 'packages/app/**'
- 'apps/expo/**'
- 'packages/ui/**'
- - 'packages/shared-types/**'
- 'packages/config/**'
- 'packages/crosspath/**'
pull_request:
@@ -17,7 +16,6 @@ on:
- 'packages/app/**'
- 'apps/expo/**'
- 'packages/ui/**'
- - 'packages/shared-types/**'
- 'packages/config/**'
- 'packages/crosspath/**'
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/map/index.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/map/index.tsx
index d0108cf7c..412f41647 100644
--- a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/map/index.tsx
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/map/index.tsx
@@ -21,7 +21,7 @@ export default function FeedNav() {
// https://reactnavigation.org/docs/headers#replacing-the-title-with-a-custom-component
}}
/>
-
+
>
);
}
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/maps/index.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/maps/index.tsx
index c7c012c59..c632702eb 100644
--- a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/maps/index.tsx
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/maps/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import Maps from 'app/screens/maps';
+import { OfflineMapsScreen } from 'app/modules/map/screens/OfflineMapsScreen';
import { Platform } from 'react-native';
import { Stack } from 'expo-router';
import Head from 'expo-router/head';
@@ -36,7 +36,7 @@ export default function MapsScreen() {
// https://reactnavigation.org/docs/headers#replacing-the-title-with-a-custom-component
}}
/>
-
+
>
);
}
diff --git a/apps/vite/src/routeTree.gen.ts b/apps/vite/src/routeTree.gen.ts
index 8e9dfcd1e..ad9342afd 100644
--- a/apps/vite/src/routeTree.gen.ts
+++ b/apps/vite/src/routeTree.gen.ts
@@ -339,31 +339,219 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
-export const routeTree = rootRoute.addChildren({
- IndexRoute,
- DestinationQueryLazyRoute,
- ItemItemIdLazyRoute,
- PackIdLazyRoute,
- PackCreateLazyRoute,
- ProfileIdLazyRoute,
- TripTripIdLazyRoute,
- TripCreateLazyRoute,
- AboutIndexLazyRoute,
- AppearanceIndexLazyRoute,
- DashboardIndexLazyRoute,
- FeedIndexLazyRoute,
- ItemsIndexLazyRoute,
- MapIndexLazyRoute,
- MapsIndexLazyRoute,
- PacksIndexLazyRoute,
- PasswordResetIndexLazyRoute,
- PrivacyIndexLazyRoute,
- ProfileIndexLazyRoute,
- RegisterIndexLazyRoute,
- SignInIndexLazyRoute,
- TripsIndexLazyRoute,
- ProfileSettingsIndexLazyRoute,
-})
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/destination/query': typeof DestinationQueryLazyRoute
+ '/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack/$id': typeof PackIdLazyRoute
+ '/pack/create': typeof PackCreateLazyRoute
+ '/profile/$id': typeof ProfileIdLazyRoute
+ '/trip/$tripId': typeof TripTripIdLazyRoute
+ '/trip/create': typeof TripCreateLazyRoute
+ '/about': typeof AboutIndexLazyRoute
+ '/appearance': typeof AppearanceIndexLazyRoute
+ '/dashboard': typeof DashboardIndexLazyRoute
+ '/feed': typeof FeedIndexLazyRoute
+ '/items': typeof ItemsIndexLazyRoute
+ '/map': typeof MapIndexLazyRoute
+ '/maps': typeof MapsIndexLazyRoute
+ '/packs': typeof PacksIndexLazyRoute
+ '/password-reset': typeof PasswordResetIndexLazyRoute
+ '/privacy': typeof PrivacyIndexLazyRoute
+ '/profile': typeof ProfileIndexLazyRoute
+ '/register': typeof RegisterIndexLazyRoute
+ '/sign-in': typeof SignInIndexLazyRoute
+ '/trips': typeof TripsIndexLazyRoute
+ '/profile/settings': typeof ProfileSettingsIndexLazyRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/destination/query': typeof DestinationQueryLazyRoute
+ '/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack/$id': typeof PackIdLazyRoute
+ '/pack/create': typeof PackCreateLazyRoute
+ '/profile/$id': typeof ProfileIdLazyRoute
+ '/trip/$tripId': typeof TripTripIdLazyRoute
+ '/trip/create': typeof TripCreateLazyRoute
+ '/about': typeof AboutIndexLazyRoute
+ '/appearance': typeof AppearanceIndexLazyRoute
+ '/dashboard': typeof DashboardIndexLazyRoute
+ '/feed': typeof FeedIndexLazyRoute
+ '/items': typeof ItemsIndexLazyRoute
+ '/map': typeof MapIndexLazyRoute
+ '/maps': typeof MapsIndexLazyRoute
+ '/packs': typeof PacksIndexLazyRoute
+ '/password-reset': typeof PasswordResetIndexLazyRoute
+ '/privacy': typeof PrivacyIndexLazyRoute
+ '/profile': typeof ProfileIndexLazyRoute
+ '/register': typeof RegisterIndexLazyRoute
+ '/sign-in': typeof SignInIndexLazyRoute
+ '/trips': typeof TripsIndexLazyRoute
+ '/profile/settings': typeof ProfileSettingsIndexLazyRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/destination/query': typeof DestinationQueryLazyRoute
+ '/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack/$id': typeof PackIdLazyRoute
+ '/pack/create': typeof PackCreateLazyRoute
+ '/profile/$id': typeof ProfileIdLazyRoute
+ '/trip/$tripId': typeof TripTripIdLazyRoute
+ '/trip/create': typeof TripCreateLazyRoute
+ '/about/': typeof AboutIndexLazyRoute
+ '/appearance/': typeof AppearanceIndexLazyRoute
+ '/dashboard/': typeof DashboardIndexLazyRoute
+ '/feed/': typeof FeedIndexLazyRoute
+ '/items/': typeof ItemsIndexLazyRoute
+ '/map/': typeof MapIndexLazyRoute
+ '/maps/': typeof MapsIndexLazyRoute
+ '/packs/': typeof PacksIndexLazyRoute
+ '/password-reset/': typeof PasswordResetIndexLazyRoute
+ '/privacy/': typeof PrivacyIndexLazyRoute
+ '/profile/': typeof ProfileIndexLazyRoute
+ '/register/': typeof RegisterIndexLazyRoute
+ '/sign-in/': typeof SignInIndexLazyRoute
+ '/trips/': typeof TripsIndexLazyRoute
+ '/profile/settings/': typeof ProfileSettingsIndexLazyRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/destination/query'
+ | '/item/$itemId'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about'
+ | '/appearance'
+ | '/dashboard'
+ | '/feed'
+ | '/items'
+ | '/map'
+ | '/maps'
+ | '/packs'
+ | '/password-reset'
+ | '/privacy'
+ | '/profile'
+ | '/register'
+ | '/sign-in'
+ | '/trips'
+ | '/profile/settings'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/destination/query'
+ | '/item/$itemId'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about'
+ | '/appearance'
+ | '/dashboard'
+ | '/feed'
+ | '/items'
+ | '/map'
+ | '/maps'
+ | '/packs'
+ | '/password-reset'
+ | '/privacy'
+ | '/profile'
+ | '/register'
+ | '/sign-in'
+ | '/trips'
+ | '/profile/settings'
+ id:
+ | '__root__'
+ | '/'
+ | '/destination/query'
+ | '/item/$itemId'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about/'
+ | '/appearance/'
+ | '/dashboard/'
+ | '/feed/'
+ | '/items/'
+ | '/map/'
+ | '/maps/'
+ | '/packs/'
+ | '/password-reset/'
+ | '/privacy/'
+ | '/profile/'
+ | '/register/'
+ | '/sign-in/'
+ | '/trips/'
+ | '/profile/settings/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ DestinationQueryLazyRoute: typeof DestinationQueryLazyRoute
+ ItemItemIdLazyRoute: typeof ItemItemIdLazyRoute
+ PackIdLazyRoute: typeof PackIdLazyRoute
+ PackCreateLazyRoute: typeof PackCreateLazyRoute
+ ProfileIdLazyRoute: typeof ProfileIdLazyRoute
+ TripTripIdLazyRoute: typeof TripTripIdLazyRoute
+ TripCreateLazyRoute: typeof TripCreateLazyRoute
+ AboutIndexLazyRoute: typeof AboutIndexLazyRoute
+ AppearanceIndexLazyRoute: typeof AppearanceIndexLazyRoute
+ DashboardIndexLazyRoute: typeof DashboardIndexLazyRoute
+ FeedIndexLazyRoute: typeof FeedIndexLazyRoute
+ ItemsIndexLazyRoute: typeof ItemsIndexLazyRoute
+ MapIndexLazyRoute: typeof MapIndexLazyRoute
+ MapsIndexLazyRoute: typeof MapsIndexLazyRoute
+ PacksIndexLazyRoute: typeof PacksIndexLazyRoute
+ PasswordResetIndexLazyRoute: typeof PasswordResetIndexLazyRoute
+ PrivacyIndexLazyRoute: typeof PrivacyIndexLazyRoute
+ ProfileIndexLazyRoute: typeof ProfileIndexLazyRoute
+ RegisterIndexLazyRoute: typeof RegisterIndexLazyRoute
+ SignInIndexLazyRoute: typeof SignInIndexLazyRoute
+ TripsIndexLazyRoute: typeof TripsIndexLazyRoute
+ ProfileSettingsIndexLazyRoute: typeof ProfileSettingsIndexLazyRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ DestinationQueryLazyRoute: DestinationQueryLazyRoute,
+ ItemItemIdLazyRoute: ItemItemIdLazyRoute,
+ PackIdLazyRoute: PackIdLazyRoute,
+ PackCreateLazyRoute: PackCreateLazyRoute,
+ ProfileIdLazyRoute: ProfileIdLazyRoute,
+ TripTripIdLazyRoute: TripTripIdLazyRoute,
+ TripCreateLazyRoute: TripCreateLazyRoute,
+ AboutIndexLazyRoute: AboutIndexLazyRoute,
+ AppearanceIndexLazyRoute: AppearanceIndexLazyRoute,
+ DashboardIndexLazyRoute: DashboardIndexLazyRoute,
+ FeedIndexLazyRoute: FeedIndexLazyRoute,
+ ItemsIndexLazyRoute: ItemsIndexLazyRoute,
+ MapIndexLazyRoute: MapIndexLazyRoute,
+ MapsIndexLazyRoute: MapsIndexLazyRoute,
+ PacksIndexLazyRoute: PacksIndexLazyRoute,
+ PasswordResetIndexLazyRoute: PasswordResetIndexLazyRoute,
+ PrivacyIndexLazyRoute: PrivacyIndexLazyRoute,
+ ProfileIndexLazyRoute: ProfileIndexLazyRoute,
+ RegisterIndexLazyRoute: RegisterIndexLazyRoute,
+ SignInIndexLazyRoute: SignInIndexLazyRoute,
+ TripsIndexLazyRoute: TripsIndexLazyRoute,
+ ProfileSettingsIndexLazyRoute: ProfileSettingsIndexLazyRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
/* prettier-ignore-end */
diff --git a/packages/app/components/Fab/FabNative.tsx b/packages/app/components/Fab/FabNative.tsx
index 81cbe2cec..500203c6b 100644
--- a/packages/app/components/Fab/FabNative.tsx
+++ b/packages/app/components/Fab/FabNative.tsx
@@ -34,7 +34,6 @@ const loadStyles = (theme) => {
bottom: 70,
right: 40,
zIndex: 1,
- backgroundColor: currentTheme.colors.background,
height: 54,
width: 150,
borderRadius: 5,
diff --git a/packages/app/components/Fab/FabWeb.tsx b/packages/app/components/Fab/FabWeb.tsx
index a25ff8786..cd6e9753b 100644
--- a/packages/app/components/Fab/FabWeb.tsx
+++ b/packages/app/components/Fab/FabWeb.tsx
@@ -32,7 +32,6 @@ const loadStyles = (theme) => {
top: 50,
right: 10,
zIndex: 1,
- backgroundColor: currentTheme.colors.background,
height: 54,
width: 150,
borderRadius: 5,
diff --git a/packages/app/components/ScoreContainer.tsx b/packages/app/components/ScoreContainer.tsx
index 5e821fe1c..994589ac5 100644
--- a/packages/app/components/ScoreContainer.tsx
+++ b/packages/app/components/ScoreContainer.tsx
@@ -206,11 +206,11 @@ export const ScoreContainer: React.FC = ({
{description}
- {isOwner && (
+ {/* {isOwner && (
Calculate Score
- )}
+ )} */}
{isAlreadyScored && (
= ({ score }) => {
+ const { currentTheme } = useTheme();
+
+ return (
+
+
+ {score}
+
+ );
+};
diff --git a/packages/app/components/ScoreLabel/index.ts b/packages/app/components/ScoreLabel/index.ts
new file mode 100644
index 000000000..738a85c08
--- /dev/null
+++ b/packages/app/components/ScoreLabel/index.ts
@@ -0,0 +1 @@
+export * from './ScoreLable';
diff --git a/packages/app/components/SearchInput/SearchInput.tsx b/packages/app/components/SearchInput/SearchInput.tsx
index 4b80163ed..baa9ab039 100644
--- a/packages/app/components/SearchInput/SearchInput.tsx
+++ b/packages/app/components/SearchInput/SearchInput.tsx
@@ -170,11 +170,13 @@ export const SearchInput = forwardRef(
alignSelf="center"
position="relative"
backgroundColor={currentTheme.colors.background}
+ paddingTop={10}
borderRadius={8}
>
(
padding: 0,
margin: 0,
flex: 1,
+ fontSize: 20,
}}
/>
{searchString && searchString.trim().length > 0 && (
diff --git a/packages/app/components/card/LargeCard.tsx b/packages/app/components/card/LargeCard.tsx
index 1868503cf..4fb23bb3d 100644
--- a/packages/app/components/card/LargeCard.tsx
+++ b/packages/app/components/card/LargeCard.tsx
@@ -52,7 +52,8 @@ const LargeCard: React.FC = ({
}) => {
const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
useTheme();
- const containerStyle = customStyle || getContainerStyle(type);
+ // const containerStyle = customStyle || getContainerStyle(type);
+ const containerStyle = { ...getContainerStyle(type), ...(customStyle || {}) };
return (
= ({
flexDirection: 'row',
gap: 15,
alignItems: 'center',
- paddingVertical: 15,
}}
>
{Icon ? : null}
@@ -106,9 +106,9 @@ const loadStyles = (theme: any) => {
padding: currentTheme.size.mobilePadding,
justifyContent: 'space-between',
alignItems: 'center',
- gap: 25,
- flex: 1,
- paddingHorizontal: 100,
+ // gap: 25,
+ // flex: 1,
+ // paddingHorizontal: 100,
},
searchContainer: {
backgroundColor: currentTheme.colors.card,
@@ -127,7 +127,7 @@ const loadStyles = (theme: any) => {
alignItems: 'center',
textAlign: 'center',
padding: Platform.OS === 'web' ? '5%' : currentTheme.size.mobilePadding,
- paddingHorizontal: currentTheme.padding.paddingInside,
+ paddingHorizontal: 20,
marginBottom: 20,
height: Platform.OS === 'web' ? 'calc(min( 80vh, 80vw))' : '23%',
minHeight: 350,
diff --git a/packages/app/components/card/PackCardHeader/PackCardHeader.tsx b/packages/app/components/card/PackCardHeader/PackCardHeader.tsx
index 2b1ed6f60..006e131c8 100644
--- a/packages/app/components/card/PackCardHeader/PackCardHeader.tsx
+++ b/packages/app/components/card/PackCardHeader/PackCardHeader.tsx
@@ -3,7 +3,7 @@ import useTheme from 'app/hooks/useTheme';
import { CustomCardHeader } from '../CustomCardHeader';
import { AntDesign, MaterialIcons } from '@expo/vector-icons';
import { useAuthUser } from 'app/modules/auth';
-import { CascadedDropdownComponent } from '@packrat/ui/src/CascadedDropdown';
+import { ActionsDropdownComponent } from '@packrat/ui';
import { EditableText } from '@packrat/ui/src/EditableText';
import RStack from '@packrat/ui/src/RStack';
import RIconButton from '@packrat/ui/src/RIconButton';
@@ -93,19 +93,10 @@ export const PackCardHeader = ({ data, title }: PackCardHeaderProps) => {
maxWidth: 100,
}}
>
- handleActionsOpenChange(value)}
- placeholder={
- }
- style={{
- height: 20,
- }}
- />
- }
native={true}
/>
diff --git a/packages/app/components/card/TripCardHeader/TripCardHeader.tsx b/packages/app/components/card/TripCardHeader/TripCardHeader.tsx
index 1ea206a6e..6eaa70a27 100644
--- a/packages/app/components/card/TripCardHeader/TripCardHeader.tsx
+++ b/packages/app/components/card/TripCardHeader/TripCardHeader.tsx
@@ -5,8 +5,8 @@ import useTheme from 'app/hooks/useTheme';
import { CustomCardHeader } from '../CustomCardHeader';
import RStack from '@packrat/ui/src/RStack';
import RIconButton from '@packrat/ui/src/RIconButton';
-import { AntDesign, MaterialIcons } from '@expo/vector-icons';
-import { CascadedDropdownComponent } from '@packrat/ui/src/CascadedDropdown';
+import { AntDesign } from '@expo/vector-icons';
+import { ActionsDropdownComponent } from '@packrat/ui/src/CascadedDropdown';
import { useFetchSingleTrip } from 'app/hooks/singletrips/useFetchSingleTrip';
import { useTripActions } from './useTripActions';
import { useAuthUser } from 'app/modules/auth';
@@ -83,19 +83,10 @@ export const TripCardHeader = ({ data, title }: TripCardHeaderProps) => {
maxWidth: 100,
}}
>
- handleActionsOpenChange(value)}
- placeholder={
- }
- style={{
- height: 20,
- }}
- />
- }
native={true}
/>
diff --git a/packages/app/components/destination/index.tsx b/packages/app/components/destination/index.tsx
index 5f53f18ca..de53d61ef 100644
--- a/packages/app/components/destination/index.tsx
+++ b/packages/app/components/destination/index.tsx
@@ -157,7 +157,7 @@ export const DestinationPage = () => {
) : (
{
-
+
Search by park, city, or trail
@@ -198,7 +195,7 @@ export const DestinationPage = () => {
contentProps={{ shape }}
type="map"
/>
-
+ {/* */}
>
)}
@@ -222,7 +219,7 @@ const loadStyles = (theme) => {
headerContainer: {
alignSelf: 'center',
width: '90%',
- backgroundColor: isDark ? '#2D2D2D' : currentTheme.colors.white,
+ backgroundColor: currentTheme.colors.card,
padding: 25,
borderRadius: 10,
marginBottom: 20,
diff --git a/packages/app/components/trip/TripCard.tsx b/packages/app/components/trip/TripCard.tsx
index e55e4826d..01facfff0 100644
--- a/packages/app/components/trip/TripCard.tsx
+++ b/packages/app/components/trip/TripCard.tsx
@@ -11,7 +11,7 @@ import useCustomStyles from 'app/hooks/useCustomStyles';
import useTheme from '../../hooks/useTheme';
import { theme } from '../../theme/index';
import Carousel from '../carousel';
-import { Map } from '@packrat/map';
+import { Map } from 'app/modules/map';
import { PlacesAutocomplete } from '../PlacesAutocomplete/PlacesAutocomplete';
interface TripCardProps {
diff --git a/packages/app/components/trip/TripCards/TripMapCard.tsx b/packages/app/components/trip/TripCards/TripMapCard.tsx
index 69e00e388..07d5bac3d 100644
--- a/packages/app/components/trip/TripCards/TripMapCard.tsx
+++ b/packages/app/components/trip/TripCards/TripMapCard.tsx
@@ -1,16 +1,26 @@
import { ErrorBoundary, RStack, RText } from '@packrat/ui';
-import { Map } from '@packrat/map';
+import { Map } from 'app/modules/map';
import useTheme from 'app/hooks/useTheme';
import { TripCardBase } from './TripCardBase';
import { FontAwesome5 } from '@expo/vector-icons';
import React from 'react';
+import { getTripGEOURI } from '../utils';
interface TripMapCardProps {
isLoading?: boolean;
- shape: any;
+ shape?: any;
+ onVisibleBoundsChange?: (bounds: number[]) => void;
+ tripId?: string;
+ initialBounds?: any;
}
-export const TripMapCard = ({ isLoading, shape }: TripMapCardProps) => {
+export const TripMapCard = ({
+ isLoading,
+ shape,
+ initialBounds,
+ tripId,
+ onVisibleBoundsChange,
+}: TripMapCardProps) => {
const { currentTheme } = useTheme();
return (
@@ -31,7 +41,13 @@ export const TripMapCard = ({ isLoading, shape }: TripMapCardProps) => {
) : (
-
+
)}
@@ -47,8 +63,7 @@ const loadStyles = (theme) => {
alignItems: 'center',
textAlign: 'center',
padding: currentTheme.size.cardPadding,
- paddingHorizontal: currentTheme.padding.paddingInside,
- marginBottom: 20,
+ margin: 80,
overflow: 'hidden',
alignSelf: 'center',
};
diff --git a/packages/app/components/trip/utils.ts b/packages/app/components/trip/utils.ts
new file mode 100644
index 000000000..99d1120d5
--- /dev/null
+++ b/packages/app/components/trip/utils.ts
@@ -0,0 +1,4 @@
+import { api } from 'app/constants/api';
+
+export const getTripGEOURI = (tripId: string) =>
+ `${api}/geojson/trip/${tripId}`;
diff --git a/packages/app/context/Auth/SessionProvider.tsx b/packages/app/context/Auth/SessionProvider.tsx
index 47e8655d0..c5cce989a 100644
--- a/packages/app/context/Auth/SessionProvider.tsx
+++ b/packages/app/context/Auth/SessionProvider.tsx
@@ -1,51 +1,7 @@
-import { useRouter } from 'app/hooks/router';
-
-import React, { useEffect } from 'react';
-import { useStorageState } from 'app/hooks/storage/useStorageState';
-
-interface AuthContextType {
- sessionSignIn: (session: any) => void; // Adjust 'any' to the specific session type
- sessionSignOut: () => void;
- session: string | null;
- isLoading: boolean;
-}
-
-const AuthContext = React.createContext(null);
-
-// // This hook can be used to access the user info. TODO: implement
-export function useAuth() {
- return React.useContext(AuthContext);
-}
-
-// This hook can be used to access the user info.
-export function useSession() {
- const value = React.useContext(AuthContext);
- if (process.env.NODE_ENV !== 'production') {
- if (!value) {
- throw new Error('useSession must be wrapped in a ');
- }
- }
-
- return value;
-}
+import { useUserQuery } from 'app/modules/auth';
export function SessionProvider(props) {
- const [[isLoading, session], setSession] = useStorageState('session');
+ useUserQuery();
- return (
- {
- setSession(session);
- },
- sessionSignOut: () => {
- setSession(null);
- },
- session,
- isLoading,
- }}
- >
- {props.children}
-
- );
+ return props.children;
}
diff --git a/packages/app/hooks/common/index.ts b/packages/app/hooks/common/index.ts
index 63fc28537..4599f670a 100644
--- a/packages/app/hooks/common/index.ts
+++ b/packages/app/hooks/common/index.ts
@@ -7,3 +7,4 @@ export { useScreenWidth } from './useScreenWidth';
export { useDebounce } from './useDebounce';
export { useHaptic } from './useHaptic';
export { useValidateSchema } from './useValidateSchema';
+export * from './usePreviewResourceState';
diff --git a/packages/app/hooks/common/usePreviewResourceState.ts b/packages/app/hooks/common/usePreviewResourceState.ts
new file mode 100644
index 000000000..c820d6b46
--- /dev/null
+++ b/packages/app/hooks/common/usePreviewResourceState.ts
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+import { type PaginationReturn } from 'app/hooks/pagination';
+
+interface PreviewResourceState {
+ isSeeAllModalOpen: boolean;
+ isAllQueryEnabled: boolean;
+ setIsSeeAllModalOpen: React.Dispatch>;
+}
+
+export interface PreviewResourceStateWithData
+ extends PreviewResourceState,
+ Partial {
+ isPreviewLoading: boolean;
+ previewData: any;
+ isAllQueryLoading: boolean;
+ allQueryData: any;
+ nextPage?: number;
+ fetchNextPage: () => void;
+}
+export const usePreviewResourceState = (): PreviewResourceState => {
+ const [isSeeAllModalOpen, setIsSeeAllModalOpen] = useState(false);
+ const [isAllQueryEnabled, setIsAllQueryEnabled] = useState(false);
+
+ useEffect(() => {
+ if (isSeeAllModalOpen) {
+ setIsAllQueryEnabled(true);
+ }
+ }, [isSeeAllModalOpen]);
+
+ return {
+ isSeeAllModalOpen,
+ setIsSeeAllModalOpen,
+ isAllQueryEnabled,
+ };
+};
diff --git a/packages/app/hooks/pagination/index.ts b/packages/app/hooks/pagination/index.ts
index 2bfee8150..612e4d2be 100644
--- a/packages/app/hooks/pagination/index.ts
+++ b/packages/app/hooks/pagination/index.ts
@@ -1 +1 @@
-export * from './useInfinitePagination';
+export * from './usePagination';
diff --git a/packages/app/hooks/pagination/useInfinitePagination.ts b/packages/app/hooks/pagination/usePagination.ts
similarity index 67%
rename from packages/app/hooks/pagination/useInfinitePagination.ts
rename to packages/app/hooks/pagination/usePagination.ts
index 868dab37d..91e482940 100644
--- a/packages/app/hooks/pagination/useInfinitePagination.ts
+++ b/packages/app/hooks/pagination/usePagination.ts
@@ -13,19 +13,30 @@ export interface PaginationParams {
}
interface PaginationOptions {
- nextPage?: number;
+ prevPage?: number | false;
+ nextPage?: number | false;
enabled?: boolean;
defaultPage?: number;
}
-export const useInfinitePagination = (
+export const usePagination = (
fetchFunction: () => void,
paginationParams: PaginationParams,
setPaginationParams: Dispatch>,
options: PaginationOptions = {},
) => {
const initialRender = useRef(false);
- const { nextPage, enabled = true } = options;
+ const { prevPage, nextPage, enabled = true } = options;
+
+ const fetchPrevPage = () => {
+ if (prevPage === false) {
+ return;
+ }
+ setPaginationParams((prev) => ({
+ ...prev,
+ offset: prevPage,
+ }));
+ };
const fetchNextPage = () => {
setPaginationParams((prev) => ({
@@ -50,9 +61,18 @@ export const useInfinitePagination = (
run();
}, [paginationParams.limit, paginationParams.offset, enabled]);
- return { fetchNextPage };
+ return { fetchPrevPage, fetchNextPage };
};
function getOffset(page: number, limit: number) {
return (page - 1) * limit;
}
+
+export interface PaginationReturn {
+ hasPrevPage: boolean;
+ hasNextPage: boolean;
+ currentPage: number;
+ totalPages: number;
+ fetchPrevPage: () => void;
+ fetchNextPage: () => void;
+}
diff --git a/packages/app/hooks/useFlatList.ts b/packages/app/hooks/useFlatList.ts
new file mode 100644
index 000000000..4e3fbe104
--- /dev/null
+++ b/packages/app/hooks/useFlatList.ts
@@ -0,0 +1,17 @@
+import { type ReactNode } from 'react';
+
+export const useFlatList = (
+ sections: Record,
+ sectionComponents: Record,
+) => {
+ const flatListData = Object.entries(sections);
+ const keyExtractor = ([key, val]) => val;
+ const renderItem = ({ item }) => {
+ const sectionKey = item[1];
+
+ console.log({ sectionKey });
+ return sectionComponents[sectionKey];
+ };
+
+ return { flatListData, keyExtractor, renderItem };
+};
diff --git a/packages/app/modules/auth/components/AuthLoader.tsx b/packages/app/modules/auth/components/AuthLoader.tsx
index 0a4b4ce67..873cfb1c2 100644
--- a/packages/app/modules/auth/components/AuthLoader.tsx
+++ b/packages/app/modules/auth/components/AuthLoader.tsx
@@ -1,12 +1,11 @@
-import { useUserQuery } from '../hooks';
+import { useUserLoader } from '../hooks';
export const AuthLoader = ({
children,
loadingElement,
unauthorizedElement,
}) => {
- const { isLoading, user } = useUserQuery();
-
+ const { user, isLoading } = useUserLoader();
if (isLoading) {
return loadingElement;
}
diff --git a/packages/app/modules/auth/hooks/useSessionSignIn.ts b/packages/app/modules/auth/hooks/useSessionSignIn.ts
index e7e2ce25b..b853bd4e8 100644
--- a/packages/app/modules/auth/hooks/useSessionSignIn.ts
+++ b/packages/app/modules/auth/hooks/useSessionSignIn.ts
@@ -11,6 +11,7 @@ export const useSessionSignIn = () => {
(async () => {
await Storage.setItem('token', tokens.accessToken);
await Storage.setItem('refreshToken', tokens.refreshToken);
+ setUser(tokens.user);
router.push('/');
})();
}, []);
diff --git a/packages/app/modules/auth/hooks/useUser.ts b/packages/app/modules/auth/hooks/useUser.ts
index 4a98c9c3c..04e78e535 100644
--- a/packages/app/modules/auth/hooks/useUser.ts
+++ b/packages/app/modules/auth/hooks/useUser.ts
@@ -1,5 +1,7 @@
+import { useState } from 'react';
import { useStorage } from 'app/hooks/storage/useStorage';
import { queryTrpc } from 'app/trpc';
+import { useLogout } from './useLogout';
export const useAuthUserToken = () => {
const [[isLoading, token]] = useStorage('token');
@@ -8,30 +10,36 @@ export const useAuthUserToken = () => {
};
export const useUserQuery = () => {
- const { token, isLoading: isTokenLoading } = useAuthUserToken();
- const isRequestEnabled = !!token && !isTokenLoading;
-
- const {
- refetch,
- data,
- isLoading: isRequestLoading,
- } = queryTrpc.getMe.useQuery(undefined, {
- enabled: isRequestEnabled,
+ const { isLoading: isTokenLoading } = useAuthUserToken();
+ const [isLoading, setIsLoading] = useState(true);
+ const logout = useLogout();
+
+ const { refetch, data } = queryTrpc.getMe.useQuery(undefined, {
+ onSuccess: () => {
+ setIsLoading(false);
+ },
+ onError: () => {
+ logout();
+ setIsLoading(false);
+ },
+ enabled: isTokenLoading === undefined || isTokenLoading,
+ retry: false,
});
- // Sometimes the isLoading state don't work as expected so we have this solution here
- // isLoading stays true when request is not enabled
- // TODO fix loading state
- const isLoading =
- (isRequestEnabled && isRequestLoading) ||
- isTokenLoading === undefined ||
- isTokenLoading;
-
return { user: data, isLoading, refetch };
};
export const useAuthUser = () => {
- const { user, isLoading, refetch } = useUserQuery();
+ const { user } = useUserLoader();
return user;
};
+
+export const useUserLoader = () => {
+ const { data: user, isLoading } = queryTrpc.getMe.useQuery(undefined, {
+ enabled: false,
+ notifyOnChangeProps: 'all',
+ });
+
+ return { user, isLoading };
+};
diff --git a/packages/app/modules/dashboard/components/QuickActionSection/QuickActionSection.tsx b/packages/app/modules/dashboard/components/QuickActionSection/QuickActionSection.tsx
index 09d32845b..979b49488 100644
--- a/packages/app/modules/dashboard/components/QuickActionSection/QuickActionSection.tsx
+++ b/packages/app/modules/dashboard/components/QuickActionSection/QuickActionSection.tsx
@@ -9,7 +9,12 @@ export const QuickActionsSection = ({ closeQuickActions }) => {
const { handleActionSelect, quickActionData } = useQuickActions();
return (
-
+
{quickActionData.map((action) => (
{
return {
section: {
display: 'flex',
- minWidth: 250,
- justifyContent: 'center',
+ minWidth: 200,
+ minHeight: 200,
+ justifyContent: 'flex-start',
},
};
};
diff --git a/packages/app/modules/feed/components/FeedCard/FeedCard.tsx b/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
index 7385a5adb..7c2dcaea1 100644
--- a/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
+++ b/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
@@ -54,209 +54,3 @@ export const FeedCard: FC = ({ item, cardType, feedType }) => {
/>
);
};
-
-/*
-export function FeedCardOld({
- type,
- id,
- owner,
- name,
- total_weight,
- is_public,
- favorited_by,
- favorites_count,
- owner_id,
- destination,
- createdAt,
- owners,
- duration,
- itemPacks,
-}: FeedCardProps) {
- console.log('CardProps:', favorited_by);
- const user = useAuthUser();
- const { currentTheme } = useTheme();
-
- const { addFavorite } = useAddFavorite();
- const [weightUnit] = useItemWeightUnit();
-
- const router = useRouter();
-
- const isFavorite =
- type !== 'trip' &&
- favorited_by?.some((obj) => obj?.['userId'] === user?.id && user?.id);
-
- const handleAddToFavorite = () => {
- if (!user) return;
- const data = {
- packId: id,
- userId: user.id,
- };
- addFavorite(data);
- };
-
- const truncatedName = truncateString(name, 25);
- const truncatedDestination = truncateString(destination, 25);
- const formattedWeight = convertWeight(total_weight, 'g', weightUnit);
- const quantity =
- itemPacks?.reduce(
- (accumulator, currentValue) => accumulator + currentValue?.item?.quantity,
- 0,
- ) ?? 0;
- let numberOfNights;
-
- if (duration) numberOfNights = JSON.parse(duration).numberOfNights;
-
- return (
-
-
-
-
-
-
-
-
-
-
- {truncatedName}
-
-
-
-
- {type === 'pack' && (
-
-
-
-
- )}
- {type === 'trip' && (
-
- )}
-
-
- {type === 'pack' && (
- <>
-
- Total Weight: {formatNumber(formattedWeight)} {weightUnit}
-
-
- Total Quantity: {quantity}
-
- >
- )}
- {type === 'trip' && (
- {truncatedDestination}
- )}
-
-
-
-
-
-
- View{' '}
- {owner?.username ? '@' + owner?.username : 'Owner'}
-
-
-
- {formatDistanceToNow(new Date(createdAt), {
- addSuffix: true,
- })}
-
-
- {type === 'pack' && (
-
-
-
-
-
- {favorites_count}
-
-
- )}
- {type === 'trip' && (
-
-
- Nights
-
-
- {numberOfNights}
-
-
- )}
-
-
-
-
-
-
- {
- router.push(type === 'pack' ? '/pack/' + id : '/trip/' + id);
- }}
- >
- View {type}
-
- {
- router.push(`/profile/${owner_id}`);
- }}
- >
- View Owner
-
-
-
-
-
-
- );
-}
-*/
diff --git a/packages/app/modules/feed/components/FeedCard/utils.ts b/packages/app/modules/feed/components/FeedCard/utils.ts
index 789f44cbc..ca2e06010 100644
--- a/packages/app/modules/feed/components/FeedCard/utils.ts
+++ b/packages/app/modules/feed/components/FeedCard/utils.ts
@@ -23,6 +23,7 @@ export const feedItemPackCardConverter: Converter<
createdAt: formatDistanceToNowStrict(new Date(input.createdAt), {
addSuffix: false,
}),
+ is_public: input.is_public,
title: truncateString(input.name, 25),
ownerId:
typeof input.owner_id === 'string'
@@ -57,6 +58,7 @@ export const feedItemTripCardConverter: Converter<
addSuffix: false,
}),
title: truncateString(input.name, 25),
+ is_public: input.is_public,
ownerId:
typeof input.owner_id === 'string'
? input.owner_id
@@ -67,6 +69,7 @@ export const feedItemTripCardConverter: Converter<
startDate: input.start_date,
endDate: input.end_date,
activity: input.activity,
+ score: !isNaN(input.total_score) ? roundNumber(input.total_score) : 0,
},
favoriteCount: input.favorites_count,
};
diff --git a/packages/app/modules/feed/hooks/index.ts b/packages/app/modules/feed/hooks/index.ts
index 88c363a55..463849d6b 100644
--- a/packages/app/modules/feed/hooks/index.ts
+++ b/packages/app/modules/feed/hooks/index.ts
@@ -1,4 +1,7 @@
export { useFeed } from './useFeed';
export { useAddFavorite } from './useAddFavorite';
-export { useFetchUserFavorites } from './useFetchUserFavorites';
+export {
+ useFetchUserFavorites,
+ useFetchUserFavoritesWithPreview,
+} from './useFetchUserFavorites';
export * from './useFeedSortOptions';
diff --git a/packages/app/modules/feed/hooks/useFeed.ts b/packages/app/modules/feed/hooks/useFeed.ts
index 911e566fe..1451d7bac 100644
--- a/packages/app/modules/feed/hooks/useFeed.ts
+++ b/packages/app/modules/feed/hooks/useFeed.ts
@@ -3,23 +3,13 @@ import { useUserPacks, useSimilarPacks } from 'app/modules/pack';
import { useUserTrips } from 'app/modules/trip';
import { useSimilarItems } from 'app/modules/item';
import { type FeedType } from '../model';
+import { type PaginationReturn } from 'app/hooks/pagination';
-interface UseFeedResult {
+interface UseFeedResult extends Partial {
data: any[] | null;
isLoading: boolean;
refetch?: () => void;
setPage?: (page: number) => void;
- nextPage?: number | boolean;
- fetchNextPage?: () => void;
-}
-
-interface UseFeedResult {
- data: any[] | null;
- isLoading: boolean;
- refetch?: () => void;
- setPage?: (page: number) => void;
- nextPage?: number | boolean;
- fetchNextPage?: () => void;
}
export const useFeed = ({
diff --git a/packages/app/modules/feed/hooks/useFetchUserFavorites.ts b/packages/app/modules/feed/hooks/useFetchUserFavorites.ts
index 978fc6bff..f9c1e73d7 100644
--- a/packages/app/modules/feed/hooks/useFetchUserFavorites.ts
+++ b/packages/app/modules/feed/hooks/useFetchUserFavorites.ts
@@ -1,17 +1,103 @@
import { queryTrpc } from 'app/trpc';
+import {
+ getPaginationInitialParams,
+ type PaginationParams,
+ usePagination,
+} from 'app/hooks/pagination';
+import { useEffect, useState } from 'react';
+import {
+ type PreviewResourceStateWithData,
+ usePreviewResourceState,
+} from 'app/hooks/common';
-export const useFetchUserFavorites = (userId: string) => {
- const enabled = !!userId;
+export const useFetchUserFavorites = (
+ userId: string,
+ { queryEnabled = true, isPreview = false, searchTerm = '' } = {},
+) => {
+ const enabled = !!userId && queryEnabled;
+ const [pagination, setPagination] = useState(
+ getPaginationInitialParams(),
+ );
const { data, error, isLoading, refetch } =
queryTrpc.getUserFavorites.useQuery(
- { userId },
+ { userId, pagination, isPreview, searchTerm },
{
enabled,
refetchOnWindowFocus: false,
- keepPreviousData: true,
},
);
+ const { fetchPrevPage, fetchNextPage } = usePagination(
+ refetch,
+ pagination,
+ setPagination,
+ {
+ prevPage: data?.prevOffset,
+ nextPage: data?.nextOffset,
+ enabled,
+ },
+ );
- return { data, error, isLoading, enabled, refetch };
+ useEffect(() => {
+ setPagination(getPaginationInitialParams());
+ }, [searchTerm, userId]);
+
+ return {
+ data: data?.data || [],
+ totalCount: data?.totalCount,
+ error,
+ isLoading,
+ enabled,
+ refetch,
+ fetchPrevPage,
+ fetchNextPage,
+ hasPrevPage: data?.prevOffset !== false,
+ hasNextPage: data?.nextOffset !== false,
+ currentPage: data?.currentPage,
+ totalPages: data?.totalPages,
+ };
+};
+
+interface FetchUserFavoritesReturn extends PreviewResourceStateWithData {
+ totalCount: number;
+}
+export const useFetchUserFavoritesWithPreview = (
+ userId: string,
+ searchTerm: string,
+): FetchUserFavoritesReturn => {
+ const { isAllQueryEnabled, ...previewResourceState } =
+ usePreviewResourceState();
+ const { data: previewData, isLoading: isPreviewLoading } =
+ useFetchUserFavorites(userId, { isPreview: true });
+
+ const {
+ data: allQueryData,
+ isLoading: isAllQueryLoading,
+ totalCount,
+ fetchPrevPage,
+ fetchNextPage,
+ totalPages,
+ currentPage,
+ hasPrevPage,
+ hasNextPage,
+ } = useFetchUserFavorites(userId, {
+ queryEnabled: isAllQueryEnabled,
+ searchTerm,
+ });
+
+ return {
+ ...previewResourceState,
+ isAllQueryEnabled,
+ previewData,
+ isPreviewLoading,
+ allQueryData,
+ isAllQueryLoading,
+ totalCount,
+ fetchPrevPage,
+ fetchNextPage,
+ totalPages,
+ currentPage,
+ hasPrevPage,
+ hasNextPage,
+ };
};
diff --git a/packages/app/modules/feed/hooks/usePublicFeed.ts b/packages/app/modules/feed/hooks/usePublicFeed.ts
index d7f8c6daa..2c43528a6 100644
--- a/packages/app/modules/feed/hooks/usePublicFeed.ts
+++ b/packages/app/modules/feed/hooks/usePublicFeed.ts
@@ -2,9 +2,9 @@ import { queryTrpc } from 'app/trpc';
import {
getPaginationInitialParams,
type PaginationParams,
- useInfinitePagination,
+ usePagination,
} from 'app/hooks/pagination';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
export const usePublicFeed = (
queryBy,
@@ -12,7 +12,6 @@ export const usePublicFeed = (
selectedTypes,
enabled = false,
) => {
- const [allData, setAllData] = useState([]);
const [pagination, setPagination] = useState(
getPaginationInitialParams(),
);
@@ -26,33 +25,34 @@ export const usePublicFeed = (
{
enabled,
refetchOnWindowFocus: false,
- onSuccess: (newData) => {
- if (newData?.data) {
- setAllData((prevData) => {
- if (pagination.offset === 0) {
- return newData.data;
- }
-
- return [...prevData, ...newData.data];
- });
- }
- },
onError: (error) => console.error('Error fetching public packs:', error),
},
);
- const { fetchNextPage } = useInfinitePagination(
+ const { fetchPrevPage, fetchNextPage } = usePagination(
refetch,
pagination,
setPagination,
- { nextPage: data?.nextOffset, enabled },
+ {
+ prevPage: data?.prevOffset,
+ nextPage: data?.nextOffset,
+ enabled,
+ },
);
+ useEffect(() => {
+ setPagination(getPaginationInitialParams());
+ }, [queryBy, searchQuery, selectedTypes?.pack, selectedTypes?.trip]);
+
return {
- data: allData,
+ data: data?.data || [],
isLoading,
refetch,
+ fetchPrevPage,
fetchNextPage,
- nextPage: data?.nextOffset || false,
+ hasPrevPage: data?.prevOffset !== false,
+ hasNextPage: data?.nextOffset !== false,
+ currentPage: data?.currentPage,
+ totalPages: data?.totalPages,
error: null,
};
};
diff --git a/packages/app/modules/feed/index.ts b/packages/app/modules/feed/index.ts
index 1b1318e65..2a168a574 100644
--- a/packages/app/modules/feed/index.ts
+++ b/packages/app/modules/feed/index.ts
@@ -1,6 +1,12 @@
export * from './widgets';
export * from './screens';
-export { useFeed, useAddFavorite, useFetchUserFavorites } from './hooks';
+export {
+ useFeed,
+ useAddFavorite,
+ useFetchUserFavorites,
+ useFetchUserFavoritesWithPreview,
+ useFeedSortOptions,
+} from './hooks';
export {
SearchProvider,
FeedSearchFilter,
diff --git a/packages/app/modules/feed/model.ts b/packages/app/modules/feed/model.ts
index c7f06ff96..7c22cef99 100644
--- a/packages/app/modules/feed/model.ts
+++ b/packages/app/modules/feed/model.ts
@@ -44,6 +44,8 @@ export interface TripFeedItem extends BaseFeedItem {
start_date: string;
end_date: string;
activity: string;
+ scores: { totalScore: number };
+ total_score: number;
}
export type FeedItem = PackFeedItem | TripFeedItem;
diff --git a/packages/app/modules/feed/screens/FeedScreen.tsx b/packages/app/modules/feed/screens/FeedScreen.tsx
index 794b8a7db..5f34ec767 100644
--- a/packages/app/modules/feed/screens/FeedScreen.tsx
+++ b/packages/app/modules/feed/screens/FeedScreen.tsx
@@ -6,7 +6,7 @@ import { fuseSearch } from 'app/utils/fuseSearch';
import useCustomStyles from 'app/hooks/useCustomStyles';
import { useFeed } from 'app/modules/feed';
import { RefreshControl } from 'react-native';
-import { RButton, RText } from '@packrat/ui';
+import { Pagination, RButton, RText } from '@packrat/ui';
import { useAuthUser } from 'app/modules/auth';
import { type FeedType } from '../model';
@@ -29,6 +29,7 @@ interface FeedProps {
const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
const router = useRouter();
+ console.log({ feedType });
const [queryString, setQueryString] = useState('Favorite');
const [selectedTypes, setSelectedTypes] = useState({
pack: true,
@@ -41,7 +42,17 @@ const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
const user = useAuthUser();
const ownerId = user?.id;
const styles = useCustomStyles(loadStyles);
- const { data, isLoading, fetchNextPage, refetch, nextPage } = useFeed({
+ const {
+ data,
+ isLoading,
+ refetch,
+ fetchPrevPage,
+ fetchNextPage,
+ hasPrevPage,
+ hasNextPage,
+ currentPage,
+ totalPages,
+ } = useFeed({
queryString,
ownerId,
feedType,
@@ -144,8 +155,15 @@ const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
showsVerticalScrollIndicator={false}
maxToRenderPerBatch={2}
/>
- {nextPage ? (
- Load more
+ {totalPages > 1 ? (
+
) : null}
diff --git a/packages/app/modules/feed/widgets/FeedPreview/FeedPreview.tsx b/packages/app/modules/feed/widgets/FeedPreview/FeedPreview.tsx
index 0950bd7d9..0bb0dfae3 100644
--- a/packages/app/modules/feed/widgets/FeedPreview/FeedPreview.tsx
+++ b/packages/app/modules/feed/widgets/FeedPreview/FeedPreview.tsx
@@ -17,16 +17,21 @@ const FeedPreviewScroll: React.FC = ({
id,
}) => {
const { data: feedData, isLoading } = useFeed({ feedType, id });
+ const validFeedData = feedData?.filter?.((item) => item.id);
return isLoading ? (
) : (
- {feedData
+ {validFeedData
?.filter((item): item is FeedItem => item.type !== null)
.map((item: FeedItem) => {
return (
-
+
);
diff --git a/packages/app/modules/item/components/AddItem.tsx b/packages/app/modules/item/components/AddItem.tsx
index 93c3afab5..ad95b563c 100644
--- a/packages/app/modules/item/components/AddItem.tsx
+++ b/packages/app/modules/item/components/AddItem.tsx
@@ -1,3 +1,4 @@
+import React, { useMemo } from 'react';
import { View } from 'react-native';
import { ItemForm } from './ItemForm'; // assuming you moved the form related code to a separate component
import { useAddPackItem, useEditPackItem } from 'app/modules/pack';
@@ -6,8 +7,12 @@ import {
editItem as editItemSchema,
type Item,
} from '@packrat/validations';
-import { useMemo } from 'react';
import { useAuthUser } from 'app/modules/auth';
+import { convertWeighToSmallestUnit } from '../utils';
+import { type ItemUnit } from '../model';
+import { convertWeight } from 'app/utils/convertWeight';
+import { SMALLEST_ITEM_UNIT } from '../constants';
+import { useItemWeightUnit } from '../hooks';
interface AddItemProps {
id?: string;
@@ -34,19 +39,13 @@ interface AddItemProps {
setRefetch?: () => void;
}
-type AddItem = Omit- & { id: string };
-
export const AddItem = ({
isEdit,
initialData,
packId,
currentPack,
- editAsDuplicate,
- setPage = (page: number) => {}, // temp fix, need props type
- page,
closeModalHandler,
isItemPage,
- setIsAddItemModalOpen = () => {},
}: AddItemProps) => {
// const [currPackId] = usePackId();
@@ -54,59 +53,34 @@ export const AddItem = ({
const ownerId = user?.id;
- const {
- // mutation: addPackItemMutation
- isLoading,
- isError,
- addPackItem,
- } = useAddPackItem();
- const {
- // mutation: addPackItemMutation
-
- editPackItem,
- } = useEditPackItem(isItemPage);
-
- const convertWeighToSmallestUnit = (unit, weight) => {
- let convertedWeight;
-
- if (unit === 'lb') {
- convertedWeight = weight * 453.592;
- } else if (unit === 'oz') {
- convertedWeight = weight * 28.3495;
- } else if (unit === 'kg') {
- convertedWeight = weight * 1000;
- } else if (unit === 'g') {
- convertedWeight = weight;
- } else {
- throw new Error(`Unsupported unit: ${unit}`);
- }
-
- return parseFloat(convertedWeight.toFixed(1));
- };
+ const { isLoading, addPackItem } = useAddPackItem();
+ const [userPreferUnit] = useItemWeightUnit();
+ const { editPackItem } = useEditPackItem(isItemPage);
const handleSubmit = (data: Item) => {
-
- const convertedData = {
- ...data,
- weight: convertWeighToSmallestUnit(data.unit, data.weight),
- }
if (isEdit) {
- editPackItem(convertedData as any);
+ editPackItem(data as any);
} else {
- addPackItem(convertedData);
+ addPackItem(data);
}
if (closeModalHandler) closeModalHandler();
};
const defaultValues = useMemo(() => {
if (!initialData) {
- return { unit: 'lb', ownerId, packId };
+ return { unit: userPreferUnit, ownerId, packId };
}
const result = {
id: '',
ownerId,
name: initialData.name || '',
- weight: initialData.weight,
+ weight: initialData.unit
+ ? convertWeight(
+ initialData.weight,
+ SMALLEST_ITEM_UNIT,
+ initialData.unit,
+ )
+ : undefined,
quantity: initialData.quantity,
type: initialData.category?.name,
unit: initialData.unit,
@@ -120,7 +94,7 @@ export const AddItem = ({
}
return result;
- }, [initialData, isEdit, packId, ownerId]);
+ }, [initialData, isEdit, packId, ownerId, userPreferUnit]);
return (
diff --git a/packages/app/modules/item/components/ImportForm.tsx b/packages/app/modules/item/components/ImportForm.tsx
index 1ca70301a..78efd4cf2 100644
--- a/packages/app/modules/item/components/ImportForm.tsx
+++ b/packages/app/modules/item/components/ImportForm.tsx
@@ -27,13 +27,11 @@ interface SelectedType {
value: string;
}
-const options = [
- { label: 'bucket 1', value: 'bucket 1', key: 'bucket 1' },
- { label: 'bucket 2', value: 'bucket 2', key: 'bucket 1' },
- { label: 'bucket 3', value: 'bucket 3', key: 'bucket 1' },
- { label: 'bucket 4', value: 'bucket 4', key: 'bucket 1' },
- { label: 'bucket 5', value: 'bucket 5', key: 'bucket 1' },
-];
+const bucketCount = 6;
+const options = Array.from({ length: bucketCount }, (_, i) => {
+ const bucket = `bucket ${i + 1}`;
+ return { label: bucket, value: bucket, key: bucket };
+});
const csvOption = [{ label: 'CSV', value: '.csv', key: '.csv' }];
@@ -149,7 +147,7 @@ export const ImportForm: FC = ({
}}
>
= ({
);
-};
\ No newline at end of file
+};
diff --git a/packages/app/modules/item/constants.ts b/packages/app/modules/item/constants.ts
index c905bce89..8c54122f8 100644
--- a/packages/app/modules/item/constants.ts
+++ b/packages/app/modules/item/constants.ts
@@ -10,3 +10,5 @@ export const ItemCategoryEnum = {
// MEDICAL: 'Medical',
// OTHER: 'Other',
};
+
+export const SMALLEST_ITEM_UNIT = 'g';
diff --git a/packages/app/modules/item/hooks/index.ts b/packages/app/modules/item/hooks/index.ts
index 9e636d619..2663b2c3d 100644
--- a/packages/app/modules/item/hooks/index.ts
+++ b/packages/app/modules/item/hooks/index.ts
@@ -8,4 +8,5 @@ export { useItemId } from './useItemId';
export { useItem } from './useItem';
export { useItemRow } from './useItemRow';
export { useImportItem } from './useImportItem';
-export { useImportFromBucket } from './useImportFromBucket';
\ No newline at end of file
+export { useImportFromBucket } from './useImportFromBucket';
+export { useItemImages } from './useItemImages';
\ No newline at end of file
diff --git a/packages/app/modules/item/hooks/useItemImages.ts b/packages/app/modules/item/hooks/useItemImages.ts
new file mode 100644
index 000000000..ab2340df1
--- /dev/null
+++ b/packages/app/modules/item/hooks/useItemImages.ts
@@ -0,0 +1,16 @@
+import { queryTrpc } from 'app/trpc';
+
+export const useItemImages = (itemId?: string) => {
+ const { refetch, data, error, isLoading, isError } =
+ queryTrpc.getItemImages.useQuery(
+ { id: itemId },
+ {
+ enabled: !!itemId,
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
+ cacheTime: Infinity,
+ },
+ );
+
+ return { refetch, data, error, isLoading, isError };
+};
\ No newline at end of file
diff --git a/packages/app/modules/item/model.ts b/packages/app/modules/item/model.ts
new file mode 100644
index 000000000..08ddbdd5f
--- /dev/null
+++ b/packages/app/modules/item/model.ts
@@ -0,0 +1 @@
+export type ItemUnit = 'lb' | 'oz' | 'kg' | 'g';
diff --git a/packages/app/modules/item/screens/ItemDetailsScreen.tsx b/packages/app/modules/item/screens/ItemDetailsScreen.tsx
index 1a21147d6..b8f6be8ec 100644
--- a/packages/app/modules/item/screens/ItemDetailsScreen.tsx
+++ b/packages/app/modules/item/screens/ItemDetailsScreen.tsx
@@ -2,20 +2,25 @@ import { View } from 'react-native';
import React from 'react';
import useTheme from 'app/hooks/useTheme';
import useCustomStyles from 'app/hooks/useCustomStyles';
-import { useItem, useItemId } from 'app/modules/item';
+import { useItem, useItemId, useItemImages } from 'app/modules/item';
import { usePagination } from 'app/hooks/common';
import { RH3, RImage, RScrollView, RStack, RText, XStack } from '@packrat/ui';
import useResponsive from 'app/hooks/useResponsive';
import { CustomCard } from 'app/components/card';
import LargeCard from 'app/components/card/LargeCard';
import { FeedPreview } from 'app/modules/feed';
+import { convertWeight } from 'app/utils/convertWeight';
+import { SMALLEST_ITEM_UNIT } from '../constants';
export function ItemDetailsScreen() {
- const { limit, handleLimitChange, page, handlePageChange } = usePagination();
+ // const { limit, handleLimitChange, page, handlePageChange } = usePagination();
const [itemId] = useItemId();
const { data: item, isError } = useItem(itemId);
const styles = useCustomStyles(loadStyles);
const { currentTheme } = useTheme();
+ const {data: itemImages, isError: isImagesError} = useItemImages(itemId);
+
+ console.log('itemImages', itemImages);
return (
@@ -28,7 +33,7 @@ export function ItemDetailsScreen() {
content={
@@ -36,7 +41,12 @@ export function ItemDetailsScreen() {
Category: {item?.category?.name || '-'}
- Weight: {item?.weight}
+ Weight:{' '}
+ {convertWeight(
+ Number(item?.weight),
+ SMALLEST_ITEM_UNIT,
+ item?.unit as any,
+ )}
{item?.unit}
Quantity: {item?.quantity}
diff --git a/packages/app/modules/item/screens/ItemsScreen.tsx b/packages/app/modules/item/screens/ItemsScreen.tsx
index db51082e6..61019f527 100644
--- a/packages/app/modules/item/screens/ItemsScreen.tsx
+++ b/packages/app/modules/item/screens/ItemsScreen.tsx
@@ -98,9 +98,11 @@ export function ItemsScreen() {
- { role === 'admin' &&
-
- }
+ {role === 'admin' && (
+
+
+
+ )}
{!isError && data?.items && Array.isArray(data.items) && (
diff --git a/packages/app/modules/item/utils.ts b/packages/app/modules/item/utils.ts
new file mode 100644
index 000000000..131f4a163
--- /dev/null
+++ b/packages/app/modules/item/utils.ts
@@ -0,0 +1,18 @@
+import { type ItemUnit } from './model';
+
+export const convertWeighToSmallestUnit = (unit: ItemUnit, weight: number) => {
+ const conversionRates = {
+ lb: 453.592,
+ oz: 28.3495,
+ kg: 1000,
+ g: 1,
+ };
+
+ if (!conversionRates[unit]) {
+ return 'Not Available';
+ }
+
+ const convertedWeight = weight * conversionRates[unit];
+
+ return parseFloat(convertedWeight.toFixed(2));
+};
diff --git a/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.tsx b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.tsx
new file mode 100644
index 000000000..8655ed9aa
--- /dev/null
+++ b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.tsx
@@ -0,0 +1,93 @@
+import React, { type FC } from 'react';
+import { View, type TouchableOpacityProps } from 'react-native';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import { useDownloadMap } from '../../hooks/useDownloadMap';
+import { useDownloadMapProgress } from '../../hooks/useDownloadMapProgress';
+import { MapActionBtn } from '../MapActionBtn';
+import { Entypo } from '@expo/vector-icons';
+import {
+ BaseModal,
+ Form,
+ FormInput,
+ RStack,
+ RText,
+ SubmitButton,
+ useModalState,
+} from '@packrat/ui';
+
+interface MapStylePickerProps {
+ currentBounds: any;
+ style?: TouchableOpacityProps['style'];
+ shape: any;
+}
+
+export const DownloadMapBtn: FC = ({
+ currentBounds,
+ style,
+ shape,
+}) => {
+ const styles = useCustomStyles(loadStyles);
+ const { isModalOpen, onClose, onOpen } = useModalState();
+ const { downloadMap, isDownloading, progress } = useDownloadMapProgress();
+ const { handleDownloadMap, isSaving } = useDownloadMap(downloadMap);
+ const formattedProgress = !isNaN(progress) ? Math.round(progress) : 0;
+
+ const handleSubmit = ({ mapName }) => {
+ handleDownloadMap({ mapName, bounds: currentBounds.current, shape });
+ onClose();
+ };
+
+ return (
+ <>
+
+ {isDownloading || isSaving ? (
+ {`${formattedProgress}%`}
+ ) : (
+
+ )}
+
+ closeModal(),
+ },
+ ]}
+ footerComponent={undefined}
+ >
+
+
+
+
+ >
+ );
+};
+
+const loadStyles = () => ({
+ downloadIcon: {
+ width: 21,
+ height: 21,
+ },
+ downloadText: {
+ fontSize: 15,
+ fontWeight: '500',
+ marginRight: 8,
+ color: '#000',
+ },
+});
diff --git a/packages/app/modules/map/components/DownloadMapBtn/index.ts b/packages/app/modules/map/components/DownloadMapBtn/index.ts
new file mode 100644
index 000000000..1a4fdb283
--- /dev/null
+++ b/packages/app/modules/map/components/DownloadMapBtn/index.ts
@@ -0,0 +1 @@
+export * from './DownloadMapBtn';
diff --git a/packages/app/modules/map/components/FullScreenBtn/FullScreenBtn.tsx b/packages/app/modules/map/components/FullScreenBtn/FullScreenBtn.tsx
new file mode 100644
index 000000000..6d997aa95
--- /dev/null
+++ b/packages/app/modules/map/components/FullScreenBtn/FullScreenBtn.tsx
@@ -0,0 +1,26 @@
+import React, { type FC } from 'react';
+import { Entypo } from '@expo/vector-icons';
+import { MapActionBtn } from '../MapActionBtn';
+import { type TouchableOpacityProps } from 'react-native';
+
+interface MapStylePickerProps {
+ isFullScreenMode: boolean;
+ toggleFullscreen: () => void;
+ style?: TouchableOpacityProps['style'];
+}
+
+export const FullscreenBtn: FC = ({
+ isFullScreenMode,
+ toggleFullscreen,
+ style,
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/app/modules/map/components/FullScreenBtn/index.ts b/packages/app/modules/map/components/FullScreenBtn/index.ts
new file mode 100644
index 000000000..42e52ad25
--- /dev/null
+++ b/packages/app/modules/map/components/FullScreenBtn/index.ts
@@ -0,0 +1 @@
+export * from './FullScreenBtn';
diff --git a/packages/app/modules/map/components/MapActionBtn/MapActionBtn.tsx b/packages/app/modules/map/components/MapActionBtn/MapActionBtn.tsx
new file mode 100644
index 000000000..f47143e83
--- /dev/null
+++ b/packages/app/modules/map/components/MapActionBtn/MapActionBtn.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import { type FC } from 'react';
+import { TouchableOpacity, type TouchableOpacityProps } from 'react-native';
+
+interface MapActionBtnProps extends TouchableOpacityProps {}
+
+export const MapActionBtn: FC = ({ style, ...props }) => {
+ const styles = useCustomStyles(loadStyles);
+
+ return ;
+};
+
+const loadStyles = () => ({
+ mapActionBtn: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ pointerEvents: 'box-only',
+ width: 40,
+ height: 40,
+ borderRadius: 30,
+ backgroundColor: 'white',
+ },
+});
diff --git a/packages/app/modules/map/components/MapActionBtn/index.ts b/packages/app/modules/map/components/MapActionBtn/index.ts
new file mode 100644
index 000000000..9ed45165c
--- /dev/null
+++ b/packages/app/modules/map/components/MapActionBtn/index.ts
@@ -0,0 +1 @@
+export * from './MapActionBtn';
diff --git a/packages/app/modules/map/components/MapButtonsOverlay/MapButtonsOverlay.tsx b/packages/app/modules/map/components/MapButtonsOverlay/MapButtonsOverlay.tsx
new file mode 100644
index 000000000..a14cc2353
--- /dev/null
+++ b/packages/app/modules/map/components/MapButtonsOverlay/MapButtonsOverlay.tsx
@@ -0,0 +1,263 @@
+import React, { useState } from 'react';
+import {
+ TouchableOpacity,
+ Image,
+ Text,
+ Alert,
+ StyleSheet,
+ Platform,
+} from 'react-native';
+import {
+ Entypo,
+ MaterialCommunityIcons,
+ FontAwesome5,
+} from '@expo/vector-icons';
+
+interface MapButtonsOverlayProps {
+ mapFullscreen: boolean;
+ enableFullScreen?: () => void;
+ disableFullScreen: () => void;
+ handleChangeMapStyle?: (style: string) => void;
+ downloadable: boolean;
+ downloading?: boolean;
+ fetchLocation?: () => void;
+ onDownload?: () => void;
+ handleGpxUpload?: () => void;
+ progress?: number;
+ navigateToMaps?: () => void;
+ styles?: any;
+ shape?: any;
+}
+
+const MapButtonsOverlay = ({
+ mapFullscreen,
+ enableFullScreen,
+ disableFullScreen,
+ handleChangeMapStyle,
+ downloadable,
+ downloading,
+ fetchLocation,
+ onDownload,
+ handleGpxUpload,
+ progress,
+ navigateToMaps,
+}: MapButtonsOverlayProps) => {
+ const styles = StyleSheet.create(loadStyles);
+
+ return (
+ <>
+ {!mapFullscreen ? (
+ // Preview map
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+ // Fullscreen map
+ <>
+
+
+
+
+ {/* Style Picker Button */}
+
+ {/* Download Button */}
+ {downloadable && (
+
+
+
+ {downloading
+ ? `Downloading... ${
+ progress ? Math.floor(progress) + '%' : ''
+ }`
+ : 'Download map'}
+
+
+ )}
+
+ {handleGpxUpload && (
+
+
+
+ )}
+
+
+
+
+ {/* Location Button */}
+
+
+
+ >
+ )}
+ >
+ );
+};
+
+const loadStyles = {
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 400,
+ width: '100%',
+ borderRadius: 10,
+ },
+ map: {
+ width: '100%',
+ minHeight: '100%', // Adjust the height to your needs
+ },
+ stylePicker: {
+ // Style Picker Button
+ position: 'absolute',
+ top: 10,
+ left: 10,
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 20,
+ backgroundColor: 'white',
+ },
+ styleModalContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ styleModalContent: {
+ backgroundColor: 'white',
+ borderRadius: 8,
+ padding: 10,
+ },
+ styleOption: {
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ },
+ styleOptionText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ locationButton: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 40,
+ height: 40,
+ position: 'absolute',
+ bottom: 30,
+ right: 10,
+ backgroundColor: 'white',
+ borderRadius: 30,
+ zIndex: 1,
+ },
+ headerBtnView: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 30,
+ marginTop: 30,
+ backgroundColor: 'white',
+ },
+ enterFullScreenBtn: {
+ width: 40,
+ height: 40,
+ position: 'absolute',
+ bottom: 10,
+ right: 10,
+ },
+ exitFullscreenBtn: {
+ width: 40,
+ height: 40,
+ position: 'absolute',
+ top: 10,
+ right: 10,
+ },
+ fullScreen: {
+ width: Platform.OS == 'web' ? '25%' : '70%',
+ height: 40,
+ padding: 10,
+ backgroundColor: 'white',
+ position: 'absolute',
+ bottom: 30,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ alignSelf: 'center',
+ borderRadius: 20,
+ },
+ downloadIcon: {
+ width: 21,
+ height: 21,
+ },
+ downloadText: {
+ fontSize: 15,
+ fontWeight: '500',
+ marginRight: 8,
+ },
+ modal: {
+ alignItems: 'center',
+ },
+} as const;
+
+export default MapButtonsOverlay;
diff --git a/packages/app/modules/map/components/MapButtonsOverlay/index.ts b/packages/app/modules/map/components/MapButtonsOverlay/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/app/modules/map/components/MapPreview/MapPreview.tsx b/packages/app/modules/map/components/MapPreview/MapPreview.tsx
new file mode 100644
index 000000000..f2acdf217
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreview/MapPreview.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { RImage } from '@packrat/ui';
+import {
+ useProcessedShape,
+ useMapPreviewData,
+ useGetObjectUrlFromImageUri,
+} from './hooks';
+// TODO ADD Map Preview Image
+export function MapPreview({ shape }: { shape: unknown }) {
+ const processedShape = useProcessedShape(shape);
+ const mapPreviewData = useMapPreviewData(shape, processedShape);
+ const uri = useGetObjectUrlFromImageUri(mapPreviewData?.uri || null);
+ if (uri) {
+ alert(uri);
+ }
+
+ if (!mapPreviewData || !uri) return null;
+
+ return (
+
+ );
+}
diff --git a/packages/app/modules/map/components/MapPreview/hooks/index.ts b/packages/app/modules/map/components/MapPreview/hooks/index.ts
new file mode 100644
index 000000000..34d834417
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreview/hooks/index.ts
@@ -0,0 +1,2 @@
+export { useGetObjectUrlFromImageUri } from './useGetObjectUrlFromImageUri';
+export * from './useMapPreview';
diff --git a/packages/app/modules/map/components/MapPreview/hooks/useGetObjectUrlFromImageUri.ts b/packages/app/modules/map/components/MapPreview/hooks/useGetObjectUrlFromImageUri.ts
new file mode 100644
index 000000000..1d2741304
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreview/hooks/useGetObjectUrlFromImageUri.ts
@@ -0,0 +1,36 @@
+import { useEffect, useState } from 'react';
+import { useAuthUserToken } from 'app/modules/auth';
+
+export const useGetObjectUrlFromImageUri = (imageURI: string | null) => {
+ const { token } = useAuthUserToken();
+ const [imageObjectURL, setImageObjectURL] = useState(null);
+
+ useEffect(() => {
+ if (!imageURI) {
+ return;
+ }
+ const fetchImage = async () => {
+ try {
+ const response = await fetch(imageURI, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const imgURL = URL.createObjectURL(blob);
+ setImageObjectURL(imgURL);
+ } else {
+ console.error('Failed to fetch image');
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ };
+
+ fetchImage();
+ }, [imageURI, token]);
+
+ return imageObjectURL;
+};
diff --git a/packages/app/modules/map/components/MapPreview/hooks/useMapPreview.ts b/packages/app/modules/map/components/MapPreview/hooks/useMapPreview.ts
new file mode 100644
index 000000000..b669b6ccd
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreview/hooks/useMapPreview.ts
@@ -0,0 +1,108 @@
+import {
+ getShapeSourceBounds,
+ isLineString,
+ isPolygonOrMultiPolygon,
+ processShapeData,
+ isPoint,
+} from '../../../mapFunctions';
+import { API_URL } from '@packrat/config';
+import { useState, useEffect } from 'react';
+
+type Feature = Record;
+
+interface MapPreviewData {
+ isPoint: boolean;
+ uri: string;
+}
+
+interface ImageShape {
+ type: string;
+ features: Feature[];
+}
+
+const useProcessedShape = (shape) => {
+ const [processedShape, setProcessedShape] = useState(null);
+
+ useEffect(() => {
+ const processed = processShapeData(shape);
+ setProcessedShape(processed);
+ }, [shape]);
+
+ return processedShape;
+};
+
+const useMapPreviewData = (shape, processedShape) => {
+ const [mapPreviewData, setMapPreviewData] = useState(
+ null,
+ );
+
+ useEffect(() => {
+ if (!processedShape) return;
+
+ const lineProperties = {
+ stroke: '#16b22d',
+ 'stroke-width': 4,
+ 'stroke-opacity': 1,
+ };
+ const pointProperties = {
+ 'marker-color': '#16b22d',
+ };
+
+ if (isLineString(shape)) {
+ shape.features[0].properties = lineProperties;
+ }
+
+ const imageShape: ImageShape = { type: 'FeatureCollection', features: [] };
+
+ const polygonObj = {
+ ...shape.features[0],
+ geometry: {
+ type: shape.features[0].geometry.type,
+ coordinates: [shape.features[0].geometry.coordinates[0]],
+ },
+ };
+
+ if (isPolygonOrMultiPolygon(shape)) {
+ imageShape.features.push([shape.features[0].geometry.coordinates[0]]);
+ } else {
+ imageShape.features.push(shape.features[0]);
+ }
+
+ processedShape.features.forEach((feature) => {
+ if (feature.properties.meta == 'end') {
+ feature.properties = pointProperties;
+ imageShape.features.push(feature);
+ }
+ });
+
+ const urlEncodedImageShapeGeoJSON = encodeURIComponent(
+ JSON.stringify(imageShape, null, 0),
+ );
+
+ let bounds = getShapeSourceBounds(shape);
+ if (Array.isArray(bounds[0]) && Array.isArray(bounds[1])) {
+ bounds = [bounds[0].concat(bounds[1])];
+ }
+
+ const {
+ coordinates: [lng, lat],
+ } = shape.features[0].geometry;
+
+ const mapPreviewEndpoint = `${API_URL}/mapPreview`;
+
+ const data = {
+ isPoint: isPoint(shape),
+ uri: isPoint(shape)
+ ? `${mapPreviewEndpoint}/pin-s+db4848(${lng},${lat})/${lng},${lat},8.63,0/900x400`
+ : `${mapPreviewEndpoint}/geojson(${urlEncodedImageShapeGeoJSON})/[${bounds.join(
+ ',',
+ )}]/900x400?padding=50,30,30,30`,
+ };
+
+ setMapPreviewData(data);
+ }, [shape, processedShape]);
+
+ return mapPreviewData;
+};
+
+export { useProcessedShape, useMapPreviewData };
diff --git a/packages/app/modules/map/components/MapPreview/index.ts b/packages/app/modules/map/components/MapPreview/index.ts
new file mode 100644
index 000000000..66877bc8b
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreview/index.ts
@@ -0,0 +1 @@
+export * from './MapPreview';
diff --git a/packages/app/modules/map/components/MapPreviewCard/MapImage.tsx b/packages/app/modules/map/components/MapPreviewCard/MapImage.tsx
new file mode 100644
index 000000000..45d5f5a07
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreviewCard/MapImage.tsx
@@ -0,0 +1,29 @@
+import React, { type FC } from 'react';
+import { View } from '@packrat/ui';
+import { MapPin } from '@tamagui/lucide-icons';
+import { type ViewProps } from 'tamagui';
+
+interface MapImageProps {
+ style?: ViewProps['style'];
+}
+
+export const MapImage: FC = ({ style = {} }) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/app/modules/map/components/MapPreviewCard/StatusLabel.tsx b/packages/app/modules/map/components/MapPreviewCard/StatusLabel.tsx
new file mode 100644
index 000000000..cd0c7ac86
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreviewCard/StatusLabel.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { RText, View } from '@packrat/ui';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+
+export const StatusLabel = () => {
+ return (
+
+
+ Offline
+
+ );
+};
diff --git a/packages/app/modules/map/components/MapPreviewCard/index.tsx b/packages/app/modules/map/components/MapPreviewCard/index.tsx
new file mode 100644
index 000000000..99dd531a9
--- /dev/null
+++ b/packages/app/modules/map/components/MapPreviewCard/index.tsx
@@ -0,0 +1,39 @@
+import React, { type FC } from 'react';
+import { StatusLabel } from './StatusLabel';
+import { View, RText, Card, YStack, Details } from '@packrat/ui';
+import useTheme from 'app/hooks/useTheme';
+import { TouchableOpacity } from 'react-native';
+import { MapImage } from './MapImage';
+
+interface MapPreviewCardProps {
+ id: string;
+ title: string;
+ isDownloaded: boolean;
+ onShowMapClick: (id: string) => void;
+}
+
+export const MapPreviewCard: FC = ({
+ id,
+ onShowMapClick,
+ title,
+ isDownloaded,
+}) => {
+ const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
+ useTheme();
+
+ return (
+ : null}
+ link=""
+ image={}
+ isFullWidth
+ type="secondary"
+ actions={
+ onShowMapClick(id)}>
+ Show map
+
+ }
+ />
+ );
+};
diff --git a/packages/app/modules/map/components/MapStylePicker/MapStylePicker.tsx b/packages/app/modules/map/components/MapStylePicker/MapStylePicker.tsx
new file mode 100644
index 000000000..57207b597
--- /dev/null
+++ b/packages/app/modules/map/components/MapStylePicker/MapStylePicker.tsx
@@ -0,0 +1,102 @@
+import React, { useState, type FC } from 'react';
+import {
+ Modal,
+ TouchableOpacity,
+ type TouchableOpacityProps,
+ View,
+} from 'react-native';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import { type MapStyle } from '../../model';
+import { RText } from '@packrat/ui';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { MapActionBtn } from '../MapActionBtn';
+
+interface MapStylePickerProps {
+ mapStyles: MapStyle[];
+ onStyleChange: (style: string) => void;
+ btnStyle?: TouchableOpacityProps['style'];
+}
+
+export const MapStylePicker: FC = ({
+ mapStyles,
+ btnStyle,
+ onStyleChange,
+}) => {
+ const [showStyleOptions, setShowStyleOptions] = useState(false);
+ const styles = useCustomStyles(loadStyles);
+
+ const handleStyleSelection = (style: string) => {
+ onStyleChange(style);
+ setShowStyleOptions(false);
+ };
+ const handleStyleOptionPress = () => {
+ setShowStyleOptions(!showStyleOptions);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {mapStyles.map((item, index) => (
+ handleStyleSelection(item.style)}
+ >
+ {item.label}
+
+ ))}
+
+
+
+ >
+ );
+};
+
+const loadStyles = () => ({
+ stylePicker: {
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 20,
+ backgroundColor: 'white',
+ },
+ styleModalContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ styleModalContent: {
+ backgroundColor: 'white',
+ borderRadius: 8,
+ padding: 10,
+ },
+ styleOption: {
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ },
+ styleOptionText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#000',
+ },
+ modal: {
+ alignItems: 'center',
+ },
+});
diff --git a/packages/app/modules/map/components/MapStylePicker/index.ts b/packages/app/modules/map/components/MapStylePicker/index.ts
new file mode 100644
index 000000000..83ce3b4fe
--- /dev/null
+++ b/packages/app/modules/map/components/MapStylePicker/index.ts
@@ -0,0 +1 @@
+export * from './MapStylePicker';
diff --git a/packages/app/modules/map/components/index.ts b/packages/app/modules/map/components/index.ts
new file mode 100644
index 000000000..b45023841
--- /dev/null
+++ b/packages/app/modules/map/components/index.ts
@@ -0,0 +1,4 @@
+export * from './MapPreview';
+export * from './FullScreenBtn';
+export * from './MapStylePicker';
+export * from './MapPreviewCard'
diff --git a/packages/app/modules/map/hooks/index.ts b/packages/app/modules/map/hooks/index.ts
new file mode 100644
index 000000000..315675587
--- /dev/null
+++ b/packages/app/modules/map/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './useMapStyles';
+export * from './useMapFullScreen';
diff --git a/packages/app/modules/map/hooks/useDownloadMap.tsx b/packages/app/modules/map/hooks/useDownloadMap.tsx
new file mode 100644
index 000000000..3426f1589
--- /dev/null
+++ b/packages/app/modules/map/hooks/useDownloadMap.tsx
@@ -0,0 +1,32 @@
+import { useAuthUser } from 'app/modules/auth';
+import { queryTrpc } from 'app/trpc';
+
+export const useDownloadMap = (onDownload) => {
+ const { mutateAsync, isLoading } = queryTrpc.saveOfflineMap.useMutation();
+ const authUser = useAuthUser();
+
+ const handleDownloadMap = ({ mapName, bounds, shape }) => {
+ const downloadOptions = {
+ name: mapName,
+ styleURL: 'mapbox://styles/mapbox/outdoors-v11',
+ bounds,
+ minZoom: 0,
+ maxZoom: 8,
+ owner_id: authUser.id,
+ metadata: {
+ shape: JSON.stringify(shape),
+ },
+ };
+
+ alert(JSON.stringify(downloadOptions));
+
+ // Save the map under user profile.
+ mutateAsync(downloadOptions)
+ .then(() => {
+ onDownload(downloadOptions);
+ })
+ .catch((e) => {});
+ };
+
+ return { handleDownloadMap, isSaving: isLoading };
+};
diff --git a/packages/app/modules/map/hooks/useDownloadMapProgress.tsx b/packages/app/modules/map/hooks/useDownloadMapProgress.tsx
new file mode 100644
index 000000000..a72ae888f
--- /dev/null
+++ b/packages/app/modules/map/hooks/useDownloadMapProgress.tsx
@@ -0,0 +1,31 @@
+import { offlineManager } from '@rnmapbox/maps';
+import { useState } from 'react';
+import { Alert } from 'react-native';
+
+export const useDownloadMapProgress = () => {
+ const [progress, setProgress] = useState(0);
+ const [downloading, setDownloading] = useState(false);
+
+ const onDownloadProgress = (offlineRegion: any, offlineRegionStatus: any) => {
+ setProgress(offlineRegionStatus.percentage);
+ setDownloading(true);
+ if (offlineRegionStatus.percentage === 100) {
+ Alert.alert('Map download successfully!');
+ setDownloading(false);
+ }
+ };
+
+ const errorListener = (offlineRegion: any, error: any) => {
+ Alert.alert(error.message);
+ };
+
+ const downloadMap = async (optionsForDownload: any) => {
+ offlineManager
+ .createPack(optionsForDownload, onDownloadProgress, errorListener)
+ .catch((error: any) => {
+ Alert.alert(error.message);
+ });
+ };
+
+ return { progress, isDownloading: downloading, downloadMap };
+};
diff --git a/packages/app/modules/map/hooks/useMapFullScreen.ts b/packages/app/modules/map/hooks/useMapFullScreen.ts
new file mode 100644
index 000000000..33df7bd7f
--- /dev/null
+++ b/packages/app/modules/map/hooks/useMapFullScreen.ts
@@ -0,0 +1,9 @@
+import { useState } from 'react';
+
+export const useMapFullScreen = (defaultMode: boolean = false) => {
+ const [isFullScreenMode, setIsFullScreenMode] = useState(defaultMode);
+
+ const toggleFullScreen = () => setIsFullScreenMode((prev) => !prev);
+
+ return { isFullScreenMode, toggleFullScreen };
+};
diff --git a/packages/app/modules/map/hooks/useMapStyles.ts b/packages/app/modules/map/hooks/useMapStyles.ts
new file mode 100644
index 000000000..5e086d664
--- /dev/null
+++ b/packages/app/modules/map/hooks/useMapStyles.ts
@@ -0,0 +1,21 @@
+import { useRef, useState } from 'react';
+
+const MAP_STYLES = [
+ { label: 'Outdoors', style: 'mapbox://styles/mapbox/outdoors-v11' },
+ { label: 'Street', style: 'mapbox://styles/mapbox/streets-v11' },
+ { label: 'Light', style: 'mapbox://styles/mapbox/light-v10' },
+ { label: 'Dark', style: 'mapbox://styles/mapbox/dark-v10' },
+ { label: 'Satellite', style: 'mapbox://styles/mapbox/satellite-v9' },
+ {
+ label: 'Satellite Street',
+ style: 'mapbox://styles/mapbox/satellite-streets-v11',
+ },
+];
+
+export const useMapStyles = () => {
+ const [selectedStyle, setSelectedStyle] = useState(MAP_STYLES[0].style);
+
+ const onStyleChange = (style: string) => setSelectedStyle(style);
+
+ return { mapStyles: MAP_STYLES, onStyleChange, selectedStyle };
+};
diff --git a/packages/app/modules/map/hooks/useOfflineMapShape.ts b/packages/app/modules/map/hooks/useOfflineMapShape.ts
new file mode 100644
index 000000000..0ab080871
--- /dev/null
+++ b/packages/app/modules/map/hooks/useOfflineMapShape.ts
@@ -0,0 +1,31 @@
+import { offlineManager } from '@rnmapbox/maps';
+import { useEffect, useState } from 'react';
+
+export const useOfflineMapShape = (offlineMapName?: string) => {
+ const [shape, setShape] = useState();
+ const [isLoading, setIsLoading] = useState(!!offlineMapName);
+ useEffect(() => {
+ let isMounted = true;
+ (async () => {
+ if (!offlineMapName) {
+ return;
+ }
+ try {
+ const offlineMapPack = await offlineManager.getPack(offlineMapName);
+ if (isMounted) {
+ setShape(offlineMapPack.metadata.shape);
+ }
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ })();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [offlineMapName]);
+
+ return { shape, isLoading };
+};
diff --git a/packages/app/modules/map/hooks/useOfflineMaps.ts b/packages/app/modules/map/hooks/useOfflineMaps.ts
new file mode 100644
index 000000000..fc3493b0a
--- /dev/null
+++ b/packages/app/modules/map/hooks/useOfflineMaps.ts
@@ -0,0 +1,40 @@
+import { queryTrpc } from 'app/trpc';
+import {
+ getPaginationInitialParams,
+ type PaginationParams,
+ usePagination,
+} from 'app/hooks/pagination';
+import { useState } from 'react';
+import { useAuthUser } from 'app/modules/auth';
+
+export const useOfflineMaps = () => {
+ const [pagination, setPagination] = useState(
+ getPaginationInitialParams(),
+ );
+ const authUser = useAuthUser();
+ const { data, isLoading, refetch } = queryTrpc.getOfflineMaps.useQuery(
+ {
+ ownerId: authUser.id,
+ pagination,
+ },
+ {
+ refetchOnWindowFocus: false,
+ onError: (error) => console.error('Error fetching public packs:', error),
+ },
+ );
+ const { fetchPrevPage, fetchNextPage } = usePagination(
+ refetch,
+ pagination,
+ setPagination,
+ { nextPage: data?.nextOffset },
+ );
+
+ return {
+ data: data?.data,
+ isLoading,
+ refetch,
+ fetchNextPage,
+ nextPage: data?.nextOffset || false,
+ error: null,
+ };
+};
diff --git a/packages/app/modules/map/index.ts b/packages/app/modules/map/index.ts
index e7e7a361d..2d9518203 100644
--- a/packages/app/modules/map/index.ts
+++ b/packages/app/modules/map/index.ts
@@ -1 +1,2 @@
export * from './widgets';
+export { getMapGEOURI } from './utils';
diff --git a/packages/app/modules/map/lib/MapView.native.tsx b/packages/app/modules/map/lib/MapView.native.tsx
new file mode 100644
index 000000000..19a0e6262
--- /dev/null
+++ b/packages/app/modules/map/lib/MapView.native.tsx
@@ -0,0 +1,55 @@
+import React, { useRef, type FC } from 'react';
+import MapboxGL, { Camera } from '@rnmapbox/maps';
+import { MAPBOX_ACCESS_TOKEN } from '@packrat/config';
+import { type MapViewProps } from './model';
+import { getBoundingBoxFromShape } from '../utils';
+import { View } from 'react-native';
+import flat from 'lodash/flatten';
+
+MapboxGL.setAccessToken(MAPBOX_ACCESS_TOKEN);
+
+export const MapView: FC = ({
+ shape,
+ shapeURI,
+ isInteractive,
+ mapStyle,
+ initialBounds,
+ onVisibleBoundsChange,
+}) => {
+ const mapView = useRef();
+ const bounds = Array.isArray(initialBounds)
+ ? flat(initialBounds)
+ : getBoundingBoxFromShape(shape);
+ const onCameraChange = ({ properties: { visibleBounds } }) => {
+ onVisibleBoundsChange(visibleBounds);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app/modules/map/lib/MapView.tsx b/packages/app/modules/map/lib/MapView.tsx
new file mode 100644
index 000000000..a36c43c5c
--- /dev/null
+++ b/packages/app/modules/map/lib/MapView.tsx
@@ -0,0 +1,97 @@
+import React, { useCallback, useRef, type FC } from 'react';
+import MapContainer, { Source, Layer } from 'react-map-gl';
+import { MAPBOX_ACCESS_TOKEN } from '@packrat/config';
+import MapLib, { type Map } from 'mapbox-gl';
+import 'mapbox-gl/dist/mapbox-gl.css';
+import { type MapViewProps } from './model';
+import { getBoundingBoxFromShape } from '../utils';
+
+MapLib.accessToken = MAPBOX_ACCESS_TOKEN;
+export const dataLayer = {
+ id: 'data',
+ type: 'fill',
+ paint: {
+ 'fill-color': {
+ property: 'percentile',
+ stops: [
+ [0, '#3288bd'],
+ [1, '#66c2a5'],
+ [2, '#abdda4'],
+ [3, '#e6f598'],
+ [4, '#ffffbf'],
+ [5, '#fee08b'],
+ [6, '#fdae61'],
+ [7, '#f46d43'],
+ [8, '#d53e4f'],
+ ],
+ },
+ 'fill-opacity': 0.3,
+ },
+};
+export const MapView: FC = ({
+ shape,
+ shapeURI,
+ mapStyle,
+ onVisibleBoundsChange,
+ initialBounds,
+ isInteractive = true,
+}) => {
+ const mapRef = useRef