diff --git a/.github/workflows/eas-cloud.yml b/.github/workflows/eas-cloud.yml
index 23c6880c2..3016314da 100644
--- a/.github/workflows/eas-cloud.yml
+++ b/.github/workflows/eas-cloud.yml
@@ -1,11 +1,11 @@
-# Native production pipeline
+# Native production pipeline
# Builds on EAS Cloud with auto submission
name: EAS Build & Submit
on:
- workflow_dispatch:
+ workflow_dispatch:
push:
- branches:
+ branches:
- main
paths:
- 'packages/app/**'
@@ -46,4 +46,4 @@ jobs:
- name: Build on EAS
working-directory: ./apps/expo
- run: eas build --platform all --profile production --non-interactive --no-wait --auto-submit
\ No newline at end of file
+ run: eas build --platform all --profile production --non-interactive --no-wait --auto-submit
diff --git a/app.json b/app.json
new file mode 100644
index 000000000..a2d883156
--- /dev/null
+++ b/app.json
@@ -0,0 +1,14 @@
+{
+ "expo": {
+ "name": "packrat-world",
+ "slug": "packrat-world",
+ "version": "0.0.1",
+ "description": "PackRat is the ultimate adventure planner designed for those who love to explore the great outdoors. Our app helps users plan and organize their trips with ease, whether it's a weekend camping trip, a day hike, or a cross-country road trip.",
+ "sdkVersion": "50.0.0",
+ "platforms": [
+ "ios",
+ "android",
+ "web"
+ ]
+ }
+}
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/item/[itemId].tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/item/[itemId].tsx
index 3f3539346..6b8869400 100644
--- a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/item/[itemId].tsx
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/item/[itemId].tsx
@@ -16,13 +16,12 @@ export default function Item() {
- {/* */}
>
);
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/[id].tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/[id].tsx
new file mode 100644
index 000000000..141dd5bd6
--- /dev/null
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/[id].tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { Platform } from 'react-native';
+import { Stack } from 'expo-router';
+import Head from 'expo-router/head';
+import useTheme from 'app/hooks/useTheme';
+import { DrawerToggleButton } from '@react-navigation/drawer';
+import { PackTemplateDetailsScreen } from 'app/modules/pack-templates';
+
+export default function Pack() {
+ const { currentTheme } = useTheme();
+
+ return (
+ <>
+ {Platform.OS === 'web' && (
+
+ Pack Template
+
+
+ )}
+ (
+
+ ),
+
+ headerStyle: {
+ backgroundColor: currentTheme.colors.background,
+ },
+ headerTitleStyle: {
+ fontSize: 24,
+ },
+ headerTintColor: currentTheme.colors.tertiaryBlue,
+ // https://reactnavigation.org/docs/headers#adjusting-header-styles
+
+ // https://reactnavigation.org/docs/headers#replacing-the-title-with-a-custom-component
+ }}
+ />
+
+ >
+ );
+}
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/index.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/index.tsx
new file mode 100644
index 000000000..84a589d3e
--- /dev/null
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/pack-templates/index.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { FeedScreen } from 'app/modules/feed';
+import { Platform } from 'react-native';
+import { Stack } from 'expo-router';
+import Head from 'expo-router/head';
+import useTheme from 'app/hooks/useTheme';
+import { DrawerToggleButton } from '@react-navigation/drawer';
+
+export default function PackTemplates() {
+ const { currentTheme } = useTheme();
+
+ return (
+ <>
+ {Platform.OS === 'web' && (
+
+ Pack Templates
+
+ )}
+ (
+
+ ),
+
+ headerStyle: {
+ backgroundColor: currentTheme.colors.background,
+ },
+ headerTitleStyle: {
+ fontSize: 24,
+ },
+ headerTintColor: currentTheme.colors.tertiaryBlue,
+
+ // https://reactnavigation.org/docs/headers#adjusting-header-styles
+
+ // https://reactnavigation.org/docs/headers#replacing-the-title-with-a-custom-component
+ }}
+ />
+
+ >
+ );
+}
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/products/index.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/products/index.tsx
new file mode 100644
index 000000000..de317698b
--- /dev/null
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/products/index.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { ProductsScreen } from 'app/modules/item';
+import { Platform } from 'react-native';
+import { Stack } from 'expo-router';
+import Head from 'expo-router/head';
+import useTheme from 'app/hooks/useTheme';
+import { DrawerToggleButton } from '@react-navigation/drawer';
+
+export default function ProductsPage() {
+ const { currentTheme } = useTheme();
+
+ return (
+ <>
+ {Platform.OS === 'web' && (
+
+ Products
+
+ )}
+ (
+
+ ),
+
+ headerStyle: {
+ backgroundColor: currentTheme.colors.background,
+ },
+ headerTitleStyle: {
+ fontSize: 24,
+ },
+ headerTintColor: currentTheme.colors.tertiaryBlue,
+ // https://reactnavigation.org/docs/headers#adjusting-header-styles
+
+ // https://reactnavigation.org/docs/headers#replacing-the-title-with-a-custom-component
+ }}
+ />
+
+ >
+ );
+}
diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/index.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/index.tsx
index 109c91904..baa0735da 100644
--- a/apps/expo/app/(app)/(drawer)/(tabs)/index.tsx
+++ b/apps/expo/app/(app)/(drawer)/(tabs)/index.tsx
@@ -1,11 +1,12 @@
import React from 'react';
import { Platform, View } from 'react-native';
-import { Stack } from 'expo-router';
+import { Redirect, Stack } from 'expo-router';
import { theme } from 'app/theme';
import { DashboardScreen } from 'app/modules/dashboard';
import useTheme from 'app/hooks/useTheme';
import { useAuthUser, LoginScreen } from 'app/modules/auth';
import Head from 'expo-router/head';
+import { useOfflineStore } from 'app/atoms';
export default function HomeScreen() {
const {
@@ -17,6 +18,7 @@ export default function HomeScreen() {
} = useTheme();
const user = useAuthUser();
+ const { connectionStatus } = useOfflineStore();
const mutualStyles = {
backgroundColor: currentTheme.colors.background,
@@ -35,9 +37,12 @@ export default function HomeScreen() {
title: 'Home',
}}
/>
-
- {!user ? : }
-
+ {connectionStatus === 'connected' && (
+
+ {!user ? : }
+
+ )}
+ {connectionStatus === 'offline' && }
>
);
}
diff --git a/apps/expo/app/entry.tsx b/apps/expo/app/entry.tsx
new file mode 100644
index 000000000..f84d1943b
--- /dev/null
+++ b/apps/expo/app/entry.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import LandingPage from 'app/components/landing_page';
+import { ConnectionGate } from 'app/components/ConnectionGate';
+import { Redirect } from 'expo-router';
+
+export default function Entry() {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/expo/app/offline/_layout.tsx b/apps/expo/app/offline/_layout.tsx
new file mode 100644
index 000000000..ba4ddd0a3
--- /dev/null
+++ b/apps/expo/app/offline/_layout.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { OfflineTabs } from 'app/components/navigation/OfflineTabs';
+import { useUserInOfflineMode } from 'app/modules/auth';
+import { Redirect } from 'expo-router';
+import { SafeArea } from 'app/provider/safe-area';
+
+export default function OfflineLayout() {
+ const { isLoading } = useUserInOfflineMode();
+ if (isLoading) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/expo/app/offline/maps.tsx b/apps/expo/app/offline/maps.tsx
new file mode 100644
index 000000000..1a433e32d
--- /dev/null
+++ b/apps/expo/app/offline/maps.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+import { OfflineMapsScreen } from 'app/modules/map/screens/OfflineMapsScreen';
+import { EmptyState } from '@packrat/ui';
+import { MapPin } from '@tamagui/lucide-icons';
+import { OfflineMessage } from 'app/components/OfflineMessage';
+
+export default function OfflineMaps() {
+ return (
+ <>
+
+
+ }
+ text="No maps available. Connect to the network to add maps."
+ />
+ }
+ />
+ >
+ );
+}
diff --git a/apps/expo/app/offline/pack.tsx b/apps/expo/app/offline/pack.tsx
new file mode 100644
index 000000000..5fdc90823
--- /dev/null
+++ b/apps/expo/app/offline/pack.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+import { FeedScreen } from 'app/modules/feed';
+import { OfflineMessage } from 'app/components/OfflineMessage';
+
+export default function OfflineMaps() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/expo/package.json b/apps/expo/package.json
index ac560ee71..778b19425 100644
--- a/apps/expo/package.json
+++ b/apps/expo/package.json
@@ -126,6 +126,7 @@
"react-native": "0.73.6",
"react-native-dotenv": "^3.4.8",
"react-native-elements": "^3.4.3",
+ "react-native-fast-image": "^8.6.3",
"react-native-flash-message": "^0.4.2",
"react-native-gesture-handler": "~2.14.0",
"react-native-google-places-autocomplete": "^2.5.1",
diff --git a/apps/next/pages/pack-templates/[id].tsx b/apps/next/pages/pack-templates/[id].tsx
new file mode 100644
index 000000000..2045bddf8
--- /dev/null
+++ b/apps/next/pages/pack-templates/[id].tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { PackTemplateDetailsScreen } from 'app/modules/pack-templates';
+import { AuthWrapper } from 'app/modules/auth';
+
+// export const runtime = 'experimental-edge'
+
+function PackTemplate() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default PackTemplate;
+
+PackTemplate.getLayout = function getLayout(page: any) {
+ return {page};
+};
diff --git a/apps/next/pages/pack-templates/index.tsx b/apps/next/pages/pack-templates/index.tsx
new file mode 100644
index 000000000..e7523a47f
--- /dev/null
+++ b/apps/next/pages/pack-templates/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { FeedScreen } from 'app/modules/feed';
+import { AuthWrapper } from 'app/modules/auth';
+
+// export const runtime = 'experimental-edge'
+
+function PackTemplates() {
+ return ;
+}
+
+export default PackTemplates;
+
+PackTemplates.getLayout = function getLayout(page: any) {
+ return {page};
+};
diff --git a/apps/next/pages/products/index.tsx b/apps/next/pages/products/index.tsx
new file mode 100644
index 000000000..cddd329dd
--- /dev/null
+++ b/apps/next/pages/products/index.tsx
@@ -0,0 +1,15 @@
+import { ProductsScreen } from 'app/modules/item';
+import { AuthWrapper } from 'app/modules/auth';
+// export const runtime = 'experimental-edge';
+
+export default function ProductsPage() {
+ return (
+ <>
+
+ >
+ );
+}
+
+ProductsPage.getLayout = function getLayout(page: any) {
+ return {page};
+};
diff --git a/apps/tauri/src/routeTree.gen.ts b/apps/tauri/src/routeTree.gen.ts
index 681586714..788c3670c 100644
--- a/apps/tauri/src/routeTree.gen.ts
+++ b/apps/tauri/src/routeTree.gen.ts
@@ -21,8 +21,10 @@ const TripsIndexLazyImport = createFileRoute('/trips/')()
const SignInIndexLazyImport = createFileRoute('/sign-in/')()
const RegisterIndexLazyImport = createFileRoute('/register/')()
const ProfileIndexLazyImport = createFileRoute('/profile/')()
+const ProductsIndexLazyImport = createFileRoute('/products/')()
const PasswordResetIndexLazyImport = createFileRoute('/password-reset/')()
const PacksIndexLazyImport = createFileRoute('/packs/')()
+const PackTemplatesIndexLazyImport = createFileRoute('/pack-templates/')()
const MapsIndexLazyImport = createFileRoute('/maps/')()
const MapIndexLazyImport = createFileRoute('/map/')()
const ItemsIndexLazyImport = createFileRoute('/items/')()
@@ -35,6 +37,7 @@ const TripTripIdLazyImport = createFileRoute('/trip/$tripId')()
const ProfileIdLazyImport = createFileRoute('/profile/$id')()
const PackCreateLazyImport = createFileRoute('/pack/create')()
const PackIdLazyImport = createFileRoute('/pack/$id')()
+const PackTemplatesIdLazyImport = createFileRoute('/pack-templates/$id')()
const DestinationQueryLazyImport = createFileRoute('/destination/query')()
const ProfileSettingsIndexLazyImport = createFileRoute('/profile/settings/')()
@@ -67,6 +70,13 @@ const ProfileIndexLazyRoute = ProfileIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/profile/index.lazy').then((d) => d.Route))
+const ProductsIndexLazyRoute = ProductsIndexLazyImport.update({
+ path: '/products/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/products/index.lazy').then((d) => d.Route),
+)
+
const PasswordResetIndexLazyRoute = PasswordResetIndexLazyImport.update({
path: '/password-reset/',
getParentRoute: () => rootRoute,
@@ -79,6 +89,13 @@ const PacksIndexLazyRoute = PacksIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/packs/index.lazy').then((d) => d.Route))
+const PackTemplatesIndexLazyRoute = PackTemplatesIndexLazyImport.update({
+ path: '/pack-templates/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/pack-templates/index.lazy').then((d) => d.Route),
+)
+
const MapsIndexLazyRoute = MapsIndexLazyImport.update({
path: '/maps/',
getParentRoute: () => rootRoute,
@@ -143,6 +160,13 @@ const PackIdLazyRoute = PackIdLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/pack/$id.lazy').then((d) => d.Route))
+const PackTemplatesIdLazyRoute = PackTemplatesIdLazyImport.update({
+ path: '/pack-templates/$id',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/pack-templates/$id.lazy').then((d) => d.Route),
+)
+
const DestinationQueryLazyRoute = DestinationQueryLazyImport.update({
path: '/destination/query',
getParentRoute: () => rootRoute,
@@ -175,6 +199,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DestinationQueryLazyImport
parentRoute: typeof rootRoute
}
+ '/pack-templates/$id': {
+ id: '/pack-templates/$id'
+ path: '/pack-templates/$id'
+ fullPath: '/pack-templates/$id'
+ preLoaderRoute: typeof PackTemplatesIdLazyImport
+ parentRoute: typeof rootRoute
+ }
'/pack/$id': {
id: '/pack/$id'
path: '/pack/$id'
@@ -259,6 +290,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MapsIndexLazyImport
parentRoute: typeof rootRoute
}
+ '/pack-templates/': {
+ id: '/pack-templates/'
+ path: '/pack-templates'
+ fullPath: '/pack-templates'
+ preLoaderRoute: typeof PackTemplatesIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
'/packs/': {
id: '/packs/'
path: '/packs'
@@ -273,6 +311,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PasswordResetIndexLazyImport
parentRoute: typeof rootRoute
}
+ '/products/': {
+ id: '/products/'
+ path: '/products'
+ fullPath: '/products'
+ preLoaderRoute: typeof ProductsIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
'/profile/': {
id: '/profile/'
path: '/profile'
@@ -313,29 +358,227 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
-export const routeTree = rootRoute.addChildren({
- IndexRoute,
- DestinationQueryLazyRoute,
- PackIdLazyRoute,
- PackCreateLazyRoute,
- ProfileIdLazyRoute,
- TripTripIdLazyRoute,
- TripCreateLazyRoute,
- AboutIndexLazyRoute,
- AppearanceIndexLazyRoute,
- DashboardIndexLazyRoute,
- FeedIndexLazyRoute,
- ItemsIndexLazyRoute,
- MapIndexLazyRoute,
- MapsIndexLazyRoute,
- PacksIndexLazyRoute,
- PasswordResetIndexLazyRoute,
- ProfileIndexLazyRoute,
- RegisterIndexLazyRoute,
- SignInIndexLazyRoute,
- TripsIndexLazyRoute,
- ProfileSettingsIndexLazyRoute,
-})
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/destination/query': typeof DestinationQueryLazyRoute
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
+ '/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
+ '/pack-templates': typeof PackTemplatesIndexLazyRoute
+ '/packs': typeof PacksIndexLazyRoute
+ '/password-reset': typeof PasswordResetIndexLazyRoute
+ '/products': typeof ProductsIndexLazyRoute
+ '/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
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
+ '/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
+ '/pack-templates': typeof PackTemplatesIndexLazyRoute
+ '/packs': typeof PacksIndexLazyRoute
+ '/password-reset': typeof PasswordResetIndexLazyRoute
+ '/products': typeof ProductsIndexLazyRoute
+ '/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
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
+ '/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
+ '/pack-templates/': typeof PackTemplatesIndexLazyRoute
+ '/packs/': typeof PacksIndexLazyRoute
+ '/password-reset/': typeof PasswordResetIndexLazyRoute
+ '/products/': typeof ProductsIndexLazyRoute
+ '/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'
+ | '/pack-templates/$id'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about'
+ | '/appearance'
+ | '/dashboard'
+ | '/feed'
+ | '/items'
+ | '/map'
+ | '/maps'
+ | '/pack-templates'
+ | '/packs'
+ | '/password-reset'
+ | '/products'
+ | '/profile'
+ | '/register'
+ | '/sign-in'
+ | '/trips'
+ | '/profile/settings'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/destination/query'
+ | '/pack-templates/$id'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about'
+ | '/appearance'
+ | '/dashboard'
+ | '/feed'
+ | '/items'
+ | '/map'
+ | '/maps'
+ | '/pack-templates'
+ | '/packs'
+ | '/password-reset'
+ | '/products'
+ | '/profile'
+ | '/register'
+ | '/sign-in'
+ | '/trips'
+ | '/profile/settings'
+ id:
+ | '__root__'
+ | '/'
+ | '/destination/query'
+ | '/pack-templates/$id'
+ | '/pack/$id'
+ | '/pack/create'
+ | '/profile/$id'
+ | '/trip/$tripId'
+ | '/trip/create'
+ | '/about/'
+ | '/appearance/'
+ | '/dashboard/'
+ | '/feed/'
+ | '/items/'
+ | '/map/'
+ | '/maps/'
+ | '/pack-templates/'
+ | '/packs/'
+ | '/password-reset/'
+ | '/products/'
+ | '/profile/'
+ | '/register/'
+ | '/sign-in/'
+ | '/trips/'
+ | '/profile/settings/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ DestinationQueryLazyRoute: typeof DestinationQueryLazyRoute
+ PackTemplatesIdLazyRoute: typeof PackTemplatesIdLazyRoute
+ 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
+ PackTemplatesIndexLazyRoute: typeof PackTemplatesIndexLazyRoute
+ PacksIndexLazyRoute: typeof PacksIndexLazyRoute
+ PasswordResetIndexLazyRoute: typeof PasswordResetIndexLazyRoute
+ ProductsIndexLazyRoute: typeof ProductsIndexLazyRoute
+ ProfileIndexLazyRoute: typeof ProfileIndexLazyRoute
+ RegisterIndexLazyRoute: typeof RegisterIndexLazyRoute
+ SignInIndexLazyRoute: typeof SignInIndexLazyRoute
+ TripsIndexLazyRoute: typeof TripsIndexLazyRoute
+ ProfileSettingsIndexLazyRoute: typeof ProfileSettingsIndexLazyRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ DestinationQueryLazyRoute: DestinationQueryLazyRoute,
+ PackTemplatesIdLazyRoute: PackTemplatesIdLazyRoute,
+ PackIdLazyRoute: PackIdLazyRoute,
+ PackCreateLazyRoute: PackCreateLazyRoute,
+ ProfileIdLazyRoute: ProfileIdLazyRoute,
+ TripTripIdLazyRoute: TripTripIdLazyRoute,
+ TripCreateLazyRoute: TripCreateLazyRoute,
+ AboutIndexLazyRoute: AboutIndexLazyRoute,
+ AppearanceIndexLazyRoute: AppearanceIndexLazyRoute,
+ DashboardIndexLazyRoute: DashboardIndexLazyRoute,
+ FeedIndexLazyRoute: FeedIndexLazyRoute,
+ ItemsIndexLazyRoute: ItemsIndexLazyRoute,
+ MapIndexLazyRoute: MapIndexLazyRoute,
+ MapsIndexLazyRoute: MapsIndexLazyRoute,
+ PackTemplatesIndexLazyRoute: PackTemplatesIndexLazyRoute,
+ PacksIndexLazyRoute: PacksIndexLazyRoute,
+ PasswordResetIndexLazyRoute: PasswordResetIndexLazyRoute,
+ ProductsIndexLazyRoute: ProductsIndexLazyRoute,
+ ProfileIndexLazyRoute: ProfileIndexLazyRoute,
+ RegisterIndexLazyRoute: RegisterIndexLazyRoute,
+ SignInIndexLazyRoute: SignInIndexLazyRoute,
+ TripsIndexLazyRoute: TripsIndexLazyRoute,
+ ProfileSettingsIndexLazyRoute: ProfileSettingsIndexLazyRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
/* prettier-ignore-end */
@@ -347,6 +590,7 @@ export const routeTree = rootRoute.addChildren({
"children": [
"/",
"/destination/query",
+ "/pack-templates/$id",
"/pack/$id",
"/pack/create",
"/profile/$id",
@@ -359,8 +603,10 @@ export const routeTree = rootRoute.addChildren({
"/items/",
"/map/",
"/maps/",
+ "/pack-templates/",
"/packs/",
"/password-reset/",
+ "/products/",
"/profile/",
"/register/",
"/sign-in/",
@@ -374,6 +620,9 @@ export const routeTree = rootRoute.addChildren({
"/destination/query": {
"filePath": "destination/query.lazy.tsx"
},
+ "/pack-templates/$id": {
+ "filePath": "pack-templates/$id.lazy.tsx"
+ },
"/pack/$id": {
"filePath": "pack/$id.lazy.tsx"
},
@@ -410,12 +659,18 @@ export const routeTree = rootRoute.addChildren({
"/maps/": {
"filePath": "maps/index.lazy.tsx"
},
+ "/pack-templates/": {
+ "filePath": "pack-templates/index.lazy.tsx"
+ },
"/packs/": {
"filePath": "packs/index.lazy.tsx"
},
"/password-reset/": {
"filePath": "password-reset/index.lazy.tsx"
},
+ "/products/": {
+ "filePath": "products/index.lazy.tsx"
+ },
"/profile/": {
"filePath": "profile/index.lazy.tsx"
},
diff --git a/apps/tauri/src/routes/pack-templates/$id.lazy.tsx b/apps/tauri/src/routes/pack-templates/$id.lazy.tsx
new file mode 100644
index 000000000..4543465ac
--- /dev/null
+++ b/apps/tauri/src/routes/pack-templates/$id.lazy.tsx
@@ -0,0 +1,15 @@
+import { createLazyFileRoute } from '@tanstack/react-router';
+import { AuthWrapper } from 'app/modules/auth';
+import { PackTemplateDetailsScreen } from 'app/modules/pack-templates';
+
+export const Route = createLazyFileRoute('/pack-templates/$id')({
+ component: PackTemplateScreen,
+});
+
+function PackTemplateScreen() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/tauri/src/routes/pack-templates/index.lazy.tsx b/apps/tauri/src/routes/pack-templates/index.lazy.tsx
new file mode 100644
index 000000000..bbe84ccbe
--- /dev/null
+++ b/apps/tauri/src/routes/pack-templates/index.lazy.tsx
@@ -0,0 +1,17 @@
+import { FeedScreen } from 'app/modules/feed';
+import { AuthWrapper } from 'app/modules/auth';
+import { createLazyFileRoute } from '@tanstack/react-router';
+
+export const Route = createLazyFileRoute('/pack-templates/')({
+ component: PackTemplatesScreen,
+});
+
+function PackTemplatesScreen() {
+ return (
+
+
+
+ );
+}
+
+export default PackTemplatesScreen;
diff --git a/apps/tauri/src/routes/products/index.lazy.tsx b/apps/tauri/src/routes/products/index.lazy.tsx
new file mode 100644
index 000000000..0ea072109
--- /dev/null
+++ b/apps/tauri/src/routes/products/index.lazy.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { ProductsScreen } from 'app/modules/item';
+import { AuthWrapper } from 'app/modules/auth';
+import { createLazyFileRoute } from '@tanstack/react-router';
+
+export const Route = createLazyFileRoute('/products/')({
+ component: ProductsPage,
+});
+
+export default function ProductsPage() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/vite/src/index.css b/apps/vite/src/index.css
index 6119ad9a8..5c3f38c1b 100644
--- a/apps/vite/src/index.css
+++ b/apps/vite/src/index.css
@@ -57,7 +57,7 @@ button:focus-visible {
@media (prefers-color-scheme: light) {
:root {
color: #213547;
- background-color: #ffffff;
+ /* background-color: #ffffff; */
}
a:hover {
color: #747bff;
diff --git a/apps/vite/src/routeTree.gen.ts b/apps/vite/src/routeTree.gen.ts
index ad9342afd..65099a5eb 100644
--- a/apps/vite/src/routeTree.gen.ts
+++ b/apps/vite/src/routeTree.gen.ts
@@ -21,9 +21,11 @@ const TripsIndexLazyImport = createFileRoute('/trips/')()
const SignInIndexLazyImport = createFileRoute('/sign-in/')()
const RegisterIndexLazyImport = createFileRoute('/register/')()
const ProfileIndexLazyImport = createFileRoute('/profile/')()
+const ProductsIndexLazyImport = createFileRoute('/products/')()
const PrivacyIndexLazyImport = createFileRoute('/privacy/')()
const PasswordResetIndexLazyImport = createFileRoute('/password-reset/')()
const PacksIndexLazyImport = createFileRoute('/packs/')()
+const PackTemplatesIndexLazyImport = createFileRoute('/pack-templates/')()
const MapsIndexLazyImport = createFileRoute('/maps/')()
const MapIndexLazyImport = createFileRoute('/map/')()
const ItemsIndexLazyImport = createFileRoute('/items/')()
@@ -36,6 +38,7 @@ const TripTripIdLazyImport = createFileRoute('/trip/$tripId')()
const ProfileIdLazyImport = createFileRoute('/profile/$id')()
const PackCreateLazyImport = createFileRoute('/pack/create')()
const PackIdLazyImport = createFileRoute('/pack/$id')()
+const PackTemplatesIdLazyImport = createFileRoute('/pack-templates/$id')()
const ItemItemIdLazyImport = createFileRoute('/item/$itemId')()
const DestinationQueryLazyImport = createFileRoute('/destination/query')()
const ProfileSettingsIndexLazyImport = createFileRoute('/profile/settings/')()
@@ -69,6 +72,13 @@ const ProfileIndexLazyRoute = ProfileIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/profile/index.lazy').then((d) => d.Route))
+const ProductsIndexLazyRoute = ProductsIndexLazyImport.update({
+ path: '/products/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/products/index.lazy').then((d) => d.Route),
+)
+
const PrivacyIndexLazyRoute = PrivacyIndexLazyImport.update({
path: '/privacy/',
getParentRoute: () => rootRoute,
@@ -86,6 +96,13 @@ const PacksIndexLazyRoute = PacksIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/packs/index.lazy').then((d) => d.Route))
+const PackTemplatesIndexLazyRoute = PackTemplatesIndexLazyImport.update({
+ path: '/pack-templates/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/pack-templates/index.lazy').then((d) => d.Route),
+)
+
const MapsIndexLazyRoute = MapsIndexLazyImport.update({
path: '/maps/',
getParentRoute: () => rootRoute,
@@ -150,6 +167,13 @@ const PackIdLazyRoute = PackIdLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/pack/$id.lazy').then((d) => d.Route))
+const PackTemplatesIdLazyRoute = PackTemplatesIdLazyImport.update({
+ path: '/pack-templates/$id',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/pack-templates/$id.lazy').then((d) => d.Route),
+)
+
const ItemItemIdLazyRoute = ItemItemIdLazyImport.update({
path: '/item/$itemId',
getParentRoute: () => rootRoute,
@@ -194,6 +218,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ItemItemIdLazyImport
parentRoute: typeof rootRoute
}
+ '/pack-templates/$id': {
+ id: '/pack-templates/$id'
+ path: '/pack-templates/$id'
+ fullPath: '/pack-templates/$id'
+ preLoaderRoute: typeof PackTemplatesIdLazyImport
+ parentRoute: typeof rootRoute
+ }
'/pack/$id': {
id: '/pack/$id'
path: '/pack/$id'
@@ -278,6 +309,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MapsIndexLazyImport
parentRoute: typeof rootRoute
}
+ '/pack-templates/': {
+ id: '/pack-templates/'
+ path: '/pack-templates'
+ fullPath: '/pack-templates'
+ preLoaderRoute: typeof PackTemplatesIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
'/packs/': {
id: '/packs/'
path: '/packs'
@@ -299,6 +337,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PrivacyIndexLazyImport
parentRoute: typeof rootRoute
}
+ '/products/': {
+ id: '/products/'
+ path: '/products'
+ fullPath: '/products'
+ preLoaderRoute: typeof ProductsIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
'/profile/': {
id: '/profile/'
path: '/profile'
@@ -343,6 +388,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/destination/query': typeof DestinationQueryLazyRoute
'/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
'/pack/$id': typeof PackIdLazyRoute
'/pack/create': typeof PackCreateLazyRoute
'/profile/$id': typeof ProfileIdLazyRoute
@@ -355,9 +401,11 @@ export interface FileRoutesByFullPath {
'/items': typeof ItemsIndexLazyRoute
'/map': typeof MapIndexLazyRoute
'/maps': typeof MapsIndexLazyRoute
+ '/pack-templates': typeof PackTemplatesIndexLazyRoute
'/packs': typeof PacksIndexLazyRoute
'/password-reset': typeof PasswordResetIndexLazyRoute
'/privacy': typeof PrivacyIndexLazyRoute
+ '/products': typeof ProductsIndexLazyRoute
'/profile': typeof ProfileIndexLazyRoute
'/register': typeof RegisterIndexLazyRoute
'/sign-in': typeof SignInIndexLazyRoute
@@ -369,6 +417,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/destination/query': typeof DestinationQueryLazyRoute
'/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
'/pack/$id': typeof PackIdLazyRoute
'/pack/create': typeof PackCreateLazyRoute
'/profile/$id': typeof ProfileIdLazyRoute
@@ -381,9 +430,11 @@ export interface FileRoutesByTo {
'/items': typeof ItemsIndexLazyRoute
'/map': typeof MapIndexLazyRoute
'/maps': typeof MapsIndexLazyRoute
+ '/pack-templates': typeof PackTemplatesIndexLazyRoute
'/packs': typeof PacksIndexLazyRoute
'/password-reset': typeof PasswordResetIndexLazyRoute
'/privacy': typeof PrivacyIndexLazyRoute
+ '/products': typeof ProductsIndexLazyRoute
'/profile': typeof ProfileIndexLazyRoute
'/register': typeof RegisterIndexLazyRoute
'/sign-in': typeof SignInIndexLazyRoute
@@ -396,6 +447,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/destination/query': typeof DestinationQueryLazyRoute
'/item/$itemId': typeof ItemItemIdLazyRoute
+ '/pack-templates/$id': typeof PackTemplatesIdLazyRoute
'/pack/$id': typeof PackIdLazyRoute
'/pack/create': typeof PackCreateLazyRoute
'/profile/$id': typeof ProfileIdLazyRoute
@@ -408,9 +460,11 @@ export interface FileRoutesById {
'/items/': typeof ItemsIndexLazyRoute
'/map/': typeof MapIndexLazyRoute
'/maps/': typeof MapsIndexLazyRoute
+ '/pack-templates/': typeof PackTemplatesIndexLazyRoute
'/packs/': typeof PacksIndexLazyRoute
'/password-reset/': typeof PasswordResetIndexLazyRoute
'/privacy/': typeof PrivacyIndexLazyRoute
+ '/products/': typeof ProductsIndexLazyRoute
'/profile/': typeof ProfileIndexLazyRoute
'/register/': typeof RegisterIndexLazyRoute
'/sign-in/': typeof SignInIndexLazyRoute
@@ -424,6 +478,7 @@ export interface FileRouteTypes {
| '/'
| '/destination/query'
| '/item/$itemId'
+ | '/pack-templates/$id'
| '/pack/$id'
| '/pack/create'
| '/profile/$id'
@@ -436,9 +491,11 @@ export interface FileRouteTypes {
| '/items'
| '/map'
| '/maps'
+ | '/pack-templates'
| '/packs'
| '/password-reset'
| '/privacy'
+ | '/products'
| '/profile'
| '/register'
| '/sign-in'
@@ -449,6 +506,7 @@ export interface FileRouteTypes {
| '/'
| '/destination/query'
| '/item/$itemId'
+ | '/pack-templates/$id'
| '/pack/$id'
| '/pack/create'
| '/profile/$id'
@@ -461,9 +519,11 @@ export interface FileRouteTypes {
| '/items'
| '/map'
| '/maps'
+ | '/pack-templates'
| '/packs'
| '/password-reset'
| '/privacy'
+ | '/products'
| '/profile'
| '/register'
| '/sign-in'
@@ -474,6 +534,7 @@ export interface FileRouteTypes {
| '/'
| '/destination/query'
| '/item/$itemId'
+ | '/pack-templates/$id'
| '/pack/$id'
| '/pack/create'
| '/profile/$id'
@@ -486,9 +547,11 @@ export interface FileRouteTypes {
| '/items/'
| '/map/'
| '/maps/'
+ | '/pack-templates/'
| '/packs/'
| '/password-reset/'
| '/privacy/'
+ | '/products/'
| '/profile/'
| '/register/'
| '/sign-in/'
@@ -501,6 +564,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DestinationQueryLazyRoute: typeof DestinationQueryLazyRoute
ItemItemIdLazyRoute: typeof ItemItemIdLazyRoute
+ PackTemplatesIdLazyRoute: typeof PackTemplatesIdLazyRoute
PackIdLazyRoute: typeof PackIdLazyRoute
PackCreateLazyRoute: typeof PackCreateLazyRoute
ProfileIdLazyRoute: typeof ProfileIdLazyRoute
@@ -513,9 +577,11 @@ export interface RootRouteChildren {
ItemsIndexLazyRoute: typeof ItemsIndexLazyRoute
MapIndexLazyRoute: typeof MapIndexLazyRoute
MapsIndexLazyRoute: typeof MapsIndexLazyRoute
+ PackTemplatesIndexLazyRoute: typeof PackTemplatesIndexLazyRoute
PacksIndexLazyRoute: typeof PacksIndexLazyRoute
PasswordResetIndexLazyRoute: typeof PasswordResetIndexLazyRoute
PrivacyIndexLazyRoute: typeof PrivacyIndexLazyRoute
+ ProductsIndexLazyRoute: typeof ProductsIndexLazyRoute
ProfileIndexLazyRoute: typeof ProfileIndexLazyRoute
RegisterIndexLazyRoute: typeof RegisterIndexLazyRoute
SignInIndexLazyRoute: typeof SignInIndexLazyRoute
@@ -527,6 +593,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DestinationQueryLazyRoute: DestinationQueryLazyRoute,
ItemItemIdLazyRoute: ItemItemIdLazyRoute,
+ PackTemplatesIdLazyRoute: PackTemplatesIdLazyRoute,
PackIdLazyRoute: PackIdLazyRoute,
PackCreateLazyRoute: PackCreateLazyRoute,
ProfileIdLazyRoute: ProfileIdLazyRoute,
@@ -539,9 +606,11 @@ const rootRouteChildren: RootRouteChildren = {
ItemsIndexLazyRoute: ItemsIndexLazyRoute,
MapIndexLazyRoute: MapIndexLazyRoute,
MapsIndexLazyRoute: MapsIndexLazyRoute,
+ PackTemplatesIndexLazyRoute: PackTemplatesIndexLazyRoute,
PacksIndexLazyRoute: PacksIndexLazyRoute,
PasswordResetIndexLazyRoute: PasswordResetIndexLazyRoute,
PrivacyIndexLazyRoute: PrivacyIndexLazyRoute,
+ ProductsIndexLazyRoute: ProductsIndexLazyRoute,
ProfileIndexLazyRoute: ProfileIndexLazyRoute,
RegisterIndexLazyRoute: RegisterIndexLazyRoute,
SignInIndexLazyRoute: SignInIndexLazyRoute,
@@ -564,6 +633,7 @@ export const routeTree = rootRoute
"/",
"/destination/query",
"/item/$itemId",
+ "/pack-templates/$id",
"/pack/$id",
"/pack/create",
"/profile/$id",
@@ -576,9 +646,11 @@ export const routeTree = rootRoute
"/items/",
"/map/",
"/maps/",
+ "/pack-templates/",
"/packs/",
"/password-reset/",
"/privacy/",
+ "/products/",
"/profile/",
"/register/",
"/sign-in/",
@@ -595,6 +667,9 @@ export const routeTree = rootRoute
"/item/$itemId": {
"filePath": "item/$itemId.lazy.tsx"
},
+ "/pack-templates/$id": {
+ "filePath": "pack-templates/$id.lazy.tsx"
+ },
"/pack/$id": {
"filePath": "pack/$id.lazy.tsx"
},
@@ -631,6 +706,9 @@ export const routeTree = rootRoute
"/maps/": {
"filePath": "maps/index.lazy.tsx"
},
+ "/pack-templates/": {
+ "filePath": "pack-templates/index.lazy.tsx"
+ },
"/packs/": {
"filePath": "packs/index.lazy.tsx"
},
@@ -640,6 +718,9 @@ export const routeTree = rootRoute
"/privacy/": {
"filePath": "privacy/index.lazy.tsx"
},
+ "/products/": {
+ "filePath": "products/index.lazy.tsx"
+ },
"/profile/": {
"filePath": "profile/index.lazy.tsx"
},
diff --git a/apps/vite/src/routes/__root.tsx b/apps/vite/src/routes/__root.tsx
index a80d0f1aa..bdb2dbab9 100644
--- a/apps/vite/src/routes/__root.tsx
+++ b/apps/vite/src/routes/__root.tsx
@@ -6,16 +6,18 @@ import { Navbar } from 'app/components/navigation';
import { Provider } from 'app/provider';
import { NODE_ENV } from '@packrat/config';
import ThemeContext from '../../../../packages/app/context/theme';
+import Footer from 'app/components/footer/Footer';
const ThemedMainContentWeb = () => {
const { isDark } = useContext(ThemeContext);
- const backgroundColor = isDark ? '#050505' : '#fdfbff';
+ const backgroundColor = isDark ? '#000000' : '#F5F5F5';
return (
+
);
};
diff --git a/apps/vite/src/routes/pack-templates/$id.lazy.tsx b/apps/vite/src/routes/pack-templates/$id.lazy.tsx
new file mode 100644
index 000000000..4543465ac
--- /dev/null
+++ b/apps/vite/src/routes/pack-templates/$id.lazy.tsx
@@ -0,0 +1,15 @@
+import { createLazyFileRoute } from '@tanstack/react-router';
+import { AuthWrapper } from 'app/modules/auth';
+import { PackTemplateDetailsScreen } from 'app/modules/pack-templates';
+
+export const Route = createLazyFileRoute('/pack-templates/$id')({
+ component: PackTemplateScreen,
+});
+
+function PackTemplateScreen() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/vite/src/routes/pack-templates/index.lazy.tsx b/apps/vite/src/routes/pack-templates/index.lazy.tsx
new file mode 100644
index 000000000..bbe84ccbe
--- /dev/null
+++ b/apps/vite/src/routes/pack-templates/index.lazy.tsx
@@ -0,0 +1,17 @@
+import { FeedScreen } from 'app/modules/feed';
+import { AuthWrapper } from 'app/modules/auth';
+import { createLazyFileRoute } from '@tanstack/react-router';
+
+export const Route = createLazyFileRoute('/pack-templates/')({
+ component: PackTemplatesScreen,
+});
+
+function PackTemplatesScreen() {
+ return (
+
+
+
+ );
+}
+
+export default PackTemplatesScreen;
diff --git a/apps/vite/src/routes/products/index.lazy.tsx b/apps/vite/src/routes/products/index.lazy.tsx
new file mode 100644
index 000000000..92544e0bf
--- /dev/null
+++ b/apps/vite/src/routes/products/index.lazy.tsx
@@ -0,0 +1,15 @@
+import { ProductsScreen } from 'app/modules/item';
+import { AuthWrapper } from 'app/modules/auth';
+import { createLazyFileRoute } from '@tanstack/react-router';
+
+export const Route = createLazyFileRoute('/products/')({
+ component: ProductsPage,
+});
+
+export default function ProductsPage() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/vite/src/styles/global.css b/apps/vite/src/styles/global.css
index 4bd185f4d..c6895e967 100644
--- a/apps/vite/src/styles/global.css
+++ b/apps/vite/src/styles/global.css
@@ -1,16 +1,8 @@
+/* body {
+ background: hsla(0, 0%, 96%, 1) !important;
+ filter: progid: DXImageTransform.Microsoft.gradient( startColorstr="#F6F6F6", endColorstr="#E1DAE6", GradientType=1 );
+}
*:focus {
outline: none;
-}
-
-@media(prefers-color-scheme:dark){
- body {
- background-color:#1A1A1D !important;
- }
-}
-
-@media(prefers-color-scheme:light){
- body {
- background-color: #fdfbff !important;
- }
-}
\ No newline at end of file
+} */
diff --git a/packages/app/assets/PackRat Preview.jpg b/packages/app/assets/PackRat Preview.jpg
new file mode 100644
index 000000000..b01c8fadd
Binary files /dev/null and b/packages/app/assets/PackRat Preview.jpg differ
diff --git a/packages/app/assets/PackRat Preview_Left.jpg b/packages/app/assets/PackRat Preview_Left.jpg
new file mode 100644
index 000000000..b8ce30cbc
Binary files /dev/null and b/packages/app/assets/PackRat Preview_Left.jpg differ
diff --git a/packages/app/assets/PackRat Preview_Right.jpg b/packages/app/assets/PackRat Preview_Right.jpg
new file mode 100644
index 000000000..310b65ebf
Binary files /dev/null and b/packages/app/assets/PackRat Preview_Right.jpg differ
diff --git a/packages/app/assets/PakRat_FAQS.png b/packages/app/assets/PakRat_FAQS.png
new file mode 100644
index 000000000..e4684d8f2
Binary files /dev/null and b/packages/app/assets/PakRat_FAQS.png differ
diff --git a/packages/app/assets/applelink.svg b/packages/app/assets/applelink.svg
new file mode 100644
index 000000000..49120bb36
--- /dev/null
+++ b/packages/app/assets/applelink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app/assets/item-placeholder.png b/packages/app/assets/item-placeholder.png
new file mode 100644
index 000000000..a66bfeb29
Binary files /dev/null and b/packages/app/assets/item-placeholder.png differ
diff --git a/packages/app/atoms/offlineStore/offlineStore.ts b/packages/app/atoms/offlineStore/offlineStore.ts
index f59b41e1e..7b2c64488 100644
--- a/packages/app/atoms/offlineStore/offlineStore.ts
+++ b/packages/app/atoms/offlineStore/offlineStore.ts
@@ -1,11 +1,13 @@
import { atom, useAtom } from 'jotai';
+export type ConnectionStatus = 'pending' | 'connected' | 'offline';
+
const requestsAtom = atom([]);
-const isConnectedAtom = atom(true);
+const connectionStatusAtom = atom('pending');
export const useOfflineStore = () => {
const [requests, setRequests] = useAtom(requestsAtom);
- const [isConnected, setIsConnected] = useAtom(isConnectedAtom);
+ const [connectionStatus, setConnectionStatus] = useAtom(connectionStatusAtom);
- return { requests, setRequests, isConnected, setIsConnected };
+ return { requests, setRequests, connectionStatus, setConnectionStatus };
};
diff --git a/packages/app/components/ConnectionGate/ConnectionGate.tsx b/packages/app/components/ConnectionGate/ConnectionGate.tsx
new file mode 100644
index 000000000..6d818fd1b
--- /dev/null
+++ b/packages/app/components/ConnectionGate/ConnectionGate.tsx
@@ -0,0 +1,12 @@
+import React, { type ReactNode, type FC } from 'react';
+import { type ConnectionStatus, useOfflineStore } from 'app/atoms';
+
+interface ConnectionGateProps {
+ mode: ConnectionStatus;
+ children: ReactNode;
+}
+export const ConnectionGate: FC = ({ mode, children }) => {
+ const { connectionStatus } = useOfflineStore();
+
+ return connectionStatus === mode ? children : null;
+};
diff --git a/packages/app/components/ConnectionGate/index.ts b/packages/app/components/ConnectionGate/index.ts
new file mode 100644
index 000000000..d14d4787b
--- /dev/null
+++ b/packages/app/components/ConnectionGate/index.ts
@@ -0,0 +1 @@
+export * from './ConnectionGate';
diff --git a/packages/app/components/Fab/FabWeb.tsx b/packages/app/components/Fab/FabWeb.tsx
index cd6e9753b..360fc3d03 100644
--- a/packages/app/components/Fab/FabWeb.tsx
+++ b/packages/app/components/Fab/FabWeb.tsx
@@ -30,22 +30,20 @@ const loadStyles = (theme) => {
quickActionsContainer: {
position: 'absolute',
top: 50,
- right: 10,
+ left: 10,
zIndex: 1,
height: 54,
width: 150,
borderRadius: 5,
},
fab: {
- position: 'absolute',
flexDirection: 'row',
width: 100,
- height: 50,
backgroundColor: currentTheme.colors.card,
- borderRadius: 28,
+ borderRadius: 9,
justifyContent: 'center',
alignItems: 'center',
- alignSelf: 'flex-end',
+ alignSelf: 'stretch',
zIndex: 2,
},
fabIcon: {
diff --git a/packages/app/components/GearList/GearList.tsx b/packages/app/components/GearList/GearList.tsx
index 0b6584aa1..21466ef40 100644
--- a/packages/app/components/GearList/GearList.tsx
+++ b/packages/app/components/GearList/GearList.tsx
@@ -4,12 +4,15 @@ import { FontAwesome5 } from '@expo/vector-icons';
import { AddPackContainer } from '../../modules/pack/widgets/AddPackContainer';
import useTheme from '../../hooks/useTheme';
import PackContainer from '../../modules/pack/widgets/PackContainer';
+import { usePackId } from 'app/modules/pack';
const RStack: any = OriginalRStack;
const RText: any = OriginalRText;
export const GearList = () => {
const { currentTheme } = useTheme();
+ const [_, setPackIdParam] = usePackId();
+
return (
{
-
-
+
+
);
};
diff --git a/packages/app/components/OfflineMessage/OfflineMessage.tsx b/packages/app/components/OfflineMessage/OfflineMessage.tsx
new file mode 100644
index 000000000..4651cb552
--- /dev/null
+++ b/packages/app/components/OfflineMessage/OfflineMessage.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import useTheme from 'app/hooks/useTheme';
+import { RText } from '@packrat/ui';
+import { TouchableOpacity, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { WifiOff, X } from '@tamagui/lucide-icons';
+import { useStorage } from 'app/hooks/storage/useStorage';
+
+export function OfflineMessage() {
+ const { currentTheme } = useTheme();
+ const { top } = useSafeAreaInsets();
+ const { showOfflineStatusBanner, onCloseBanner } =
+ useOfflineStatusBannerState();
+
+ return showOfflineStatusBanner ? (
+
+
+
+ You are currently offline. Please check your internet connection.
+
+
+
+
+
+ ) : null;
+}
+
+const useOfflineStatusBannerState = () => {
+ const [[isLoading, showOfflineStatusBannerStr], setShowOfflineStatusBanner] =
+ useStorage('showOfflineStatusBanngyer');
+ const showOfflineStatusBanner =
+ !isLoading && showOfflineStatusBannerStr === null;
+
+ const onCloseBanner = () => setShowOfflineStatusBanner('false');
+
+ return { showOfflineStatusBanner, onCloseBanner };
+};
diff --git a/packages/app/components/OfflineMessage/index.ts b/packages/app/components/OfflineMessage/index.ts
new file mode 100644
index 000000000..2c9dd347d
--- /dev/null
+++ b/packages/app/components/OfflineMessage/index.ts
@@ -0,0 +1 @@
+export { OfflineMessage } from './OfflineMessage';
diff --git a/packages/app/components/SearchInput/SearchInput.tsx b/packages/app/components/SearchInput/SearchInput.tsx
index baa9ab039..d238353e2 100644
--- a/packages/app/components/SearchInput/SearchInput.tsx
+++ b/packages/app/components/SearchInput/SearchInput.tsx
@@ -1,4 +1,4 @@
-import React, { cloneElement, type ReactNode, forwardRef } from 'react';
+import React, { cloneElement, forwardRef, useMemo } from 'react';
import { Platform, type TextInput } from 'react-native';
import { MaterialIcons, MaterialCommunityIcons } from '@expo/vector-icons';
import useSearchInput from './useSearchInput';
@@ -22,10 +22,12 @@ const Pressable: any = OriginalPressable;
interface SearchInputProps {
onSelect: (result: any, index: number) => void;
+ onCreate: (result: any, index: number) => void;
results: any[];
onChange: (text: string) => void;
searchString?: string;
placeholder?: string;
+ canCreateNewItem?: boolean;
resultItemComponent: React.ReactElement;
}
@@ -33,11 +35,13 @@ export const SearchInput = forwardRef(
function SearchInput(
{
onSelect,
+ onCreate,
placeholder,
resultItemComponent: ResultItemComponent,
results,
onChange,
searchString,
+ canCreateNewItem = false,
},
inputRef,
) {
@@ -48,10 +52,12 @@ export const SearchInput = forwardRef(
showSearchResults,
isLoadingMobile,
isVisible,
- } = useSearchInput({ onSelect, onChange, searchString });
+ } = useSearchInput({ onSelect, onChange, onCreate, searchString });
+ const options = useSearchOptions(results, searchString, canCreateNewItem);
const { isDark, currentTheme } = useTheme();
const styles = useCustomStyles(loadStyles);
+
if (Platform.OS === 'web') {
return (
@@ -90,9 +96,7 @@ export const SearchInput = forwardRef(
/>
{searchString && (
{
- handleClearSearch();
- }}
+ onPress={handleClearSearch}
style={{
position: 'absolute',
right: 1,
@@ -111,13 +115,13 @@ export const SearchInput = forwardRef(
display: isVisible ? 'block' : 'none',
}}
>
- {showSearchResults && results && results?.length > 0 && (
+ {showSearchResults && (
(
: currentTheme.colors.white,
}}
>
- {results.map((result, i) => (
+ {options.map((result, i) => (
{
- handleSearchResultClick(result);
- }}
- style={{
- cursor: 'pointer',
- }}
+ onPress={() =>
+ !result.isDisabled && handleSearchResultClick(result)
+ }
+ style={{ cursor: 'pointer' }}
>
{cloneElement(ResultItemComponent, { item: result })}
@@ -230,16 +232,14 @@ export const SearchInput = forwardRef(
display: isVisible ? 'block' : 'none',
}}
>
- {showSearchResults && results?.length > 0 && (
+ {showSearchResults && (
- {results.map((result, i) => (
+ {options.map((result, i) => (
{
- handleSearchResultClick(result);
- }}
+ onPress={() => handleSearchResultClick(result)}
paddingHorizontal={16}
paddingVertical={8}
>
@@ -256,11 +256,41 @@ export const SearchInput = forwardRef(
},
);
+const useSearchOptions = (
+ results: any,
+ searchString: string,
+ canCreateNewItem: boolean,
+) => {
+ return useMemo(() => {
+ if (!Array.isArray(results)) {
+ return [];
+ }
+
+ if (!canCreateNewItem) {
+ return results;
+ }
+
+ const hasExactMatch = results.some(
+ (result) => result.name.toLowerCase() === searchString?.toLowerCase(),
+ );
+
+ return hasExactMatch
+ ? [...results]
+ : [
+ {
+ id: 'create',
+ name: `Create "${searchString}"`,
+ title: searchString,
+ },
+ ...results,
+ ];
+ }, [results]);
+};
+
const loadStyles = () => ({
container: {
marginTop: 20,
marginBottom: 15,
- // maxWidth: 800,
width: '100%',
display: 'flex',
justifyContent: 'center',
diff --git a/packages/app/components/SearchInput/useSearchInput.ts b/packages/app/components/SearchInput/useSearchInput.ts
index 64a534546..234dd4bdc 100644
--- a/packages/app/components/SearchInput/useSearchInput.ts
+++ b/packages/app/components/SearchInput/useSearchInput.ts
@@ -1,16 +1,30 @@
import { set } from 'lodash';
import { useState } from 'react';
+import { useFetchSinglePack, usePackId } from 'app/modules/pack';
+import { OfflineCreatePackOptions } from '@rnmapbox/maps';
-const useSearchInput = ({ onSelect, onChange, searchString }) => {
+const useSearchInput = ({ onSelect, onChange, searchString, onCreate }) => {
const [isLoadingMobile, setIsLoadingMobile] = useState(false);
const showSearchResults = !!searchString;
const [isVisible, setIsVisible] = useState(false);
+ const [isAddItemModalOpen, setIsAddItemModalOpen] = useState(false);
+ const [refetch, setRefetch] = useState(false);
+ const [packId] = usePackId();
+ const { data: currentPack } = useFetchSinglePack(packId);
+ const currentPackId = currentPack && currentPack.id;
+
+ const [createItemTitle, setCreateItemTitle] = useState('');
+
const handleSearchResultClick = (result) => {
if (onSelect) {
- onSelect(result);
onChange('');
setIsVisible(false);
+ if (result.id === 'create') {
+ onCreate(result);
+ } else {
+ onSelect(result);
+ }
}
};
diff --git a/packages/app/components/card/CustomCard.tsx b/packages/app/components/card/CustomCard.tsx
index d112b4dc6..7997a2e7e 100644
--- a/packages/app/components/card/CustomCard.tsx
+++ b/packages/app/components/card/CustomCard.tsx
@@ -7,13 +7,15 @@ import { TripCardHeader } from './TripCardHeader';
import { PackCardHeader } from './PackCardHeader';
import { ItemCardHeader } from './ItemCardHeader';
import { useAuthUser } from 'app/modules/auth';
+import { PackTemplateHeader } from 'app/modules/pack-templates';
+import { ConnectionGate } from 'app/components/ConnectionGate';
interface CustomCardProps {
title: string;
content: React.ReactNode;
footer: React.ReactNode;
link?: string;
- type: 'pack' | 'trip' | 'item';
+ type: 'pack' | 'trip' | 'item' | 'packTemplate';
destination?: string;
data: {
owner_id?: string;
@@ -25,6 +27,7 @@ const HEADER_COMPONENTS = {
trip: TripCardHeader,
pack: PackCardHeader,
item: ItemCardHeader,
+ packTemplate: PackTemplateHeader,
};
export const CustomCard = ({
@@ -74,25 +77,27 @@ export const CustomCard = ({
- {type === 'pack' && authUser?.id === data.owner_id ? (
- <>
-
-
-
-
- >
- ) : null}
+
+ {type === 'pack' && authUser?.id === data.owner_id ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
{typeof title === 'string' ? {title} : title}
-
-
-
- {user?.id === ownerId
- ? 'Your Profile'
- : `View ${
- data.owners && data.owners?.length
- ? data.owners[0]?.name
- : 'Profile'
- }`}
-
-
-
- {user?.id !== ownerId && COPY_TYPES.includes(data.type) && (
- {
- setIsCopyPackModalOpen(true);
- }}
- style={{ backgroundColor: 'transparent' }}
- >
- Copy Pack
-
+ {data.type != 'packTemplate' && (
+
+
+
+
+ {user?.id === ownerId
+ ? 'Your Profile'
+ : `View ${
+ data.owners && data.owners?.length
+ ? data.owners[0]?.name
+ : 'Profile'
+ }`}
+
+
+
+ {user?.id !== ownerId && COPY_TYPES.includes(data.type) && (
+ {
+ setIsCopyPackModalOpen(true);
+ }}
+ style={{ backgroundColor: 'transparent' }}
+ >
+ Copy Pack
+
+ )}
+
)}
{actionsComponent}
{
}
actionsComponent={
user?.id === data.owner_id && (
-
- handleActionsOpenChange(value)}
- native={true}
- />
-
+
+
+ handleActionsOpenChange(value)}
+ native={true}
+ />
+
+
)
}
/>
diff --git a/packages/app/components/details/index.tsx b/packages/app/components/details/index.tsx
index ba71b78be..66bf351f8 100644
--- a/packages/app/components/details/index.tsx
+++ b/packages/app/components/details/index.tsx
@@ -46,6 +46,17 @@ export const DetailsComponent = ({
/>
>
);
+ case 'packTemplate':
+ return (
+
+ );
case 'trip':
// Add trip-specific logic here
return (
diff --git a/packages/app/components/footer/Footer.tsx b/packages/app/components/footer/Footer.tsx
index 80bdfe08b..2e2132391 100644
--- a/packages/app/components/footer/Footer.tsx
+++ b/packages/app/components/footer/Footer.tsx
@@ -1,32 +1,245 @@
-import { Text, View } from 'react-native';
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
import { theme } from '../../theme';
import useTheme from '../../hooks/useTheme';
+import { RImage, RLink, RSeparator, RText } from '@packrat/ui';
+import { useNavigate } from 'app/hooks/navigation';
+import { useMemo } from 'react';
+import { footorLinks } from 'app/constants/footorLinks';
+import { NewsLetter } from 'app/components/newsLetter';
+import useResponsive from 'app/hooks/useResponsive';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
export default function Footer() {
const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
useTheme();
+ const { xs, sm, md } = useResponsive();
+ const firstFootorLinksGroup = footorLinks.slice(0, 4);
+ const secondFootorLinksGroup = footorLinks.slice(4);
+ const styles = StyleSheet.create(loadStyles(currentTheme, xs, sm, md));
const year = new Date().getFullYear();
+ const navigate = useNavigate();
return (
-
-
- Copyright © {year}
-
+
+
+
+
+
+
+ {
+ navigate('/');
+ }}
+ />
+
+ PackRat
+
+
+
+
+ {firstFootorLinksGroup.map((item) => {
+ return (
+
+ {item.label}
+
+ );
+ })}
+
+
+ {secondFootorLinksGroup.map((item) => {
+ return (
+
+ {item.label}
+
+ );
+ })}
+
+
+
+
+
+
+ Facebook
+
+
+
+
+
+ Instagram
+
+
+
+
+
+ X
+
+
+
+
+
+ Github
+
+
+
+
+
+ © 2024 Bierman Collective. All rights reserved.
+
+
+
);
}
+
+const loadStyles = (currentTheme, xs, sm, md) => {
+ return StyleSheet.create({
+ mainContainer: {
+ width: '100%',
+ flexDirection: 'column',
+ paddingTop: 80,
+ paddingBottom: 40,
+ alignSelf: 'center',
+ alignItems: 'center',
+ justifyContent: 'space-evenly',
+ position: 'relative',
+ bottom: 0,
+ },
+ mainFirstContainer: {
+ width: '100%',
+ flexDirection: xs || sm || md ? 'column' : 'row',
+ flexWrap: 'wrap',
+ paddingTop: 80,
+ paddingBottom: 40,
+ alignSelf: 'center',
+ alignItems: 'center',
+ justifyContent: 'space-evenly',
+ position: 'relative',
+ bottom: 0,
+ gap: xs || sm || md ? 10 : 0,
+ },
+ firstMainContainer: {
+ width: '95%',
+ // justifyContent: 'center',
+ // alignItems: 'center',
+ },
+ firstContainer: {
+ width: '100%',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ gap: 4,
+ paddingVertical: 18,
+ },
+ logo: {
+ backgroundColor: currentTheme.colors.tertiaryBlue,
+ borderRadius: 10,
+ cursor: 'pointer',
+ },
+ navLinks: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 16,
+ margin: 10,
+ fontSize: 14,
+ color: currentTheme.colors.textPrimary,
+ paddingBottom: 40,
+ },
+ navItem: { color: currentTheme.colors.textPrimary },
+ credit: {
+ color: currentTheme.colors.textPrimary,
+ fontSize: 13,
+ textAlign: xs ? 'center' : 'left',
+ fontWeight: 'normal',
+ paddingTop: sm || md ? 10 : 'auto',
+ },
+ lastContainer: {
+ display: 'flex',
+ flexWrap: 'wrap-reverse',
+ flexDirection: sm ? 'column-reverse' : 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: sm || md ? 'auto' : '95%',
+ },
+ });
+};
diff --git a/packages/app/components/landing_page/FAQS.tsx b/packages/app/components/landing_page/FAQS.tsx
new file mode 100644
index 000000000..fdb1e16a4
--- /dev/null
+++ b/packages/app/components/landing_page/FAQS.tsx
@@ -0,0 +1,115 @@
+import { RImage, RText } from '@packrat/ui';
+import { View } from 'tamagui';
+import PakRat_FAQS from 'app/assets/PakRat_FAQS.png';
+import { StyleSheet } from 'react-native';
+import useTheme from 'app/hooks/useTheme';
+import useResponsive from 'app/hooks/useResponsive';
+import { FaqList } from 'app/constants/FAQS';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { useState } from 'react';
+
+export const FAQS = () => {
+ const { currentTheme } = useTheme();
+ const { xs, sm, md } = useResponsive();
+ const styles = StyleSheet.create(loadStyles(currentTheme, xs, sm, md));
+
+ const [visibleAnswers, setVisibleAnswers] = useState({});
+
+ const toggleAnswer = (index) => {
+ setVisibleAnswers((prev) => ({
+ ...prev,
+ [index]: !prev[index],
+ }));
+ };
+
+ return (
+
+
+ Frequently Asked Questions
+
+ {FaqList.map((faq, index) => {
+ return (
+
+ toggleAnswer(index)}
+ style={styles.faqQuestion}
+ >
+ {faq.question}
+
+
+ {/*
+ {faq.answer}
+ */}
+ {visibleAnswers[index] && (
+
+ {faq.answer}
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
+
+const loadStyles = (currentTheme, xs, sm, md) => StyleSheet.create({
+ faqMainContainer: {
+ flexDirection: xs || sm || md ? 'column' : 'row',
+ alignItems: 'center',
+ justifyContent: 'space-evenly',
+ gap: 10,
+ width: '100vw',
+ maxWidth: '100vw',
+ paddingTop: 20,
+ paddingBottom: 20,
+ },
+ faqFirstContainer: {
+ alignItems: 'center',
+ },
+ faqBox: {
+ width: xs || sm || md ? '100%' : '24vw',
+ },
+ faqMainTitle: {
+ fontSize: xs || sm || md ? 20 : 26,
+ textAlign : xs || sm || md ? 'center' : 'auto',
+ fontWeight: 'bold',
+ color: currentTheme.colors.textPrimary,
+ marginBottom: 30,
+ },
+ faqQuestion: {
+ borderBottomWidth: 1,
+ borderBottomColor: currentTheme.colors.textPrimary,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginTop: 4,
+ marginBottom: 4,
+ marginLeft: xs || sm || md ? 10 : 0,
+ marginRight: xs || sm || md ? 10 : 0,
+ gap: xs || sm || md ? 2 : 10,
+ },
+ faqAnswer: {
+ marginLeft: 0,
+ width: xs || sm || md ? '100%' : '24vw',
+ marginLeft: xs || sm || md ? 10 : 0,
+ marginRight: xs || sm || md ? 10 : 0,
+ },
+ faqSecondContainer: {},
+});
diff --git a/packages/app/components/landing_page/LandingPageAccordion.tsx b/packages/app/components/landing_page/LandingPageAccordion.tsx
index 3ee5ac1f6..89308a5c2 100644
--- a/packages/app/components/landing_page/LandingPageAccordion.tsx
+++ b/packages/app/components/landing_page/LandingPageAccordion.tsx
@@ -1,39 +1,114 @@
import { View } from 'react-native';
-import { RButton as OriginalRButton, RCard, RText } from '@packrat/ui';
-import { MaterialIcons } from '@expo/vector-icons';
+import { RButton as OriginalRButton, RCard, RImage, RText } from '@packrat/ui';
+import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
import useCustomStyles from 'app/hooks/useCustomStyles';
import useAccordionState from './useAccordionState';
import loadStyles from './landingpage.style';
+import { FAQ_ITEMS } from './constants';
+import useTheme from 'app/hooks/useTheme';
+import PackRatPreview from 'app/assets/PackRat Preview.jpg';
+import { useEffect, useState } from 'react';
+import useResponsive from 'app/hooks/useResponsive';
+import { Platform } from 'react-native';
const RButton: any = OriginalRButton;
export const LandingPageAccordion = ({ title, content, iconName }) => {
const styles = useCustomStyles(loadStyles);
+ const { currentTheme } = useTheme();
+ const [index, setIndex] = useState(0);
+ const [data, setData] = useState(FAQ_ITEMS[index]);
+ const { xxs, xs, xxl, sm, lg, md } = useResponsive();
const [expanded, toggleExpanded] = useAccordionState();
+ useEffect(() => {
+ setData(FAQ_ITEMS[index]);
+ }, [index]);
+
+ const panDown = async () => {
+ if (index < 5) {
+ setIndex(index + 1);
+ }
+ return false;
+ };
+ const panUp = async () => {
+ if (index >= 1) {
+ setIndex(index - 1);
+ }
+ return false;
+ };
+
+ const handleTextClick = (item) => {
+ setIndex(item.index);
+ };
return (
-
-
-
-
- {title}
-
-
-
-
-
- {expanded && (
-
- {content}
-
- )}
-
+
+ {
+ Platform.OS === 'web' ? (
+
+
+
+
+
+
+
+
+ {data.content}
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ {title}
+
+
+
+
+
+ {expanded && (
+
+ {content}
+
+ )}
+
+ )
+ }
+
);
};
diff --git a/packages/app/components/landing_page/Pricing.tsx b/packages/app/components/landing_page/Pricing.tsx
new file mode 100644
index 000000000..a083bd7b0
--- /dev/null
+++ b/packages/app/components/landing_page/Pricing.tsx
@@ -0,0 +1,117 @@
+import { RButton, RText } from '@packrat/ui';
+import useTheme from 'app/hooks/useTheme';
+import useResponsive from 'app/hooks/useResponsive';
+import { View } from 'tamagui';
+import { StyleSheet } from 'react-native';
+import { PricingData } from 'app/constants/PricingData';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+
+export const Pricing = () => {
+ const { currentTheme } = useTheme();
+ const { xs, sm, md } = useResponsive();
+ const styles = StyleSheet.create(loadStyles(currentTheme, xs, sm, md));
+
+ return (
+
+
+
+
+ Explore PackRat with Our Free Subscription Plan!
+
+
+ Get Started with PackRat—Absolutely Free!
+
+
+ We’re thrilled to offer you our Free Subscription Plan so you can
+ explore and manage your trips with PackRat at no cost! This plan is
+ designed to give you full access to a range of features, making your
+ trip planning easy and enjoyable.
+
+
+
+
+ Free Access
+
+
+ $0
+
+
+
+ Get Started For Free
+
+
+
+ {PricingData.freeVersion.map((index) => {
+ return (
+
+
+
+ {index}
+
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+const loadStyles = (currentTheme, xs, sm, md) => {
+ return StyleSheet.create({
+ mainContainer: {
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 50,
+ color: currentTheme.colors.background,
+ marginLeft: xs || sm || md ? 10 : 0,
+ marginRight: xs || sm || md ? 10 : 0,
+ },
+ firstContainer: {
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 20,
+
+ },
+ card: {
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 10,
+ backgroundColor: currentTheme.colors.textPrimary,
+ color: currentTheme.colors.background,
+ paddingTop: 50,
+ paddingBottom: 50,
+ paddingLeft: 25,
+ paddingRight: 25,
+ borderRadius: 20,
+ },
+ });
+};
diff --git a/packages/app/components/landing_page/constants.ts b/packages/app/components/landing_page/constants.ts
index 8250ac490..c61e9e7d3 100644
--- a/packages/app/components/landing_page/constants.ts
+++ b/packages/app/components/landing_page/constants.ts
@@ -1,38 +1,58 @@
+import PackRatPreview from 'app/assets/PackRat Preview.jpg';
+import PackRatPreviewLeft from 'app/assets/PackRat Preview_Left.jpg';
+import PackRatPreview_Right from 'app/assets/PackRat Preview_Right.jpg';
+
export const FAQ_ITEMS = [
{
+ index: 0,
title: 'Create and manage trips',
content:
'Easily create new trips and manage existing ones by adding details such as dates, locations, and activities.',
iconName: 'directions',
+ frameLink: PackRatPreview,
},
{
+ index: 1,
title: 'Map integration with route planning',
content:
'PackRat integrates with OpenStreetMap to provide users with accurate maps and directions to their destinations.',
iconName: 'map',
+ frameLink: PackRatPreviewLeft,
+
},
{
+ index: 2,
title: 'Activity suggestions',
content:
'The app suggests popular outdoor activities based on the location and season of the trip.',
iconName: 'landscape',
+ frameLink: PackRatPreview_Right,
+
},
{
+ index: 3,
title: 'Packing list',
content:
'Users can create and manage packing lists for their trips to ensure they have everything they need.',
iconName: 'backpack',
+ frameLink: PackRatPreview,
+
},
{
+ index: 4,
title: 'Weather forecast',
content:
'PackRat provides up-to-date weather forecasts for the trip location to help users prepare accordingly.',
iconName: 'wb-sunny',
+ frameLink: PackRatPreviewLeft,
},
{
+ index: 5,
title: 'Save your hikes and packs, and sync between devices',
content:
'User authentication ensures privacy and data security, while enabling you to save and sync your hikes and packs between devices.',
iconName: 'lock',
+ frameLink: PackRatPreview_Right,
+
},
];
diff --git a/packages/app/components/landing_page/index.tsx b/packages/app/components/landing_page/index.tsx
index 2941ff86c..b1c2abc24 100644
--- a/packages/app/components/landing_page/index.tsx
+++ b/packages/app/components/landing_page/index.tsx
@@ -1,6 +1,10 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { Platform, ScrollView, View, Text } from 'react-native';
+import PackRatPreview from 'app/assets/PackRat Preview.jpg';
+import PackRatPreviewLeft from 'app/assets/PackRat Preview_Left.jpg';
+import PackRatPreviewRight from 'app/assets/PackRat Preview_Right.jpg';
+import AppleLink from 'app/assets/applelink.svg';
import {
RButton as OriginalRButton,
RText,
@@ -8,7 +12,10 @@ import {
RH1,
RCard,
RH3,
+ YStack,
+ RImage,
} from '@packrat/ui';
+
import useTheme from '../../hooks/useTheme';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { RLink } from '@packrat/ui';
@@ -17,6 +24,10 @@ import { FAQ_ITEMS } from './constants';
import { LandingPageAccordion } from './LandingPageAccordion';
import loadStyles from './landingpage.style';
import { Redirect } from 'app/components/Redirect';
+import useResponsive from 'app/hooks/useResponsive';
+import Footer from 'app/components/footer/Footer';
+import { FAQS } from './FAQS';
+import { Pricing } from './Pricing';
const RButton: any = OriginalRButton;
const RStack: any = OriginalRStack;
@@ -24,99 +35,387 @@ const RStack: any = OriginalRStack;
const LandingPage = () => {
const { currentTheme } = useTheme();
const styles = useCustomStyles(loadStyles);
-
- const handleGetStarted = () => {
- return ;
- };
+ const { xs, sm, md, lg, xl, xxl } = useResponsive();
return (
-
-
- {Platform.OS === 'web' ? (
-
- PackRat
-
- ) : (
-
- PackRat
-
- )}
-
- The Ultimate Travel App
-
-
-
- {/* */}
- {/* */}
-
-
- PackRat is the ultimate adventure planner designed for those who
- love to explore the great outdoors. Plan and organize your trips
- with ease, whether it's a weekend camping trip, a day hike, or a
- cross-country road trip.
-
- {Platform.OS === 'web' && (
-
-
+
+
+
+
-
-
+
+
+ PackRat is the ultimate adventure planner designed for those who
+ love to explore the great outdoors. Plan and organize your trips
+ with ease, whether it's a weekend camping trip, a day hike, or a
+ cross-country road trip.
+
+
+ {Platform.OS === 'web' && (
+
+
+
+
+
+
+
+
+
+ Get it on the
+
+
+ App Store
+
+
+
+
+
+
+
+
+
+
+
+ Get it on
+
+
+ Google Play
+
+
+
+
+
+
+ )}
+
+
+
+
+
-
- Download on the App Store
+
+
+
+ Signup
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Discover how the PackRat app is designed to make your adventures
+ easier and more enjoyable.
+
+
+ With a suite of powerful features tailored specifically for
+ backpackers, you'll have everything you need right at your
+ fingertips. From seamless route planning to real-time weather
+ updates, PackRat ensures you're prepared for every step of your
+ journey. Dive into the key features below to see how we can help
+ you elevate your backpacking experience:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ {Platform.OS === 'web' ? (
+
+ PackRat
+
+ ) : (
+
+ PackRat
+
+ )}
+
+ The Ultimate Travel App
+
+
+
+ {/* */}
+ {/* */}
+
+
+ PackRat is the ultimate adventure planner designed for those who
+ love to explore the great outdoors. Plan and organize your trips
+ with ease, whether it's a weekend camping trip, a day hike, or a
+ cross-country road trip.
+
+ {Platform.OS === 'web' && (
+
+
+
+
+
+
+ Download on the App Store
+
+
+
+
+
+
+
+ Download on Google Play
+
+
+
+
{
}}
>
-
- Download on Google Play
-
+ Use on Web
-
-
-
- Use on Web
-
-
-
- )}
-
- {FAQ_ITEMS.map((item, index) => (
-
- ))}
-
-
-
-
-
- Get Started
-
-
+ )}
+
+ {FAQ_ITEMS.map((item, index) => (
+
+ ))}
+
+
+
+
+
+ Get Started
+
+
+
+
+ {/* */}
-
- {/* */}
-
-
+
+ )}
);
};
diff --git a/packages/app/components/landing_page/landingpage.style.tsx b/packages/app/components/landing_page/landingpage.style.tsx
index d2ce11de3..26cc0f87f 100644
--- a/packages/app/components/landing_page/landingpage.style.tsx
+++ b/packages/app/components/landing_page/landingpage.style.tsx
@@ -1,110 +1,348 @@
+import useResponsive from 'app/hooks/useResponsive';
import { Platform } from 'react-native';
const loadStyles = (theme) => {
const { currentTheme } = theme;
- return {
- mutualStyles: {
- backgroundColor: currentTheme.colors.background,
- flex: 1,
- flexDirection: 'column',
- height: '100%',
- },
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- width: '100%',
- backgroundColor: currentTheme.colors.background,
- padding: 20,
- },
- secondaryContentContainer: {
- flex: 1,
- width: '100%',
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: currentTheme.colors.background,
- },
- appBadges: {
- flexDirection: 'column',
- justifyContent: 'center',
- alignItems: 'center',
- marginVertical: 20,
- marginBottom: 20,
- },
- backgroundImage: {
- flex: 1,
- resizeMode: 'cover',
- justifyContent: 'center',
- },
- contentContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingHorizontal: 20,
- width: '100%',
- },
- introText: {
- fontSize: Platform.OS === 'web' ? 24 : 20,
- fontWeight: Platform.OS === 'web' ? 'bold' : 'normal',
- textAlign: 'center',
- color: currentTheme.colors.tertiaryBlue,
- marginBottom: 20, // Ensure spacing between text and next elements
- paddingHorizontal: 10, // Adjust text alignment on smaller screens
- },
- buttonContainer: {
- paddingHorizontal: 20,
- paddingBottom: 20,
- // width: '100%', // Ensure buttons are well-spaced and aligned
- display: 'flex',
- justifyContent: 'center', // Center buttons horizontally
- },
- getStartedButton: {
- backgroundColor: currentTheme.colors.tertiaryBlue,
- height: 50,
- paddingVertical: 12, // Increase padding for better touch area
- paddingHorizontal: 30,
- borderRadius: 8, // Rounded corners for modern look
- alignItems: 'center', // Ensure text is centered within button
- },
- footerText: {
- color: currentTheme.colors.white,
- fontSize: 18,
- fontWeight: 'bold',
- },
- card: {
- marginBottom: 10,
- width: '100%',
- backgroundColor: currentTheme.colors.border,
- },
- cardHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 20,
- paddingVertical: 10,
- textWrap: 'wrap',
- width: '100%',
- },
- transparentButton: {
- backgroundColor: 'transparent',
- height: 80,
- },
- icon: {
- fontSize: 40,
- color: currentTheme.colors.iconColor,
- marginRight: 10,
- },
- featureText: {
- fontSize: 22,
- color: currentTheme.colors.text,
- },
- cardContent: {
- paddingHorizontal: 20,
- paddingVertical: 10,
- fontSize: 16,
- color: currentTheme.colors.text,
- },
- };
+ const { xxs, xs, xxl, sm, lg, md } = useResponsive();
+ const isWeb = Platform.OS === 'web';
+
+ if (isWeb) {
+ return {
+ mutualStyles: {
+ // flex: 1,
+ flexDirection: 'column',
+ height: '100%',
+ },
+ container: {
+ flex: 1,
+ // backgroundColor: currentTheme.colors.background,
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ },
+ firstMainContainer: {
+ width: '100%',
+ textAlign: 'center',
+ flexDirection: xs || sm || md ? 'column' : 'row',
+ justifyContent: xs || sm || md ? 'center' : 'space-evenly',
+ marginTop: Platform.OS !== 'web' ? 25 : 20,
+ flex: 1,
+ paddingTop: 50,
+ paddingBottom: 50,
+ },
+ secondaryContentContainer: {
+ // flex: 1,
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ // backgroundColor: 'rgb(248, 248, 248)',
+ // background: 'hsla(0, 0%, 96%, 1)',
+ // filter:
+ // 'progid: DXImageTransform.Microsoft.gradient( startColorstr="#F6F6F6", endColorstr="#E1DAE6", GradientType=1 )',
+ },
+ featureImageContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ featureImage: {
+ zIndex: 2,
+ width: xs ? '100%' : 'auto',
+ mixBlendMode: 'multiply',
+ justifyContent: 'center',
+ alignItems: 'center',
+
+ // filter: "brightness(100) invert(0)",
+ // width: 259,
+ // height: 530,
+ },
+ featureLeftImage: {
+ backgroundColor: 'transparent',
+ width: xs || sm || md ? 162 : 215,
+ height: xs || sm || md ? 340 : 410,
+ position: 'absolute',
+ zIndex: 0,
+ right: 120,
+ },
+ featureCenterImage: {
+ backgroundColor: 'transparent',
+ width: xs || sm || md ? 220 : 259,
+ height: xs || sm || md ? 450 : 530,
+ zIndex: 1,
+ },
+ featureRightImage: {
+ backgroundColor: 'transparent',
+ width: xs || sm || md ? 162 : 215,
+ height: xs || sm || md ? 340 : 410,
+ position: 'absolute',
+ zIndex: 0,
+ left: 120,
+ },
+ overlay: {
+ border: '1px solid black',
+ width: '100%',
+ height: '100%',
+ // background:
+ // 'linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%)',
+ },
+ appBadges: {
+ width: xs || sm || md ? '100%' : 'auto',
+ flexDirection: xs || sm || md ? 'column' : 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginVertical: 20,
+ },
+ backgroundImage: {
+ flex: 1,
+ resizeMode: 'cover',
+ justifyContent: 'center',
+ },
+ contentContainer: {
+ justifyContent: 'start',
+ alignItems: 'center',
+ width: '100%',
+ },
+ introText: {
+ fontSize: xs || sm ? 18 : 20,
+ fontWeight: Platform.OS === 'web' ? 'normal' : 'normal',
+ textAlign: xs || sm || md ? 'center' : 'left',
+ marginTop: xs ? 0 : 30,
+ color: currentTheme.colors.textPrimary,
+ width: xs || sm || md ? '100vw' : '50vw',
+ marginBottom: 20,
+ },
+ buttonContainer: {
+ display: 'flex',
+ alignItems: xs || sm ? 'center' : 'start',
+ justifyContent: 'center', // Center buttons horizontally
+ },
+ getStartedButton: {
+ backgroundColor: 'transparent',
+ height: 50,
+ borderWidth: 1,
+ borderColor: '#315173',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ paddingVertical: 12, // Increase padding for better touch area
+ paddingHorizontal: 30,
+ borderRadius: 8, // Rounded corners for modern look
+ alignItems: 'center', // Ensure text is centered within button
+ },
+ footerText: {
+ color: currentTheme.colors.textPrimary,
+ fontSize: 18,
+ // fontWeight: 'bold',
+ },
+ landingPageAccordion: {
+ flexDirection: 'column',
+ flexWrap: 'wrap',
+ width: xs || sm ? 'auto' : '80%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ landingPageAccordionContainer: {
+ flexDirection: xs || sm || md ? 'row' : 'column',
+ flexWrap: 'no-wrap',
+ width: xs || sm || md ? '100%' : 'auto',
+ paddingTop: 100,
+ paddingBottom: 100,
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: 50,
+ },
+ landingPageAccordionFirstContainer: {
+ width: 'auto',
+ flexDirection: 'row',
+ alignItems: 'center',
+ textAlign: 'left',
+ justifyContent: 'space-evenly',
+ gap: 10,
+ },
+ landingPageAccordationSecondContainer: {
+ // width: '100%',
+ flexDirection: xs || sm || md ? 'column' : 'row',
+ alignItems: 'center',
+ justifyContent: 'space-evenly',
+ gap: 50,
+ transition: '0.3s ease-in-out'
+ },
+ panButton: {
+ backgroundColor: 'transparent',
+ borderWidth: 1,
+ borderRadius: 0,
+ borderColor: currentTheme.colors.textPrimary,
+ // width: '100%',
+ },
+ card: {
+ width: xs || sm || md ? '95%' : '30%',
+ margin: 8,
+ // backgroundColor: 'transparent',
+ flexDirection: 'row',
+ borderWidth: 1,
+ borderColor: currentTheme.colors.cardBorderPrimary,
+ borderOpacity: '1',
+ borderRadius: 16,
+ },
+ cardHeader: {
+ paddingHorizontal: 20,
+ paddingVertical: 10,
+ width: '100%',
+ },
+ icon: {
+ fontSize: 35,
+ width: '100%',
+ color: currentTheme.colors.icon,
+ },
+ featureText: {
+ fontSize: 16,
+ color: currentTheme.colors.textPrimary,
+ marginTop: 10,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ width: '100%',
+ },
+ cardContent: {
+ fontSize: xs || sm || md ? 20 : 25,
+ color: currentTheme.colors.textPrimary,
+ width: xs || sm ? '100%' : '30vw',
+ textAlign: xs || sm || md ? 'center' : 'left',
+ marginLeft: xs || sm || md ? 10 : 0,
+ marginRight: xs || sm || md ? 10 : 0,
+ fontWeight: 'normal',
+ // border: '1px solid black'
+ },
+ secondaryContainerIntroDiv: {
+ width: xs ? '100%' : '100%',
+ paddingTop: 100,
+ paddingBottom: 100,
+ justifyContent: 'center',
+ backgroundColor: currentTheme.colors.textPrimary,
+ alignItems: 'center',
+ },
+ secondaryContainerIntroText: {
+ fontSize: 28,
+ width: xs ? '95%' : '80%',
+ fontWeight: 'bold',
+ textAlign: 'center',
+ marginBottom: 40,
+ color: currentTheme.colors.background,
+ },
+ secondaryContainerDescriptionText: {
+ fontSize: 16,
+ width: xs ? '95%' : '70%',
+ fontWeight: 'normal',
+ textAlign: 'center',
+ marginBottom: 30,
+ color: currentTheme.colors.background,
+ },
+
+ };
+ }
+ else {
+ return {
+ mutualStyles: {
+ backgroundColor: currentTheme.colors.background,
+ flex: 1,
+ flexDirection: 'column',
+ height: '100%',
+ },
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ backgroundColor: currentTheme.colors.background,
+ padding: 20,
+ },
+ secondaryContentContainer: {
+ flex: 1,
+ width: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: currentTheme.colors.background,
+ },
+ appBadges: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginVertical: 20,
+ marginBottom: 20,
+ },
+ backgroundImage: {
+ flex: 1,
+ resizeMode: 'cover',
+ justifyContent: 'center',
+ },
+ contentContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ width: '100%',
+ },
+ introText: {
+ fontSize: Platform.OS === 'web' ? 24 : 20,
+ fontWeight: Platform.OS === 'web' ? 'bold' : 'normal',
+ textAlign: 'center',
+ color: currentTheme.colors.tertiaryBlue,
+ marginBottom: 20, // Ensure spacing between text and next elements
+ paddingHorizontal: 10, // Adjust text alignment on smaller screens
+ },
+ buttonContainer: {
+ paddingHorizontal: 20,
+ paddingBottom: 20,
+ // width: '100%', // Ensure buttons are well-spaced and aligned
+ display: 'flex',
+ justifyContent: 'center', // Center buttons horizontally
+ },
+ getStartedButton: {
+ backgroundColor: currentTheme.colors.tertiaryBlue,
+ height: 50,
+ paddingVertical: 12, // Increase padding for better touch area
+ paddingHorizontal: 30,
+ borderRadius: 8, // Rounded corners for modern look
+ alignItems: 'center', // Ensure text is centered within button
+ },
+ footerText: {
+ color: currentTheme.colors.white,
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ card: {
+ marginBottom: 10,
+ width: '100%',
+ backgroundColor: currentTheme.colors.border,
+ },
+ cardHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 20,
+ paddingVertical: 10,
+ textWrap: 'wrap',
+ width: '100%',
+ },
+ transparentButton: {
+ backgroundColor: 'transparent',
+ height: 80,
+ },
+ icon: {
+ fontSize: 40,
+ color: currentTheme.colors.iconColor,
+ marginRight: 10,
+ },
+ featureText: {
+ fontSize: 22,
+ color: currentTheme.colors.text,
+ },
+ cardContent: {
+ paddingHorizontal: 20,
+ paddingVertical: 10,
+ fontSize: 16,
+ color: currentTheme.colors.text,
+ },
+ };
+ }
};
export default loadStyles;
diff --git a/packages/app/components/navigation/Drawer.tsx b/packages/app/components/navigation/Drawer.tsx
index 696aad8eb..ce2a4a635 100644
--- a/packages/app/components/navigation/Drawer.tsx
+++ b/packages/app/components/navigation/Drawer.tsx
@@ -9,7 +9,7 @@ import { NavigationList } from './NavigationList';
const Popover: any = OriginalPopover;
export function Drawer() {
- const { currentTheme } = useTheme();
+ const { currentTheme, isDark, isLight } = useTheme();
const styles = useCustomStyles(loadStyles);
return (
@@ -19,7 +19,7 @@ export function Drawer() {
icon={}
bg="transparent"
outlineColor="transparent"
- color={currentTheme.colors.tertiaryBlue}
+ color={currentTheme.colors.textPrimary}
fontWeight="bold"
focusStyle={{
bg: 'transparent',
@@ -30,7 +30,6 @@ export function Drawer() {
bg: 'transparent',
}}
>
- Menu
@@ -84,7 +83,7 @@ const loadStyles = (theme) => {
modalOverlay: {
flex: 1,
flexDirection: 'row',
- backgroundColor: 'rgba(0,0,0,0.5)',
+ backgroundColor: currentTheme.colors.background,
},
fullScreenTouchable: {
flex: 1,
@@ -106,6 +105,7 @@ const loadStyles = (theme) => {
},
popover: {
alignItems: 'flex-start',
+ boxShadow: '0px 0px 30px 0px rgba(0,0,0,0.29)'
},
};
};
diff --git a/packages/app/components/navigation/NavigationItem.tsx b/packages/app/components/navigation/NavigationItem.tsx
index 7d27aea2c..04604e8be 100644
--- a/packages/app/components/navigation/NavigationItem.tsx
+++ b/packages/app/components/navigation/NavigationItem.tsx
@@ -29,7 +29,7 @@ export const NavigationItem = ({
onSelect,
}: NavigationItemProps) => {
const styles = useCustomStyles(loadStyles);
- const { currentTheme } = useTheme();
+ const { currentTheme, isDark, isLight } = useTheme();
const { IconComponent, isCurrentPage, handleItemPress } = useNavigationItem(
item,
onSelect,
@@ -42,11 +42,7 @@ export const NavigationItem = ({
)}
label={text}
@@ -61,7 +57,7 @@ export const NavigationItem = ({
};
const loadStyles = (theme) => {
- const { currentTheme } = theme;
+ const { currentTheme, isDark, isLight } = theme;
return {
menuBarItem: {
@@ -71,7 +67,7 @@ const loadStyles = (theme) => {
paddingHorizontal: 12,
},
menuBarItemText: {
- color: currentTheme.colors.tertiaryBlue,
+ color: isDark ? currentTheme.colors.text : '#315173',
fontSize: 15,
},
menuBarItemActive: {
diff --git a/packages/app/components/navigation/NavigationList.tsx b/packages/app/components/navigation/NavigationList.tsx
index 6f40bcd71..c3d1d30bb 100644
--- a/packages/app/components/navigation/NavigationList.tsx
+++ b/packages/app/components/navigation/NavigationList.tsx
@@ -15,7 +15,7 @@ export const NavigationList: React.FC = ({
onItemSelect = () => {},
}) => {
const isMobileView = useIsMobileView();
- const { currentTheme } = useTheme();
+ const { currentTheme, isLight, isDark } = useTheme();
const { navigationItems } = useNavigationList();
return (
<>
@@ -27,11 +27,11 @@ export const NavigationList: React.FC = ({
width: '100%',
borderRadius: 8,
marginBottom: isMobileView ? 6 : 0,
- backgroundColor: currentTheme.colors.background,
- color: currentTheme.colors.white,
+ backgroundColor: isDark ? currentTheme.colors.background : '#f0f2f5',
+ color: isDark ? currentTheme.colors.white : '#315173',
}}
hoverStyle={{
- bg: currentTheme.colors.border as any,
+ bg: isDark ? currentTheme.colors.secondaryBlue as any : 'rgb(249, 249, 249)',
}}
key={item.href + index}
>
diff --git a/packages/app/components/navigation/OfflineTabs.tsx b/packages/app/components/navigation/OfflineTabs.tsx
new file mode 100644
index 000000000..aad17cfbc
--- /dev/null
+++ b/packages/app/components/navigation/OfflineTabs.tsx
@@ -0,0 +1,85 @@
+import React, { useContext } from 'react';
+import { Tabs as ExpoTabs } from 'expo-router/tabs';
+import { DrawerToggleButton } from '@react-navigation/drawer';
+import { usePathname } from 'expo-router';
+import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
+import useTheme from 'app/hooks/useTheme';
+import { RIconButton } from '@packrat/ui';
+import ThemeContext from '../../context/theme';
+import { View } from 'react-native';
+import { Stack } from 'tamagui';
+import { Backpack } from '@tamagui/lucide-icons';
+
+export const OfflineTabs = () => {
+ const { isDark, enableDarkMode, enableLightMode } = useContext(ThemeContext);
+
+ const { currentTheme } = useTheme();
+ const iconName = isDark ? 'moon' : 'sun';
+ const iconColor = isDark ? 'white' : 'black';
+ const handlePress = () => {
+ if (isDark) {
+ enableLightMode();
+ } else {
+ enableDarkMode();
+ }
+ };
+
+ return (
+ <>
+ (
+
+ }
+ onPress={handlePress}
+ />
+
+
+ ),
+ headerTitleStyle: {
+ fontSize: 24,
+ },
+ headerStyle: {
+ backgroundColor: currentTheme.colors.background,
+ },
+ headerTintColor: currentTheme.colors.tertiaryBlue,
+ }}
+ >
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ >
+ );
+};
diff --git a/packages/app/components/newsLetter/index.tsx b/packages/app/components/newsLetter/index.tsx
new file mode 100644
index 000000000..34b9f6b82
--- /dev/null
+++ b/packages/app/components/newsLetter/index.tsx
@@ -0,0 +1,47 @@
+import { Form, FormInput, RText, SubmitButton } from '@packrat/ui';
+import useTheme from 'app/hooks/useTheme';
+import useResponsive from 'app/hooks/useResponsive';
+import { View } from 'tamagui';
+import { StyleSheet } from 'react-native';
+
+export const NewsLetter = () => {
+ const { currentTheme } = useTheme();
+ const {xs, sm, md} = useResponsive()
+ const styles = StyleSheet.create(loadStyles(currentTheme, xs, sm, md));
+ return (
+
+ );
+};
+
+const loadStyles = (currentTheme, xs, sm, md) => {
+ return StyleSheet.create({
+ container: {
+ width: xs ? '100%' : 'auto',
+ flexDirection: xs ? 'column' : 'row',
+ alignItems: xs ? '' : 'center',
+ justifyContent: 'center',
+ gap: 10,
+
+ },
+ submitButton: {
+ backgroundColor: '#232323',
+ color: 'white',
+ width: xs ? '100%' : 'auto',
+ },
+ });
+};
diff --git a/packages/app/components/settings/components/inputParts.tsx b/packages/app/components/settings/components/inputParts.tsx
new file mode 100644
index 000000000..7a800ec75
--- /dev/null
+++ b/packages/app/components/settings/components/inputParts.tsx
@@ -0,0 +1,356 @@
+import { getFontSized } from '@tamagui/get-font-sized';
+import { getSpace } from '@tamagui/get-token';
+import { User } from '@tamagui/lucide-icons';
+import type { SizeVariantSpreadFunction } from '@tamagui/web';
+import { useState } from 'react';
+import type { ColorTokens, FontSizeTokens } from 'tamagui';
+import {
+ Label,
+ Button as TButton,
+ Input as TInput,
+ Text,
+ View,
+ XGroup,
+ createStyledContext,
+ getFontSize,
+ getVariable,
+ isWeb,
+ styled,
+ useGetThemedIcon,
+ useTheme,
+ withStaticProperties,
+} from 'tamagui';
+
+const defaultContextValues = {
+ size: '$true',
+ scaleIcon: 1.2,
+ color: undefined,
+} as const;
+
+export const InputContext = createStyledContext<{
+ size: FontSizeTokens;
+ scaleIcon: number;
+ color?: ColorTokens | string;
+}>(defaultContextValues);
+
+export const defaultInputGroupStyles = {
+ size: '$true',
+ fontFamily: '$body',
+ borderWidth: 1,
+ outlineWidth: 0,
+ color: '$color',
+
+ ...(isWeb
+ ? {
+ tabIndex: 0,
+ }
+ : {
+ focusable: true,
+ }),
+
+ borderColor: '$borderColor',
+ backgroundColor: '$color2',
+
+ // this fixes a flex bug where it overflows container
+ minWidth: 0,
+
+ hoverStyle: {
+ borderColor: '$borderColorHover',
+ },
+
+ focusStyle: {
+ outlineColor: '$outlineColor',
+ outlineWidth: 2,
+ outlineStyle: 'solid',
+ borderColor: '$borderColorFocus',
+ },
+} as const;
+
+const InputGroupFrame = styled(XGroup, {
+ justifyContent: 'space-between',
+ context: InputContext,
+ variants: {
+ unstyled: {
+ false: defaultInputGroupStyles,
+ },
+ scaleIcon: {
+ ':number': {} as any,
+ },
+ applyFocusStyle: {
+ ':boolean': (val, { props }) => {
+ if (val) {
+ return props.focusStyle || defaultInputGroupStyles.focusStyle;
+ }
+ },
+ },
+ size: {
+ '...size': (val, { tokens }) => {
+ return {
+ borderRadius: tokens.radius[val],
+ };
+ },
+ },
+ } as const,
+ defaultVariants: {
+ unstyled: process.env.TAMAGUI_HEADLESS === '1' ? true : false,
+ },
+});
+
+const FocusContext = createStyledContext({
+ setFocused: (val: boolean) => {},
+ focused: false,
+});
+
+const InputGroupImpl = InputGroupFrame.styleable((props, forwardedRef) => {
+ const { children, ...rest } = props;
+ const [focused, setFocused] = useState(false);
+
+ return (
+
+
+ {children}
+
+
+ );
+});
+
+export const inputSizeVariant: SizeVariantSpreadFunction = (
+ val = '$true',
+ extras,
+) => {
+ const radiusToken =
+ extras.tokens.radius[val] ?? extras.tokens.radius['$true'];
+ const paddingHorizontal = getSpace(val, {
+ shift: -1,
+ bounds: [2],
+ });
+ const fontStyle = getFontSized(val as any, extras);
+ // lineHeight messes up input on native
+ if (!isWeb && fontStyle) {
+ delete fontStyle['lineHeight'];
+ }
+ return {
+ ...fontStyle,
+ height: val,
+ borderRadius: extras.props.circular ? 100_000 : radiusToken,
+ paddingHorizontal,
+ };
+};
+
+const InputFrame = styled(TInput, {
+ unstyled: true,
+ context: InputContext,
+});
+
+const InputImpl = InputFrame.styleable((props, ref) => {
+ const { setFocused } = FocusContext.useStyledContext();
+ const { size } = InputContext.useStyledContext();
+ const { ...rest } = props;
+ return (
+
+ {
+ setFocused(true);
+ }}
+ onBlur={() => setFocused(false)}
+ size={size}
+ {...rest}
+ />
+
+ );
+});
+
+const InputSection = styled(XGroup.Item, {
+ justifyContent: 'center',
+ alignItems: 'center',
+ overflow: 'hidden',
+ context: InputContext,
+});
+
+const Button = styled(TButton, {
+ context: InputContext,
+ justifyContent: 'center',
+ alignItems: 'center',
+
+ variants: {
+ size: {
+ '...size': (val = '$true', { tokens }) => {
+ if (typeof val === 'number') {
+ return {
+ paddingHorizontal: 0,
+ height: val,
+ borderRadius: val * 0.2,
+ };
+ }
+ return {
+ paddingHorizontal: 0,
+ height: val,
+ borderRadius: tokens.radius[val],
+ };
+ },
+ },
+ } as const,
+});
+
+// Icon starts
+
+export const InputIconFrame = styled(View, {
+ justifyContent: 'center',
+ alignItems: 'center',
+ context: InputContext,
+
+ variants: {
+ size: {
+ '...size': (val, { tokens }) => {
+ return {
+ paddingHorizontal: tokens.space[val],
+ };
+ },
+ },
+ } as const,
+});
+
+const getIconSize = (size: FontSizeTokens, scale: number) => {
+ return (
+ (typeof size === 'number'
+ ? size * 0.5
+ : getFontSize(size as FontSizeTokens)) * scale
+ );
+};
+
+const InputIcon = InputIconFrame.styleable<{
+ scaleIcon?: number;
+ color?: ColorTokens | string;
+}>((props, ref) => {
+ const { children, color: colorProp, ...rest } = props;
+ const inputContext = InputContext.useStyledContext();
+ const { size = '$true', color: contextColor, scaleIcon = 1 } = inputContext;
+
+ const theme = useTheme();
+ const color = getVariable(
+ contextColor ||
+ theme[contextColor as any]?.get('web') ||
+ theme.color10?.get('web'),
+ );
+ const iconSize = getIconSize(size as FontSizeTokens, scaleIcon);
+
+ const getThemedIcon = useGetThemedIcon({
+ size: iconSize,
+ color: color as any,
+ });
+ return (
+
+ {getThemedIcon(children)}
+
+ );
+});
+
+export const InputContainerFrame = styled(View, {
+ context: InputContext,
+ flexDirection: 'column',
+
+ variants: {
+ size: {
+ '...size': (val, { tokens }) => ({
+ gap: tokens.space[val].val * 0.3,
+ }),
+ },
+ color: {
+ '...color': () => ({}),
+ },
+ gapScale: {
+ ':number': {} as any,
+ },
+ } as const,
+
+ defaultVariants: {
+ size: '$4',
+ },
+});
+
+export const InputLabel = styled(Label, {
+ context: InputContext,
+ variants: {
+ size: {
+ '...fontSize': getFontSized as any,
+ },
+ } as const,
+});
+
+export const InputInfo = styled(Text, {
+ context: InputContext,
+ color: '$color10',
+
+ variants: {
+ size: {
+ '...fontSize': (val, { font }) => {
+ if (!font) return;
+ const fontSize = font.size[val].val * 0.8;
+ const lineHeight = font.lineHeight?.[val].val * 0.8;
+ const fontWeight = font.weight?.['$2'];
+ const letterSpacing = font.letterSpacing?.[val];
+ const textTransform = font.transform?.[val];
+ const fontStyle = font.style?.[val];
+ return {
+ fontSize,
+ lineHeight,
+ fontWeight,
+ letterSpacing,
+ textTransform,
+ fontStyle,
+ };
+ },
+ },
+ } as const,
+});
+
+const InputXGroup = styled(XGroup, {
+ context: InputContext,
+
+ variants: {
+ size: {
+ '...size': (val, { tokens }) => {
+ const radiusToken = tokens.radius[val] ?? tokens.radius['$true'];
+ return {
+ borderRadius: radiusToken,
+ };
+ },
+ },
+ } as const,
+});
+
+export const Input = withStaticProperties(InputContainerFrame, {
+ Box: InputGroupImpl,
+ Area: InputImpl,
+ Section: InputSection,
+ Button,
+ Icon: InputIcon,
+ Info: InputInfo,
+ Label: InputLabel,
+ XGroup: withStaticProperties(InputXGroup, { Item: XGroup.Item }),
+});
+
+export const InputNew = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app/components/settings/components/layoutParts.tsx b/packages/app/components/settings/components/layoutParts.tsx
new file mode 100644
index 000000000..1228c352a
--- /dev/null
+++ b/packages/app/components/settings/components/layoutParts.tsx
@@ -0,0 +1,47 @@
+import { View, styled } from 'tamagui';
+import { useMedia } from 'tamagui';
+import type { MediaQueryKey } from '@tamagui/web';
+
+export const FormCard = styled(View, {
+ tag: 'form',
+ flexDirection: 'row',
+ maxWidth: '100%',
+ borderRadius: 30,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '$6',
+ '$group-window-gtSm': {
+
+ shadowColor: '$shadowColor',
+ shadowOffset: {
+ width: 0,
+ height: 9,
+ },
+ shadowOpacity: 0.5,
+ shadowRadius: 12.35,
+ },
+ '$theme-dark': {
+ borderWidth: 1,
+ borderColor: '$borderColor',
+ },
+ '$group-window-xs': {
+ borderWidth: 0,
+ borderRadius: 0,
+ paddingHorizontal: '$1',
+ },
+});
+
+export const Hide = ({
+ children,
+ when = 'sm',
+}: {
+ children: React.ReactNode;
+ when: MediaQueryKey;
+}) => {
+ const hide = useMedia()[when];
+
+ if (hide) {
+ return null;
+ }
+ return children;
+};
diff --git a/packages/app/components/settings/index.tsx b/packages/app/components/settings/index.tsx
new file mode 100644
index 000000000..479c5e826
--- /dev/null
+++ b/packages/app/components/settings/index.tsx
@@ -0,0 +1,304 @@
+import {
+ AnimatePresence,
+ Button,
+ H1,
+ Label,
+ Spinner,
+ View,
+} from 'tamagui';
+import { Input } from './components/inputParts';
+import { useState } from 'react';
+import { Info } from '@tamagui/lucide-icons';
+import { FormCard } from './components/layoutParts';
+import { useForm, Controller } from 'react-hook-form';
+import {
+ Form,
+ FormInput,
+ FormSelect,
+ ImageUpload,
+ RH5,
+ RLabel,
+ RStack,
+ SubmitButton,
+} from '@packrat/ui';
+import Avatar from '../Avatar/Avatar';
+import { useProfileSettings } from 'app/modules/user/hooks';
+import {
+ deleteUserForm,
+ passwordChangeSchema,
+ userSettingsSchema,
+} from '@packrat/validations';
+import { useDeleteProfile } from 'app/modules/user/hooks';
+import useResponsive from 'app/hooks/useResponsive';
+
+export function SettingsForm() {
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const { user, handleEditUser, handleUpdatePassword } = useProfileSettings();
+ const { deleteProfile, isLoading } = useDeleteProfile();
+ const {xs, sm, md } = useResponsive();
+
+ const {
+ control,
+ formState: { errors },
+ } = useForm({
+ defaultValues: {
+ name: user?.name,
+ username: user?.username,
+ email: user?.email,
+ preferredWeather: user?.preferredWeather,
+ preferredWeight: user?.preferredWeight,
+ profilepicture: user?.profileImage,
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: '',
+ confirmText: '',
+ },
+ });
+
+ const weatherOptions = ['celsius', 'fahrenheit'].map((key) => ({
+ label: key,
+ value: key,
+ }));
+
+ const weightOptions = ['lb', 'oz', 'kg', 'g'].map((key) => ({
+ label: key,
+ value: key,
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+SettingsForm.fileName = 'SettingsForm';
diff --git a/packages/app/constants/FAQS.ts b/packages/app/constants/FAQS.ts
new file mode 100644
index 000000000..b25279d4e
--- /dev/null
+++ b/packages/app/constants/FAQS.ts
@@ -0,0 +1,38 @@
+export const FaqList = [
+ {
+ question: '1. What is PackRat?',
+ answer: 'PackRat is the ultimate adventure planner designed for outdoor enthusiasts. Whether you’re planning a weekend camping trip, a day hike, or a cross-country road trip, PackRat helps you organize and manage your adventures with ease.',
+ },
+ {
+ question: '2. How do I create an account?',
+ answer: 'To create an account, select “Sign Up” on the login screen. You can register using your email address and follow the prompts to complete the registration process.',
+ },
+ {
+ question: '3. Is PackRat free to use?',
+ answer: 'PackRat offers a free version with basic features. For access to advanced features such as offline maps, premium trail guides, and enhanced weather forecasts, you can upgrade to our premium subscription.',
+ },
+ {
+ question: '4. How do I create and manage my packing list?',
+ answer: 'To create a packing list, go to the “Menu” section and select “Packs.” Add items you need for your trip, and you can also browse the “Feed” to view packs created by other users. You can copy items from these packs and customize them as needed. You can edit, add, or remove items from your list at any time.',
+ },
+ {
+ question: '5. What should I do if I encounter issues with the app?',
+ answer: 'If you encounter any issues, please contact our support team via the “Contact Support” option in the page footer menu. Provide details about the problem, and we will assist you promptly.',
+ },
+ {
+ question: '6. Is my personal information secure?',
+ answer: 'Yes, we prioritize the security of your personal information. We use advanced encryption and follow strict privacy protocols to protect your data. For more details, please review our Privacy Policy.',
+ },
+ {
+ question: '7. How can I delete my account or data?',
+ answer: 'To delete your account or data, go to the “Settings” menu, select “Profile,” and choose “Profile Settings.” Click on “Delete Account” and follow the instructions to permanently remove your account and associated data. Please note that this action is irreversible.',
+ },
+ {
+ question: '8. How do I stay informed about app updates and new features?',
+ answer: `To stay informed about app updates and new features, follow us on social media, subscribe to our newsletter, or check the “What's New” section in the App Store and Google Play Store where PackRat is available.`,
+ },
+ {
+ question: '9. How can I provide feedback or suggest new features?',
+ answer: `We value your feedback! To share your thoughts or suggest new features, go to the App Store or Google Play Store and select “Write a Review.” Your input helps us enhance the app and better meet your needs.`,
+ },
+]
diff --git a/packages/app/constants/PricingData.ts b/packages/app/constants/PricingData.ts
new file mode 100644
index 000000000..801a9ee52
--- /dev/null
+++ b/packages/app/constants/PricingData.ts
@@ -0,0 +1,9 @@
+export const PricingData = {
+ freeVersion: [
+ 'Track trips with dates and locations.',
+ 'Access accurate routes via OpenStreetMap.',
+ 'Get suggestions for local outdoor activities.',
+ 'Organize and manage your packing lists.',
+ 'Check current forecasts to plan your trip effectively.',
+ ]
+};
\ No newline at end of file
diff --git a/packages/app/constants/api.ts b/packages/app/constants/api.ts
index e7cc9ced6..e53ab0aca 100644
--- a/packages/app/constants/api.ts
+++ b/packages/app/constants/api.ts
@@ -6,4 +6,7 @@ import { API_URL } from '@packrat/config';
* format: "{scheme}://{serverhost}:{port}/api"
* e.g: "http://localhost:4200/api"
*/
+
+// use this for android emulator
+// export const api = 'http://10.0.2.2:8787/api';
export const api = API_URL;
diff --git a/packages/app/constants/footorLinks.ts b/packages/app/constants/footorLinks.ts
new file mode 100644
index 000000000..c42d11a20
--- /dev/null
+++ b/packages/app/constants/footorLinks.ts
@@ -0,0 +1,26 @@
+export const footorLinks = [
+ {
+ label: 'Home',
+ link: '/',
+ },
+ {
+ label: 'Features',
+ link: '',
+ },
+ {
+ label: 'About the App',
+ link: '/about',
+ },
+ {
+ label: 'FAQs',
+ link: 'https://github.com/andrew-bierman/PackRat',
+ },
+ {
+ label: 'Contact Us ',
+ link: '',
+ },
+ {
+ label: 'Privacy Policy ',
+ link: '/privacy',
+ },
+]
diff --git a/packages/app/hooks/common/useDeepCompareEffect.ts b/packages/app/hooks/common/useDeepCompareEffect.ts
new file mode 100644
index 000000000..08f28e4c7
--- /dev/null
+++ b/packages/app/hooks/common/useDeepCompareEffect.ts
@@ -0,0 +1,19 @@
+import { useEffect, useRef } from 'react';
+import isEqual from 'lodash/isEqual';
+
+export function useDeepCompareEffect(effect, deps) {
+ const ref = useRef(deps);
+
+ useEffect(() => {
+ console.log({
+ prevDeps: ref.current,
+ deps,
+ isEqual: isEqual(ref.current, deps),
+ });
+
+ if (!isEqual(ref.current, deps)) {
+ ref.current = deps;
+ effect();
+ }
+ }, deps);
+}
diff --git a/packages/app/hooks/navigation/useNavigationList.ts b/packages/app/hooks/navigation/useNavigationList.ts
index f14749eba..8424495dc 100644
--- a/packages/app/hooks/navigation/useNavigationList.ts
+++ b/packages/app/hooks/navigation/useNavigationList.ts
@@ -30,6 +30,84 @@ type NavigationItem =
export const useNavigationList = () => {
const user = useAuthUser();
+ const logedInMenuItems: NavigationItem[] = [
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/feed',
+ icon: 'newspaper-variant',
+ text: 'Feed',
+ iconSource: MaterialCommunityIcons,
+ },
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/trips',
+ icon: 'routes',
+ text: 'Trips',
+ iconSource: MaterialCommunityIcons,
+ },
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/packs',
+ icon: 'backpack',
+ text: 'Packs',
+ iconSource: MaterialIcons,
+ },
+ ...((Platform.OS != 'web'
+ ? [
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/maps',
+ icon: 'map',
+ text: 'Downloaded Maps',
+ iconSource: Entypo,
+ },
+ ]
+ : []) as NavigationItem[]),
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/products',
+ icon: 'tent',
+ text: 'Products',
+ iconSource: Fontisto,
+ },
+ ...((user?.role === 'admin'
+ ? [
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/items',
+ icon: 'map',
+ text: 'items',
+ iconSource: Fontisto,
+ },
+ ]
+ : []) as NavigationItem[]),
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/profile',
+ icon: 'book',
+ text: 'Profile',
+ iconSource: FontAwesome,
+ },
+ {
+ type: NavigationItemTypeEnum.DIVIDER,
+ Component: Separator,
+ },
+ // DISABLE SCREEN TEMP
+ // {
+ // type: NavigationItemTypeEnum.LINK,
+ // href: '/appearance',
+ // icon: 'theme-light-dark',
+ // text: 'Appearance',
+ // iconSource: MaterialCommunityIcons,
+ // },
+ {
+ type: NavigationItemTypeEnum.LINK,
+ href: '/logout',
+ icon: 'logout',
+ text: 'Logout',
+ iconSource: MaterialIcons,
+ },
+ ];
const navigationItems = useMemo(() => {
const additionalMenuItems = user ? logedInMenuItems : loggeOutMenuItems;
@@ -76,71 +154,3 @@ const loggeOutMenuItems: NavigationItem[] = [
iconSource: MaterialIcons,
},
];
-
-const logedInMenuItems: NavigationItem[] = [
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/feed',
- icon: 'newspaper-variant',
- text: 'Feed',
- iconSource: MaterialCommunityIcons,
- },
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/trips',
- icon: 'routes',
- text: 'Trips',
- iconSource: MaterialCommunityIcons,
- },
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/packs',
- icon: 'backpack',
- text: 'Packs',
- iconSource: MaterialIcons,
- },
- // ...((Platform.OS != 'web'
- // ? [
- // {
- // type: NavigationItemTypeEnum.LINK,
- // href: '/maps',
- // icon: 'map',
- // text: 'Downloaded Maps',
- // iconSource: Entypo,
- // },
- // ]
- // : []) as NavigationItem[]),
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/items',
- icon: 'tent',
- text: 'Items',
- iconSource: Fontisto,
- },
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/profile',
- icon: 'book',
- text: 'Profile',
- iconSource: FontAwesome,
- },
- {
- type: NavigationItemTypeEnum.DIVIDER,
- Component: Separator,
- },
- // DISABLE SCREEN TEMP
- // {
- // type: NavigationItemTypeEnum.LINK,
- // href: '/appearance',
- // icon: 'theme-light-dark',
- // text: 'Appearance',
- // iconSource: MaterialCommunityIcons,
- // },
- {
- type: NavigationItemTypeEnum.LINK,
- href: '/logout',
- icon: 'logout',
- text: 'Logout',
- iconSource: MaterialIcons,
- },
-];
diff --git a/packages/app/hooks/offline/useNetworkStatusProvider.ts b/packages/app/hooks/offline/useNetworkStatusProvider.ts
index 7317b073f..f09b205dc 100644
--- a/packages/app/hooks/offline/useNetworkStatusProvider.ts
+++ b/packages/app/hooks/offline/useNetworkStatusProvider.ts
@@ -1,16 +1,20 @@
-import NetInfo from '@react-native-community/netinfo';
+import NetInfo, { type NetInfoState } from '@react-native-community/netinfo';
import { useOfflineStore } from '../../atoms';
import { useEffect } from 'react';
+import { onlineManager } from '@tanstack/react-query';
export const useNetworkStatusProvider = () => {
- const { setIsConnected } = useOfflineStore();
+ const { setConnectionStatus } = useOfflineStore();
useEffect(() => {
- NetInfo.addEventListener((state) => {
- // Check if state.isConnected is not null before using it
+ const setNetworkStatus = (state: NetInfoState) => {
if (state.isConnected !== null) {
- setIsConnected(state.isConnected);
+ const connectionStatus = state.isConnected ? 'connected' : 'offline';
+ onlineManager.setOnline(connectionStatus === 'connected');
+ setConnectionStatus(connectionStatus);
}
- });
+ };
+ NetInfo.fetch().then(setNetworkStatus);
+ NetInfo.addEventListener(setNetworkStatus);
}, []);
};
diff --git a/packages/app/hooks/useFlatList.ts b/packages/app/hooks/useFlatList.ts
index 4e3fbe104..4b8fe3c9e 100644
--- a/packages/app/hooks/useFlatList.ts
+++ b/packages/app/hooks/useFlatList.ts
@@ -9,7 +9,6 @@ export const useFlatList = (
const renderItem = ({ item }) => {
const sectionKey = item[1];
- console.log({ sectionKey });
return sectionComponents[sectionKey];
};
diff --git a/packages/app/modules/auth/components/AuthWrapper.tsx b/packages/app/modules/auth/components/AuthWrapper.tsx
index c7a20e7ca..9cb5c805f 100644
--- a/packages/app/modules/auth/components/AuthWrapper.tsx
+++ b/packages/app/modules/auth/components/AuthWrapper.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import { AuthLoader } from './AuthLoader';
import { Redirect } from 'app/components/Redirect';
+import { Redirect as ExpoRedirect } from 'expo-router';
import { RSpinner, RText } from '@packrat/ui';
import { Platform, View } from 'react-native';
-import LandingPage from 'app/components/landing_page';
import useTheme from 'app/hooks/useTheme';
interface AuthWrapperProps {
@@ -27,7 +27,11 @@ export const AuthWrapper = ({
);
const defaultUnauthorizedElement =
- Platform.OS === 'web' ? : ;
+ Platform.OS === 'web' ? (
+
+ ) : (
+
+ );
return (
{
const [[isLoading, token]] = useStorage('token');
@@ -26,13 +29,20 @@ export const useUserQuery = () => {
retry: false,
});
+ useDeepCompareEffect(() => {
+ if (data) {
+ Storage.setItem('user', JSON.stringify(data));
+ }
+ }, [data]);
+
return { user: data, isLoading, refetch };
};
export const useAuthUser = () => {
const { user } = useUserLoader();
+ const { userFromStorage } = useUserInOfflineMode();
- return user;
+ return user || userFromStorage || {};
};
export const useUserLoader = () => {
@@ -43,3 +53,21 @@ export const useUserLoader = () => {
return { user, isLoading };
};
+
+export const useUserInOfflineMode = () => {
+ const [[isLoading, userFromStorageStr]] = useStorage('user');
+ const { connectionStatus } = useOfflineStore();
+ const userFromStorage = useMemo(() => {
+ try {
+ const parsedUserFromStorage = JSON.parse(userFromStorageStr as String);
+ return parsedUserFromStorage;
+ } catch {
+ return null;
+ }
+ }, [userFromStorageStr]);
+
+ return {
+ userFromStorage: connectionStatus === 'offline' ? userFromStorage : null,
+ isLoading,
+ };
+};
diff --git a/packages/app/modules/auth/screens/LoginScreen.tsx b/packages/app/modules/auth/screens/LoginScreen.tsx
index 9abeec2af..e7a23071a 100644
--- a/packages/app/modules/auth/screens/LoginScreen.tsx
+++ b/packages/app/modules/auth/screens/LoginScreen.tsx
@@ -2,8 +2,6 @@ import React, { useState } from 'react';
import useTheme from '../../../hooks/useTheme';
import { useGoogleAuth, useLogin } from 'app/modules/auth';
import { SignInScreen } from '@packrat/ui/src/Bento/forms/layouts';
-import { View } from 'react-native';
-import { ScrollView } from 'react-native';
const demoUser = {
email: 'zoot3@email.com',
@@ -33,33 +31,11 @@ export function LoginScreen() {
const { currentTheme } = useTheme();
return (
-
-
-
-
-
-
-
+
);
}
diff --git a/packages/app/modules/dashboard/screens/DashboardScreen/DashboardScreen.tsx b/packages/app/modules/dashboard/screens/DashboardScreen/DashboardScreen.tsx
index a3a925752..7142e1111 100644
--- a/packages/app/modules/dashboard/screens/DashboardScreen/DashboardScreen.tsx
+++ b/packages/app/modules/dashboard/screens/DashboardScreen/DashboardScreen.tsx
@@ -8,9 +8,12 @@ import { SCREEN_WIDTH } from 'app/constants/breakpoint';
import { useScreenWidth } from 'app/hooks/common';
import FAB from 'app/components/Fab/Fab';
import { FeedPreview } from 'app/modules/feed';
+import { Button, Stack } from 'tamagui';
+import { useRouter } from '@packrat/crosspath';
export const DashboardScreen = () => {
const styles = useCustomStyles(loadStyles);
+ const router = useRouter();
return (
@@ -22,7 +25,25 @@ export const DashboardScreen = () => {
]}
>
- {Platform.OS === 'web' ? : null}
+ {Platform.OS === 'web' ? (
+
+
+
+
+ ) : null}
diff --git a/packages/app/modules/feed/components/FeedCard/FeedCard.tsx b/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
index 7c2dcaea1..b0fbc9976 100644
--- a/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
+++ b/packages/app/modules/feed/components/FeedCard/FeedCard.tsx
@@ -1,20 +1,31 @@
import React, { type FC } from 'react';
import { type FeedItem, type FeedResource } from 'app/modules/feed/model';
-import { feedItemPackCardConverter, feedItemTripCardConverter } from './utils';
+import {
+ feedItemPackCardConverter,
+ feedItemTripCardConverter,
+ feedItemPackTemplateCardConverter,
+ feedItemCardConverter,
+} from './utils';
import { PackCard } from 'app/modules/pack';
import { type CardType } from '@packrat/ui';
import { useAddFavorite } from 'app/modules/feed';
import { useAuthUser } from 'app/modules/auth';
import { TripCard } from 'app/modules/trip';
+import { PackTemplateCard } from 'app/modules/pack-templates';
+import { ItemCard } from 'app/modules/item';
const convertersByType = {
pack: feedItemPackCardConverter,
trip: feedItemTripCardConverter,
+ item: feedItemCardConverter,
+ packTemplate: feedItemPackTemplateCardConverter,
};
-const cardComponentsByType = {
+const cardComponentsByType: Record> = {
pack: PackCard,
trip: TripCard,
+ item: ItemCard,
+ packTemplate: PackTemplateCard,
};
interface FeedCardProps {
diff --git a/packages/app/modules/feed/components/FeedCard/utils.ts b/packages/app/modules/feed/components/FeedCard/utils.ts
index ca2e06010..bf6dcafbb 100644
--- a/packages/app/modules/feed/components/FeedCard/utils.ts
+++ b/packages/app/modules/feed/components/FeedCard/utils.ts
@@ -4,6 +4,7 @@ import { type PackDetails } from 'app/modules/pack';
import { truncateString } from 'app/utils/truncateString';
import { type TripDetails } from 'modules/trip/model';
import { roundNumber } from 'app/utils';
+import { type RouterOutput } from 'app/trpc';
type Converter = (
input: Input,
@@ -74,3 +75,17 @@ export const feedItemTripCardConverter: Converter<
favoriteCount: input.favorites_count,
};
};
+
+export const feedItemCardConverter: Converter<
+ FeedItem,
+ RouterOutput['getPackTemplates'][0]
+> = (input) => {
+ return { item: input };
+};
+
+export const feedItemPackTemplateCardConverter: Converter<
+ FeedItem,
+ RouterOutput['getPackTemplates'][0]
+> = (input) => {
+ return input;
+};
diff --git a/packages/app/modules/feed/components/FeedList.tsx b/packages/app/modules/feed/components/FeedList.tsx
new file mode 100644
index 000000000..927185520
--- /dev/null
+++ b/packages/app/modules/feed/components/FeedList.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import {
+ FlatList,
+ View,
+ ActivityIndicator,
+ RefreshControl,
+ type ViewProps,
+} from 'react-native';
+import { RText } from '@packrat/ui';
+import useResponsive from 'app/hooks/useResponsive';
+import useTheme from 'app/hooks/useTheme';
+
+interface FeedListProps {
+ data: any;
+ CardComponent: React.ComponentType<{ item: any }>;
+ refreshing?: boolean;
+ onRefresh?: () => void;
+ isLoading?: boolean;
+ errorMessage?: string;
+ separatorHeight?: number;
+ footerComponent?: JSX.Element;
+ keyExtractor?: (any, number) => string;
+ style?: ViewProps['style'];
+}
+
+export const FeedList = ({
+ data,
+ CardComponent,
+ refreshing = false,
+ onRefresh,
+ isLoading = false,
+ errorMessage = 'No Data Available',
+ footerComponent,
+ style,
+ keyExtractor,
+}: FeedListProps) => {
+ const { xs, xxs, sm, md, lg } = useResponsive();
+ const { currentTheme } = useTheme();
+
+ const getNumColumns = () => {
+ if (xxs || xs) return 1;
+ if (sm) return 1;
+ if (md) return 2;
+ if (lg) return 3;
+ return 4;
+ };
+
+ const numColumns = getNumColumns();
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ index.toString())}
+ renderItem={({ item }) => (
+
+
+
+ )}
+ ListFooterComponent={
+ footerComponent ||
+ }
+ ListEmptyComponent={() => {errorMessage}}
+ refreshControl={
+ onRefresh ? (
+
+ ) : undefined
+ }
+ showsVerticalScrollIndicator={false}
+ />
+ )}
+
+ );
+};
diff --git a/packages/app/modules/feed/components/FeedSearchFilter.tsx b/packages/app/modules/feed/components/FeedSearchFilter.tsx
index 881412e19..5abb7a5eb 100644
--- a/packages/app/modules/feed/components/FeedSearchFilter.tsx
+++ b/packages/app/modules/feed/components/FeedSearchFilter.tsx
@@ -49,6 +49,7 @@ export const FeedSearchFilter = ({
const [searchValue, setSearchValue] = useState();
const debounceTimerRef = useRef(null);
const sortOptions = useFeedSortOptions(
+ feedType,
selectedTypes?.trip || feedType === 'userTrips',
);
@@ -142,6 +143,15 @@ export const FeedSearchFilter = ({
)}
+ {feedType === 'packTemplates' && (
+
+ Discover our curated pack templates to help you get started.
+
+ )}
{
width: '100%',
borderRadius: 10,
marginTop: 20,
+ marginBottom: 8,
},
searchContainer: {
flexDirection: 'row',
diff --git a/packages/app/modules/feed/components/index.ts b/packages/app/modules/feed/components/index.ts
index 3c0d35173..8f7b956bf 100644
--- a/packages/app/modules/feed/components/index.ts
+++ b/packages/app/modules/feed/components/index.ts
@@ -3,3 +3,4 @@ export { FeedSearchFilter } from './FeedSearchFilter';
export { SearchProvider, SearchContext } from './SearchProvider';
export { FavoriteButton } from './FavoriteButton';
export { CreatedAtLabel } from './CreatedAtLabel';
+export { FeedList } from './FeedList';
diff --git a/packages/app/modules/feed/hooks/useFeed.ts b/packages/app/modules/feed/hooks/useFeed.ts
index 1451d7bac..a08cfa9b1 100644
--- a/packages/app/modules/feed/hooks/useFeed.ts
+++ b/packages/app/modules/feed/hooks/useFeed.ts
@@ -2,6 +2,7 @@ import { usePublicFeed } from './usePublicFeed';
import { useUserPacks, useSimilarPacks } from 'app/modules/pack';
import { useUserTrips } from 'app/modules/trip';
import { useSimilarItems } from 'app/modules/item';
+import { usePackTemplates } from 'app/modules/pack-templates';
import { type FeedType } from '../model';
import { type PaginationReturn } from 'app/hooks/pagination';
@@ -46,6 +47,10 @@ export const useFeed = ({
);
const similarPacks = useSimilarPacks(id, feedType === 'similarPacks');
const similarItems = useSimilarItems(id, feedType === 'similarItems');
+ const packTemplates = usePackTemplates(
+ { searchQuery, orderBy: queryString },
+ feedType === 'packTemplates',
+ );
switch (feedType) {
case 'public':
@@ -58,6 +63,8 @@ export const useFeed = ({
return similarPacks;
case 'similarItems':
return similarItems;
+ case 'packTemplates':
+ return packTemplates;
default:
return { data: null, isLoading: true };
}
diff --git a/packages/app/modules/feed/hooks/useFeedSortOptions.ts b/packages/app/modules/feed/hooks/useFeedSortOptions.ts
index d6f14b6db..92f71761f 100644
--- a/packages/app/modules/feed/hooks/useFeedSortOptions.ts
+++ b/packages/app/modules/feed/hooks/useFeedSortOptions.ts
@@ -17,11 +17,16 @@ const packSortOptions = [
SORT_OPTIONS.OLDEST,
];
+const productsSortOptions = [SORT_OPTIONS.MOST_RECENT, SORT_OPTIONS.OLDEST];
+
const commonOptions = [SORT_OPTIONS.MOST_RECENT, SORT_OPTIONS.OLDEST];
-export const useFeedSortOptions = (isTripsEnabled = false) => {
- return useMemo(
- () => (isTripsEnabled ? commonOptions : packSortOptions),
- [isTripsEnabled],
- );
+const packTemplateSortOptions = [SORT_OPTIONS.LIGHTEST, SORT_OPTIONS.HEAVIEST];
+
+export const useFeedSortOptions = (feedType, isTripsEnabled = false) => {
+ return useMemo(() => {
+ if (feedType === 'packTemplates') return packTemplateSortOptions;
+ if (feedType === 'products') return productsSortOptions;
+ return isTripsEnabled ? commonOptions : packSortOptions;
+ }, [isTripsEnabled]);
};
diff --git a/packages/app/modules/feed/model.ts b/packages/app/modules/feed/model.ts
index 7c22cef99..4dbef57a8 100644
--- a/packages/app/modules/feed/model.ts
+++ b/packages/app/modules/feed/model.ts
@@ -5,8 +5,9 @@ export type FeedType =
| 'userPacks'
| 'userTrips'
| 'similarPacks'
- | 'similarItems';
-export type FeedResource = 'pack' | 'trip';
+ | 'similarItems'
+ | 'packTemplates';
+export type FeedResource = 'pack' | 'item' | 'trip' | 'packTemplate';
export interface BaseFeedItem {
id: string;
@@ -24,6 +25,7 @@ export interface BaseFeedItem {
owner_id: string | { id: string };
createdAt: string;
owners: Array<{ any: any }>;
+ ga;
}
interface PackFeedItem extends BaseFeedItem {
diff --git a/packages/app/modules/feed/screens/FeedScreen.tsx b/packages/app/modules/feed/screens/FeedScreen.tsx
index 5f34ec767..a07fb1df0 100644
--- a/packages/app/modules/feed/screens/FeedScreen.tsx
+++ b/packages/app/modules/feed/screens/FeedScreen.tsx
@@ -1,14 +1,20 @@
import React, { useMemo, useState, useEffect, memo } from 'react';
import { FlatList, View, Platform, ActivityIndicator } from 'react-native';
-import { FeedCard, FeedSearchFilter, SearchProvider } from '../components';
+import {
+ FeedCard,
+ FeedList,
+ FeedSearchFilter,
+ SearchProvider,
+} from '../components';
import { useRouter } from 'app/hooks/router';
import { fuseSearch } from 'app/utils/fuseSearch';
import useCustomStyles from 'app/hooks/useCustomStyles';
import { useFeed } from 'app/modules/feed';
-import { RefreshControl } from 'react-native';
import { Pagination, RButton, RText } from '@packrat/ui';
import { useAuthUser } from 'app/modules/auth';
import { type FeedType } from '../model';
+import { ConnectionGate } from 'app/components/ConnectionGate';
+import { type ViewProps } from 'tamagui';
const URL_PATHS = {
userPacks: '/pack/',
@@ -25,11 +31,11 @@ const ERROR_MESSAGES = {
interface FeedProps {
feedType?: FeedType;
+ listStyle?: ViewProps['style'];
}
-const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
+const Feed = memo(function Feed({ feedType = 'public', listStyle }: FeedProps) {
const router = useRouter();
- console.log({ feedType });
const [queryString, setQueryString] = useState('Favorite');
const [selectedTypes, setSelectedTypes] = useState({
pack: true,
@@ -110,17 +116,19 @@ const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
-
-
+
+
+ {/* (
@@ -154,6 +162,20 @@ const Feed = memo(function Feed({ feedType = 'public' }: FeedProps) {
onEndReachedThreshold={0.5} // Trigger when 50% from the bottom
showsVerticalScrollIndicator={false}
maxToRenderPerBatch={2}
+ /> */}
+ (
+
+ )}
+ isLoading={isLoading}
+ separatorHeight={12}
/>
{totalPages > 1 ? (
= ({
) : (
{validFeedData
- ?.filter((item): item is FeedItem => item.type !== null)
+ ?.filter(
+ (item): item is FeedItem =>
+ item.type !== null || feedType === 'similarItems',
+ )
.map((item: FeedItem) => {
return (
-
+
);
})}
diff --git a/packages/app/modules/item/components/AddItemGlobal.tsx b/packages/app/modules/item/components/AddItemGlobal.tsx
index 195775ed8..a1c039b47 100644
--- a/packages/app/modules/item/components/AddItemGlobal.tsx
+++ b/packages/app/modules/item/components/AddItemGlobal.tsx
@@ -2,7 +2,7 @@ import { View } from 'react-native';
import { ItemForm } from './ItemForm'; // assuming you moved the form related code to a separate component
import { useModal } from '@packrat/ui';
-import { useAddItem, useItems } from '../hooks';
+import { useAddItem, useUserItems } from '../hooks';
import { usePagination } from 'app/hooks/common';
import {
addItemGlobal as addItemSchema,
@@ -12,7 +12,7 @@ import { useAuthUser } from 'app/modules/auth';
export const AddItemGlobal = () => {
const { limit, page } = usePagination();
- const { isLoading } = useItems({ limit, page });
+ const { isLoading } = useUserItems({ limit, page });
const authUser = useAuthUser();
if (!authUser) {
diff --git a/packages/app/modules/item/components/AddItemModal.tsx b/packages/app/modules/item/components/AddItemModal.tsx
index 158f3a953..875e956be 100644
--- a/packages/app/modules/item/components/AddItemModal.tsx
+++ b/packages/app/modules/item/components/AddItemModal.tsx
@@ -8,6 +8,8 @@ interface AddItemModalProps {
isAddItemModalOpen: boolean;
setIsAddItemModalOpen: any;
setRefetch?: () => void;
+ showTrigger: boolean;
+ initialData: any;
}
export const AddItemModal = ({
@@ -15,7 +17,9 @@ export const AddItemModal = ({
currentPack,
isAddItemModalOpen,
setIsAddItemModalOpen,
+ showTrigger = true,
setRefetch = () => {},
+ initialData,
}: AddItemModalProps) => {
const { currentTheme } = useTheme();
@@ -23,6 +27,7 @@ export const AddItemModal = ({
setIsAddItemModalOpen(false)}
>
+
+
+ {({ open }) => (
+ <>
+ Product Details
+
+
+
+ >
+ )}
+
+
+
+ {
+ return { key, label: key, value };
+ })}
+ />
+
+
+
+
+ );
+}
diff --git a/packages/app/modules/item/components/ItemCard/ItemCard.tsx b/packages/app/modules/item/components/ItemCard/ItemCard.tsx
new file mode 100644
index 000000000..878ecbffc
--- /dev/null
+++ b/packages/app/modules/item/components/ItemCard/ItemCard.tsx
@@ -0,0 +1,19 @@
+import React, { type FC } from 'react';
+import { ItemPrimaryCard } from './ItemPrimaryCard';
+import { ItemSecondaryCard } from './ItemSecondaryCard';
+import type { ItemCardProps } from './model';
+
+interface Props extends ItemCardProps {
+ cardType: 'primary' | 'secondary';
+}
+
+const ItemCards = {
+ primary: ItemPrimaryCard,
+ secondary: ItemSecondaryCard,
+};
+
+export const ItemCard: FC = (props) => {
+ const PackCardComponent = ItemCards[props.cardType];
+
+ return ;
+};
diff --git a/packages/app/modules/item/components/ItemCard/ItemPrimaryCard.tsx b/packages/app/modules/item/components/ItemCard/ItemPrimaryCard.tsx
new file mode 100644
index 000000000..5aa324742
--- /dev/null
+++ b/packages/app/modules/item/components/ItemCard/ItemPrimaryCard.tsx
@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import { Card } from 'tamagui';
+import ItemDetailsContent from '../ItemDetailsContent';
+import { RStack, Image, RButton } from '@packrat/ui';
+import { TouchableOpacity, View, Platform } from 'react-native';
+import useTheme from 'app/hooks/useTheme';
+import { useRouter } from 'app/hooks/router';
+import useResponsive from 'app/hooks/useResponsive';
+import { PlusCircle } from '@tamagui/lucide-icons';
+import ItemPlaceholder from 'app/assets/item-placeholder.png';
+import type { ItemCardProps } from './model';
+
+export const ItemPrimaryCard: React.FC = ({
+ item,
+ onAddPackPress,
+}) => {
+ const { currentTheme } = useTheme();
+ const router = useRouter();
+ const { xxs } = useResponsive();
+
+ const handlePress = (e) => {
+ e.stopPropagation();
+ router.push({
+ pathname: `/item/${item.id}`,
+ query: { itemId: item.id },
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {
+ onAddPackPress(item.id, e);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/app/modules/item/components/ItemCard/ItemSecondaryCard.tsx b/packages/app/modules/item/components/ItemCard/ItemSecondaryCard.tsx
new file mode 100644
index 000000000..dbe52de09
--- /dev/null
+++ b/packages/app/modules/item/components/ItemCard/ItemSecondaryCard.tsx
@@ -0,0 +1,31 @@
+import { Card, Image, RText } from '@packrat/ui';
+import React, { type FC } from 'react';
+import type { ItemCardProps } from './model';
+import ItemPlaceholder from 'app/assets/item-placeholder.png';
+import { convertWeight } from 'app/utils/convertWeight';
+
+export const ItemSecondaryCard: FC = ({ item }) => {
+ return (
+
+ }
+ subtitle={`${convertWeight(item.weight, 'g', item.unit)}
+ ${item.unit}`}
+ actions={undefined}
+ type="secondary"
+ />
+ );
+};
diff --git a/packages/app/modules/item/components/ItemCard/index.ts b/packages/app/modules/item/components/ItemCard/index.ts
new file mode 100644
index 000000000..3279d334e
--- /dev/null
+++ b/packages/app/modules/item/components/ItemCard/index.ts
@@ -0,0 +1 @@
+export * from './ItemCard';
diff --git a/packages/app/modules/item/components/ItemCard/model.ts b/packages/app/modules/item/components/ItemCard/model.ts
new file mode 100644
index 000000000..b8d3a9cb4
--- /dev/null
+++ b/packages/app/modules/item/components/ItemCard/model.ts
@@ -0,0 +1,19 @@
+export interface Item {
+ id: string;
+ name: string;
+ category: {
+ name: string;
+ };
+ sku: string;
+ seller: string;
+ weight: number;
+ unit: string;
+ description: string;
+ productUrl?: string;
+ images?: Array<{ url }>;
+}
+
+export interface ItemCardProps {
+ item: Item;
+ onAddPackPress?: (itemId: string, e: any) => void;
+}
diff --git a/packages/app/modules/item/components/ItemDetailsContent.tsx b/packages/app/modules/item/components/ItemDetailsContent.tsx
new file mode 100644
index 000000000..860db0831
--- /dev/null
+++ b/packages/app/modules/item/components/ItemDetailsContent.tsx
@@ -0,0 +1,158 @@
+import React from 'react';
+import { TouchableOpacity } from 'react-native';
+import { RText, RStack } from '@packrat/ui';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import { convertWeight } from 'app/utils/convertWeight';
+import { SMALLEST_ITEM_UNIT } from '../constants';
+import useTheme from 'app/hooks/useTheme';
+import useResponsive from 'app/hooks/useResponsive';
+import { openExternalLink } from 'app/utils';
+
+interface ItemData {
+ title: string;
+ sku: string;
+ seller: string;
+ category: string;
+ weight: number;
+ unit: string;
+ description: string;
+ productUrl: string;
+}
+
+const ItemDetailsContent = ({ itemData }: { itemData: ItemData }) => {
+ const styles = useCustomStyles(loadStyles);
+
+ return (
+
+
+
+ {itemData.title}
+
+
+
+ {itemData.category}
+
+ {convertWeight(
+ itemData.weight,
+ SMALLEST_ITEM_UNIT,
+ itemData.unit,
+ )}
+ {itemData.unit}
+
+
+
+
+
+ {itemData.description}
+
+
+
+
+ SKU: {itemData.sku}
+
+
+ Seller: {itemData.seller}
+
+
+ {
+ openExternalLink(itemData.productUrl);
+ }}
+ style={styles.GoToStoreButton}
+ >
+ Go to Store
+
+
+
+ );
+};
+
+const loadStyles = (theme: any) => {
+ const { currentTheme } = useTheme();
+ const { xxs, xs, sm } = useResponsive();
+
+ return {
+ container: {
+ flex: 1,
+ padding: xxs ? 0 : xs ? 8 : 10,
+ },
+ detailsContainer: {
+ flex: 1,
+ padding: xxs ? 0 : xs ? 8 : 10,
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ },
+ title: {
+ fontSize: xxs ? 16 : xs ? 16 : sm ? 18 : 20,
+ fontWeight: 'bold',
+ maxHeight: 60,
+ marginVertical: xxs ? 0 : 5,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginVertical: xxs ? 0 : xs ? 3 : 5,
+ flexShrink: 1,
+ },
+ categoryText: {
+ fontSize: xxs ? 12 : xs ? 12 : sm ? 14 : 16,
+ fontWeight: '400',
+ },
+ weightText: {
+ fontSize: xxs ? 16 : xs ? 16 : 18,
+ fontWeight: xxs ? '800' : xs ? '700' : '600',
+ },
+ descriptionSection: {
+ maxHeight: 60,
+ marginVertical: xxs ? 0 : 5,
+ padding: xxs ? 0 : 5,
+ },
+ descriptionText: {
+ fontSize: xxs ? 12 : xs ? 12 : sm ? 14 : 16,
+ },
+ skuSellerRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingBottom: xxs ? 0 : 5,
+ height: 30,
+ },
+ skuText: {
+ fontSize: xxs ? 9 : 10,
+ fontWeight: '600',
+ flexShrink: 1,
+ maxWidth: '40%',
+ paddingTop: xxs ? 13 : 0,
+ },
+ sellerText: {
+ fontSize: xxs ? 9 : 10,
+ fontWeight: '600',
+ flexGrow: 0,
+ flexShrink: 0,
+ },
+ GoToStoreButton: {
+ backgroundColor: currentTheme.colors.secondaryBlue,
+ paddingVertical: xxs ? 4 : 6,
+ paddingHorizontal: xxs ? 0 : 10,
+ borderRadius: 3,
+ alignItems: 'center',
+ marginTop: 5,
+ },
+ buttonText: {
+ color: currentTheme.colors.text,
+ fontWeight: 'bold',
+ fontSize: xxs ? 10 : 12,
+ },
+ };
+};
+
+export default ItemDetailsContent;
diff --git a/packages/app/modules/item/components/ItemDetailsSection.tsx b/packages/app/modules/item/components/ItemDetailsSection.tsx
new file mode 100644
index 000000000..d7902c5f9
--- /dev/null
+++ b/packages/app/modules/item/components/ItemDetailsSection.tsx
@@ -0,0 +1,127 @@
+import React, { useMemo } from 'react';
+import { RStack, ImageGallery, View, RButton } from '@packrat/ui';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import useTheme from 'app/hooks/useTheme';
+import { ExpandableDetailsSection } from './ExpandableDetailsSection';
+import { useItemPackPicker } from '../hooks/useItemPackPicker';
+
+import ItemDetailsContent from './ItemDetailsContent';
+import useResponsive from 'app/hooks/useResponsive';
+import RadioButtonGroup from 'react-native-paper/lib/typescript/components/RadioButton/RadioButtonGroup';
+import { PlusCircle } from '@tamagui/lucide-icons';
+import { PackPickerOverlay } from 'app/modules/pack';
+
+interface ItemData {
+ id: string;
+ name: string;
+ sku: string;
+ seller: string;
+ category: { name: string };
+ weight: number;
+ unit: string;
+ description: string;
+ images: Array<{ url: string }>;
+ productDetails?: string;
+ productUrl: string;
+}
+
+export function ItemDetailsSection({ itemData }: { itemData: ItemData }) {
+ const styles = useCustomStyles(loadStyles);
+ const { overlayProps, onTriggerOpen } = useItemPackPicker();
+
+ const productDetails = useMemo(() => {
+ try {
+ const parsedDetails = JSON.parse(
+ itemData?.productDetails?.replace?.(/'/g, '"'),
+ );
+ return parsedDetails;
+ } catch (e) {
+ console.log(e);
+ return null;
+ }
+ }, [itemData?.productDetails]);
+
+ return (
+
+
+
+ {
+ onTriggerOpen(itemData.id, e);
+ }}
+ >
+
+
+ url) || []}
+ />
+
+
+
+
+
+ {productDetails && (
+
+
+
+ )}
+
+
+ );
+}
+
+const loadStyles = (theme: any) => {
+ const { currentTheme } = useTheme();
+ const { xxs } = useResponsive();
+
+ return {
+ container: {
+ flex: 1,
+ padding: 10,
+ flexDirection: 'column',
+ backgroundColor: currentTheme.colors.background,
+ },
+ contentContainer: {
+ flexDirection: xxs ? 'column' : 'row',
+ },
+ detailsContainer: {
+ flex: 1,
+ padding: 10,
+ },
+ imagePlaceholder: {
+ flex: 1,
+ overflow: 'hidden',
+ height: 300,
+ backgroundColor: currentTheme.colors.border,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 10,
+ },
+ productDetailsSection: {
+ width: '100%',
+ padding: 5,
+ marginTop: 20,
+ backgroundColor: currentTheme.colors.background,
+ borderRadius: 5,
+ },
+ };
+};
diff --git a/packages/app/modules/item/components/SearchItem/SearchItem.tsx b/packages/app/modules/item/components/SearchItem/SearchItem.tsx
index e0f763529..a76a819f9 100644
--- a/packages/app/modules/item/components/SearchItem/SearchItem.tsx
+++ b/packages/app/modules/item/components/SearchItem/SearchItem.tsx
@@ -1,28 +1,56 @@
import { RStack, RText as OriginalRText } from '@packrat/ui';
import { SearchInput } from 'app/components/SearchInput';
import { useSearchItem } from './useSearchItem';
+import React, { useState } from 'react';
+
+import { AddItemModal } from '../AddItemModal';
+import { useFetchSinglePack, usePackId } from 'app/modules/pack';
const RText: any = OriginalRText;
export const SearchItem = () => {
const { searchString, handleSearchResultClick, results, setSearchString } =
useSearchItem();
+ const [isAddItemModalOpen, setIsAddItemModalOpen] = useState(false);
+ const [refetch, setRefetch] = useState(false);
+ const [packId] = usePackId();
+ const { data: currentPack } = useFetchSinglePack(packId);
+ const currentPackId = currentPack && currentPack.id;
+ const [createItemTitle, setCreateItemTitle] = useState('');
return (
- }
- placeholder="Search Global Item"
- results={results}
- onSelect={handleSearchResultClick}
- />
+ <>
+ }
+ placeholder="Looking for an item?"
+ canCreateNewItem
+ results={results}
+ onSelect={handleSearchResultClick}
+ onCreate={({ title }) => {
+ setIsAddItemModalOpen(true);
+ setCreateItemTitle(title);
+ }}
+ />
+ setRefetch((prev) => !prev)}
+ />
+ >
);
};
const ResultItem = ({ item }: any) => {
return (
-
+
{item.name}
diff --git a/packages/app/modules/item/components/SearchItem/useSearchItem.ts b/packages/app/modules/item/components/SearchItem/useSearchItem.ts
index 3a61c98bc..df44d2911 100644
--- a/packages/app/modules/item/components/SearchItem/useSearchItem.ts
+++ b/packages/app/modules/item/components/SearchItem/useSearchItem.ts
@@ -1,15 +1,12 @@
import { useMemo, useState } from 'react';
-import { useItems } from 'app/modules/item';
-import { queryTrpc } from 'app/trpc';
+import { useUserItems, useTogglePackItem } from 'app/modules/item';
import { useAuthUser } from 'app/modules/auth';
import { useFetchSinglePack, usePackId } from 'app/modules/pack';
export const useSearchItem = () => {
const [packId] = usePackId();
const currentPack = useFetchSinglePack(packId);
- const { mutateAsync: addItemToPack } =
- queryTrpc.addGlobalItemToPack.useMutation();
- const utils = queryTrpc.useUtils();
+ const { togglePackItem } = useTogglePackItem();
const user = useAuthUser();
const [searchString, setSearchString] = useState('');
@@ -22,19 +19,22 @@ export const useSearchItem = () => {
};
}, [searchString]);
- const { data } = useItems(itemFilters);
+ const { data } = useUserItems(itemFilters);
const results = useMemo(() => {
const packItems = currentPack?.data?.items;
if (!Array.isArray(data?.items)) {
return [];
}
- return data.items.filter((globalItem) => {
+ return data.items.map((item) => {
if (!Array.isArray(packItems)) {
- return true;
+ return item;
}
- return !packItems.some(({ id }) => id === globalItem.id);
+ return {
+ ...item,
+ isDisabled: packItems.some(({ id }) => id === item.id),
+ };
});
}, [data]);
@@ -48,8 +48,7 @@ export const useSearchItem = () => {
(async () => {
try {
- await addItemToPack({ itemId, ownerId, packId });
- utils.getPackById.invalidate();
+ await togglePackItem({ itemId, packId });
} catch {}
})();
diff --git a/packages/app/modules/item/components/index.ts b/packages/app/modules/item/components/index.ts
index 7172ee83b..fa17accb7 100644
--- a/packages/app/modules/item/components/index.ts
+++ b/packages/app/modules/item/components/index.ts
@@ -4,3 +4,4 @@ export { ImportItemModal } from './ImportItemModal';
export { SearchItem } from './SearchItem';
export { AddItem } from './AddItem';
export { AddItemModal } from './AddItemModal';
+export { ItemCard } from './ItemCard/ItemCard';
diff --git a/packages/app/modules/item/hooks/index.ts b/packages/app/modules/item/hooks/index.ts
index 2663b2c3d..9b4471dfb 100644
--- a/packages/app/modules/item/hooks/index.ts
+++ b/packages/app/modules/item/hooks/index.ts
@@ -1,6 +1,7 @@
export { useSimilarItems } from './useSimilarItems';
export { useAddItem } from './useAddItem';
-export { useItems } from './useItems';
+export { useGlobalItems } from './useGlobalItems';
+export { useUserItems } from './useUserItems';
export { useDeleteItem } from './useDeleteItem';
export { useItemsUpdater } from './useItemsUpdater';
export { useItemWeightUnit } from './useItemWeightUnit';
@@ -9,4 +10,7 @@ export { useItem } from './useItem';
export { useItemRow } from './useItemRow';
export { useImportItem } from './useImportItem';
export { useImportFromBucket } from './useImportFromBucket';
-export { useItemImages } from './useItemImages';
\ No newline at end of file
+export { useItemImages } from './useItemImages';
+export { useItemsFeed } from './useItemsFeed';
+export { useSetItemQuantity } from './useSetItemQuantity';
+export { useTogglePackItem } from './useToggleItemPack';
diff --git a/packages/app/modules/item/hooks/useItems.ts b/packages/app/modules/item/hooks/useGlobalItems.ts
similarity index 71%
rename from packages/app/modules/item/hooks/useItems.ts
rename to packages/app/modules/item/hooks/useGlobalItems.ts
index 918d73538..674c421cb 100644
--- a/packages/app/modules/item/hooks/useItems.ts
+++ b/packages/app/modules/item/hooks/useGlobalItems.ts
@@ -1,9 +1,8 @@
import { queryTrpc } from 'app/trpc';
-import { usePagination } from 'app/hooks/common';
-import { useOfflineQueue, useOfflineQueueProcessor } from 'app/hooks/offline';
+import { useOfflineQueueProcessor } from 'app/hooks/offline';
// TODO handle offline requests
-export const useItems = (filters) => {
+export const useGlobalItems = (filters) => {
const { refetch, data, isLoading, isError, isFetching } =
queryTrpc.getItemsGlobally.useQuery(filters, {
refetchOnWindowFocus: true,
diff --git a/packages/app/modules/item/hooks/useItemPackPicker.ts b/packages/app/modules/item/hooks/useItemPackPicker.ts
new file mode 100644
index 000000000..857c7c0c5
--- /dev/null
+++ b/packages/app/modules/item/hooks/useItemPackPicker.ts
@@ -0,0 +1,37 @@
+import { useState } from 'react';
+import { useModalState } from '@packrat/ui';
+import { useTogglePackItem } from './useToggleItemPack';
+
+export const useItemPackPicker = () => {
+ const { isModalOpen, onOpen, onClose } = useModalState();
+ const [itemId, setItemId] = useState('');
+ const { togglePackItem } = useTogglePackItem();
+
+ const onPress = (id: string, e: any) => {
+ e.stopPropagation();
+ setItemId(id);
+ onOpen();
+ };
+
+ const handleClose = () => {
+ setItemId('');
+ onClose();
+ };
+
+ const handleChange = async (packId: string) => {
+ try {
+ await togglePackItem({ itemId, packId });
+ } catch {}
+ };
+
+ return {
+ onTriggerOpen: onPress,
+ overlayProps: {
+ onClose: handleClose,
+ itemId,
+ onChange: handleChange,
+ isOpen: isModalOpen,
+ title: 'Add Item To Your Packs',
+ },
+ };
+};
diff --git a/packages/app/modules/item/hooks/useItemsFeed.ts b/packages/app/modules/item/hooks/useItemsFeed.ts
new file mode 100644
index 000000000..24fef23fc
--- /dev/null
+++ b/packages/app/modules/item/hooks/useItemsFeed.ts
@@ -0,0 +1,56 @@
+import { queryTrpc } from 'app/trpc';
+import {
+ getPaginationInitialParams,
+ type PaginationParams,
+ usePagination,
+} from 'app/hooks/pagination';
+import { useEffect, useState } from 'react';
+
+export const useItemsFeed = (
+ queryBy = '',
+ searchQuery = '',
+ enabled = true,
+) => {
+ const [pagination, setPagination] = useState(
+ getPaginationInitialParams(),
+ );
+ const { data, isLoading, refetch } = queryTrpc.getItemsFeed.useQuery(
+ {
+ queryBy: queryBy ?? 'Most Recent',
+ pagination,
+ searchTerm: searchQuery,
+ },
+ {
+ enabled,
+ refetchOnWindowFocus: false,
+ onError: (error) => console.error('Error fetching public packs:', error),
+ },
+ );
+ const { fetchPrevPage, fetchNextPage } = usePagination(
+ refetch,
+ pagination,
+ setPagination,
+ {
+ prevPage: data?.prevOffset,
+ nextPage: data?.nextOffset,
+ enabled,
+ },
+ );
+
+ useEffect(() => {
+ setPagination(getPaginationInitialParams());
+ }, [queryBy, searchQuery]);
+
+ return {
+ data: data?.data || [],
+ isLoading,
+ refetch,
+ fetchPrevPage,
+ fetchNextPage,
+ hasPrevPage: data?.prevOffset !== false,
+ hasNextPage: data?.nextOffset !== false,
+ currentPage: data?.currentPage,
+ totalPages: data?.totalPages,
+ error: null,
+ };
+};
diff --git a/packages/app/modules/item/hooks/useSetItemQuantity.ts b/packages/app/modules/item/hooks/useSetItemQuantity.ts
new file mode 100644
index 000000000..18e606ba6
--- /dev/null
+++ b/packages/app/modules/item/hooks/useSetItemQuantity.ts
@@ -0,0 +1,28 @@
+import { queryTrpc } from 'app/trpc';
+
+export const useSetItemQuantity = () => {
+ const utils = queryTrpc.useUtils();
+
+ const mutation = queryTrpc.setItemQuantity.useMutation();
+
+ const setItemQuantity = ({
+ itemId,
+ packId,
+ quantity,
+ }: {
+ itemId: string;
+ packId: string;
+ quantity: number;
+ }) => {
+ mutation.mutate(
+ { itemId, packId, quantity },
+ {
+ onSuccess: () => {
+ utils.getPacksFeed.invalidate();
+ },
+ },
+ );
+ };
+
+ return { setItemQuantity, ...mutation };
+};
diff --git a/packages/app/modules/item/hooks/useToggleItemPack.ts b/packages/app/modules/item/hooks/useToggleItemPack.ts
new file mode 100644
index 000000000..5e833e296
--- /dev/null
+++ b/packages/app/modules/item/hooks/useToggleItemPack.ts
@@ -0,0 +1,19 @@
+import { queryTrpc } from 'app/trpc';
+
+export const useTogglePackItem = () => {
+ const utils = queryTrpc.useContext();
+ const mutation = queryTrpc.toggleItemPack.useMutation({
+ onSuccess: () => {
+ utils.getUserPacksFeed.invalidate();
+ utils.getPackById.invalidate();
+ },
+ });
+
+ return {
+ mutation,
+ togglePackItem: mutation.mutateAsync,
+ isLoading: mutation.isLoading,
+ isError: mutation.isError,
+ error: mutation.error,
+ };
+};
diff --git a/packages/app/modules/item/hooks/useUserItems.ts b/packages/app/modules/item/hooks/useUserItems.ts
new file mode 100644
index 000000000..6b340c763
--- /dev/null
+++ b/packages/app/modules/item/hooks/useUserItems.ts
@@ -0,0 +1,27 @@
+import { queryTrpc } from 'app/trpc';
+import { useOfflineQueueProcessor } from 'app/hooks/offline';
+import { useAuthUser } from 'app/modules/auth';
+
+// TODO handle offline requests
+export const useUserItems = (filters) => {
+ const authUser = useAuthUser();
+ const { refetch, data, isLoading, isError, isFetching } =
+ queryTrpc.getUserItems.useQuery(
+ { ...filters, ownerId: authUser.id },
+ {
+ refetchOnWindowFocus: true,
+ keepPreviousData: true,
+ staleTime: Infinity,
+ cacheTime: Infinity,
+ },
+ );
+ useOfflineQueueProcessor();
+
+ return {
+ data,
+ isLoading,
+ isError,
+ isFetching,
+ refetch,
+ };
+};
diff --git a/packages/app/modules/item/index.ts b/packages/app/modules/item/index.ts
index cd4cf62d5..518f85c39 100644
--- a/packages/app/modules/item/index.ts
+++ b/packages/app/modules/item/index.ts
@@ -6,4 +6,6 @@ export {
SearchItem,
AddItem,
AddItemModal,
+ ItemCard,
} from './components';
+export * from './model';
diff --git a/packages/app/modules/item/model.ts b/packages/app/modules/item/model.ts
index 08ddbdd5f..c52054cc2 100644
--- a/packages/app/modules/item/model.ts
+++ b/packages/app/modules/item/model.ts
@@ -1 +1,16 @@
export type ItemUnit = 'lb' | 'oz' | 'kg' | 'g';
+
+interface Category {
+ id: string;
+ name: string;
+}
+
+export interface Item {
+ id: string;
+ name: string;
+ ownerId: string;
+ weight: number;
+ quantity: number;
+ unit: string;
+ category: Category;
+}
diff --git a/packages/app/modules/item/screens/ItemDetailsScreen.tsx b/packages/app/modules/item/screens/ItemDetailsScreen.tsx
index b8f6be8ec..9c539f255 100644
--- a/packages/app/modules/item/screens/ItemDetailsScreen.tsx
+++ b/packages/app/modules/item/screens/ItemDetailsScreen.tsx
@@ -1,120 +1,111 @@
-import { View } from 'react-native';
import React from 'react';
+import { RScrollView, RStack, RH3, RText } from '@packrat/ui';
+import { useItem, useItemId } from 'app/modules/item';
+import { ItemDetailsSection } from '../components/ItemDetailsSection';
import useTheme from 'app/hooks/useTheme';
-import useCustomStyles from 'app/hooks/useCustomStyles';
-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';
+import LargeCard from 'app/components/card/LargeCard';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import { TouchableOpacity, Platform } from 'react-native';
+import { useRouter } from 'app/hooks/router';
+import RLink from '@packrat/ui/src/RLink';
export function ItemDetailsScreen() {
- // const { limit, handleLimitChange, page, handlePageChange } = usePagination();
+ const { currentTheme } = useTheme();
const [itemId] = useItemId();
- const { data: item, isError } = useItem(itemId);
+ const { data: item, isLoading } = useItem(itemId);
const styles = useCustomStyles(loadStyles);
- const { currentTheme } = useTheme();
- const {data: itemImages, isError: isImagesError} = useItemImages(itemId);
+ const router = useRouter();
- console.log('itemImages', itemImages);
+ if (isLoading) {
+ return null;
+ }
return (
-
-
- {!isError && item && (
-
-
-
- {item?.name && (
-
- Category: {item?.category?.name || '-'}
-
- Weight:{' '}
- {convertWeight(
- Number(item?.weight),
- SMALLEST_ITEM_UNIT,
- item?.unit as any,
- )}
- {item?.unit}
-
- Quantity: {item?.quantity}
-
- )}
-
- }
- type="item"
- />
-
-
- Similar Items
-
-
-
-
- )}
+
+
+
+ {Platform.OS === 'web' ? (
+
+ Products
+
+ ) : (
+ router.push('/products')}>
+ Products
+
+ )}
+
+ /
+
+ {Platform.OS === 'web' ? (
+
+
+ {item?.category?.name}
+
+
+ ) : (
+ router.push('/products')}>
+
+ {item?.category?.name}
+
+
+ )}
+
+
+
+
+ Similar Items
+
+
+
);
}
const loadStyles = (theme) => {
const { currentTheme } = theme;
- const { xxs, xs } = useResponsive();
return {
- mainContainer: {
- backgroundColor: currentTheme.colors.background,
- flexDirection: 'column',
- flex: 1,
- padding: 10,
- alignItems: 'center',
- },
- button: {
- color: currentTheme.colors.white,
- display: 'flex',
+ breadcrumbContainer: {
+ flexDirection: 'row',
alignItems: 'center',
- textAlign: 'center',
+ backgroundColor: currentTheme.colors.border,
+ padding: 8,
+ paddingLeft: 16,
+ borderRadius: 20,
+ marginBottom: 10,
+ gap: 4,
},
- container: {
- backgroundColor: currentTheme.colors.card,
- flexDirection: xs || xxs ? 'column' : 'row',
- gap: xs || xxs ? 4 : 0,
- justifyContent: 'space-between',
- width: '100%',
- padding: 30,
- borderRadius: 10,
+ breadcrumbLink: {
+ color: currentTheme.colors.text,
+ fontSize: 14,
+ fontWeight: 'bold',
},
- sortContainer: {
- width: xxs ? '100%' : xs ? '100%' : '20%',
- justifyContent: 'space-between',
- flexDirection: 'row',
- alignItems: 'center',
+ breadcrumbSeparator: {
+ marginHorizontal: 5,
+ color: currentTheme.colors.text,
},
};
};
diff --git a/packages/app/modules/item/screens/ItemsScreen.tsx b/packages/app/modules/item/screens/ItemsScreen.tsx
index 61019f527..5246f861c 100644
--- a/packages/app/modules/item/screens/ItemsScreen.tsx
+++ b/packages/app/modules/item/screens/ItemsScreen.tsx
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { AddItemGlobal, ImportItemGlobal } from '../components';
import { ItemsTable } from 'app/modules/item/components/itemtable/itemTable';
import useCustomStyles from 'app/hooks/useCustomStyles';
-import { useItems } from 'app/modules/item';
+import { useGlobalItems } from 'app/modules/item';
import { usePagination } from 'app/hooks/common';
import {
BaseModal,
@@ -17,7 +17,7 @@ import { useAuthUser } from 'app/modules/auth';
export function ItemsScreen() {
const { limit, handleLimitChange, page, handlePageChange } = usePagination();
- const { data, isFetching, isError } = useItems({ limit, page });
+ const { data, isFetching, isError } = useGlobalItems({ limit, page });
const styles = useCustomStyles(loadStyles);
const [value, setValue] = useState('Food');
@@ -98,11 +98,9 @@ export function ItemsScreen() {
- {role === 'admin' && (
-
-
-
- )}
+
+
+
{!isError && data?.items && Array.isArray(data.items) && (
diff --git a/packages/app/modules/item/screens/ProductsScreen.tsx b/packages/app/modules/item/screens/ProductsScreen.tsx
new file mode 100644
index 000000000..a00dcecaf
--- /dev/null
+++ b/packages/app/modules/item/screens/ProductsScreen.tsx
@@ -0,0 +1,180 @@
+import React, { useState } from 'react';
+import { FlatList, View } from 'react-native';
+import useCustomStyles from 'app/hooks/useCustomStyles';
+import {
+ DropdownComponent,
+ Form,
+ InputWithIcon,
+ Pagination,
+ RScrollView,
+ RStack,
+ RText,
+} from '@packrat/ui';
+import useResponsive from 'app/hooks/useResponsive';
+import { ItemCard } from '../components/ItemCard';
+import { useFeedSortOptions } from 'app/modules/feed/hooks/useFeedSortOptions';
+import { useItemsFeed } from 'app/modules/item/hooks/useItemsFeed';
+import { Search, X } from '@tamagui/lucide-icons';
+import { PackPickerOverlay } from 'app/modules/pack';
+import { useItemPackPicker } from '../hooks/useItemPackPicker';
+import { FeedList } from 'app/modules/feed/components/FeedList';
+
+export function ProductsScreen() {
+ const sortOptions = useFeedSortOptions('products');
+ const [sortValue, setSortValue] = useState(sortOptions[0]);
+ const { overlayProps, onTriggerOpen } = useItemPackPicker();
+ const [searchValue, setSearchValue] = useState();
+ const {
+ data,
+ isLoading,
+ hasNextPage,
+ fetchNextPage,
+ fetchPrevPage,
+ currentPage,
+ hasPrevPage,
+ totalPages,
+ } = useItemsFeed(sortValue, searchValue);
+ const styles = useCustomStyles(loadStyles);
+ const { xxs, xs, sm, md, lg } = useResponsive();
+
+ const handleSortChange = (newSortValue: string) => {
+ setSortValue(newSortValue);
+ };
+
+ const handleSetSearchValue = (v: string) => {
+ setSearchValue(v);
+ };
+
+ const getNumColumns = () => {
+ if (xxs || xs) return 1;
+ if (sm) return 2;
+ if (md) return 3;
+ return 4;
+ };
+
+ const numColumns = getNumColumns();
+
+ return (
+
+
+
+
+
+
+
+
+ Sort By:
+
+
+
+
+
+
+
+ {isLoading ? (
+ Loading...
+ ) : (
+ <>
+ (
+
+ )}
+ isLoading={isLoading}
+ separatorHeight={12}
+ />
+ {totalPages > 1 && (
+
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+}
+
+const loadStyles = (theme: any) => {
+ const { currentTheme } = theme;
+ const { xxs, xs } = useResponsive();
+
+ return {
+ mainContainer: {
+ flexDirection: 'column',
+ height: '100%',
+ padding: 10,
+ alignItems: 'center',
+ backgroundColor: currentTheme.colors.background,
+ marginBottom: 50,
+ },
+ container: {
+ backgroundColor: currentTheme.colors.card,
+ flexDirection: xs || xxs ? 'column' : 'row',
+ gap: xs || xxs ? 4 : 0,
+ justifyContent: 'space-between',
+ width: '100%',
+ padding: 30,
+ borderRadius: 10,
+ },
+ sortContainer: {
+ width: xxs ? '100%' : xs ? '100%' : '20%',
+ justifyContent: 'space-between',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ cardsContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 10,
+ padding: 10,
+ },
+ filterContainer: {
+ backgroundColor: currentTheme.colors.card,
+ padding: 15,
+ fontSize: 18,
+ width: '100%',
+ borderRadius: 10,
+ marginTop: 20,
+ marginBottom: 8,
+ },
+ searchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 10,
+ padding: 10,
+ borderRadius: 5,
+ },
+ };
+};
diff --git a/packages/app/modules/item/screens/index.ts b/packages/app/modules/item/screens/index.ts
index 7e006f12d..0944707bd 100644
--- a/packages/app/modules/item/screens/index.ts
+++ b/packages/app/modules/item/screens/index.ts
@@ -1,2 +1,3 @@
+export { ProductsScreen } from './ProductsScreen';
export { ItemsScreen } from './ItemsScreen';
export { ItemDetailsScreen } from './ItemDetailsScreen';
diff --git a/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx
new file mode 100644
index 000000000..6e6a23f51
--- /dev/null
+++ b/packages/app/modules/map/components/DownloadMapBtn/DownloadMapBtn.web.tsx
@@ -0,0 +1,7 @@
+import React, { type FC } from 'react';
+import { View } from 'react-native';
+
+export const DownloadMapBtn: FC = () => {
+ // Returns empty component on the on the web implementation.
+ return ;
+};
diff --git a/packages/app/modules/map/components/MapPreviewCard/index.tsx b/packages/app/modules/map/components/MapPreviewCard/index.tsx
index 99dd531a9..b1465271c 100644
--- a/packages/app/modules/map/components/MapPreviewCard/index.tsx
+++ b/packages/app/modules/map/components/MapPreviewCard/index.tsx
@@ -1,30 +1,72 @@
-import React, { type FC } from 'react';
+import React, { useEffect, useState, type FC } from 'react';
import { StatusLabel } from './StatusLabel';
-import { View, RText, Card, YStack, Details } from '@packrat/ui';
-import useTheme from 'app/hooks/useTheme';
+import { View, RText, Card } from '@packrat/ui';
import { TouchableOpacity } from 'react-native';
import { MapImage } from './MapImage';
+import { Entypo } from '@expo/vector-icons';
+import { useDownloadMapProgress } from 'app/modules/map/hooks/useDownloadMapProgress';
+import { type OfflineMap } from 'app/modules/map/screens/OfflineMapsScreen';
interface MapPreviewCardProps {
id: string;
- title: string;
- isDownloaded: boolean;
+ item: OfflineMap;
onShowMapClick: (id: string) => void;
}
export const MapPreviewCard: FC = ({
id,
onShowMapClick,
- title,
- isDownloaded,
+ item,
}) => {
- const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } =
- useTheme();
+ const [isDownloaded, setIsDownloaded] = useState(item.downloaded);
+ const { downloadMap, isDownloading, progress } = useDownloadMapProgress(() =>
+ setIsDownloaded(true),
+ );
+
+ const handleDownloadMap = () => {
+ downloadMap({
+ name: item.name,
+ bounds: item.bounds,
+ styleURL: item.styleURL,
+ minZoom: item.minZoom,
+ maxZoom: item.maxZoom,
+ metadata: {
+ id: item.id,
+ userId: item.userId,
+ },
+ });
+ };
+
+ useEffect(() => {
+ setIsDownloaded(item.downloaded);
+ }, [item.downloaded]);
return (
: null}
+ title={item.name}
+ subtitle={
+ isDownloaded ? (
+
+ ) : (
+
+ {isDownloading ? (
+ {`${progress}%`}
+ ) : (
+
+
+ Download
+
+ )}
+
+ )
+ }
link=""
image={}
isFullWidth
diff --git a/packages/app/modules/map/components/index.ts b/packages/app/modules/map/components/index.ts
index b45023841..50bc86d4f 100644
--- a/packages/app/modules/map/components/index.ts
+++ b/packages/app/modules/map/components/index.ts
@@ -1,4 +1,3 @@
export * from './MapPreview';
export * from './FullScreenBtn';
export * from './MapStylePicker';
-export * from './MapPreviewCard'
diff --git a/packages/app/modules/map/constants.ts b/packages/app/modules/map/constants.ts
new file mode 100644
index 000000000..691b196de
--- /dev/null
+++ b/packages/app/modules/map/constants.ts
@@ -0,0 +1 @@
+export const OFFLINE_MAP_STYLE_URL = 'mapbox://styles/mapbox/outdoors-v11';
diff --git a/packages/app/modules/map/hooks/useDownloadMap.tsx b/packages/app/modules/map/hooks/useDownloadMap.tsx
index 3426f1589..9f6e97f5c 100644
--- a/packages/app/modules/map/hooks/useDownloadMap.tsx
+++ b/packages/app/modules/map/hooks/useDownloadMap.tsx
@@ -1,29 +1,49 @@
import { useAuthUser } from 'app/modules/auth';
import { queryTrpc } from 'app/trpc';
+import { OFFLINE_MAP_STYLE_URL } from '../constants';
+
+export interface UserRemoteMap {
+ name: string;
+ styleURL: string;
+ bounds: any;
+ minZoom: number;
+ maxZoom: number;
+ owner_id: string;
+ metadata: {
+ userId: string;
+ id?: string;
+ shape?: string;
+ };
+}
export const useDownloadMap = (onDownload) => {
const { mutateAsync, isLoading } = queryTrpc.saveOfflineMap.useMutation();
const authUser = useAuthUser();
const handleDownloadMap = ({ mapName, bounds, shape }) => {
- const downloadOptions = {
+ const downloadOptions: UserRemoteMap = {
name: mapName,
- styleURL: 'mapbox://styles/mapbox/outdoors-v11',
+ styleURL: OFFLINE_MAP_STYLE_URL,
bounds,
minZoom: 0,
maxZoom: 8,
owner_id: authUser.id,
metadata: {
+ userId: authUser?.id,
shape: JSON.stringify(shape),
},
};
- alert(JSON.stringify(downloadOptions));
-
// Save the map under user profile.
mutateAsync(downloadOptions)
- .then(() => {
- onDownload(downloadOptions);
+ .then((data) => {
+ onDownload({
+ ...downloadOptions,
+ metadata: {
+ ...data.metadata,
+ id: data.id,
+ },
+ });
})
.catch((e) => {});
};
diff --git a/packages/app/modules/map/hooks/useDownloadMapProgress.tsx b/packages/app/modules/map/hooks/useDownloadMapProgress.tsx
index a72ae888f..730403d22 100644
--- a/packages/app/modules/map/hooks/useDownloadMapProgress.tsx
+++ b/packages/app/modules/map/hooks/useDownloadMapProgress.tsx
@@ -1,8 +1,9 @@
import { offlineManager } from '@rnmapbox/maps';
+import { type OfflineCreatePackOptionsArgs } from '@rnmapbox/maps/lib/typescript/src/modules/offline/OfflineCreatePackOptions';
import { useState } from 'react';
import { Alert } from 'react-native';
-export const useDownloadMapProgress = () => {
+export const useDownloadMapProgress = (onDownloadEnd?: () => void) => {
const [progress, setProgress] = useState(0);
const [downloading, setDownloading] = useState(false);
@@ -11,6 +12,7 @@ export const useDownloadMapProgress = () => {
setDownloading(true);
if (offlineRegionStatus.percentage === 100) {
Alert.alert('Map download successfully!');
+ onDownloadEnd?.();
setDownloading(false);
}
};
@@ -19,7 +21,9 @@ export const useDownloadMapProgress = () => {
Alert.alert(error.message);
};
- const downloadMap = async (optionsForDownload: any) => {
+ const downloadMap = async (
+ optionsForDownload: OfflineCreatePackOptionsArgs,
+ ) => {
offlineManager
.createPack(optionsForDownload, onDownloadProgress, errorListener)
.catch((error: any) => {
diff --git a/packages/app/modules/map/hooks/useOfflineMaps.ts b/packages/app/modules/map/hooks/useOfflineMaps.ts
index fc3493b0a..d18cbe754 100644
--- a/packages/app/modules/map/hooks/useOfflineMaps.ts
+++ b/packages/app/modules/map/hooks/useOfflineMaps.ts
@@ -7,7 +7,7 @@ import {
import { useState } from 'react';
import { useAuthUser } from 'app/modules/auth';
-export const useOfflineMaps = () => {
+export const useOfflineMaps = (enabled = true) => {
const [pagination, setPagination] = useState(
getPaginationInitialParams(),
);
@@ -18,6 +18,7 @@ export const useOfflineMaps = () => {
pagination,
},
{
+ enabled,
refetchOnWindowFocus: false,
onError: (error) => console.error('Error fetching public packs:', error),
},
diff --git a/packages/app/modules/map/screens/OfflineMapsScreen/OfflineMap.tsx b/packages/app/modules/map/screens/OfflineMapsScreen/OfflineMap.tsx
index 369549eb8..80da1c667 100644
--- a/packages/app/modules/map/screens/OfflineMapsScreen/OfflineMap.tsx
+++ b/packages/app/modules/map/screens/OfflineMapsScreen/OfflineMap.tsx
@@ -10,8 +10,9 @@ interface OfflineMapProps {
export const OfflineMapComponent: FC = ({ map, onClose }) => {
return (
);
};
+
+const loadStyles = (theme: any) => {
+ const { currentTheme } = theme;
+ const { sm } = useResponsive();
+
+ return {
+ container: {
+ flex: 1,
+ padding: 10,
+ width: '100%',
+ },
+ layoutContainer: {
+ flexDirection: sm ? 'column' : 'row',
+ justifyContent: 'space-between',
+ },
+ itemsList: {
+ flex: 2,
+ marginRight: 20,
+ backgroundColor: currentTheme.colors.card,
+ width: '100%',
+ height: '100%',
+ },
+ summarySection: {
+ marginTop: sm ? 20 : 0,
+ flex: 1,
+ padding: sm ? 10 : 5,
+ borderRadius: 10,
+ elevation: 8,
+ backgroundColor: currentTheme.colors.card,
+ },
+ separator: {
+ marginVertical: 10,
+ },
+ noItemsText: {
+ fontWeight: 'bold',
+ fontSize: 16,
+ margin: 20,
+ textAlign: 'center',
+ },
+ };
+};
diff --git a/packages/app/modules/pack/components/PackTable/TableHelperComponents.tsx b/packages/app/modules/pack/components/PackTable/TableHelperComponents.tsx
index 46142d4c9..3ae0c0043 100644
--- a/packages/app/modules/pack/components/PackTable/TableHelperComponents.tsx
+++ b/packages/app/modules/pack/components/PackTable/TableHelperComponents.tsx
@@ -118,13 +118,15 @@ const IgnoreItemCheckbox = ({
const WeightUnitDropdown = ({ value, onChange }: WeightUnitDropdownProps) => {
const { xxs, xxl, xs } = useResponsive();
+ const { currentTheme } = useTheme();
return (
{
const styles = useCustomStyles(loadStyles);
return (
- {label}
+ {label}
{`${formatNumber(weight)} (${unit})`}
);
diff --git a/packages/app/modules/pack/components/PackTable/packtable.style.tsx b/packages/app/modules/pack/components/PackTable/packtable.style.tsx
index c6bd16501..497dde0fa 100644
--- a/packages/app/modules/pack/components/PackTable/packtable.style.tsx
+++ b/packages/app/modules/pack/components/PackTable/packtable.style.tsx
@@ -1,8 +1,10 @@
import { Platform } from 'react-native';
+import useResponsive from 'app/hooks/useResponsive';
const isWeb = Platform.OS === 'web';
export const loadStyles = (theme) => {
const { currentTheme } = theme;
+ const { sm } = useResponsive();
return {
container: {
@@ -70,18 +72,13 @@ export const loadStyles = (theme) => {
padding: 25,
backgroundColor: currentTheme.colors.white,
},
- noItemsText: {
- fontWeight: 'bold',
- fontSize: 16,
- margin: 20,
- textAlign: 'center',
- },
+
totalWeightBox: {
flexDirection: 'row',
justifyContent: 'space-between',
width: isWeb ? '100%' : 300,
- paddingHorizontal: 25,
- marginVertical: 30,
+ paddingHorizontal: sm ? 10 : 20,
+ marginVertical: sm ? 10 : 20,
},
};
};
diff --git a/packages/app/modules/pack/components/index.ts b/packages/app/modules/pack/components/index.ts
index 3fa8751e1..931577c4f 100644
--- a/packages/app/modules/pack/components/index.ts
+++ b/packages/app/modules/pack/components/index.ts
@@ -5,4 +5,5 @@ export {
EditPackItemModal,
} from './PackTable';
export { CopyPackModal } from './CopyPackModal';
-export { PackCard } from './PackCard';
+export * from './PackCard';
+export * from './PackPickerOverlay';
diff --git a/packages/app/modules/pack/hooks/useAddGlobalItemToPack.ts b/packages/app/modules/pack/hooks/useAddGlobalItemToPack.ts
new file mode 100644
index 000000000..1918d2812
--- /dev/null
+++ b/packages/app/modules/pack/hooks/useAddGlobalItemToPack.ts
@@ -0,0 +1,20 @@
+import { queryTrpc } from 'app/trpc';
+
+export const useAddGlobalItemToPack = ({ onSuccess, onError }) => {
+ const utils = queryTrpc.useUtils();
+
+ const mutation = queryTrpc.addGlobalItemToPack.useMutation({
+ onSuccess: () => {
+ utils.getPackById.invalidate();
+ onSuccess();
+ },
+ onError,
+ });
+
+ return {
+ isLoading: mutation.isLoading,
+ isError: mutation.isError,
+ isSuccess: mutation.isSuccess,
+ addGlobalItemToPack: mutation.mutate,
+ };
+};
diff --git a/packages/app/modules/pack/hooks/useDeletePackItem.ts b/packages/app/modules/pack/hooks/useDeletePackItem.ts
index e01ba2adb..9d8003061 100644
--- a/packages/app/modules/pack/hooks/useDeletePackItem.ts
+++ b/packages/app/modules/pack/hooks/useDeletePackItem.ts
@@ -2,37 +2,7 @@ import { queryTrpc } from 'app/trpc';
export const useDeletePackItem = () => {
const utils = queryTrpc.useContext();
- const mutation = queryTrpc.deleteItem.useMutation({
- onMutate: async (deleteItem) => {
- // const previousPack = utils.getPackById.getData({
- // packId: deleteItem.packId,
- // });
- // const itemIndex = previousPack.items.findIndex(
- // (item) => item.id === deleteItem.itemId,
- // );
- // if (itemIndex === -1) {
- // throw new Error('Item not found in the pack.');
- // }
- // const newQueryData = {
- // ...previousPack,
- // items: previousPack.itemPacks.filter((itemPack, index) => {
- // return index !== itemIndex;
- // }),
- // validatorPack: deleteItem.packId,
- // };
- // utils.getPackById.setData({ packId: deleteItem.packId }, newQueryData);
- // return {
- // previousPack,
- // };
- },
- onError: (err, deleteItem, context) => {
- // if (context.previousPack) {
- // utils.getPackById.setData(
- // { packId: deleteItem.packId },
- // context.previousPack,
- // );
- // }
- },
+ const mutation = queryTrpc.deleteItemFromPack.useMutation({
onSuccess: (result) => {
utils.getPackById.invalidate();
utils.getTripById.invalidate();
diff --git a/packages/app/modules/pack/hooks/useUserPacks.ts b/packages/app/modules/pack/hooks/useUserPacks.ts
index 6efe952ef..e6babf6ee 100644
--- a/packages/app/modules/pack/hooks/useUserPacks.ts
+++ b/packages/app/modules/pack/hooks/useUserPacks.ts
@@ -14,6 +14,7 @@ interface QueryOptions {
isPublic?: boolean;
isPreview?: boolean;
searchTerm?: string;
+ itemId?: string;
}
export const useUserPacks = (
@@ -22,7 +23,7 @@ export const useUserPacks = (
queryString = '',
queryEnabled = false,
) => {
- const { isPublic, searchTerm, isPreview } = options;
+ const { isPublic, searchTerm, isPreview, itemId } = options;
const [pagination, setPagination] = useState(
getPaginationInitialParams(),
);
@@ -37,10 +38,14 @@ export const useUserPacks = (
pagination,
searchTerm,
isPreview,
+ itemId,
},
{
enabled,
refetchOnWindowFocus: false,
+ staleTime: 5 * 60,
+ cacheTime: 60 * 60 * 24,
+ networkMode: 'offlineFirst',
},
);
utils.getPacks.setData({
diff --git a/packages/app/modules/pack/model.ts b/packages/app/modules/pack/model.ts
index 362f02368..a28e0fea5 100644
--- a/packages/app/modules/pack/model.ts
+++ b/packages/app/modules/pack/model.ts
@@ -3,4 +3,4 @@ export interface PackDetails {
similarityScore?: number;
quantity: number;
weight: number;
-}
+}
\ No newline at end of file
diff --git a/packages/app/modules/pack/screens/AddPackScreen.tsx b/packages/app/modules/pack/screens/AddPackScreen.tsx
index a7ffa2b80..d2d30ac30 100644
--- a/packages/app/modules/pack/screens/AddPackScreen.tsx
+++ b/packages/app/modules/pack/screens/AddPackScreen.tsx
@@ -1,5 +1,13 @@
import React from 'react';
import { AddPackForm } from '../components';
+import { RText } from '@packrat/ui';
+import { useRouter } from '@packrat/crosspath';
+import { Button, Card, XStack } from 'tamagui';
+import { Backpack } from '@tamagui/lucide-icons';
+import Layout from 'app/components/layout/Layout';
+import useResponsive from 'app/hooks/useResponsive';
+import useTheme from 'app/hooks/useTheme';
+import { Platform } from 'react-native';
export const AddPackScreen = ({
isCreatingTrip = false,
@@ -8,5 +16,55 @@ export const AddPackScreen = ({
isCreatingTrip?: boolean;
onSuccess?: any;
}) => {
- return ;
+ const theme = useTheme();
+ const router = useRouter();
+ const { gtSm } = useResponsive();
+
+ return (
+
+
+
+
+
+
+ Need help getting started?
+
+
+
+
+
+
+
+ );
};
diff --git a/packages/app/modules/pack/screens/PackDetailsScreen.tsx b/packages/app/modules/pack/screens/PackDetailsScreen.tsx
index d90194e10..d70381958 100644
--- a/packages/app/modules/pack/screens/PackDetailsScreen.tsx
+++ b/packages/app/modules/pack/screens/PackDetailsScreen.tsx
@@ -23,6 +23,7 @@ import ChatModalTrigger from 'app/components/chat';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'app/hooks/router';
+import { ConnectionGate } from 'app/components/ConnectionGate';
const SECTION = {
TABLE: 'TABLE',
@@ -99,52 +100,56 @@ export function PackDetailsScreen() {
/>
);
case SECTION.CTA:
- return isAuthUserPack ? (
-
- setRefetch((prev) => !prev)}
- />
-
-
- ) : (
-
-
- You don't have permission to edit this pack. You
- can create your own pack{' '}
-
-
-
+ {isAuthUserPack ? (
+
- here
+ setRefetch((prev) => !prev)}
+ />
+
+
+ ) : (
+
+
+ You don't have permission to edit this pack.
+ You can create your own pack{' '}
+
+
+
+ here
+
+
-
-
+ )}
+
);
case SECTION.SCORECARD:
return (
@@ -158,32 +163,34 @@ export function PackDetailsScreen() {
);
case SECTION.SIMILAR_PACKS:
return (
-
-
+
- Similar Packs
-
-
-
+
+ Similar Packs
+
+
+
+
);
default:
return null;
@@ -196,71 +203,72 @@ export function PackDetailsScreen() {
/>
)}
- {/* Disable Chat */}
- {Platform.OS === 'web' ? (
-
-
-
- ) : (
-
- {/*
+ {Platform.OS === 'web' ? (
+ */}
-
+
+
+ ) : (
+
+ {/*
- }
- onPress={() => {
- router.push({
- pathname: '/chat',
- query: {
- itemTypeId: currentPackId,
- type: 'pack',
- },
- });
- }}
- />
-
- //
- )}
+ > */}
+
+ }
+ onPress={() => {
+ router.push({
+ pathname: '/chat',
+ query: {
+ itemTypeId: currentPackId,
+ type: 'pack',
+ },
+ });
+ }}
+ />
+
+ //
+ )}
+
);
}
diff --git a/packages/app/modules/pack/widgets/AddPackContainer.tsx b/packages/app/modules/pack/widgets/AddPackContainer.tsx
index cb24061c7..2e1b9e56c 100644
--- a/packages/app/modules/pack/widgets/AddPackContainer.tsx
+++ b/packages/app/modules/pack/widgets/AddPackContainer.tsx
@@ -1,29 +1,36 @@
import React from 'react';
import { BaseModal, useModal } from '@packrat/ui';
import { AddPackForm } from '../components';
-import { useUserPacks } from 'app/modules/pack';
-import { useAuthUser } from 'app/modules/auth';
+import { queryTrpc } from 'app/trpc';
export const AddPackContainer = ({
isCreatingTrip,
+ onSuccess,
}: {
isCreatingTrip: boolean;
+ onSuccess?: (packId: string) => void;
}) => {
return (
-
+
);
};
-const PackModalContent = ({ isCreatingTrip }: { isCreatingTrip?: boolean }) => {
+const PackModalContent = ({
+ isCreatingTrip,
+ onSuccess,
+}: {
+ isCreatingTrip?: boolean;
+ onSuccess?: (packId: string) => void;
+}) => {
const { setIsModalOpen } = useModal();
- const user = useAuthUser();
+ const utils = queryTrpc.useUtils();
- const { refetch } = useUserPacks(user?.id);
- const handleOnSuccess = () => {
- refetch();
+ const handleOnSuccess = (packId: string) => {
+ utils.getUserPacksFeed.invalidate();
setIsModalOpen(false);
+ onSuccess?.(packId);
};
return (
diff --git a/packages/app/modules/pack/widgets/PackContainer.tsx b/packages/app/modules/pack/widgets/PackContainer.tsx
index 9b05319ca..4a22ee1f6 100644
--- a/packages/app/modules/pack/widgets/PackContainer.tsx
+++ b/packages/app/modules/pack/widgets/PackContainer.tsx
@@ -1,86 +1,77 @@
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useState, useRef, useCallback } from 'react';
import { View } from 'react-native';
import { AddItemModal } from 'app/modules/item';
import useCustomStyles from 'app/hooks/useCustomStyles';
import { useAuthUser } from 'app/modules/auth';
-import { usePackId, useUserPacks, TableContainer } from 'app/modules/pack';
-import { DropdownComponent } from '@packrat/ui';
+import {
+ usePackId,
+ useUserPacks,
+ PackPickerOverlay,
+ useFetchSinglePack,
+} from 'app/modules/pack';
+import {
+ DropdownComponent,
+ RButton,
+ RListItem,
+ RStack,
+ useModalState,
+} from '@packrat/ui';
import { Spinner } from 'tamagui';
import useTheme from 'app/hooks/useTheme';
import { TableContainerComponent } from 'app/screens/trip/TripDetailsComponents';
+import { Backpack, Edit3 } from '@tamagui/lucide-icons';
-export default function PackContainer({ isCreatingTrip = false }) {
- const [isAddItemModalOpen, setIsAddItemModalOpen] = useState(false);
+export default function PackContainer() {
const [packIdParam, setPackIdParam] = usePackId();
- const [currentPackId, setCurrentPackId] = useState(packIdParam);
- const user = useAuthUser();
- const { currentTheme } = useTheme();
-
- const [refetch, setRefetch] = useState(false);
+ const { isModalOpen, onClose, onOpen } = useModalState();
+ const { data: currentPack } = useFetchSinglePack(packIdParam);
const styles = useCustomStyles(loadStyles);
- // TODO - improve refetch logic. Should be handled entirely by the hook
-
- let ownerId;
- const {
- data: packs,
- error,
- isLoading,
- refetch: refetchQuery,
- } = useUserPacks(user?.id);
-
- const oldPacks = useRef([]).current;
-
- useEffect(() => {
- refetchQuery();
- }, [refetch]);
-
- useEffect(() => {
- if (packs.length > oldPacks.length && isCreatingTrip) {
- const newPack = packs.find((pack) => !oldPacks.includes(pack.id));
- setCurrentPackId(newPack?.id);
- setPackIdParam(newPack?.id);
- oldPacks.push(newPack?.id);
- }
- }, [packs]);
- const handlePack = (val) => {
- const selectedPack = packs.find((pack) => pack.id == val);
-
- setCurrentPackId(selectedPack?.id);
-
- if (isCreatingTrip && selectedPack?.id) {
- setPackIdParam(selectedPack?.id);
- }
+ const onSelectPack = (packId: string) => {
+ onClose();
+ setPackIdParam(packId);
};
- const currentPack = packs?.find((pack) => pack.id === currentPackId);
-
- const dataValues = packs?.map((item) => item?.name) ?? [];
+ const onFirstTimeLoad = useCallback(
+ (packs: any[]) => {
+ const firstPackId = packs?.[0]?.id;
+ if (!packIdParam && firstPackId) {
+ setPackIdParam(firstPackId);
+ }
+ },
+ [packIdParam],
+ );
return (
- {dataValues?.length === 0 ? (
-
- ) : (
- dataValues?.length > 0 && (
- <>
-
- {currentPackId && (
- <>
-
- >
- )}
- >
- )
- )}
+
+ {currentPack ? (
+
+ {currentPack?.name}
+
+ ) : null}
+ {currentPack ? (
+
+
+
+ ) : null}
);
}
diff --git a/packages/app/modules/user/components/UserDetailList.tsx b/packages/app/modules/user/components/UserDetailList.tsx
index 6afdafd19..240e56a8b 100644
--- a/packages/app/modules/user/components/UserDetailList.tsx
+++ b/packages/app/modules/user/components/UserDetailList.tsx
@@ -89,47 +89,45 @@ export const UserDataList = ({
) : (
-
- closeModal(),
- },
- ]}
- footerComponent={undefined}
- >
- closeModal(),
+ },
+ ]}
+ footerComponent={undefined}
+ >
+
+
+ item?._id}
+ ItemSeparatorComponent={() => }
+ renderItem={({ item }) => (
+
+ )}
+ showsVerticalScrollIndicator={false}
+ maxToRenderPerBatch={2}
/>
-
- item?._id}
- ItemSeparatorComponent={() => }
- renderItem={({ item }) => (
-
- )}
- showsVerticalScrollIndicator={false}
- maxToRenderPerBatch={2}
- />
-
- {resource.nextPage ? (
- Load more
- ) : null}
-
-
+
+ {resource.nextPage ? (
+ Load more
+ ) : null}
+
)}
>
);
diff --git a/packages/app/modules/user/screens/SettingsScreen.tsx b/packages/app/modules/user/screens/SettingsScreen.tsx
index 8a5d1ba95..d368fce7b 100644
--- a/packages/app/modules/user/screens/SettingsScreen.tsx
+++ b/packages/app/modules/user/screens/SettingsScreen.tsx
@@ -26,6 +26,7 @@ import {
} from '@packrat/validations';
import { Platform, View } from 'react-native';
import { useNavigate } from 'app/hooks/navigation';
+import { SettingsForm } from 'app/components/settings';
const weatherOptions = ['celsius', 'fahrenheit'].map((key) => ({
label: key,
@@ -59,7 +60,8 @@ export function SettingsScreen() {
marginHorizontal="auto"
marginVertical="auto"
>
-
Profile
-
-
+ */}
) : null;
diff --git a/packages/app/modules/user/widgets/ProfileContainer.tsx b/packages/app/modules/user/widgets/ProfileContainer.tsx
index aec35a81e..674ddcf93 100644
--- a/packages/app/modules/user/widgets/ProfileContainer.tsx
+++ b/packages/app/modules/user/widgets/ProfileContainer.tsx
@@ -229,47 +229,41 @@ export function ProfileContainer({ id = null }) {
{isLoading && }
-
- {favoritesQuery?.previewData?.length > 0 ? (
-
- ) : (
-
- No favorites yet
-
- )}
-
+ {favoritesQuery?.previewData?.length > 0 ? (
+
+ ) : (
+
+ No favorites yet
+
+ )}
{userPacksQuery?.previewData?.length > 0 && (
-
-
-
+
)}
{userTripsQuery?.previewData?.length > 0 && (
-
-
-
+
)}
diff --git a/packages/app/modules/user/widgets/UserDataContainer.tsx b/packages/app/modules/user/widgets/UserDataContainer.tsx
index e3e9f6f17..b97a1e6cd 100644
--- a/packages/app/modules/user/widgets/UserDataContainer.tsx
+++ b/packages/app/modules/user/widgets/UserDataContainer.tsx
@@ -95,16 +95,33 @@ export const UserDataContainer = memo(function UserDataContainer({
width: '100%',
}}
>
-
- {differentUser ? `${typeUppercase}` : `Your ${typeUppercase}`}
-
+
+ {differentUser ? `${typeUppercase}` : `Your ${typeUppercase}`}
+
+
+ onSearchChange(search, type)}
+ />
+
+
}
/>
-
-
- onSearchChange(search, type)}
- />
-
>
) : currentUser?.id === userId ? (
diff --git a/packages/app/provider/BootstrapApp/index.tsx b/packages/app/provider/BootstrapApp/index.tsx
new file mode 100644
index 000000000..92f51035c
--- /dev/null
+++ b/packages/app/provider/BootstrapApp/index.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { useAttachListeners } from '../useAttachListeners';
+import { useOfflineMaps } from 'app/modules/map/hooks/useOfflineMaps';
+import { Platform } from 'react-native';
+
+export function BootstrapApp({ children }: { children: React.ReactNode }) {
+ useAttachListeners();
+ // Prefetch Offline maps for offline mode
+ useOfflineMaps(Platform.OS !== 'web');
+
+ return <>{children}>;
+}
diff --git a/packages/app/provider/CombinedProvider.tsx b/packages/app/provider/CombinedProvider.tsx
index b1cf36354..49745e3e9 100644
--- a/packages/app/provider/CombinedProvider.tsx
+++ b/packages/app/provider/CombinedProvider.tsx
@@ -4,19 +4,22 @@ import { ThemeProvider } from '../context/theme';
import { BugsnagErrorBoundary } from './BugsnagProvider';
import { JotaiProvider } from './JotaiProvider';
import { TrpcTanstackProvider } from './TrpcTanstackProvider';
-import { useAttachListeners } from './useAttachListeners';
+import { NetworkStatusProvider } from './NetworkStatusProvider';
+import { BootstrapApp } from './BootstrapApp';
export function CombinedProvider({ children }: { children: React.ReactNode }) {
- useAttachListeners();
-
return (
-
-
- {children}
-
-
+
+
+
+
+ {children}
+
+
+
+
);
diff --git a/packages/app/provider/useAttachListeners.ts b/packages/app/provider/useAttachListeners.ts
index e49390ae7..c33990c7b 100644
--- a/packages/app/provider/useAttachListeners.ts
+++ b/packages/app/provider/useAttachListeners.ts
@@ -1,7 +1,5 @@
-import { useNetworkStatusProvider } from 'app/hooks/offline';
import { useProgressListener } from '../atoms/progressStore';
export const useAttachListeners = () => {
- useNetworkStatusProvider();
useProgressListener();
};
diff --git a/packages/app/screens/trip/createTrip.tsx b/packages/app/screens/trip/createTrip.tsx
index cbef0e8dd..7173de576 100644
--- a/packages/app/screens/trip/createTrip.tsx
+++ b/packages/app/screens/trip/createTrip.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { RStack } from '@packrat/ui';
-import { FlatList, ScrollView } from 'react-native';
+import { FlatList, ScrollView, View } from 'react-native';
import { theme } from '../../theme';
import { useRef } from 'react';
import { GearList } from '../../components/GearList/GearList';
@@ -87,13 +87,12 @@ function Trips() {
isLoading={isPhotonLoading}
shape={photonDetails}
onVisibleBoundsChange={(bounds) => {
- console.log({ bounds });
setTripValue('bounds', bounds);
}}
/>
) : null,
[SECTIONS.FOOTER]: isValid && (
-
+
),
@@ -107,6 +106,9 @@ function Trips() {
data={flatListData}
keyExtractor={keyExtractor}
renderItem={renderItem}
+ CellRendererComponent={({ style, index, ...props }) => (
+
+ )}
/>
diff --git a/packages/app/theme/indessx.ts b/packages/app/theme/indessx.ts
new file mode 100644
index 000000000..f64a712db
--- /dev/null
+++ b/packages/app/theme/indessx.ts
@@ -0,0 +1,127 @@
+import { extendTheme } from 'native-base';
+import { DefaultTheme } from 'react-native-paper';
+
+export const theme = {
+ colors: {
+ background: Platform.OS === 'web' ? 'hsla(0, 0%, 96%, 1)' : '#fcfcfc',
+ secondaryBlue: Platform.OS === 'web' ?'#0C66A1' : '#cce5ff',
+ tertiaryBlue: Platform.OS === 'web' ? '#0C66A1' : '#0C66A1',
+ accentPurple: Platform.OS === 'web' ? '#6C63FF' : '#6C63FF',
+ card: Platform.OS === 'web' ? '#f8f8f8' : '#f8f8f8',
+ text: Platform.OS === 'web' ? '#333333' : '#333333',
+ border: Platform.OS === 'web' ? '#f3f3f3' : '#f3f3f3',
+ notification: Platform.OS === 'web' ? '#0A84FF' : '#0A84FF',
+ error: '#FF453A',
+ textGreen: Platform.OS === 'web' ? undefined : '#22c55e',
+ tertiaryBlueGrey: Platform.OS === 'web' ? undefined : '#3B3B3B',
+ cardIconColor: Platform.OS === 'web' ? '#22c55e' : '#22c55e',
+ iconColor: Platform.OS === 'web' ? '#FFFFFF' : '#003064',
+ weatherIcon: Platform.OS === 'web' ? '#0284c7' : '#0284c7',
+ drawerIconColor: Platform.OS === 'web' ? '#3B3B3B' : '#3B3B3B',
+ white: '#FFFFFF',
+ black: '#000000',
+ },
+ font: {
+ headerFont: 56,
+ size: 18,
+ desktop: 36,
+ },
+ padding: {
+ paddingDesktop: 24,
+ paddingInside: 105,
+ paddingTablet: 80,
+ },
+ size: {
+ cardPadding: 45,
+ mobilePadding: 30,
+ },
+ width: {
+ widthDesktop: '85%',
+ },
+};
+
+export const darkTheme = {
+ colors: {
+ primary: '#0A84FF',
+ background: '#050505',
+ secondaryBlue: '#0C66A1',
+ tertiaryBlue: '#96c7f2',
+ accentPurple: '#6C63FF',
+ card: '#1c1a17',
+ text: '#eaf6ff',
+ border: '#221f1c',
+ notification: '#0A84FF',
+ error: '#FF453A',
+ textGreen: '#22c55e',
+ tertiaryBlueGrey: '#3B3B3B',
+ cardIconColor: '#d6e3ff',
+ iconColor: '#cfe5ff',
+ weatherIcon: '#0A84FF',
+ drawerIconColor: '#3B3B3B',
+ white: '#FFFFFF',
+ },
+ font: {
+ headerFont: 56,
+ size: 18,
+ desktop: 36,
+ },
+ padding: {
+ paddingDesktop: 24,
+ paddingInside: 105,
+ paddingTablet: 80,
+ },
+ size: {
+ cardPadding: 45,
+ mobilePadding: 30,
+ },
+ width: {
+ widthDesktop: '85%',
+ },
+};
+
+export const nativeBaseLightTheme = extendTheme({
+ colors: {
+ primary: {
+ 500: theme.colors.background,
+ },
+ amber: {
+ 100: theme.colors.white,
+ },
+ },
+});
+export const nativeBaseDarkTheme = extendTheme({
+ colors: {
+ primary: {
+ 500: darkTheme.colors.background,
+ },
+ amber: {
+ 100: darkTheme.colors.white,
+ },
+ },
+});
+
+export const lightThemePaper = {
+ ...DefaultTheme,
+ colors: {
+ ...DefaultTheme.colors,
+ primary: theme.colors.primary,
+ onSurface: theme.colors.white,
+ elevation: {
+ ...DefaultTheme.colors.elevation,
+ level1: theme.colors.background,
+ },
+ },
+};
+
+export const darkPaperTheme = {
+ ...DefaultTheme,
+ colors: {
+ ...DefaultTheme.colors,
+ primary: darkTheme.colors.primary,
+ onSurface: darkTheme.colors.white,
+ elevation: {
+ ...DefaultTheme.colors.elevation,
+ level1: darkTheme.colors.background,
+ },
+ },
+};
diff --git a/packages/app/theme/index.ts b/packages/app/theme/index.ts
index 637fbe275..4b0fb307e 100644
--- a/packages/app/theme/index.ts
+++ b/packages/app/theme/index.ts
@@ -1,26 +1,37 @@
import { extendTheme } from 'native-base';
+import { Platform } from 'react-native';
import { DefaultTheme } from 'react-native-paper';
export const theme = {
colors: {
primary: '#0A84FF',
- background: '#fcfcfc',
- secondaryBlue: '#cce5ff',
- tertiaryBlue: '#0C66A1',
- accentPurple: '#6C63FF',
- card: '#f8f8f8',
- text: '#333333',
- border: '#f3f3f3',
+ background: Platform.OS === 'web' ? 'hsla(0, 0%, 96%, 1)' : '#fcfcfc',
+ secondaryBlue: Platform.OS === 'web' ?'#0C66A1' : '#cce5ff',
+ accentPurple: Platform.OS === 'web' ? '#6C63FF' : '#6C63FF',
+ card: Platform.OS === 'web' ? '#f8f8f8' : '#f8f8f8',
+ text: Platform.OS === 'web' ? '#333333' : '#333333',
+ border: Platform.OS === 'web' ? '#f3f3f3' : '#f3f3f3',
notification: '#0A84FF',
+ textGreen: Platform.OS === 'web' ? undefined : '#22c55e',
+ tertiaryBlueGrey: Platform.OS === 'web' ? undefined : '#3B3B3B',
error: '#FF453A',
- textGreen: '#22c55e',
- tertiaryBlueGrey: '#3B3B3B',
+ textPrimary: 'black',
+ textSecondary: '#EBEBF599',
+ tertiaryBlue: Platform.OS === 'web' ? '#0C66A1' : '#0C66A1',
+ textDarkGrey: '#3B3B3B',
cardIconColor: '#22c55e',
- iconColor: '#003064',
+ iconColor: Platform.OS === 'web' ? '#FFFFFF' : '#003064',
+ icon: 'black',
weatherIcon: '#0284c7',
drawerIconColor: '#3B3B3B',
white: '#FFFFFF',
black: '#000000',
+ cardBorderPrimary: 'rgba(0, 0, 0, 0.2)',
+ buttonBackgroundPrimary: '#404040',
+ floatingBg: '#f0f2f5',
+ navbarBoxShadow: '0px 0px 30px 0px rgba(0, 0, 0, 0.29)',
+ navbarPrimaryBackground: '#f6f6f6',
+ logo: 'rgb(12, 102, 161)'
},
font: {
headerFont: 56,
@@ -44,7 +55,7 @@ export const theme = {
export const darkTheme = {
colors: {
primary: '#0A84FF',
- background: '#050505',
+ background: 'black',
secondaryBlue: '#0C66A1',
tertiaryBlue: '#96c7f2',
accentPurple: '#6C63FF',
@@ -53,13 +64,21 @@ export const darkTheme = {
border: '#221f1c',
notification: '#0A84FF',
error: '#FF453A',
- textGreen: '#22c55e',
- tertiaryBlueGrey: '#3B3B3B',
- cardIconColor: '#d6e3ff',
- iconColor: '#cfe5ff',
+ textPrimary: 'white',
+ textSecondary: '#C5C6C799',
+ textDarkGrey: '#B0B0B0',
+ cardIconColor: '#22c55e',
+ iconColor: '#C5C6C7',
+ icon: 'white',
weatherIcon: '#0A84FF',
drawerIconColor: '#3B3B3B',
white: '#FFFFFF',
+ cardBorderPrimary: 'rgba(255, 255, 255, 0.3)',
+ buttonBackgroundPrimary: '#B0B0B0',
+ floatingBg: '#232323',
+ navbarBoxShadow: '0px 0px 30px 0px rgba(0, 0, 0, 0.29)',
+ navbarPrimaryBackground: 'black',
+ logo: 'rgb(150, 199, 242)'
},
font: {
headerFont: 56,
diff --git a/packages/app/trpc.ts b/packages/app/trpc.ts
index 658b8eeed..d6ef2167a 100644
--- a/packages/app/trpc.ts
+++ b/packages/app/trpc.ts
@@ -1,4 +1,5 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
+import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from 'server/src/routes/trpcRouter';
import { api } from './constants/api';
import { createTRPCReact } from '@trpc/react-query';
@@ -63,3 +64,7 @@ export const vanillaTrpcClient = createTRPCProxyClient({
}); // For calling procedures imperatively (outside of a component)
export const queryClient = new QueryClient();
+
+export type RouterInput = inferRouterInputs;
+export type RouterOutput = inferRouterOutputs;
+
\ No newline at end of file
diff --git a/packages/app/utils/index.ts b/packages/app/utils/index.ts
index d4b7f8f7b..d2f86bb3c 100644
--- a/packages/app/utils/index.ts
+++ b/packages/app/utils/index.ts
@@ -1 +1,3 @@
export * from './numberUtils';
+export * from './formatNumber';
+export * from './navigation';
diff --git a/packages/app/utils/navigation.ts b/packages/app/utils/navigation.ts
new file mode 100644
index 000000000..278ae3863
--- /dev/null
+++ b/packages/app/utils/navigation.ts
@@ -0,0 +1,14 @@
+import { Linking, Platform } from 'react-native';
+
+export const openExternalLink = async (link: string) => {
+ if (Platform.OS === 'web') {
+ window.open(link, '_blank', 'noopener,noreferrer');
+ } else {
+ const supported = await Linking.canOpenURL(link);
+ if (!supported) {
+ return;
+ }
+
+ await Linking.openURL(link);
+ }
+};
diff --git a/packages/ui/src/Bento/elements/tables/Basic.tsx b/packages/ui/src/Bento/elements/tables/Basic.tsx
index cd767469a..cb407beb7 100644
--- a/packages/ui/src/Bento/elements/tables/Basic.tsx
+++ b/packages/ui/src/Bento/elements/tables/Basic.tsx
@@ -1,292 +1,59 @@
-import {
- createColumnHelper,
- flexRender,
- getCoreRowModel,
- useReactTable,
-} from '@tanstack/react-table';
+import { type HeaderGroup, type Row, flexRender } from '@tanstack/react-table';
import { useMedia } from 'tamagui';
import * as React from 'react';
import { Text, View, getTokenValue } from 'tamagui';
import { Table } from './common/tableParts';
-import { AddItem } from 'app/modules/item';
-import { MaterialIcons } from '@expo/vector-icons';
-import { EditPackItemModal } from 'app/modules/pack';
-import { RText } from '@packrat/ui';
-import RIconButton from '../../../RIconButton';
-import { BaseAlert } from '@packrat/ui';
-import { useProfile } from 'app/modules/user/hooks';
-import { useAuthUser } from 'app/modules/auth';
-import { convertWeight } from 'app/utils/convertWeight';
-import { SMALLEST_ITEM_UNIT } from 'app/modules/item/constants';
-import { ActionsDropdownComponent } from '@packrat/ui';
-
-type ModalName = 'edit' | 'delete';
-
-interface Category {
- id: string;
- name: string;
-}
-
-interface Item {
- id: string;
- name: string;
- ownerId: string;
- weight: number;
- quantity: number;
- unit: string;
- category: Category;
-}
-
-interface GroupedData {
- [key: string]: Item[];
+interface BasicTableProps {
+ headerGroup: HeaderGroup;
+ tableRows: Row[];
+ footerGroup?: HeaderGroup;
+ columnsLength: number;
}
-interface optionValues {
- label: string;
- value: string;
-}
-
-interface BasicTableProps {
- groupedData: GroupedData;
- handleCheckboxChange: (itemId: string) => void;
- onDelete: (params: { itemId: string; packId: string }) => void;
- hasPermissions: boolean;
- currentPack: any;
- refetch: boolean;
- setRefetch: React.Dispatch>;
-}
-
-/** ------ EXAMPLE ------ */
-export function BasicTable({
- groupedData,
- onDelete,
- hasPermissions,
- currentPack,
- refetch,
- setRefetch,
-}: BasicTableProps) {
- const user = useAuthUser();
- console.log('user', user);
- const ActionButtons = ({ item }) => {
- const [activeModal, setActiveModal] = React.useState(
- null,
- );
- const [selectedItemId, setSelectedItemId] = React.useState(
- null,
- );
-
- const openModal = (modalName: ModalName, itemId: string) => {
- setActiveModal(modalName);
- setSelectedItemId(itemId);
- };
-
- const closeModal = () => {
- setActiveModal(null);
- setSelectedItemId(null);
- };
-
- const handleActionsOpenChange = (state) => {
- switch (state) {
- case 'Edit':
- openModal('edit', item.id);
- break;
- case 'Delete':
- openModal('delete', item.id);
- break;
- }
- };
-
- const optionValues: optionValues[] = [
- { label: 'Edit', value: 'Edit' },
- { label: 'Delete', value: 'Delete' },
- ];
-
- return (
- <>
-
- {selectedItemId === item.id && (
-
- )}
-
- {
- closeModal();
- },
- color: 'gray',
- disabled: false,
- },
- {
- label: 'Delete',
- onClick: () => {
- closeModal();
- onDelete({ itemId: item.id, packId: currentPack.id });
- },
- color: '#B22222',
- disabled: false,
- },
- ]}
- >
- Are you sure you want to delete this item?
-
-
- {hasPermissions ? (
-
- handleActionsOpenChange(value)}
- native={true}
- />
-
- ) : null}
- >
- );
- };
-
- const columnHelper = createColumnHelper- ();
- const columns = [
- columnHelper.accessor('name', {
- cell: (info) => info.getValue(),
- header: () => 'Name',
- // footer: (info) => info.column.id,
- }),
- // columnHelper.accessor('weight', {
- // cell: (info) => info.getValue(),
- // header: () => 'Weight',
- // // footer: (info) => info.column.id,
- // }),
- columnHelper.accessor('weight', {
- cell: (info) => {
- const weightInGrams = info.getValue();
-
- const preferredWeight = convertWeight(
- weightInGrams,
- SMALLEST_ITEM_UNIT,
- info.row.original.unit as any,
- );
- return preferredWeight;
- },
- header: () => 'Weight',
- }),
- columnHelper.accessor('quantity', {
- header: () => 'Quantity',
- cell: (info) => info.renderValue(),
- // footer: (info) => info.column.id,
- }),
- columnHelper.accessor('category.name', {
- header: () => 'Category',
- cell: (info) => info.getValue(),
- // footer: (info) => 'category',
- }),
- ];
-
- if (hasPermissions) {
- columns.push(
- columnHelper.display({
- id: 'actions',
- cell: (props) => ,
- header: () => 'Actions',
- }),
- );
- }
-
+export function BasicTable({
+ headerGroup,
+ tableRows,
+ footerGroup,
+ columnsLength,
+}: BasicTableProps) {
const CELL_WIDTH = '$18';
- const [activeModal, setActiveModal] = React.useState(null);
-
- // Flatten the grouped data into a single array of items
- const data = Object.values(groupedData).flat();
-
- const [tableData, setTableData] = React.useState
- (data);
- React.useEffect(() => {
- setTableData(Object.values(groupedData).flat());
- setActiveModal(null);
- }, [groupedData]);
- const table = useReactTable({
- data: tableData,
- columns,
- getCoreRowModel: getCoreRowModel(),
- });
-
const { sm } = useMedia();
- const headerGroups = table.getHeaderGroups();
- const tableRows = table.getRowModel().rows;
- const footerGroups = table.getFooterGroups();
-
- const allRowsLength =
- tableRows.length + headerGroups.length + footerGroups.length;
const rowCounter = React.useRef(-1);
rowCounter.current = -1;
if (sm) {
return (
- {tableData.map((row, i) => (
-
-
- {Object.entries(row).map(([name, value], i) => {
- if (name === 'ownerId' || name === 'id') {
- return null;
- }
- const finalValue =
- name === 'weight'
- ? convertWeight(value, SMALLEST_ITEM_UNIT, row.unit)
- : value;
- return (
-
- {name.charAt(0).toUpperCase() + name.slice(1)}
- {name === 'category' ? (
- {String(value?.name)}
- ) : (
- {String(finalValue)}
+ {tableRows.map((row) => {
+ return (
+
+ {row.getVisibleCells().map((cell) => (
+
+
+ {flexRender(
+ cell.column.columnDef.header,
+ headerGroup.headers[cell.column.getIndex()].getContext(),
)}
-
- );
- })}
- {hasPermissions && (
-
- Action
-
+
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
- )}
+ ))}
-
- ))}
+ );
+ })}
);
}
@@ -305,36 +72,30 @@ export function BasicTable({
cellWidth={CELL_WIDTH}
cellHeight="$5"
borderWidth={0.5}
- maxWidth={getTokenValue(CELL_WIDTH) * columns.length}
+ maxWidth={getTokenValue(CELL_WIDTH) * columnsLength}
>
- {headerGroups.map((headerGroup) => (
-
- {headerGroup.headers.map((header) => (
-
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext(),
- )}
-
-
- ))}
-
- ))}
+
+ {headerGroup.headers.map((header) => (
+
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+
+ ))}
+
{tableRows.map((row) => (
{row.getVisibleCells().map((cell) => (
-
-
+
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
diff --git a/packages/ui/src/Bento/elements/tables/PaginatedSortedTable.tsx b/packages/ui/src/Bento/elements/tables/PaginatedSortedTable.tsx
index f3186dda5..a0fcd01e6 100644
--- a/packages/ui/src/Bento/elements/tables/PaginatedSortedTable.tsx
+++ b/packages/ui/src/Bento/elements/tables/PaginatedSortedTable.tsx
@@ -63,7 +63,13 @@ export function PaginatedSortedTable({
const columns = [
columnHelper.accessor('name', {
- cell: (info) => info.getValue(),
+ cell: (info) => (
+
+
+ {info.getValue()}
+
+
+ ),
header: () => 'Name',
footer: (info) => info.column.id,
}),
@@ -77,11 +83,6 @@ export function PaginatedSortedTable({
header: () => 'Weight',
footer: (info) => info.column.id,
}),
- columnHelper.accessor('quantity', {
- header: () => 'Quantity',
- cell: (info) => info.renderValue(),
- footer: (info) => info.column.id,
- }),
columnHelper.accessor('category.name', {
header: () => 'Category',
cell: (info) => info.getValue(),
diff --git a/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx b/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx
index 3c326756a..baca7aac7 100644
--- a/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx
+++ b/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import * as LocalAuthentication from 'expo-local-authentication';
+// import { Facebook, Github } from '@tamagui/lucide-icons';
import {
AnimatePresence,
H1,
@@ -10,11 +9,13 @@ import {
Theme,
View,
} from 'tamagui';
-import { Text, Platform, Alert } from 'react-native';
+import { Text, Platform } from 'react-native';
import { FormCard } from './components/layoutParts';
import { RLink } from '@packrat/ui';
import { Form, FormInput, SubmitButton } from '@packrat/ui';
import { userSignIn } from '@packrat/validations';
+import { FontAwesome } from '@expo/vector-icons';
+import { RIconButton } from '@packrat/ui';
import useTheme from 'app/hooks/useTheme';
import useResponsive from 'app/hooks/useResponsive';
@@ -26,64 +27,24 @@ export function SignInScreen({
}) {
const { currentTheme } = useTheme();
const { xxs, xs } = useResponsive();
-
- const handleBiometricAuth = async () => {
- if (Platform.OS === 'web' || true) {
- return true;
- }
-
- const hasHardware = await LocalAuthentication.hasHardwareAsync();
- if (!hasHardware) {
- Alert.alert(
- 'Error',
- 'Your device does not support biometric authentication.',
- );
- return false;
- }
-
- const hasBiometrics = await LocalAuthentication.isEnrolledAsync();
- if (!hasBiometrics) {
- Alert.alert(
- 'Error',
- 'No biometrics are enrolled. Please set up biometrics in your device settings.',
- );
- return false;
- }
-
- const result = await LocalAuthentication.authenticateAsync({
- promptMessage: 'Authenticate to continue',
- fallbackLabel: 'Use Passcode',
- });
-
- if (!result.success) {
- Alert.alert('Authentication failed', 'Please try again.');
- }
-
- return result.success;
- };
-
- const handleSubmit = async (data) => {
- const isBiometricallyAuthenticated = await handleBiometricAuth();
- if (isBiometricallyAuthenticated) {
- signIn(data);
- }
- };
-
return (
-
+
diff --git a/packages/ui/src/EmptyState/EmptyState.tsx b/packages/ui/src/EmptyState/EmptyState.tsx
new file mode 100644
index 000000000..11e8e9c9f
--- /dev/null
+++ b/packages/ui/src/EmptyState/EmptyState.tsx
@@ -0,0 +1,17 @@
+import React, { type ReactNode, type FC } from 'react';
+import { RText } from '@packrat/ui';
+import { Stack } from 'tamagui';
+
+interface EmptyStateProps {
+ icon: ReactNode;
+ text: string;
+}
+
+export const EmptyState: FC = ({ text, icon }) => {
+ return (
+
+ {icon}
+ {text}
+
+ );
+};
diff --git a/packages/ui/src/EmptyState/index.ts b/packages/ui/src/EmptyState/index.ts
new file mode 100644
index 000000000..89798dbd6
--- /dev/null
+++ b/packages/ui/src/EmptyState/index.ts
@@ -0,0 +1 @@
+export { EmptyState } from './EmptyState';
diff --git a/packages/ui/src/Image/Image.native.tsx b/packages/ui/src/Image/Image.native.tsx
new file mode 100644
index 000000000..b521aac9c
--- /dev/null
+++ b/packages/ui/src/Image/Image.native.tsx
@@ -0,0 +1,6 @@
+import React, { type FC } from 'react';
+import RNImage, { type FastImageProps } from 'react-native-fast-image';
+
+export const Image: FC = (props) => {
+ return ;
+};
diff --git a/packages/ui/src/Image/Image.tsx b/packages/ui/src/Image/Image.tsx
new file mode 100644
index 000000000..2ee8934ea
--- /dev/null
+++ b/packages/ui/src/Image/Image.tsx
@@ -0,0 +1,6 @@
+import React, { type FC } from 'react';
+import { Image as RNImage, type ImageProps } from 'react-native';
+
+export const Image: FC = (props) => {
+ return ;
+};
diff --git a/packages/ui/src/Image/index.ts b/packages/ui/src/Image/index.ts
new file mode 100644
index 000000000..4bbac9014
--- /dev/null
+++ b/packages/ui/src/Image/index.ts
@@ -0,0 +1 @@
+export * from './Image';
diff --git a/packages/ui/src/ImageGallery/ImageGallery.tsx b/packages/ui/src/ImageGallery/ImageGallery.tsx
new file mode 100644
index 000000000..f2b05b606
--- /dev/null
+++ b/packages/ui/src/ImageGallery/ImageGallery.tsx
@@ -0,0 +1,47 @@
+import React, { type FC } from 'react';
+import { XStack } from 'tamagui';
+import { useImageGallery } from './useImageGallery';
+import { ArrowLeft, ArrowRight } from '@tamagui/lucide-icons';
+import { Image } from '../Image';
+import { TouchableOpacity } from 'react-native';
+
+interface ImageGalleryProps {
+ images: string[];
+}
+
+export const ImageGallery: FC = ({ images }) => {
+ const { activeImageSrc, goPrev, goNext } = useImageGallery(images);
+
+ return (
+
+ {images?.length > 1 ? (
+ 1 ? 1 : 0,
+ }}
+ onPress={goPrev}
+ >
+
+
+ ) : null}
+
+ {images?.length > 1 ? (
+
+
+
+ ) : null}
+
+ );
+};
diff --git a/packages/ui/src/ImageGallery/index.ts b/packages/ui/src/ImageGallery/index.ts
new file mode 100644
index 000000000..1cd889327
--- /dev/null
+++ b/packages/ui/src/ImageGallery/index.ts
@@ -0,0 +1,2 @@
+export * from './ImageGallery';
+export * from './mock';
diff --git a/packages/ui/src/ImageGallery/mock.ts b/packages/ui/src/ImageGallery/mock.ts
new file mode 100644
index 000000000..c5bca0ac9
--- /dev/null
+++ b/packages/ui/src/ImageGallery/mock.ts
@@ -0,0 +1,10 @@
+const MOCK_COUNT = 5;
+const MOCK_RATIO = { width: 300, height: 200 };
+
+export const mockImages = (() =>
+ new Array(MOCK_COUNT)
+ .fill(null)
+ .map(
+ (_, i) =>
+ `https://picsum.photos/id/${i + 1}/${MOCK_RATIO.width}/${MOCK_RATIO.height}`,
+ ))();
diff --git a/packages/ui/src/ImageGallery/useImageGallery.ts b/packages/ui/src/ImageGallery/useImageGallery.ts
new file mode 100644
index 000000000..305cd4770
--- /dev/null
+++ b/packages/ui/src/ImageGallery/useImageGallery.ts
@@ -0,0 +1,18 @@
+import { useState } from 'react';
+
+export const useImageGallery = (images) => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const activeImageSrc = images[currentIndex];
+
+ const goNext = () => {
+ setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length);
+ };
+
+ const goPrev = () => {
+ setCurrentIndex(
+ (prevIndex) => (prevIndex - 1 + images.length) % images.length,
+ );
+ };
+
+ return { goNext, goPrev, activeImageSrc };
+};
diff --git a/packages/ui/src/ItemPickerOverlay/ItemPickerOverlay.tsx b/packages/ui/src/ItemPickerOverlay/ItemPickerOverlay.tsx
new file mode 100644
index 000000000..d04878899
--- /dev/null
+++ b/packages/ui/src/ItemPickerOverlay/ItemPickerOverlay.tsx
@@ -0,0 +1,85 @@
+import React, { type JSX, type FC } from 'react';
+import { BaseModal } from '../modal';
+import { type BaseModalProps } from '../modal/BaseModal';
+import { View } from 'tamagui';
+import { Form, FormInput } from '../form';
+import { Dimensions, FlatList, ScrollView } from 'react-native';
+
+interface ItemPickerOverlayProps {
+ modalProps: Omit;
+ data: any[];
+ ListEmptyComponent: (params: { item: any }) => JSX.Element;
+ renderItem: (params: { item: any }) => JSX.Element;
+ onSearchChange: (search: string) => void;
+ searchTerm: string;
+ title: string;
+ saveBtnText?: string;
+ onSave?: () => void;
+}
+export const ItemPickerOverlay: FC = ({
+ modalProps,
+ searchTerm,
+ onSearchChange,
+ saveBtnText = 'Done',
+ onSave = () => {},
+ ListEmptyComponent,
+ data,
+ renderItem,
+}) => {
+ const windowHeight = Dimensions.get('window').height;
+
+ return (
+ {
+ onSave();
+ closeModal();
+ },
+ },
+ ]}
+ footerComponent={undefined}
+ >
+
+
+
+ (
+
+ )}
+ ListEmptyComponent={ListEmptyComponent}
+ keyExtractor={(item, index) => `${item?.id}_${item?.type}_${index}`} // Ensure unique keys
+ renderItem={renderItem}
+ onEndReachedThreshold={0.5}
+ showsVerticalScrollIndicator={false}
+ maxToRenderPerBatch={2}
+ />
+
+
+
+ );
+};
diff --git a/packages/ui/src/ItemPickerOverlay/index.ts b/packages/ui/src/ItemPickerOverlay/index.ts
new file mode 100644
index 000000000..1d9628a43
--- /dev/null
+++ b/packages/ui/src/ItemPickerOverlay/index.ts
@@ -0,0 +1 @@
+export * from './ItemPickerOverlay';
diff --git a/packages/ui/src/RLink/index.tsx b/packages/ui/src/RLink/index.tsx
index 64161d1a3..2b8617daa 100644
--- a/packages/ui/src/RLink/index.tsx
+++ b/packages/ui/src/RLink/index.tsx
@@ -1,7 +1,7 @@
import { Link as OriginalLink } from '@packrat/crosspath';
import React from 'react';
-const Link: React.FC = ({ children, ...props }) => {
+const Link: React.FC = ({ children, linkStyle, ...props }) => {
return (
{children}
diff --git a/packages/ui/src/RSelect/index.tsx b/packages/ui/src/RSelect/index.tsx
index 1e9918f5e..a4b83421d 100644
--- a/packages/ui/src/RSelect/index.tsx
+++ b/packages/ui/src/RSelect/index.tsx
@@ -110,8 +110,8 @@ export function SelectItem(props) {
onValueChange={handleChange}
{...forwardedProps}
>
-
-
+
+
{selectedItemLabel ?? placeholder}
diff --git a/packages/ui/src/card/SecondaryCard.tsx b/packages/ui/src/card/SecondaryCard.tsx
index 1f5493817..049f31ac7 100644
--- a/packages/ui/src/card/SecondaryCard.tsx
+++ b/packages/ui/src/card/SecondaryCard.tsx
@@ -1,6 +1,6 @@
import React, { type FC } from 'react';
import { type BaseCardProps } from './model';
-import { Card, View, XStack, YStack } from 'tamagui';
+import { Card, View, XStack, YStack, useTheme } from 'tamagui';
import RText from '../RText';
interface SecondaryCardProps extends Omit {
@@ -8,21 +8,30 @@ interface SecondaryCardProps extends Omit {
}
export const SecondaryCard: FC = (props) => {
+ const theme = useTheme();
+
return (
{props.image}
= (props) => {
flexDirection: 'row',
}}
>
-
+
{props.title}
{props.subtitle}
- {props.actions}
+ {props.actions ? {props.actions} : null}
diff --git a/packages/ui/src/dialog/BaseDialog.tsx b/packages/ui/src/dialog/BaseDialog.tsx
index 924da238c..3975dac00 100644
--- a/packages/ui/src/dialog/BaseDialog.tsx
+++ b/packages/ui/src/dialog/BaseDialog.tsx
@@ -7,6 +7,7 @@ import {
Dialog as OriginalDialog,
Sheet as OriginalSheet,
} from 'tamagui';
+import RButton from '../RButton';
const Dialog: any = OriginalDialog;
const Sheet: any = OriginalSheet;
@@ -34,7 +35,7 @@ export const BaseDialog = ({
}}
>
-
+ {trigger}
diff --git a/packages/ui/src/form/components/ImageUpload/ImageUpload.tsx b/packages/ui/src/form/components/ImageUpload/ImageUpload.tsx
index fffcc067a..cafee6784 100644
--- a/packages/ui/src/form/components/ImageUpload/ImageUpload.tsx
+++ b/packages/ui/src/form/components/ImageUpload/ImageUpload.tsx
@@ -4,42 +4,97 @@ import OriginalRStack from '../../../RStack';
import OriginalRH5 from '../../../RH5';
import OriginalRButton from '../../../RButton';
import { useImageUpload } from './useImageUpload';
-import { cloneElement } from 'react';
+import { cloneElement, useState } from 'react';
+import { Button } from 'tamagui';
+import { Upload, Popcorn } from '@tamagui/lucide-icons';
const RButton: any = OriginalRButton;
const RStack: any = OriginalRStack;
const RH5: any = OriginalRH5;
-export const ImageUpload = ({ previewElement, name, label }) => {
+export const ImageUpload = ({ hasProfileImage, previewElement, name, label }) => {
const { pickImage, removeImage, src } = useImageUpload(name);
+ const [hasImage, setHasImage] = useState(hasProfileImage);
+
+ const handleRemoveImage = async () => {
+ await removeImage();
+ setHasImage(false);
+ };
+ const handlePickImage = async () => {
+ await pickImage();
+ setHasImage(true);
+ };
return (
-
- {cloneElement(previewElement, { src })}
-
+
+ {cloneElement(previewElement, { src })}
+
{label}
}
+ icon={
+
+
+
+ }
color="white"
- style={{ backgroundColor: '#0284c7' }}
- onPress={pickImage}
+ size="$3"
+ style={{
+ backgroundColor: '#232323',
+ color: 'white',
+ textAlign: 'center'
+ }}
+ onPress={handlePickImage}
>
- Upload
+ Update Profile Picture
-
+
+
+ }
+ color="white"
+ style={{
+ backgroundColor: '#232323',
+ color: 'white',
+ textAlign: 'center'
+ }}
+ onPress={handleRemoveImage}
+ >
+ Remove Profile Picture
+
+ )}
+ {/*
Remove
-
+ */}
diff --git a/packages/ui/src/form/components/InputWithIcon.tsx b/packages/ui/src/form/components/InputWithIcon.tsx
index 38b077ac8..460569f46 100644
--- a/packages/ui/src/form/components/InputWithIcon.tsx
+++ b/packages/ui/src/form/components/InputWithIcon.tsx
@@ -1,4 +1,4 @@
-import { useRef } from 'react';
+import React, { useRef } from 'react';
import type { SizeTokens } from 'tamagui';
import { View } from 'tamagui';
import { Input } from './inputsParts';
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx
index 62d1ab1dc..83f5f7d7d 100644
--- a/packages/ui/src/index.tsx
+++ b/packages/ui/src/index.tsx
@@ -86,15 +86,21 @@ export * from './Details';
export { View } from 'tamagui';
export { config } from './tamagui.config';
// export * from 'tamagui';
+
+export { ListItem as RListItem } from 'tamagui';
export * from '@tamagui/toast';
+export * from './EmptyState';
export * from './Bento';
export * from './DateRangePicker';
export * from './dialog';
+export * from './ImageGallery';
+export * from './Image';
export * from './list';
export * from './modal';
export * from './toast';
export * from './alert';
export * from './RCard';
+export * from './ItemPickerOverlay';
export * from './RImage';
export * from './RInput';
export * from './RScrollview';
diff --git a/packages/validations/package.json b/packages/validations/package.json
index e197bd10e..6f728ffe4 100644
--- a/packages/validations/package.json
+++ b/packages/validations/package.json
@@ -1,6 +1,6 @@
{
"name": "@packrat/validations",
- "version": "1.1.1",
+ "version": "1.2.0",
"source": "src/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/validations/src/validations/index.ts b/packages/validations/src/validations/index.ts
index d25c364bb..37506ae74 100644
--- a/packages/validations/src/validations/index.ts
+++ b/packages/validations/src/validations/index.ts
@@ -10,6 +10,7 @@ export * from './trailsRouteValidator';
export * from './openAIRoutesValidator';
export * from './extrasValidator';
export * from './templateRouteValidator';
+export * from './packTemplateRoutesValidator';
export * from './weatherRoutesValidator';
export * from './roleValidator';
export * from './authTokenValidator';
diff --git a/packages/validations/src/validations/itemRoutesValidator.ts b/packages/validations/src/validations/itemRoutesValidator.ts
index a6e999a4b..890257491 100644
--- a/packages/validations/src/validations/itemRoutesValidator.ts
+++ b/packages/validations/src/validations/itemRoutesValidator.ts
@@ -15,7 +15,7 @@ export const getItemById = z.object({
export const addItem = z.object({
name: z.string(),
weight: z.number(),
- quantity: z.number(),
+ quantity: z.number().int().positive(),
unit: z.string(),
packId: z.string(),
type: z.string(),
@@ -38,12 +38,14 @@ export const editItem = z.object({
quantity: z.number(),
unit: z.string(),
type: z.string(),
+ packId: z.string(),
});
export const addGlobalItemToPack = z.object({
packId: z.string(),
itemId: z.string(),
ownerId: z.string(),
+ quantity: z.number().int().positive(),
});
export const deleteGlobalItem = z.object({
@@ -63,7 +65,6 @@ export const deleteItem = z.object({
export const addItemGlobal = z.object({
name: z.string(),
weight: z.number(),
- quantity: z.number(),
unit: z.string(),
type: z.string(),
ownerId: z.string(),
diff --git a/packages/validations/src/validations/packTemplateRoutesValidator.ts b/packages/validations/src/validations/packTemplateRoutesValidator.ts
new file mode 100644
index 000000000..a98162019
--- /dev/null
+++ b/packages/validations/src/validations/packTemplateRoutesValidator.ts
@@ -0,0 +1,23 @@
+import { z } from 'zod';
+
+export const getPackTemplates = z.object({
+ filter: z
+ .object({
+ searchQuery: z.string().optional(),
+ })
+ .optional(),
+ orderBy: z.string().optional(),
+ pagination: z.object({
+ offset: z.number(),
+ limit: z.number(),
+ }),
+});
+
+export const getPackTemplate = z.object({
+ id: z.string().min(1),
+});
+
+export const createPackFromTemplate = z.object({
+ packTemplateId: z.string().min(1),
+ newPackName: z.string().min(1),
+});
diff --git a/server/migrations-preview/0001_true_red_hulk.sql b/server/migrations-preview/0001_true_red_hulk.sql
new file mode 100644
index 000000000..cfe186cfd
--- /dev/null
+++ b/server/migrations-preview/0001_true_red_hulk.sql
@@ -0,0 +1,14 @@
+CREATE TABLE `item_pack_templates` (
+ `item_id` text,
+ `pack_template_id` text,
+ PRIMARY KEY(`item_id`, `pack_template_id`),
+ FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`pack_template_id`) REFERENCES `pack_template`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `pack_template` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `description` text NOT NULL,
+ `type` text DEFAULT 'packTemplate'
+);
diff --git a/server/migrations-preview/0002_luxuriant_gunslinger.sql b/server/migrations-preview/0002_luxuriant_gunslinger.sql
new file mode 100644
index 000000000..e5e98f0dd
--- /dev/null
+++ b/server/migrations-preview/0002_luxuriant_gunslinger.sql
@@ -0,0 +1,8 @@
+ALTER TABLE item ADD `sku` text;--> statement-breakpoint
+ALTER TABLE item ADD `product_url` text;--> statement-breakpoint
+ALTER TABLE item ADD `description` text;--> statement-breakpoint
+ALTER TABLE item ADD `product_details` text;--> statement-breakpoint
+ALTER TABLE item ADD `seller` text;--> statement-breakpoint
+ALTER TABLE item_pack_templates ADD `quantity` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
+ALTER TABLE item_packs ADD `quantity` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
+ALTER TABLE `item` DROP COLUMN `quantity`;
\ No newline at end of file
diff --git a/server/migrations-preview/meta/0001_snapshot.json b/server/migrations-preview/meta/0001_snapshot.json
new file mode 100644
index 000000000..fd0a7bd59
--- /dev/null
+++ b/server/migrations-preview/meta/0001_snapshot.json
@@ -0,0 +1,1381 @@
+{
+ "version": "5",
+ "dialect": "sqlite",
+ "id": "0d205b6a-e6e3-4845-a792-97473f08d0d8",
+ "prevId": "b3663479-7080-4d0d-a708-bf60d02af6b9",
+ "tables": {
+ "conversation": {
+ "name": "conversation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "itemTypeId": {
+ "name": "itemTypeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "history": {
+ "name": "history",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "geojson": {
+ "name": "geojson",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "geoJSON": {
+ "name": "geoJSON",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "weight": {
+ "name": "weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "global": {
+ "name": "global",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_category_id_item_category_id_fk": {
+ "name": "item_category_id_item_category_id_fk",
+ "tableFrom": "item",
+ "tableTo": "item_category",
+ "columnsFrom": ["category_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "item_owner_id_user_id_fk": {
+ "name": "item_owner_id_user_id_fk",
+ "tableFrom": "item",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_category": {
+ "name": "item_category",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_image": {
+ "name": "item_image",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_image_item_id_item_id_fk": {
+ "name": "item_image_item_id_item_id_fk",
+ "tableFrom": "item_image",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_owners": {
+ "name": "item_owners",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_owners_item_id_item_id_fk": {
+ "name": "item_owners_item_id_item_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_owners_owner_id_user_id_fk": {
+ "name": "item_owners_owner_id_user_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["item_id", "owner_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_pack_templates": {
+ "name": "item_pack_templates",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_template_id": {
+ "name": "pack_template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_pack_templates_item_id_item_id_fk": {
+ "name": "item_pack_templates_item_id_item_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_pack_templates_pack_template_id_pack_template_id_fk": {
+ "name": "item_pack_templates_pack_template_id_pack_template_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "pack_template",
+ "columnsFrom": ["pack_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["item_id", "pack_template_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_packs": {
+ "name": "item_packs",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_packs_item_id_item_id_fk": {
+ "name": "item_packs_item_id_item_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_packs_pack_id_pack_id_fk": {
+ "name": "item_packs_pack_id_pack_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "pack",
+ "columnsFrom": ["pack_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["item_id", "pack_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "node": {
+ "name": "node",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lat": {
+ "name": "lat",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lon": {
+ "name": "lon",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offlineMap": {
+ "name": "offlineMap",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "minZoom": {
+ "name": "minZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "maxZoom": {
+ "name": "maxZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "offlineMap_name_owner_id_unique": {
+ "name": "offlineMap_name_owner_id_unique",
+ "columns": ["name", "owner_id"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "offlineMap_owner_id_user_id_fk": {
+ "name": "offlineMap_owner_id_user_id_fk",
+ "tableFrom": "offlineMap",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack": {
+ "name": "pack",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "grades": {
+ "name": "grades",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weight\":\"\",\"essentialItems\":\"\",\"redundancyAndVersatility\":\"\"}'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weightScore\":0,\"essentialItemsScore\":0,\"redundancyAndVersatilityScore\":0}'"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pack_owner_id_user_id_fk": {
+ "name": "pack_owner_id_user_id_fk",
+ "tableFrom": "pack",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack_template": {
+ "name": "pack_template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'packTemplate'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "refresh_tokens": {
+ "name": "refresh_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "refresh_tokens_user_id_user_id_fk": {
+ "name": "refresh_tokens_user_id_user_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "relation": {
+ "name": "relation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'relation'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "members": {
+ "name": "members",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "template": {
+ "name": "template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_global_template": {
+ "name": "is_global_template",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "template_created_by_user_id_fk": {
+ "name": "template_created_by_user_id_fk",
+ "tableFrom": "template",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip": {
+ "name": "trip",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parks": {
+ "name": "parks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "packs_id": {
+ "name": "packs_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "activity": {
+ "name": "activity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"totalScore\":0}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_owner_id_user_id_fk": {
+ "name": "trip_owner_id_user_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "trip_packs_id_pack_id_fk": {
+ "name": "trip_packs_id_pack_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "pack",
+ "columnsFrom": ["packs_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip_geojsons": {
+ "name": "trip_geojsons",
+ "columns": {
+ "trip_id": {
+ "name": "trip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geojson_id": {
+ "name": "geojson_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_geojsons_trip_id_trip_id_fk": {
+ "name": "trip_geojsons_trip_id_trip_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "trip",
+ "columnsFrom": ["trip_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "trip_geojsons_geojson_id_geojson_id_fk": {
+ "name": "trip_geojsons_geojson_id_geojson_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "geojson",
+ "columnsFrom": ["geojson_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["geojson_id", "trip_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_certified_guide": {
+ "name": "is_certified_guide",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token": {
+ "name": "password_reset_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token_expiration": {
+ "name": "password_reset_token_expiration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "offline_maps": {
+ "name": "offline_maps",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "profile_image": {
+ "name": "profile_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferred_weather": {
+ "name": "preferred_weather",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'celsius'"
+ },
+ "preferred_weight": {
+ "name": "preferred_weight",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'lb'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": ["email"],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": ["username"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user_favorite_packs": {
+ "name": "user_favorite_packs",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_favorite_packs_user_id_user_id_fk": {
+ "name": "user_favorite_packs_user_id_user_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_favorite_packs_pack_id_pack_id_fk": {
+ "name": "user_favorite_packs_pack_id_pack_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "pack",
+ "columnsFrom": ["pack_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["pack_id", "user_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "way": {
+ "name": "way",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "way_nodes": {
+ "name": "way_nodes",
+ "columns": {
+ "way_id": {
+ "name": "way_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "node_id": {
+ "name": "node_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "way_nodes_way_id_way_id_fk": {
+ "name": "way_nodes_way_id_way_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "way",
+ "columnsFrom": ["way_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "way_nodes_node_id_node_id_fk": {
+ "name": "way_nodes_node_id_node_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "node",
+ "columnsFrom": ["node_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": ["node_id", "way_id"],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
diff --git a/server/migrations-preview/meta/0002_snapshot.json b/server/migrations-preview/meta/0002_snapshot.json
new file mode 100644
index 000000000..10349858a
--- /dev/null
+++ b/server/migrations-preview/meta/0002_snapshot.json
@@ -0,0 +1,1534 @@
+{
+ "version": "5",
+ "dialect": "sqlite",
+ "id": "a70ff809-b028-4fe7-a94c-31e53cbddaae",
+ "prevId": "0d205b6a-e6e3-4845-a792-97473f08d0d8",
+ "tables": {
+ "conversation": {
+ "name": "conversation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "itemTypeId": {
+ "name": "itemTypeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "history": {
+ "name": "history",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "geojson": {
+ "name": "geojson",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "geoJSON": {
+ "name": "geoJSON",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "weight": {
+ "name": "weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "global": {
+ "name": "global",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sku": {
+ "name": "sku",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_url": {
+ "name": "product_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_details": {
+ "name": "product_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "seller": {
+ "name": "seller",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_category_id_item_category_id_fk": {
+ "name": "item_category_id_item_category_id_fk",
+ "tableFrom": "item",
+ "tableTo": "item_category",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "item_owner_id_user_id_fk": {
+ "name": "item_owner_id_user_id_fk",
+ "tableFrom": "item",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_category": {
+ "name": "item_category",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_image": {
+ "name": "item_image",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_image_item_id_item_id_fk": {
+ "name": "item_image_item_id_item_id_fk",
+ "tableFrom": "item_image",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_owners": {
+ "name": "item_owners",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_owners_item_id_item_id_fk": {
+ "name": "item_owners_item_id_item_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_owners_owner_id_user_id_fk": {
+ "name": "item_owners_owner_id_user_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "owner_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_pack_templates": {
+ "name": "item_pack_templates",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_template_id": {
+ "name": "pack_template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_pack_templates_item_id_item_id_fk": {
+ "name": "item_pack_templates_item_id_item_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_pack_templates_pack_template_id_pack_template_id_fk": {
+ "name": "item_pack_templates_pack_template_id_pack_template_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "pack_template",
+ "columnsFrom": [
+ "pack_template_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "pack_template_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_packs": {
+ "name": "item_packs",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_packs_item_id_item_id_fk": {
+ "name": "item_packs_item_id_item_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_packs_pack_id_pack_id_fk": {
+ "name": "item_packs_pack_id_pack_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "pack_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "node": {
+ "name": "node",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lat": {
+ "name": "lat",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lon": {
+ "name": "lon",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offlineMap": {
+ "name": "offlineMap",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "minZoom": {
+ "name": "minZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "maxZoom": {
+ "name": "maxZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "offlineMap_name_owner_id_unique": {
+ "name": "offlineMap_name_owner_id_unique",
+ "columns": [
+ "name",
+ "owner_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "offlineMap_owner_id_user_id_fk": {
+ "name": "offlineMap_owner_id_user_id_fk",
+ "tableFrom": "offlineMap",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack": {
+ "name": "pack",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "grades": {
+ "name": "grades",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weight\":\"\",\"essentialItems\":\"\",\"redundancyAndVersatility\":\"\"}'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weightScore\":0,\"essentialItemsScore\":0,\"redundancyAndVersatilityScore\":0}'"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pack_owner_id_user_id_fk": {
+ "name": "pack_owner_id_user_id_fk",
+ "tableFrom": "pack",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack_template": {
+ "name": "pack_template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'packTemplate'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "refresh_tokens": {
+ "name": "refresh_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "refresh_tokens_user_id_user_id_fk": {
+ "name": "refresh_tokens_user_id_user_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "relation": {
+ "name": "relation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'relation'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "members": {
+ "name": "members",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "template": {
+ "name": "template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_global_template": {
+ "name": "is_global_template",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "template_created_by_user_id_fk": {
+ "name": "template_created_by_user_id_fk",
+ "tableFrom": "template",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip": {
+ "name": "trip",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parks": {
+ "name": "parks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "packs_id": {
+ "name": "packs_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "activity": {
+ "name": "activity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"totalScore\":0}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_owner_id_user_id_fk": {
+ "name": "trip_owner_id_user_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "trip_packs_id_pack_id_fk": {
+ "name": "trip_packs_id_pack_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "packs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip_geojsons": {
+ "name": "trip_geojsons",
+ "columns": {
+ "trip_id": {
+ "name": "trip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geojson_id": {
+ "name": "geojson_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_geojsons_trip_id_trip_id_fk": {
+ "name": "trip_geojsons_trip_id_trip_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "trip",
+ "columnsFrom": [
+ "trip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "trip_geojsons_geojson_id_geojson_id_fk": {
+ "name": "trip_geojsons_geojson_id_geojson_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "geojson",
+ "columnsFrom": [
+ "geojson_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "geojson_id",
+ "trip_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_certified_guide": {
+ "name": "is_certified_guide",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token": {
+ "name": "password_reset_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token_expiration": {
+ "name": "password_reset_token_expiration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "offline_maps": {
+ "name": "offline_maps",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "profile_image": {
+ "name": "profile_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferred_weather": {
+ "name": "preferred_weather",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'celsius'"
+ },
+ "preferred_weight": {
+ "name": "preferred_weight",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'lb'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user_favorite_packs": {
+ "name": "user_favorite_packs",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_favorite_packs_user_id_user_id_fk": {
+ "name": "user_favorite_packs_user_id_user_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_favorite_packs_pack_id_pack_id_fk": {
+ "name": "user_favorite_packs_pack_id_pack_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "pack_id",
+ "user_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "way": {
+ "name": "way",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "way_nodes": {
+ "name": "way_nodes",
+ "columns": {
+ "way_id": {
+ "name": "way_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "node_id": {
+ "name": "node_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "way_nodes_way_id_way_id_fk": {
+ "name": "way_nodes_way_id_way_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "way",
+ "columnsFrom": [
+ "way_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "way_nodes_node_id_node_id_fk": {
+ "name": "way_nodes_node_id_node_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "node",
+ "columnsFrom": [
+ "node_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "node_id",
+ "way_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/server/migrations-preview/meta/_journal.json b/server/migrations-preview/meta/_journal.json
index b71e74840..d24518431 100644
--- a/server/migrations-preview/meta/_journal.json
+++ b/server/migrations-preview/meta/_journal.json
@@ -8,6 +8,20 @@
"when": 1728379132533,
"tag": "0000_modern_the_professor",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "5",
+ "when": 1728418305717,
+ "tag": "0001_true_red_hulk",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "5",
+ "when": 1729778627679,
+ "tag": "0002_luxuriant_gunslinger",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/server/migrations/0006_opposite_human_torch.sql b/server/migrations/0006_opposite_human_torch.sql
new file mode 100644
index 000000000..824054970
--- /dev/null
+++ b/server/migrations/0006_opposite_human_torch.sql
@@ -0,0 +1,5 @@
+ALTER TABLE item ADD `sku` text;--> statement-breakpoint
+ALTER TABLE item ADD `product_url` text;--> statement-breakpoint
+ALTER TABLE item ADD `description` text;--> statement-breakpoint
+ALTER TABLE item ADD `product_details` text;--> statement-breakpoint
+ALTER TABLE item ADD `seller` text;
\ No newline at end of file
diff --git a/server/migrations/0007_wealthy_next_avengers.sql b/server/migrations/0007_wealthy_next_avengers.sql
new file mode 100644
index 000000000..28966c146
--- /dev/null
+++ b/server/migrations/0007_wealthy_next_avengers.sql
@@ -0,0 +1,18 @@
+CREATE TABLE `item_pack_templates` (
+ `item_id` text,
+ `pack_template_id` text,
+ `quantity` integer DEFAULT 1 NOT NULL,
+ PRIMARY KEY(`item_id`, `pack_template_id`),
+ FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`pack_template_id`) REFERENCES `pack_template`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `pack_template` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `description` text NOT NULL,
+ `type` text DEFAULT 'packTemplate'
+);
+--> statement-breakpoint
+ALTER TABLE item_packs ADD `quantity` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
+ALTER TABLE `item` DROP COLUMN `quantity`;
\ No newline at end of file
diff --git a/server/migrations/meta/0006_snapshot.json b/server/migrations/meta/0006_snapshot.json
new file mode 100644
index 000000000..b58a61089
--- /dev/null
+++ b/server/migrations/meta/0006_snapshot.json
@@ -0,0 +1,1429 @@
+{
+ "version": "5",
+ "dialect": "sqlite",
+ "id": "7a8bb3c3-93eb-4741-8e7d-5e6a9a9df7b9",
+ "prevId": "3ab7eb05-31cb-4f55-9e77-fb8847764038",
+ "tables": {
+ "conversation": {
+ "name": "conversation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "itemTypeId": {
+ "name": "itemTypeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "history": {
+ "name": "history",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "geojson": {
+ "name": "geojson",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "geoJSON": {
+ "name": "geoJSON",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "weight": {
+ "name": "weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "global": {
+ "name": "global",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sku": {
+ "name": "sku",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_url": {
+ "name": "product_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_details": {
+ "name": "product_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "seller": {
+ "name": "seller",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_category_id_item_category_id_fk": {
+ "name": "item_category_id_item_category_id_fk",
+ "tableFrom": "item",
+ "tableTo": "item_category",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "item_owner_id_user_id_fk": {
+ "name": "item_owner_id_user_id_fk",
+ "tableFrom": "item",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_category": {
+ "name": "item_category",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_image": {
+ "name": "item_image",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_image_item_id_item_id_fk": {
+ "name": "item_image_item_id_item_id_fk",
+ "tableFrom": "item_image",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_owners": {
+ "name": "item_owners",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_owners_item_id_item_id_fk": {
+ "name": "item_owners_item_id_item_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_owners_owner_id_user_id_fk": {
+ "name": "item_owners_owner_id_user_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "owner_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_packs": {
+ "name": "item_packs",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_packs_item_id_item_id_fk": {
+ "name": "item_packs_item_id_item_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_packs_pack_id_pack_id_fk": {
+ "name": "item_packs_pack_id_pack_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "pack_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "node": {
+ "name": "node",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lat": {
+ "name": "lat",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lon": {
+ "name": "lon",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offlineMap": {
+ "name": "offlineMap",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "minZoom": {
+ "name": "minZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "maxZoom": {
+ "name": "maxZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "offlineMap_name_owner_id_unique": {
+ "name": "offlineMap_name_owner_id_unique",
+ "columns": [
+ "name",
+ "owner_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "offlineMap_owner_id_user_id_fk": {
+ "name": "offlineMap_owner_id_user_id_fk",
+ "tableFrom": "offlineMap",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack": {
+ "name": "pack",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "grades": {
+ "name": "grades",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weight\":\"\",\"essentialItems\":\"\",\"redundancyAndVersatility\":\"\"}'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weightScore\":0,\"essentialItemsScore\":0,\"redundancyAndVersatilityScore\":0}'"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pack_owner_id_user_id_fk": {
+ "name": "pack_owner_id_user_id_fk",
+ "tableFrom": "pack",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "refresh_tokens": {
+ "name": "refresh_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "refresh_tokens_user_id_user_id_fk": {
+ "name": "refresh_tokens_user_id_user_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "relation": {
+ "name": "relation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'relation'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "members": {
+ "name": "members",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "template": {
+ "name": "template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_global_template": {
+ "name": "is_global_template",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "template_created_by_user_id_fk": {
+ "name": "template_created_by_user_id_fk",
+ "tableFrom": "template",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip": {
+ "name": "trip",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parks": {
+ "name": "parks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "packs_id": {
+ "name": "packs_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "activity": {
+ "name": "activity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"totalScore\":0}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_owner_id_user_id_fk": {
+ "name": "trip_owner_id_user_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "trip_packs_id_pack_id_fk": {
+ "name": "trip_packs_id_pack_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "packs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip_geojsons": {
+ "name": "trip_geojsons",
+ "columns": {
+ "trip_id": {
+ "name": "trip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geojson_id": {
+ "name": "geojson_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_geojsons_trip_id_trip_id_fk": {
+ "name": "trip_geojsons_trip_id_trip_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "trip",
+ "columnsFrom": [
+ "trip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "trip_geojsons_geojson_id_geojson_id_fk": {
+ "name": "trip_geojsons_geojson_id_geojson_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "geojson",
+ "columnsFrom": [
+ "geojson_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "geojson_id",
+ "trip_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_certified_guide": {
+ "name": "is_certified_guide",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token": {
+ "name": "password_reset_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token_expiration": {
+ "name": "password_reset_token_expiration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "offline_maps": {
+ "name": "offline_maps",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "profile_image": {
+ "name": "profile_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferred_weather": {
+ "name": "preferred_weather",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'celsius'"
+ },
+ "preferred_weight": {
+ "name": "preferred_weight",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'lb'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user_favorite_packs": {
+ "name": "user_favorite_packs",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_favorite_packs_user_id_user_id_fk": {
+ "name": "user_favorite_packs_user_id_user_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_favorite_packs_pack_id_pack_id_fk": {
+ "name": "user_favorite_packs_pack_id_pack_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "pack_id",
+ "user_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "way": {
+ "name": "way",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "way_nodes": {
+ "name": "way_nodes",
+ "columns": {
+ "way_id": {
+ "name": "way_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "node_id": {
+ "name": "node_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "way_nodes_way_id_way_id_fk": {
+ "name": "way_nodes_way_id_way_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "way",
+ "columnsFrom": [
+ "way_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "way_nodes_node_id_node_id_fk": {
+ "name": "way_nodes_node_id_node_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "node",
+ "columnsFrom": [
+ "node_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "node_id",
+ "way_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/server/migrations/meta/0007_snapshot.json b/server/migrations/meta/0007_snapshot.json
new file mode 100644
index 000000000..a5668932f
--- /dev/null
+++ b/server/migrations/meta/0007_snapshot.json
@@ -0,0 +1,1534 @@
+{
+ "version": "5",
+ "dialect": "sqlite",
+ "id": "76e62144-1925-48e1-8815-5f26d57095d9",
+ "prevId": "7a8bb3c3-93eb-4741-8e7d-5e6a9a9df7b9",
+ "tables": {
+ "conversation": {
+ "name": "conversation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "itemTypeId": {
+ "name": "itemTypeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "history": {
+ "name": "history",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "geojson": {
+ "name": "geojson",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "geoJSON": {
+ "name": "geoJSON",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "weight": {
+ "name": "weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "global": {
+ "name": "global",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sku": {
+ "name": "sku",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_url": {
+ "name": "product_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_details": {
+ "name": "product_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "seller": {
+ "name": "seller",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_category_id_item_category_id_fk": {
+ "name": "item_category_id_item_category_id_fk",
+ "tableFrom": "item",
+ "tableTo": "item_category",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "item_owner_id_user_id_fk": {
+ "name": "item_owner_id_user_id_fk",
+ "tableFrom": "item",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_category": {
+ "name": "item_category",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_image": {
+ "name": "item_image",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_image_item_id_item_id_fk": {
+ "name": "item_image_item_id_item_id_fk",
+ "tableFrom": "item_image",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "item_owners": {
+ "name": "item_owners",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_owners_item_id_item_id_fk": {
+ "name": "item_owners_item_id_item_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_owners_owner_id_user_id_fk": {
+ "name": "item_owners_owner_id_user_id_fk",
+ "tableFrom": "item_owners",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "owner_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_pack_templates": {
+ "name": "item_pack_templates",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_template_id": {
+ "name": "pack_template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_pack_templates_item_id_item_id_fk": {
+ "name": "item_pack_templates_item_id_item_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_pack_templates_pack_template_id_pack_template_id_fk": {
+ "name": "item_pack_templates_pack_template_id_pack_template_id_fk",
+ "tableFrom": "item_pack_templates",
+ "tableTo": "pack_template",
+ "columnsFrom": [
+ "pack_template_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "pack_template_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "item_packs": {
+ "name": "item_packs",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_packs_item_id_item_id_fk": {
+ "name": "item_packs_item_id_item_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "item",
+ "columnsFrom": [
+ "item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_packs_pack_id_pack_id_fk": {
+ "name": "item_packs_pack_id_pack_id_fk",
+ "tableFrom": "item_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "item_id",
+ "pack_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "node": {
+ "name": "node",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lat": {
+ "name": "lat",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lon": {
+ "name": "lon",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offlineMap": {
+ "name": "offlineMap",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "minZoom": {
+ "name": "minZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "maxZoom": {
+ "name": "maxZoom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "offlineMap_name_owner_id_unique": {
+ "name": "offlineMap_name_owner_id_unique",
+ "columns": [
+ "name",
+ "owner_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "offlineMap_owner_id_user_id_fk": {
+ "name": "offlineMap_owner_id_user_id_fk",
+ "tableFrom": "offlineMap",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack": {
+ "name": "pack",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "grades": {
+ "name": "grades",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weight\":\"\",\"essentialItems\":\"\",\"redundancyAndVersatility\":\"\"}'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"weightScore\":0,\"essentialItemsScore\":0,\"redundancyAndVersatilityScore\":0}'"
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "pack_owner_id_user_id_fk": {
+ "name": "pack_owner_id_user_id_fk",
+ "tableFrom": "pack",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "pack_template": {
+ "name": "pack_template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'packTemplate'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "refresh_tokens": {
+ "name": "refresh_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "refresh_tokens_user_id_user_id_fk": {
+ "name": "refresh_tokens_user_id_user_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "relation": {
+ "name": "relation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'relation'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "members": {
+ "name": "members",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "template": {
+ "name": "template",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pack'"
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_global_template": {
+ "name": "is_global_template",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "template_created_by_user_id_fk": {
+ "name": "template_created_by_user_id_fk",
+ "tableFrom": "template",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip": {
+ "name": "trip",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parks": {
+ "name": "parks",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "packs_id": {
+ "name": "packs_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "activity": {
+ "name": "activity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "bounds": {
+ "name": "bounds",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'trip'"
+ },
+ "scores": {
+ "name": "scores",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"totalScore\":0}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_owner_id_user_id_fk": {
+ "name": "trip_owner_id_user_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "trip_packs_id_pack_id_fk": {
+ "name": "trip_packs_id_pack_id_fk",
+ "tableFrom": "trip",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "packs_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "trip_geojsons": {
+ "name": "trip_geojsons",
+ "columns": {
+ "trip_id": {
+ "name": "trip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geojson_id": {
+ "name": "geojson_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "trip_geojsons_trip_id_trip_id_fk": {
+ "name": "trip_geojsons_trip_id_trip_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "trip",
+ "columnsFrom": [
+ "trip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "trip_geojsons_geojson_id_geojson_id_fk": {
+ "name": "trip_geojsons_geojson_id_geojson_id_fk",
+ "tableFrom": "trip_geojsons",
+ "tableTo": "geojson",
+ "columnsFrom": [
+ "geojson_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "geojson_id",
+ "trip_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "google_id": {
+ "name": "google_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_certified_guide": {
+ "name": "is_certified_guide",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token": {
+ "name": "password_reset_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password_reset_token_expiration": {
+ "name": "password_reset_token_expiration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "offline_maps": {
+ "name": "offline_maps",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "profile_image": {
+ "name": "profile_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "preferred_weather": {
+ "name": "preferred_weather",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'celsius'"
+ },
+ "preferred_weight": {
+ "name": "preferred_weight",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'lb'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user_favorite_packs": {
+ "name": "user_favorite_packs",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pack_id": {
+ "name": "pack_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_favorite_packs_user_id_user_id_fk": {
+ "name": "user_favorite_packs_user_id_user_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_favorite_packs_pack_id_pack_id_fk": {
+ "name": "user_favorite_packs_pack_id_pack_id_fk",
+ "tableFrom": "user_favorite_packs",
+ "tableTo": "pack",
+ "columnsFrom": [
+ "pack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "pack_id",
+ "user_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "way": {
+ "name": "way",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "osm_id": {
+ "name": "osm_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "osm_type": {
+ "name": "osm_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "geo_json": {
+ "name": "geo_json",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "way_nodes": {
+ "name": "way_nodes",
+ "columns": {
+ "way_id": {
+ "name": "way_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "node_id": {
+ "name": "node_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "way_nodes_way_id_way_id_fk": {
+ "name": "way_nodes_way_id_way_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "way",
+ "columnsFrom": [
+ "way_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "way_nodes_node_id_node_id_fk": {
+ "name": "way_nodes_node_id_node_id_fk",
+ "tableFrom": "way_nodes",
+ "tableTo": "node",
+ "columnsFrom": [
+ "node_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "id": {
+ "columns": [
+ "node_id",
+ "way_id"
+ ],
+ "name": "id"
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/server/migrations/meta/_journal.json b/server/migrations/meta/_journal.json
index 9e63443a9..4bc32aad3 100644
--- a/server/migrations/meta/_journal.json
+++ b/server/migrations/meta/_journal.json
@@ -43,6 +43,20 @@
"when": 1727955752820,
"tag": "0005_heavy_the_initiative",
"breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "5",
+ "when": 1728649744203,
+ "tag": "0006_opposite_human_torch",
+ "breakpoints": true
+ },
+ {
+ "idx": 7,
+ "version": "5",
+ "when": 1732718976207,
+ "tag": "0007_wealthy_next_avengers",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/server/src/controllers/item/addGlobalItemToPack.ts b/server/src/controllers/item/addGlobalItemToPack.ts
index b8a1e17ee..3f874b9af 100644
--- a/server/src/controllers/item/addGlobalItemToPack.ts
+++ b/server/src/controllers/item/addGlobalItemToPack.ts
@@ -1,12 +1,17 @@
import { protectedProcedure } from '../../trpc';
import { addGlobalItemToPackService } from '../../services/item/item.service';
-import { z } from 'zod';
import { type Context } from 'hono';
+import * as validators from '@packrat/validations';
export const addGlobalItemToPack = async (c: Context) => {
try {
- const { packId, itemId, ownerId } = await c.req.json();
- const item = await addGlobalItemToPackService(packId, itemId, ownerId);
+ const { packId, itemId, ownerId, quantity } = await c.req.json();
+ const item = await addGlobalItemToPackService(
+ packId,
+ itemId,
+ ownerId,
+ quantity,
+ );
return c.json({ item }, 200);
} catch (error) {
return c.json({ error: `${error.message}` }, 500);
@@ -15,16 +20,15 @@ export const addGlobalItemToPack = async (c: Context) => {
export function addGlobalItemToPackRoute() {
return protectedProcedure
- .input(
- z.object({
- packId: z.string(),
- itemId: z.string(),
- ownerId: z.string(),
- }),
- )
+ .input(validators.addGlobalItemToPack)
.mutation(async (opts) => {
- const { packId, itemId, ownerId } = opts.input;
- const item = await addGlobalItemToPackService(packId, itemId, ownerId);
+ const { packId, itemId, ownerId, quantity } = opts.input;
+ const item = await addGlobalItemToPackService(
+ packId,
+ itemId,
+ ownerId,
+ quantity,
+ );
return item;
});
}
diff --git a/server/src/controllers/item/addItemGlobal.ts b/server/src/controllers/item/addItemGlobal.ts
index 4f8176b2e..21d184bd2 100644
--- a/server/src/controllers/item/addItemGlobal.ts
+++ b/server/src/controllers/item/addItemGlobal.ts
@@ -5,16 +5,17 @@ import * as validator from '@packrat/validations';
export const addItemGlobal = async (c: Context) => {
try {
- const { name, weight, quantity, unit, type, ownerId } = await c.req.json();
+ const { name, weight, unit, type, ownerId } = await c.req.json();
const item = await addItemGlobalService(
- name,
- weight,
- quantity,
- unit,
- type,
- ownerId,
- c.ctx.executionCtx,
+ {
+ name,
+ weight,
+ unit,
+ type,
+ ownerId,
+ },
+ c.executionCtx,
);
return c.json({ item }, 200);
} catch (error) {
@@ -26,19 +27,20 @@ export function addItemGlobalRoute() {
return protectedProcedure
.input(validator.addItemGlobal)
.mutation(async (opts) => {
- const { name, weight, quantity, unit, type, ownerId } = opts.input;
+ const { name, weight, unit, type, ownerId } = opts.input;
if (type !== 'Food' && type !== 'Water' && type !== 'Essentials') {
throw new Error('Invalid item type');
}
const item = await addItemGlobalService(
- name,
- weight,
- quantity,
- unit,
- type,
- ownerId,
+ {
+ name,
+ weight,
+ unit,
+ type,
+ ownerId,
+ },
opts.ctx.executionCtx,
);
return item;
diff --git a/server/src/controllers/item/deleteItemFromPack.ts b/server/src/controllers/item/deleteItemFromPack.ts
new file mode 100644
index 000000000..cf3de979e
--- /dev/null
+++ b/server/src/controllers/item/deleteItemFromPack.ts
@@ -0,0 +1,19 @@
+import { protectedProcedure } from '../../trpc';
+import { z } from 'zod';
+import { type Context } from 'hono';
+import { deleteItemFromPack } from 'src/services/item/deleteItemFromPack';
+
+export function deleteItemFromPackRoute() {
+ return protectedProcedure
+ .input(
+ z.object({
+ packId: z.string(),
+ itemId: z.string(),
+ }),
+ )
+ .mutation(async (opts) => {
+ const { packId, itemId } = opts.input;
+ const item = await deleteItemFromPack(packId, itemId);
+ return item;
+ });
+}
diff --git a/server/src/controllers/item/editGlobalItemAsDuplicate.ts b/server/src/controllers/item/editGlobalItemAsDuplicate.ts
index 86e2bcf50..f75425524 100644
--- a/server/src/controllers/item/editGlobalItemAsDuplicate.ts
+++ b/server/src/controllers/item/editGlobalItemAsDuplicate.ts
@@ -5,15 +5,13 @@ import { protectedProcedure } from '../../trpc';
export const editGlobalItemAsDuplicate = async (c) => {
try {
const { itemId } = await c.req.param();
- const { packId, name, weight, quantity, unit, type } =
- await c.req.parseBody();
+ const { packId, name, weight, unit, type } = await c.req.parseBody();
const item = await editGlobalItemAsDuplicateService(
itemId,
packId,
name,
weight,
- quantity,
unit,
type,
c.ctx.executionCtx,
@@ -35,19 +33,17 @@ export function editGlobalItemAsDuplicateRoute() {
packId: z.string(),
name: z.string(),
weight: z.number(),
- quantity: z.number(),
unit: z.string(),
type: z.string(),
}),
)
.mutation(async (opts) => {
- const { itemId, packId, name, weight, quantity, unit, type } = opts.input;
+ const { itemId, packId, name, weight, unit, type } = opts.input;
const item = await editGlobalItemAsDuplicateService(
itemId,
packId,
name,
weight,
- quantity,
unit,
type,
opts.ctx.executionCtx,
diff --git a/server/src/controllers/item/editItem.ts b/server/src/controllers/item/editItem.ts
index dbee8b393..864c72a21 100644
--- a/server/src/controllers/item/editItem.ts
+++ b/server/src/controllers/item/editItem.ts
@@ -23,7 +23,7 @@ export const editItem = async (c) => {
export function editItemRoute() {
return protectedProcedure.input(validator.editItem).mutation(async (opts) => {
- const { id, name, weight, unit, quantity, type } = opts.input;
+ const { id, packId, name, weight, unit, quantity, type } = opts.input;
if (type !== 'Food' && type !== 'Water' && type !== 'Essentials') {
throw new Error('Invalid item type');
@@ -32,6 +32,7 @@ export function editItemRoute() {
const item = await editItemService(
opts.ctx.executionCtx,
id,
+ packId,
name,
weight,
unit,
diff --git a/server/src/controllers/item/getItemsFeed.ts b/server/src/controllers/item/getItemsFeed.ts
new file mode 100644
index 000000000..04d32c328
--- /dev/null
+++ b/server/src/controllers/item/getItemsFeed.ts
@@ -0,0 +1,29 @@
+import { getPaginationResponse } from 'src/helpers/pagination';
+import { protectedProcedure } from '../../trpc';
+import { z } from 'zod';
+import { getItemsFeedService } from 'src/services/item/getItemsFeedService';
+
+export function getItemsFeedRoute() {
+ return protectedProcedure
+ .input(
+ z.object({
+ queryBy: z.string(),
+ searchTerm: z.string().optional(),
+ pagination: z
+ .object({ limit: z.number(), offset: z.number() })
+ .optional(),
+ }),
+ )
+ .query(async (opts) => {
+ const { queryBy, searchTerm, pagination } = opts.input;
+ const { data, totalCount } = await getItemsFeedService({
+ queryBy,
+ searchTerm,
+ pagination,
+ });
+ return {
+ data,
+ ...getPaginationResponse(pagination, totalCount as number),
+ };
+ });
+}
diff --git a/server/src/controllers/item/getUserItems.ts b/server/src/controllers/item/getUserItems.ts
new file mode 100644
index 000000000..5a08b3cf1
--- /dev/null
+++ b/server/src/controllers/item/getUserItems.ts
@@ -0,0 +1,25 @@
+import { protectedProcedure } from '../../trpc';
+import { z } from 'zod';
+import { getUserItemsService } from 'src/services/item/getUserItemsService';
+export function getUserItemsRoute() {
+ return protectedProcedure
+ .input(
+ z.object({
+ limit: z.number(),
+ page: z.number(),
+ searchString: z.string().optional(),
+ ownerId: z.string(),
+ }),
+ )
+ .query(async (opts) => {
+ const { limit, page, searchString, ownerId } = opts.input;
+ const result = await getUserItemsService(limit, page, {
+ searchString,
+ ownerId,
+ });
+ return {
+ ...result,
+ items: result.items,
+ };
+ });
+}
diff --git a/server/src/controllers/item/importFromBucket.ts b/server/src/controllers/item/importFromBucket.ts
index 3ec08d6de..8852bad70 100644
--- a/server/src/controllers/item/importFromBucket.ts
+++ b/server/src/controllers/item/importFromBucket.ts
@@ -54,7 +54,10 @@ async function importItemsFromBucket(directory, ownerId, env, executionCtx) {
);
const itemsToInsert = await parseCSVData(fileData, ownerId);
- const insertedItems = await bulkAddItemsGlobalService(itemsToInsert, executionCtx);
+ const insertedItems = await bulkAddItemsGlobalService(
+ itemsToInsert,
+ executionCtx,
+ );
return insertedItems;
} catch (err) {
@@ -67,7 +70,12 @@ export const importFromBucket = async (c) => {
const { directory, ownerId } = await c.req.query();
try {
- const insertedItems = await importItemsFromBucket(directory, ownerId, c.env, c.executionCtx);
+ const insertedItems = await importItemsFromBucket(
+ directory,
+ ownerId,
+ c.env,
+ c.executionCtx,
+ );
return c.json({
message: 'Items inserted successfully',
@@ -86,7 +94,12 @@ export function importFromBucketRoute() {
const { env, executionCtx } = opts.ctx;
try {
- const insertedItems = await importItemsFromBucket(directory, ownerId, env, executionCtx);
+ const insertedItems = await importItemsFromBucket(
+ directory,
+ ownerId,
+ env,
+ executionCtx,
+ );
return insertedItems;
} catch (err) {
diff --git a/server/src/controllers/item/importItemsGlobal.ts b/server/src/controllers/item/importItemsGlobal.ts
index ebf67ac93..e29dc511d 100644
--- a/server/src/controllers/item/importItemsGlobal.ts
+++ b/server/src/controllers/item/importItemsGlobal.ts
@@ -1,8 +1,12 @@
import { type Context } from 'hono';
-import { addItemGlobalService } from '../../services/item/item.service';
+import {
+ addItemGlobalService,
+ addItemGlobalServiceBatch,
+} from '../../services/item/item.service';
import { protectedProcedure } from '../../trpc';
import * as validator from '@packrat/validations';
import Papa from 'papaparse';
+import { ItemCategoryEnum } from 'src/utils/itemCategory';
export const importItemsGlobal = async (c: Context) => {
try {
@@ -40,14 +44,15 @@ export const importItemsGlobal = async (c: Context) => {
}
await addItemGlobalService(
- item.Name,
- item.Weight,
- item.Quantity,
- item.Unit,
- item.Category,
- ownerId,
- c.ctx.executionCtx,
- item.image_urls,
+ {
+ name: item.Name,
+ weight: item.Weight,
+ unit: item.Unit,
+ type: item.Category,
+ ownerId,
+ image_urls: item.image_urls,
+ },
+ c.executionCtx,
);
}
resolve('items');
@@ -68,22 +73,26 @@ export const importItemsGlobal = async (c: Context) => {
};
export function importItemsGlobalRoute() {
+ const expectedHeaders = [
+ 'Name',
+ 'Weight',
+ 'Unit',
+ 'Category',
+ 'image_urls',
+ 'sku',
+ 'product_url',
+ 'description',
+ 'techs',
+ 'seller',
+ ] as const;
return protectedProcedure
.input(validator.importItemsGlobal)
.mutation(async (opts) => {
const { content, ownerId } = opts.input;
return new Promise((resolve, reject) => {
- Papa.parse(content, {
+ Papa.parse>(content, {
header: true,
complete: async function (results) {
- const expectedHeaders = [
- 'Name',
- 'Weight',
- 'Unit',
- 'Quantity',
- 'Category',
- 'image_urls',
- ];
const parsedHeaders = results.meta.fields;
try {
const allHeadersPresent = expectedHeaders.every((header) =>
@@ -97,27 +106,59 @@ export function importItemsGlobalRoute() {
);
}
- for (const [index, item] of results.data.entries()) {
- if (
- index === results.data.length - 1 &&
- Object.values(item).every((value) => value === '')
- ) {
- continue;
- }
-
- await addItemGlobalService(
- item.Name,
- item.Weight,
- item.Quantity,
- item.Unit,
- item.Category,
- ownerId,
- opts.ctx.executionCtx,
- item.image_urls,
- );
+ const lastRawItem = results.data[results.data.length - 1];
+ if (
+ lastRawItem &&
+ Object.values(lastRawItem).every((value) => value === '')
+ ) {
+ results.data.pop();
}
+
+ let idx = 0;
+ await addItemGlobalServiceBatch(
+ results.data,
+ (item) => {
+ const productDetailsStr = `${item.techs}`
+ .replace(/'([^']*)'\s*:/g, '"$1":') // Replace single quotes keys with double quotes.
+ .replace(/:\s*'([^']*)'/g, ': "$1"') // Replace single quotes values with double quotes.
+ .replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => {
+ // Replace hex escape sequences with UTF-8 characters
+ const codePoint = parseInt(hex, 16);
+ return String.fromCharCode(codePoint);
+ });
+
+ idx++;
+ console.log(`${idx} / ${results.data.length}`);
+ try {
+ const parsedProductDetails = JSON.parse(productDetailsStr);
+ } catch (e) {
+ console.log(
+ `${productDetailsStr}\nFailed to parse product details for item ${item.Name}: ${e.message}`,
+ );
+ throw e;
+ }
+
+ return {
+ name: String(item.Name),
+ weight: Number(item.Weight),
+ unit: String(item.Unit),
+ type: String(item.Category) as ItemCategoryEnum,
+ ownerId,
+ executionCtx: opts.ctx.executionCtx,
+ image_urls: item.image_urls && String(item.image_urls),
+ sku: item.sku && String(item.sku),
+ productUrl: item.product_url && String(item.product_url),
+ description: item.description && String(item.description),
+ seller: item.seller && String(item.seller),
+ productDetails: JSON.parse(productDetailsStr),
+ };
+ },
+ false,
+ opts.ctx.executionCtx,
+ );
return resolve('items');
} catch (error) {
+ console.error(error);
return reject(new Error(`Failed to add items: ${error.message}`));
}
},
diff --git a/server/src/controllers/item/index.ts b/server/src/controllers/item/index.ts
index 41cdba713..b53b3561b 100644
--- a/server/src/controllers/item/index.ts
+++ b/server/src/controllers/item/index.ts
@@ -15,3 +15,8 @@ export * from './searchItemsByName';
export * from './getSimilarItems';
export * from './importFromBucket';
export * from './importNotifiedETL';
+export * from './getItemsFeed';
+export * from './deleteItemFromPack';
+export * from './toggleItemPack';
+export * from './setItemQuantity';
+export * from './getUserItems';
diff --git a/server/src/controllers/item/setItemQuantity.ts b/server/src/controllers/item/setItemQuantity.ts
new file mode 100644
index 000000000..fa227105e
--- /dev/null
+++ b/server/src/controllers/item/setItemQuantity.ts
@@ -0,0 +1,18 @@
+import { editGlobalItemAsDuplicateService } from '../../services/item/item.service';
+import { z } from 'zod';
+import { protectedProcedure } from '../../trpc';
+import { setItemQuantityService } from 'src/services/item/setItemQuantity';
+export function setItemQuantityRoute() {
+ return protectedProcedure
+ .input(
+ z.object({
+ itemId: z.string(),
+ packId: z.string(),
+ quantity: z.number(),
+ }),
+ )
+ .mutation(async (opts) => {
+ const { itemId, packId, quantity } = opts.input;
+ setItemQuantityService({ itemId, packId, quantity });
+ });
+}
diff --git a/server/src/controllers/item/toggleItemPack.ts b/server/src/controllers/item/toggleItemPack.ts
new file mode 100644
index 000000000..3cb11f84e
--- /dev/null
+++ b/server/src/controllers/item/toggleItemPack.ts
@@ -0,0 +1,18 @@
+import { protectedProcedure } from '../../trpc';
+import { z } from 'zod';
+import { toggleItemPackService } from 'src/services/item/toggleItemPackService';
+
+export function toggleItemPack() {
+ return protectedProcedure
+ .input(
+ z.object({
+ packId: z.string(),
+ itemId: z.string(),
+ }),
+ )
+ .mutation(async (opts) => {
+ const { packId, itemId } = opts.input;
+ const item = await toggleItemPackService({ packId, itemId });
+ return item;
+ });
+}
diff --git a/server/src/controllers/packTemplates/createPackFromTemplate.ts b/server/src/controllers/packTemplates/createPackFromTemplate.ts
new file mode 100644
index 000000000..7345934f5
--- /dev/null
+++ b/server/src/controllers/packTemplates/createPackFromTemplate.ts
@@ -0,0 +1,17 @@
+import * as validator from '@packrat/validations';
+import { createPackFromTemplateService } from 'src/services/packTemplate/packTemplate.service';
+import { protectedProcedure } from 'src/trpc';
+
+export function createPackFromTemplateRoute() {
+ return protectedProcedure
+ .input(validator.createPackFromTemplate)
+ .mutation(async (opts) => {
+ const pack = await createPackFromTemplateService(
+ opts.ctx.user.id,
+ opts.input.packTemplateId,
+ opts.input.newPackName,
+ opts.ctx.executionCtx,
+ );
+ return pack;
+ });
+}
diff --git a/server/src/controllers/packTemplates/getPackTemplate.ts b/server/src/controllers/packTemplates/getPackTemplate.ts
new file mode 100644
index 000000000..41713c24a
--- /dev/null
+++ b/server/src/controllers/packTemplates/getPackTemplate.ts
@@ -0,0 +1,11 @@
+import * as validator from '@packrat/validations';
+import { protectedProcedure } from 'src/trpc';
+import { getPackTemplateService } from 'src/services/packTemplate/packTemplate.service';
+
+export function getPackTemplateRoute() {
+ return protectedProcedure
+ .input(validator.getPackTemplate)
+ .query(async ({ input }) => {
+ return await getPackTemplateService(input.id);
+ });
+}
diff --git a/server/src/controllers/packTemplates/getPackTemplates.ts b/server/src/controllers/packTemplates/getPackTemplates.ts
new file mode 100644
index 000000000..d5741766d
--- /dev/null
+++ b/server/src/controllers/packTemplates/getPackTemplates.ts
@@ -0,0 +1,12 @@
+import * as validator from '@packrat/validations';
+import { getPackTemplatesService } from 'src/services/packTemplate/packTemplate.service';
+import { protectedProcedure } from 'src/trpc';
+
+export function getPackTemplatesRoute() {
+ return protectedProcedure
+ .input(validator.getPackTemplates)
+ .query(async (opts) => {
+ const { filter, orderBy, pagination } = opts.input;
+ return await getPackTemplatesService(pagination, filter, orderBy);
+ });
+}
diff --git a/server/src/controllers/packTemplates/index.ts b/server/src/controllers/packTemplates/index.ts
new file mode 100644
index 000000000..e83211275
--- /dev/null
+++ b/server/src/controllers/packTemplates/index.ts
@@ -0,0 +1,3 @@
+export * from './getPackTemplates';
+export * from './getPackTemplate'
+export * from './createPackFromTemplate';
diff --git a/server/src/controllers/user/getMe.ts b/server/src/controllers/user/getMe.ts
index 970ad5cbd..1cb5ef380 100644
--- a/server/src/controllers/user/getMe.ts
+++ b/server/src/controllers/user/getMe.ts
@@ -5,7 +5,6 @@ import { protectedProcedure } from '../../trpc';
export const getMe = async (c: Context) => {
try {
const { user } = await c.req.json();
- console.log('user ', user);
return c.json({ user }, 200);
} catch (error) {
return c.json({ error: `Failed to get user: ${error.message}` }, 500);
diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts
index 0ac49512d..7ce1a31b8 100644
--- a/server/src/db/schema.ts
+++ b/server/src/db/schema.ts
@@ -172,6 +172,52 @@ export const packRelations = relations(pack, ({ one, many }) => ({
trips: many(trip),
}));
+export const packTemplate = sqliteTable('pack_template', {
+ id: text('id')
+ .primaryKey()
+ .$defaultFn(() => createId()),
+ name: text('name').notNull(),
+ description: text('description').notNull(),
+ type: text('type').default('packTemplate'),
+});
+
+export const packTemplateRelations = relations(packTemplate, ({ many }) => ({
+ itemPackTemplates: many(itemPackTemplates),
+}));
+
+export const itemPackTemplates = sqliteTable(
+ 'item_pack_templates',
+ {
+ itemId: text('item_id').references(() => item.id, { onDelete: 'cascade' }),
+ packTemplateId: text('pack_template_id').references(() => packTemplate.id, {
+ onDelete: 'cascade',
+ }),
+ quantity: integer('quantity').notNull().default(1),
+ },
+ (table) => {
+ return {
+ pkWithCustomName: primaryKey({
+ name: 'id',
+ columns: [table.itemId, table.packTemplateId],
+ }),
+ };
+ },
+);
+
+export const itemPackTemplatesRelations = relations(
+ itemPackTemplates,
+ ({ one }) => ({
+ packTemplate: one(packTemplate, {
+ fields: [itemPackTemplates.packTemplateId],
+ references: [packTemplate.id],
+ }),
+ item: one(item, {
+ fields: [itemPackTemplates.itemId],
+ references: [item.id],
+ }),
+ }),
+);
+
export const itemCategory = sqliteTable('item_category', {
id: text('id')
.primaryKey()
@@ -188,13 +234,13 @@ export const itemCategoryRelations = relations(itemCategory, ({ many }) => ({
items: many(item),
}));
-export const item = sqliteTable('item', {
+export const ITEM_TABLE_NAME = 'item';
+export const item = sqliteTable(ITEM_TABLE_NAME, {
id: text('id')
.primaryKey()
.$defaultFn(() => createId()),
name: text('name').notNull(),
weight: real('weight').notNull(),
- quantity: integer('quantity').notNull(),
unit: text('unit').notNull(),
categoryId: text('category_id').references(() => itemCategory.id, {
onDelete: 'set null',
@@ -203,6 +249,13 @@ export const item = sqliteTable('item', {
onDelete: 'cascade',
}),
global: integer('global', { mode: 'boolean' }).default(false),
+ sku: text('sku'),
+ productUrl: text('product_url'),
+ description: text('description'),
+ productDetails: text('product_details', { mode: 'json' }).$type<{
+ [key: string]: string | number | boolean | null;
+ }>(),
+ seller: text('seller'),
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
// @@map("items"): undefined,
@@ -277,6 +330,7 @@ export const itemPacks = sqliteTable(
{
itemId: text('item_id').references(() => item.id, { onDelete: 'cascade' }),
packId: text('pack_id').references(() => pack.id, { onDelete: 'cascade' }),
+ quantity: integer('quantity').notNull().default(1),
},
(table) => {
return {
@@ -300,11 +354,19 @@ export const itemPacksRelations = relations(itemPacks, ({ one }) => ({
}),
}));
+export const itemImageRelations = relations(itemImage, ({ one }) => ({
+ author: one(item, {
+ fields: [itemImage.itemId],
+ references: [item.id],
+ }),
+}));
+
export const itemRelations = relations(item, ({ one, many }) => ({
category: one(itemCategory, {
fields: [item.categoryId],
references: [itemCategory.id],
}),
+ images: many(itemImage),
itemOwners: many(itemOwners),
itemPacks: many(itemPacks),
}));
@@ -548,6 +610,9 @@ export type InsertTemplate = InferInsertModel;
export const insertTemplateSchema = createInsertSchema(template);
export const selectTemplateSchema = createSelectSchema(template);
+export type PackTemplate = InferSelectModel;
+export const selectPackTemplateSchema = createSelectSchema(packTemplate);
+
export type Pack = InferSelectModel;
export type InsertPack = InferInsertModel;
export const insertPackSchema = createInsertSchema(pack);
diff --git a/server/src/drizzle/methods/Item.ts b/server/src/drizzle/methods/Item.ts
index 249be5815..f2e5bbc18 100644
--- a/server/src/drizzle/methods/Item.ts
+++ b/server/src/drizzle/methods/Item.ts
@@ -1,13 +1,17 @@
import { DbClient } from '../../db/client';
-import { and, count, eq, inArray, like, sql } from 'drizzle-orm';
-import { type InsertItem, item as ItemTable } from '../../db/schema';
-import { and, count, eq, like, sql } from 'drizzle-orm';
-import { type InsertItem, itemPacks, item as ItemTable } from '../../db/schema';
+import { and, asc, count, desc, eq, inArray, like, or, sql } from 'drizzle-orm';
+import {
+ type InsertItem,
+ ITEM_TABLE_NAME,
+ itemPacks,
+ item as ItemTable,
+} from '../../db/schema';
import { scorePackService } from '../../services/pack/scorePackService';
import { ItemPacks } from './ItemPacks';
+import { getPaginationParams, PaginationParams } from 'src/helpers/pagination';
export class Item {
- async create(data: InsertItem, packId?: string) {
+ async create(data: InsertItem) {
try {
const item = await DbClient.instance
.insert(ItemTable)
@@ -15,18 +19,42 @@ export class Item {
.returning()
.get();
- if (packId) {
- const itemPacksClass = new ItemPacks();
- await itemPacksClass.create({ itemId: item.id, packId });
- await this.updateScoreIfNeeded(packId);
- }
-
return item;
} catch (error) {
throw new Error(`Failed to create item: ${error.message}`);
}
}
+ async createPackItem(data: InsertItem, packId: string, quantity: number) {
+ // TODO wrap in transaction
+ const item = await this.create(data);
+
+ const itemPacksClass = new ItemPacks();
+ await itemPacksClass.create({ itemId: item.id, packId, quantity });
+ await this.updateScoreIfNeeded(packId);
+
+ return item;
+ }
+
+ async updatePackItem(
+ id: string,
+ data: Partial,
+ packId: string,
+ quantity?: number,
+ ) {
+ const item = await this.update(id, data);
+
+ if (quantity)
+ await DbClient.instance
+ .update(itemPacks)
+ .set({ quantity })
+ .where(and(eq(itemPacks.packId, packId), eq(itemPacks.itemId, id)));
+
+ await this.updateScoreIfNeeded(packId);
+
+ return item;
+ }
+
async createBulk(data: InsertItem[]) {
try {
const insertedItems = [];
@@ -56,15 +84,6 @@ export class Item {
.where(filter)
.returning()
.get();
- const packIds = await DbClient.instance
- .select()
- .from(itemPacks)
- .where(eq(itemPacks.itemId, item.id))
- .all();
-
- for (const { packId } of packIds) {
- await this.updateScoreIfNeeded(packId);
- }
return item;
} catch (error) {
@@ -72,8 +91,9 @@ export class Item {
}
}
- async delete(id: string, filter = eq(ItemTable.id, id), packId?: string) {
+ async delete(id: string, filter = eq(ItemTable.id, id)) {
try {
+ const { packId } = await new ItemPacks().find({ itemId: id });
const deletedItem = await DbClient.instance
.delete(ItemTable)
.where(filter)
@@ -96,6 +116,11 @@ export class Item {
const item = await DbClient.instance.query.item.findFirst({
where: filter,
with: {
+ images: {
+ columns: {
+ url: true,
+ },
+ },
category: {
columns: {
id: true,
@@ -141,11 +166,44 @@ export class Item {
}
}
+ async findUserItems(
+ ownerId: string,
+ searchString: string,
+ { limit, offset }: { limit: number; offset: number },
+ ) {
+ try {
+ const items = await DbClient.instance.query.item.findMany({
+ where: and(
+ or(eq(ItemTable.global, true), eq(ItemTable.ownerId, ownerId)),
+ like(ItemTable.name, `%${searchString}%`),
+ ),
+ with: {
+ category: {
+ columns: { id: true, name: true },
+ },
+ },
+ offset,
+ limit,
+ orderBy: (item, { desc }) => desc(item.createdAt),
+ });
+ return items;
+ } catch (error) {
+ throw new Error(`Failed to find user items: ${error.message}`);
+ }
+ }
+
async findAllInArray(arr: string[]) {
- return await DbClient.instance
- .select()
- .from(ItemTable)
- .where(inArray(ItemTable.id, arr));
+ return await DbClient.instance.query.item.findMany({
+ where: inArray(ItemTable.id, arr),
+ with: {
+ category: {
+ columns: { id: true, name: true },
+ },
+ images: {
+ columns: { url: true },
+ },
+ },
+ });
}
async findGlobal(limit: number, offset: number, searchString: string) {
@@ -170,6 +228,52 @@ export class Item {
}
}
+ async findFeed(filters: {
+ pagination?: PaginationParams;
+ searchTerm?: string;
+ queryBy?: string;
+ }) {
+ try {
+ const { pagination, searchTerm, queryBy } = filters;
+ const { limit, offset } = getPaginationParams(pagination);
+ const orderByFunction = this.applyFeedOrdersOrders(queryBy);
+ const items = await DbClient.instance.query.item.findMany({
+ where: and(
+ eq(ItemTable.global, true),
+ like(ItemTable.name, `%${searchTerm}%`),
+ ),
+ with: {
+ category: {
+ columns: { id: true, name: true },
+ },
+ images: {
+ columns: { url: true },
+ },
+ },
+ offset,
+ limit,
+ orderBy: orderByFunction,
+ });
+
+ const totalCountQuery = await DbClient.instance
+ .select({
+ totalCount: sql`COUNT(*)`,
+ })
+ .from(ItemTable)
+ .where(
+ and(
+ eq(ItemTable.global, true),
+ like(ItemTable.name, `%${searchTerm}%`),
+ ),
+ )
+ .all();
+
+ return { data: items, totalCount: totalCountQuery?.[0]?.totalCount || 0 };
+ } catch (error) {
+ throw new Error(`Failed to find global items: ${error.message}`);
+ }
+ }
+
async findItemsByName(name: string) {
try {
const searchName = `%${name}%`;
@@ -208,4 +312,17 @@ export class Item {
await scorePackService(packId);
}
+
+ applyFeedOrdersOrders(queryBy: string) {
+ if (!['Most Recent', 'Oldest'].includes(queryBy)) {
+ return desc(ItemTable.createdAt);
+ }
+
+ const orderConfig = {
+ 'Most Recent': desc(ItemTable.createdAt),
+ Oldest: asc(ItemTable.createdAt),
+ };
+
+ return orderConfig[queryBy];
+ }
}
diff --git a/server/src/drizzle/methods/ItemPacks.ts b/server/src/drizzle/methods/ItemPacks.ts
index 96e885f84..563705f45 100644
--- a/server/src/drizzle/methods/ItemPacks.ts
+++ b/server/src/drizzle/methods/ItemPacks.ts
@@ -4,6 +4,7 @@ import {
itemPacks as ItemPacksTable,
type InsertItemPack,
} from '../../db/schema';
+import { scorePackService } from 'src/services/pack/scorePackService';
export class ItemPacks {
async create(itemPack: InsertItemPack) {
@@ -13,6 +14,8 @@ export class ItemPacks {
.values(itemPack)
.returning()
.get();
+ await this.updateScoreIfNeeded(itemPack.packId);
+
return record;
} catch (error) {
throw new Error(`Failed to create item pack record: ${error.message}`);
@@ -31,12 +34,60 @@ export class ItemPacks {
)
.returning()
.get();
+ await this.updateScoreIfNeeded(packId);
+
return deletedRecord;
} catch (error) {
throw new Error(`Failed to delete item pack record: ${error.message}`);
}
}
+ async find({ itemId, packId }: { itemId?: string; packId?: string }) {
+ const itemFilter = itemId ? eq(ItemPacksTable.itemId, itemId) : undefined;
+ const packFilter = packId ? eq(ItemPacksTable.packId, packId) : undefined;
+
+ let filter;
+ if (itemId && packId) {
+ filter = and(itemFilter!, packFilter!);
+ } else if (itemId) {
+ filter = itemFilter;
+ } else if (packId) {
+ filter = packFilter;
+ }
+
+ return await DbClient.instance.query.itemPacks.findFirst({
+ where: filter,
+ });
+ }
+
+ async toggle(itemId: string, packId: string) {
+ try {
+ const existingRecord = await DbClient.instance
+ .select()
+ .from(ItemPacksTable)
+ .where(
+ and(
+ eq(ItemPacksTable.itemId, itemId),
+ eq(ItemPacksTable.packId, packId),
+ ),
+ )
+ .get();
+
+ if (existingRecord) {
+ const deletedRecord = await this.delete(itemId, packId);
+
+ return deletedRecord;
+ }
+
+ const newRecord = await this.create({ itemId, packId });
+ await this.updateScoreIfNeeded(packId);
+
+ return newRecord;
+ } catch (error) {
+ throw new Error(`Failed to delete item pack record: ${error.message}`);
+ }
+ }
+
async updateRelation({ oldItemId, newItemId, packId }) {
await DbClient.instance
.delete(ItemPacksTable)
@@ -48,10 +99,28 @@ export class ItemPacks {
)
.execute();
const newRelation = { itemId: newItemId, packId };
- await await DbClient.instance
+ await DbClient.instance
.insert(ItemPacksTable)
.values(newRelation)
.returning()
.execute();
}
+
+ async setItemQuantity({ packId, itemId, quantity }) {
+ await DbClient.instance
+ .update(ItemPacksTable)
+ .set({ quantity })
+ .where(
+ and(
+ eq(ItemPacksTable.packId, packId),
+ eq(ItemPacksTable.itemId, itemId),
+ ),
+ );
+ }
+
+ async updateScoreIfNeeded(packId?: string) {
+ if (!packId) return;
+
+ await scorePackService(packId);
+ }
}
diff --git a/server/src/drizzle/methods/PackTemplate.ts b/server/src/drizzle/methods/PackTemplate.ts
new file mode 100644
index 000000000..1413bd8b8
--- /dev/null
+++ b/server/src/drizzle/methods/PackTemplate.ts
@@ -0,0 +1,105 @@
+import { asc, count, desc, eq, like, sql } from 'drizzle-orm';
+import { DbClient } from '../../db/client';
+import { convertWeight, type WeightUnit } from 'src/utils/convertWeight';
+import { packTemplate, item, itemPackTemplates } from 'src/db/schema';
+import { PaginationParams } from 'src/helpers/pagination';
+
+export type Filter = {
+ searchQuery?: string;
+};
+
+export type ORDER_BY = 'Lightest' | 'Heaviest';
+
+export class PackTemplate {
+ async findMany({
+ filter,
+ orderBy,
+ pagination,
+ }: {
+ filter?: Filter;
+ orderBy?: ORDER_BY;
+ pagination: PaginationParams;
+ }) {
+ const query = DbClient.instance
+ .select({
+ id: packTemplate.id,
+ name: packTemplate.name,
+ type: packTemplate.type,
+ description: packTemplate.description,
+ total_weight:
+ sql`SUM(${item.weight} * ${itemPackTemplates.quantity})`.as(
+ 'total_weight',
+ ),
+ quantity: sql`SUM(${itemPackTemplates.quantity})`,
+ })
+ .from(packTemplate)
+ .leftJoin(
+ itemPackTemplates,
+ eq(packTemplate.id, itemPackTemplates.packTemplateId),
+ )
+ .leftJoin(item, eq(itemPackTemplates.itemId, item.id))
+ .groupBy(packTemplate.id);
+
+ if (filter?.searchQuery) {
+ query.where(like(packTemplate.name, `%${filter.searchQuery}%`));
+ }
+
+ if (orderBy) {
+ query.orderBy(
+ orderBy === 'Lightest'
+ ? asc(sql`total_weight`)
+ : desc(sql`total_weight`),
+ );
+ }
+
+ const totalCountResult = await DbClient.instance
+ .select({ count: count() })
+ .from(query.as('pack_templates'))
+ .all();
+
+ const data = await query
+ .offset(pagination.offset)
+ .limit(pagination.limit)
+ .all();
+
+ return {
+ data,
+ totalCount: totalCountResult[0].count,
+ };
+ }
+
+ async findPackTemplate(id: string) {
+ const packTemplateResult =
+ await DbClient.instance.query.packTemplate.findFirst({
+ where: eq(packTemplate.id, id),
+ with: {
+ itemPackTemplates: { with: { item: { with: { category: {} } } } },
+ },
+ });
+
+ const items = packTemplateResult.itemPackTemplates.map(
+ (itemPackTemplate) => ({
+ ...itemPackTemplate.item,
+ quantity: itemPackTemplate.quantity,
+ }),
+ );
+ const total_weight = items.reduce((sum, item) => {
+ const weightInGrams = convertWeight(
+ item.weight,
+ item.unit as WeightUnit,
+ 'g',
+ );
+ return sum + weightInGrams * item.quantity;
+ }, 0);
+ const quantity = items.reduce((sum, item) => sum + item.quantity, 0);
+
+ delete packTemplateResult.itemPackTemplates;
+
+ return {
+ ...packTemplateResult,
+ items,
+ total_weight,
+ quantity,
+ };
+ }
+}
diff --git a/server/src/drizzle/methods/pack.ts b/server/src/drizzle/methods/pack.ts
index 1b9d8b243..9bf2ad83f 100644
--- a/server/src/drizzle/methods/pack.ts
+++ b/server/src/drizzle/methods/pack.ts
@@ -18,7 +18,7 @@ export class Pack {
userFavoritePacks: { columns: { userId: true } },
itemPacks: completeItems
? {
- columns: { packId: true },
+ columns: { packId: true, quantity: true },
with: {
item: {
columns: {
@@ -26,7 +26,6 @@ export class Pack {
name: true,
ownerId: true,
weight: true,
- quantity: true,
unit: true,
},
with: {
diff --git a/server/src/index.ts b/server/src/index.ts
index dcc41b6fa..cfc433cb8 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -46,6 +46,7 @@ app.use('*', securityHeaders()); // Apply to all routes so avoid commenting this
// SETUP CORS
app.use('*', async (c, next) => {
const CORS_ORIGIN = String(c.env.CORS_ORIGIN);
+ console.log('SETUP CORS');
const corsMiddleware = cors({
// origin: CORS_ORIGIN, // uncomment this line to enable CORS
origin: '*', // temporary
diff --git a/server/src/integrations/ai/client.ts b/server/src/integrations/ai/client.ts
index 0217e8563..4f9a4c7d2 100644
--- a/server/src/integrations/ai/client.ts
+++ b/server/src/integrations/ai/client.ts
@@ -32,7 +32,7 @@ class AiClient {
}
}
- public async run(text: string) {
+ public async run(text: string | string[]) {
const response = await fetch(this.EXECUTE_AI_MODEL_URL, {
method: 'POST',
headers: {
@@ -52,6 +52,40 @@ class AiClient {
return await response.json();
}
+ /**
+ * Transform a list of given text into a list of compact vectors.
+ * @param {string[]} contentList - List of text to transform.
+ * @returns {Promise} - List of compact vectors.
+ */
+ public static async getEmbeddingBash(
+ contentList: string[],
+ transform: (embedding: number[], index: number) => T,
+ ): Promise {
+ const MAX_BATCH_SIZE = 100; // REF: https://developers.cloudflare.com/workers-ai/models/bge-base-en-v1.5/
+ const MAX_ROUND = Math.ceil(contentList.length / MAX_BATCH_SIZE);
+
+ const result: T[] = [];
+
+ for (let round = 0; round < MAX_ROUND; round++) {
+ const batch = contentList.slice(
+ round * MAX_BATCH_SIZE,
+ (round + 1) * MAX_BATCH_SIZE,
+ );
+
+ const {
+ result: { data },
+ } = await AiClient.instance.run(batch);
+
+ // Flatten the result
+ for (let idx = 0; idx < batch.length; idx++) {
+ const embedding = data[idx];
+ result.push(transform(embedding, round * MAX_BATCH_SIZE + idx));
+ }
+ }
+
+ return result;
+ }
+
public static async getEmbedding(content: string): Promise {
const {
result: { data },
diff --git a/server/src/modules/feed/controllers/getUserPacksFeed.ts b/server/src/modules/feed/controllers/getUserPacksFeed.ts
index 934b08bc0..d419690be 100644
--- a/server/src/modules/feed/controllers/getUserPacksFeed.ts
+++ b/server/src/modules/feed/controllers/getUserPacksFeed.ts
@@ -9,6 +9,7 @@ export function getUserPacksFeedRoute() {
z.object({
queryBy: z.string(),
ownerId: z.string(),
+ itemId: z.string().optional(),
isPublic: z.boolean().optional(),
isPreview: z.boolean().optional(),
searchTerm: z.string().optional(),
@@ -18,10 +19,11 @@ export function getUserPacksFeedRoute() {
}),
)
.query(async (opts) => {
- const { queryBy, searchTerm, ownerId, pagination, isPublic } = opts.input;
+ const { queryBy, searchTerm, ownerId, pagination, isPublic, itemId } =
+ opts.input;
const { data, totalCount, currentPagination } = await getFeedService(
queryBy,
- { searchTerm, ownerId, isPublic },
+ { searchTerm, ownerId, isPublic, itemId },
'trips',
pagination,
);
diff --git a/server/src/modules/feed/model/feed.ts b/server/src/modules/feed/model/feed.ts
index 223c97222..5540901e2 100644
--- a/server/src/modules/feed/model/feed.ts
+++ b/server/src/modules/feed/model/feed.ts
@@ -79,9 +79,12 @@ export class Feed {
description: literal(''),
destination: literal(''),
favorites_count: sql`COALESCE(COUNT(DISTINCT ${userFavoritePacks.userId}), 0) as favorites_count`,
- quantity: sql`COALESCE(SUM(DISTINCT ${item.quantity}), 0)`,
+ quantity: sql`COALESCE(SUM(${itemPacks.quantity}), 0)`,
userFavorites: sql`GROUP_CONCAT(DISTINCT ${userFavoritePacks.userId}) as userFavorites`,
- total_weight: sql`COALESCE(SUM(DISTINCT ${item.weight} * ${item.quantity}), 0) as total_weight`,
+ total_weight: sql`COALESCE(SUM(${item.weight} * ${itemPacks.quantity}), 0) as total_weight`,
+ hasItem: modifiers.itemId
+ ? sql`CASE WHEN COUNT(DISTINCT CASE WHEN ${itemPacks.itemId} = ${modifiers.itemId} THEN 1 ELSE NULL END) > 0 THEN TRUE ELSE FALSE END as hasItem`
+ : literal(null),
activity: literal(null),
start_date: literal(null),
end_date: literal(null),
@@ -120,6 +123,7 @@ export class Feed {
quantity: literal(null),
userFavorites: literal('[]'),
total_weight: literal('0'),
+ hasItem: literal(null),
activity: trip.activity,
start_date: trip.start_date,
end_date: trip.end_date,
diff --git a/server/src/modules/feed/models.ts b/server/src/modules/feed/models.ts
index c82a3294d..daa1c5b27 100644
--- a/server/src/modules/feed/models.ts
+++ b/server/src/modules/feed/models.ts
@@ -2,6 +2,7 @@ export interface Modifiers {
isPublic?: boolean;
ownerId?: string;
searchTerm?: string;
+ itemId?: string;
includeUserFavoritesOnly?: boolean;
}
diff --git a/server/src/modules/map/controllers/saveOfflineMap.ts b/server/src/modules/map/controllers/saveOfflineMap.ts
index e6ceec60e..5eda96e31 100644
--- a/server/src/modules/map/controllers/saveOfflineMap.ts
+++ b/server/src/modules/map/controllers/saveOfflineMap.ts
@@ -12,7 +12,7 @@ export function saveOfflineMapRoute() {
minZoom: z.number(),
maxZoom: z.number(),
owner_id: z.string(),
- metadata: z.object({ shape: z.string() }),
+ metadata: z.object({ shape: z.string(), userId: z.string() }),
}),
)
.mutation(async (opts) => {
diff --git a/server/src/modules/map/models.ts b/server/src/modules/map/models.ts
index 76612784e..a001ab67a 100644
--- a/server/src/modules/map/models.ts
+++ b/server/src/modules/map/models.ts
@@ -5,6 +5,6 @@ export interface OfflineMap {
minZoom: number;
maxZoom: number;
metadata: {
- shape: string;
+ userId: string;
};
}
diff --git a/server/src/modules/map/services/saveOfflineMapService.ts b/server/src/modules/map/services/saveOfflineMapService.ts
index 6d3e595ac..6f98feb11 100644
--- a/server/src/modules/map/services/saveOfflineMapService.ts
+++ b/server/src/modules/map/services/saveOfflineMapService.ts
@@ -10,18 +10,20 @@ export const saveOfflineMapService = async (
executionCtx: ExecutionContext,
) => {
try {
- const { metadata, ...offlineMapData } = offlineMap;
+ const {
+ metadata: { shape, ...metadata },
+ ...offlineMapData
+ } = offlineMap;
const offlineMapClass = new OfflineMap();
- console.log({ offlineMapData });
const newOfflineMap = await offlineMapClass.create({
- metadata: null,
+ metadata,
...offlineMapData,
});
executionCtx.waitUntil(
GeojsonStorageService.save(
'map',
- JSON.stringify(metadata.shape),
+ JSON.stringify(shape),
newOfflineMap.id,
),
);
diff --git a/server/src/routes/trpcRouter.ts b/server/src/routes/trpcRouter.ts
index 49d2e21e8..97d36e693 100644
--- a/server/src/routes/trpcRouter.ts
+++ b/server/src/routes/trpcRouter.ts
@@ -75,6 +75,11 @@ import {
searchItemsByNameRoute,
getSimilarItemsRoute,
importFromBucketRoute,
+ getItemsFeedRoute,
+ deleteItemFromPackRoute,
+ toggleItemPack,
+ setItemQuantityRoute,
+ getUserItemsRoute,
} from '../controllers/item';
import { getTrailsRoute } from '../controllers/getTrail';
import { getParksRoute } from '../controllers/getParks';
@@ -92,13 +97,20 @@ import {
getTrailsOSMRoute,
postSingleGeoJSONRoute,
} from '../controllers/getOsm';
-
-import { router as trpcRouter } from '../trpc';
import {
getPublicFeedRoute,
getUserPacksFeedRoute,
getUserTripsFeedRoute,
} from '../modules/feed';
+import {
+ getPackTemplatesRoute,
+ getPackTemplateRoute,
+ createPackFromTemplateRoute,
+} from '../controllers/packTemplates';
+
+import { router as trpcRouter } from '../trpc';
+
+import { getOfflineMapsRoute, saveOfflineMapRoute } from '../modules/map';
import { getOfflineMapsRoute, saveOfflineMapRoute } from '../modules/map';
@@ -136,6 +148,9 @@ export const appRouter = trpcRouter({
editTrip: editTripRoute(),
deleteTrip: deleteTripRoute(),
// templates routes
+ getPackTemplates: getPackTemplatesRoute(),
+ getPackTemplate: getPackTemplateRoute(),
+ createPackFromTemplate: createPackFromTemplateRoute(),
getTemplates: getTemplatesRoute(),
getTemplateById: getTemplateByIdRoute(),
addTemplate: addTemplateRoute(),
@@ -178,11 +193,16 @@ export const appRouter = trpcRouter({
addItemGlobal: addItemGlobalRoute(), // Done
importItemsGlobal: importItemsGlobalRoute(), // Done
getItemsGlobally: getItemsGloballyRoute(), // Done
+ getUserItems: getUserItemsRoute(), // Done
addGlobalItemToPack: addGlobalItemToPackRoute(), // Done
editGlobalItemAsDuplicate: editGlobalItemAsDuplicateRoute(), // Not Implemented
deleteGlobalItem: deleteGlobalItemRoute(), // Done,
+ deleteItemFromPack: deleteItemFromPackRoute(),
getSimilarItems: getSimilarItemsRoute(),
importFromBucket: importFromBucketRoute(),
+ getItemsFeed: getItemsFeedRoute(),
+ toggleItemPack: toggleItemPack(),
+ setItemQuantity: setItemQuantityRoute(),
// trails routes
getTrails: getTrailsRoute(),
// // parks route
diff --git a/server/src/services/item/addGlobalItemToPackService.ts b/server/src/services/item/addGlobalItemToPackService.ts
index aa1b1485e..4a168fbbf 100644
--- a/server/src/services/item/addGlobalItemToPackService.ts
+++ b/server/src/services/item/addGlobalItemToPackService.ts
@@ -17,6 +17,7 @@ export const addGlobalItemToPackService = async (
packId: string,
itemId: string,
ownerId: string,
+ quantity: number,
) => {
const itemClass = new Item();
const itemPacksClass = new ItemPacks();
@@ -27,15 +28,5 @@ export const addGlobalItemToPackService = async (
if (!item) {
throw new Error('Global Item does not exist!');
}
- const { id, ...duplicatedItemValues } = item;
- const newItem = await itemClass.create(
- {
- ...duplicatedItemValues,
- global: false,
- ownerId,
- },
- packId,
- );
-
- return newItem;
+ await itemPacksClass.create({ itemId, packId });
};
diff --git a/server/src/services/item/addItemGlobalService.ts b/server/src/services/item/addItemGlobalService.ts
index 594bdb85c..01b8af03c 100644
--- a/server/src/services/item/addItemGlobalService.ts
+++ b/server/src/services/item/addItemGlobalService.ts
@@ -1,39 +1,69 @@
import { type ExecutionContext } from 'hono';
-import { type InsertItemCategory } from '../../db/schema';
-import { Item } from '../../drizzle/methods/Item';
+import {
+ Item,
+ ITEM_TABLE_NAME,
+ type InsertItemCategory,
+} from '../../db/schema';
+import { Item as ItemClass } from '../../drizzle/methods/Item';
import { ItemCategory } from '../../drizzle/methods/itemcategory';
import { ItemCategory as categories } from '../../utils/itemCategory';
import { VectorClient } from '../../vector/client';
import { convertWeight, SMALLEST_WEIGHT_UNIT } from 'src/utils/convertWeight';
import { DbClient } from 'src/db/client';
import { itemImage as itemImageTable } from '../../db/schema';
+import { summarizeItem } from 'src/utils/item';
// import { prisma } from '../../prisma';
+interface AddItemGlobalServiceParams {
+ /** The name of the item. */
+ name: string;
+ /** The description of the item. */
+ description?: string;
+ /** The weight of the item. */
+ weight: number;
+ /** The unit of measurement for the item. */
+ unit: string;
+ /** The category of the item. */
+ type: (typeof categories)[number];
+ /** The ID of the owner of the item. */
+ ownerId: string;
+ /** The URLs of the images of the item. */
+ image_urls?: string;
+ /** The SKU of the item. */
+ sku?: string;
+ /** The URL of the product of the item. */
+ productUrl?: string;
+ /** The product details of the item. */
+ productDetails?: Record;
+ /** The seller of the item. */
+ seller?: string;
+}
+
/**
* Adds an item to the global service.
- * @param {PrismaClient} prisma - Prisma client.
- * @param {string} name - The name of the item.
- * @param {number} weight - The weight of the item.
- * @param {number} quantity - The quantity of the item.
- * @param {string} unit - The unit of measurement for the item.
- * @param {string} type - The category of the item.
* @return {Promise