diff --git a/.gitignore b/.gitignore index 0f5b548..3f74850 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ coverage-final.json build/ # MongoDB data directory -/data/ \ No newline at end of file +/data/ +.vercel diff --git a/README.md b/README.md index a8d9790..c4a38af 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ MONGO_PASSWORD="dbUserPassword" SUPABASE_PASSWORD="p4JeQ2sCH3OXC1jP" SUPABASE_URL="https://npyvbmfnusakklalqxcz.supabase.co" SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5weXZibWZudXNha2tsYWxxeGN6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjkyODg2MTgsImV4cCI6MjA0NDg2NDYxOH0.P_gUi5uPuALkeXtHeWKYrPDVaIyESW5BQS_NvdvRkNs" +IMAGE_KIT_ID=https://ik.imagekit.io/bfd79mqcsx/tr:w-300,h-300/ ``` ### 4. Running the Application diff --git a/backend/src/config/config.ts b/backend/src/config/config.ts index efb577f..fbaec9c 100644 --- a/backend/src/config/config.ts +++ b/backend/src/config/config.ts @@ -11,6 +11,7 @@ const AWS_PUBLIC_KEY = process.env.PUBLIC_KEY_AWS || ''; const AWS_PRIVATE_KEY = process.env.SECRET_KEY_AWS || ''; const AWS_BUCKET_NAME = process.env.S3BUCKETNAME || ''; const AWS_BUCKET_REGION = process.env.S3BUCKETREGION || ''; +const IMAGE_KIT = process.env.IMAGE_KIT_ID || ''; const SERVER_PORT = process.env.SERVER_PORT ? Number(process.env.SERVER_PORT) @@ -38,4 +39,7 @@ export const config = { name: AWS_BUCKET_NAME, region: AWS_BUCKET_REGION, }, + imagekit : { + key : IMAGE_KIT + } }; diff --git a/backend/src/controllers/species/get.ts b/backend/src/controllers/species/get.ts index 0995e36..5f918ed 100644 --- a/backend/src/controllers/species/get.ts +++ b/backend/src/controllers/species/get.ts @@ -1,5 +1,6 @@ import express from 'express'; import { Species } from '../../models/species'; +import { DEFAULT_LIMIT, DEFAULT_PAGE } from '../../consts/pagination'; export const getById = async (req: express.Request, res: express.Response) => { const id = req.params.id; @@ -38,3 +39,55 @@ export const searchSpecies = async ( return res.status(500).json({ message: 'Internal server error' }); } }; + +export const searchAndPaginatedSpecies = async ( + req: express.Request, + res: express.Response, +) => { + try { + const { text = '', page = DEFAULT_PAGE, limit = DEFAULT_LIMIT } = req.query; + const pageSize = parseInt(page as string); + const limitSize = parseInt(limit as string); + + if (pageSize < 1 || limitSize < 1) { + return res.status(400).json({ + error: + 'Invalid pagination parameters. Page and limit must be positive numbers.', + }); + } + + const skip = (pageSize - 1) * limitSize; + + const query = [ + { + $search: { + index: 'commonNames', + autocomplete: { + query: text, + path: 'commonNames', + fuzzy: { + maxEdits: 2, + prefixLength: 0, + maxExpansions: 50, + }, + }, + }, + }, + { $skip: skip }, + { $limit: limitSize }, + ]; + + const results = await Species.aggregate(query); + + return res.status(200).json({ + page: pageSize, + limit: limitSize, + results, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + error: 'An error occurred while fetching users.', + }); + } +}; diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index dfcc0a9..4c5f2d1 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -2,6 +2,7 @@ import express from 'express'; import { getById, getByScientificName, + searchAndPaginatedSpecies, searchSpecies, } from '../controllers/species/get'; import { isAuthenticated } from '../middlewares/authMiddleware'; @@ -53,4 +54,5 @@ export default (router: express.Router) => { getByScientificName, ); router.get('/species/search/:searchRequest', isAuthenticated, searchSpecies); + router.get('/species/paginated', isAuthenticated, searchAndPaginatedSpecies); }; diff --git a/backend/src/services/s3Service.ts b/backend/src/services/s3Service.ts index c5450f7..2ab9f87 100644 --- a/backend/src/services/s3Service.ts +++ b/backend/src/services/s3Service.ts @@ -60,7 +60,7 @@ export class S3ServiceImpl implements S3Service { try { await this.client.send(command); const url = encodeURI( - `https://${this.name}.s3.${this.region}.amazonaws.com/${fileKey}`, + config.imagekit.key + fileKey ); return url; } catch (error) { diff --git a/backend/src/services/userService.ts b/backend/src/services/userService.ts index a8c227a..ab933c7 100644 --- a/backend/src/services/userService.ts +++ b/backend/src/services/userService.ts @@ -331,9 +331,9 @@ export class UserServiceImpl implements UserService { ], }, }, + { $sort: { date: -1 } }, { $skip: (page - 1) * limit }, { $limit: limit }, - { $sort: { date: -1 } }, { $lookup: { from: 'users', diff --git a/frontend/app.json b/frontend/app.json index 248a9be..3aa2938 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -16,7 +16,6 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "com.generate.snapper", - "googleMapsApiKey": "process.env.GOOGLE_MAPS_API_KEY", "infoPlist": { "NSUserNotificationUsageDescription": "Snapper would like to send you notifications." } diff --git a/frontend/app/(app)/(tabs)/explore.tsx b/frontend/app/(app)/(tabs)/explore.tsx index e9b154d..6e24d78 100644 --- a/frontend/app/(app)/(tabs)/explore.tsx +++ b/frontend/app/(app)/(tabs)/explore.tsx @@ -31,9 +31,6 @@ export default function Explore() { ); const [coordinate, setCoordinate] = useState([200, 200]); const [mapSearch, setMapSearch] = useState(''); - const changeText = (input: string) => { - setSearch(input); - }; const changeMapText = () => { const delimeter = ' '; @@ -62,6 +59,8 @@ export default function Explore() { } }; + //These should be hooks but oh well. + /** * On Query, will pattern match against the toggle options which will * trigger different endpoints @@ -72,7 +71,7 @@ export default function Explore() { let endpoint; switch (toggle) { case 'Fish': - endpoint = `/species/search/${search}`; + endpoint = `/species/paginated?text=${search}`; break; case 'Posts': endpoint = `/divelogs/search?text=${search}`; @@ -85,7 +84,7 @@ export default function Explore() { }; const { isPending, error, data } = useQuery({ - queryKey: ['search', search], + queryKey: ['search', search, toggle], queryFn: () => onQueryFunction(), enabled: search.length > 0, }); @@ -170,7 +169,7 @@ export default function Explore() { const renderSearchPage = () => { return ( - {renderCustomInput(changeText, search)} + {renderCustomInput(setSearch, search)} @@ -217,7 +216,7 @@ export default function Explore() { }; return ( - + {selectedCategory === 'Map' && renderMapPage()} diff --git a/frontend/app/(app)/(tabs)/index.tsx b/frontend/app/(app)/(tabs)/index.tsx index 1840c95..00c4b94 100644 --- a/frontend/app/(app)/(tabs)/index.tsx +++ b/frontend/app/(app)/(tabs)/index.tsx @@ -9,7 +9,7 @@ import { import { useAuthStore } from '../../../auth/authStore'; import HomeMenu from '../../../components/home/menu-bar'; import { Category, Filter } from '../../../consts/home-menu'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useUserFollowingPosts } from '../../../hooks/user'; import BigDiveLog from '../../../components/divelog/divelog'; import NearbyDiveLog from '../../../components/home/nearby-divelog'; @@ -19,10 +19,8 @@ import { DEFAULT_SHERM_LOCATION } from '../../../consts/location'; import { PROFILE_PHOTO } from '../../../consts/profile'; import { useNearbyDiveLogs } from '../../../hooks/divelog'; import FilterMenu from '../../../components/home/filter'; -import usePulsingAnimation from '../../../utils/skeleton'; import { useInfoPopup } from '../../../contexts/info-popup-context'; import InfoPopup from '../../../components/info-popup'; -import { useQueryClient } from '@tanstack/react-query'; import { useFocusEffect } from 'expo-router'; const Home = () => { @@ -38,7 +36,45 @@ const Home = () => { const [selectedFilters, setSelectedFilters] = useState([ Filter.ALL, ]); - const opacity = usePulsingAnimation(); + + const memoizeUserFollowingPosts = () => { + const queryResult = useUserFollowingPosts(mongoDBId!, selectedFilters); + + return useMemo( + () => ({ + data: queryResult.data, + isLoading: queryResult.isLoading, + fetchNextPage: queryResult.fetchNextPage, + hasNextPage: queryResult.hasNextPage, + isFetchingNextPage: queryResult.isFetchingNextPage, + refetch: queryResult.refetch, + isRefetching: queryResult.isRefetching, + error: queryResult.error, + }), + [queryResult.data], + ); + }; + + const memoizeNearbyDiveLogs = () => { + const queryResult = useNearbyDiveLogs( + currentLocation.latitude, + currentLocation.longitude, + selectedFilters, + ); + + return useMemo( + () => ({ + data: queryResult.data, + isLoading: queryResult.isLoading, + fetchNextPage: queryResult.fetchNextPage, + hasNextPage: queryResult.hasNextPage, + refetch: queryResult.refetch, + isFetching: queryResult.isFetching, + error: queryResult.error, + }), + [queryResult.data], + ); + }; // hooks call for following posts const { @@ -50,7 +86,7 @@ const Home = () => { refetch: refetchFollowingPosts, isRefetching: isRefetchingFollowingPosts, error: errorFollowingPosts, - } = useUserFollowingPosts(mongoDBId!, selectedFilters); + } = memoizeUserFollowingPosts(); // hooks call for nearby posts const { @@ -61,11 +97,7 @@ const Home = () => { refetch: refetchNearByPosts, isFetching: isFetchingNearbyPosts, error: errorNearbyPosts, - } = useNearbyDiveLogs( - currentLocation.latitude, - currentLocation.longitude, - selectedFilters, - ); + } = memoizeNearbyDiveLogs(); // fetch the current location of user useEffect(() => { @@ -197,16 +229,14 @@ const Home = () => { renderItem={renderFollowingPost} key="following-divelogs" showsVerticalScrollIndicator={false} - onEndReached={() => - loadMorePosts(fetchNextPageFollowing, hasNextPageFollowing) + onEndReached={() => { + loadMorePosts(fetchNextPageFollowing, hasNextPageFollowing)} } onEndReachedThreshold={0.3} ListFooterComponent={ - isFetchingNextPageFollowing ? ( - + - ) : null } contentContainerStyle={{ paddingBottom: 150 }} ItemSeparatorComponent={() => } @@ -242,8 +272,8 @@ const Home = () => { return ( { const contentHeight = e.nativeEvent.contentSize.height; const layoutHeight = e.nativeEvent.layoutMeasurement.height; @@ -253,7 +283,6 @@ const Home = () => { } else { } }} - */ > diff --git a/frontend/app/(app)/(tabs)/notification.tsx b/frontend/app/(app)/(tabs)/notification.tsx index 3dfa73e..b007d1b 100644 --- a/frontend/app/(app)/(tabs)/notification.tsx +++ b/frontend/app/(app)/(tabs)/notification.tsx @@ -1,5 +1,5 @@ import { useFocusEffect } from 'expo-router'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FlatList, SafeAreaView, SectionList, Text, View } from 'react-native'; import { useAuthStore } from '../../../auth/authStore'; import NotificationEntry from '../../../components/notification/notification'; @@ -17,12 +17,16 @@ const Notification = () => { data, isLoading, error, - refetch, + refetch: originalRefetch, fetchNextPage, hasNextPage, isFetchingNextPage, } = useUserNotification(mongoDBId || ''); + const refetch = useCallback(() => { + originalRefetch(); + }, [originalRefetch]); + useFocusEffect( useCallback(() => { refetch(); diff --git a/frontend/app/(app)/user/[id].tsx b/frontend/app/(app)/user/[id].tsx index 20692aa..e7bb58d 100644 --- a/frontend/app/(app)/user/[id].tsx +++ b/frontend/app/(app)/user/[id].tsx @@ -1,10 +1,26 @@ import React from 'react'; -import { useLocalSearchParams } from 'expo-router'; +import { router, Stack, useLocalSearchParams } from 'expo-router'; import User from './components/user-profile'; +import Arrow from '../../../components/arrow'; const Profile = () => { const { id } = useLocalSearchParams<{ id: string }>(); - return ; + + return ( + <> + ( + router.back()} /> + ), + }} + /> + + + ); }; export default Profile; diff --git a/frontend/app/(app)/user/components/menu.tsx b/frontend/app/(app)/user/components/menu.tsx index 8f59e16..98a6985 100644 --- a/frontend/app/(app)/user/components/menu.tsx +++ b/frontend/app/(app)/user/components/menu.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Dimensions, FlatList, @@ -66,15 +66,15 @@ const Menu = ({ id }: { id: string }) => { refetch: refetchSpecies, } = useUserSpecies(id); + const memoizedRefetch = useMemo(() => { + return category === 'Dives' ? refetchDivelog : refetchSpecies; + }, [category, refetchDivelog, refetchSpecies]); + useFocusEffect( useCallback(() => { console.log(`Refetching ${category} data`); - if (category === 'Dives') { - refetchDivelog(); - } else if (category === 'Species') { - refetchSpecies(); - } - }, [refetchDivelog, refetchSpecies, category]), + memoizedRefetch(); + }, [memoizedRefetch, category]), ); const diveLogData = diveLogPages?.pages.flatMap((page) => page) ?? []; diff --git a/frontend/app/(app)/user/components/user-profile.tsx b/frontend/app/(app)/user/components/user-profile.tsx index 0d1e0af..ba79b0b 100644 --- a/frontend/app/(app)/user/components/user-profile.tsx +++ b/frontend/app/(app)/user/components/user-profile.tsx @@ -29,17 +29,6 @@ const User = ({ id }: { id: string }) => { colors={['#549ac7', '#ffffff', '#ffffff', '#ffffff']} style={{ flex: 1 }} > - - !isViewingOwnProfile ? ( - router.back()} /> - ) : null, - }} - /> { refresh(); }, [isAuthenticated]); + useEffect(() => { + const permission = async () => await Notifications.requestPermissionsAsync(); + permission(); + }, []); + useNotificationPermission({ isAuthenticated, mongoDBId }); useLocationPermission(); diff --git a/frontend/components/home/nearby-divelog.tsx b/frontend/components/home/nearby-divelog.tsx index 7d8141a..ad76fbb 100644 --- a/frontend/components/home/nearby-divelog.tsx +++ b/frontend/components/home/nearby-divelog.tsx @@ -1,10 +1,15 @@ -import { View, Text, Pressable, Animated } from 'react-native'; -import Profile from '../profile'; -import { useEffect, useState } from 'react'; -import { router } from 'expo-router'; import { Image } from 'expo-image'; -import ImageSize from 'react-native-image-size'; +import { router } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { + Animated, + Pressable, + Image as ReactNativeImage, + Text, + View, +} from 'react-native'; import usePulsingAnimation from '../../utils/skeleton'; +import Profile from '../profile'; interface NearbyDivelogProps { profilePhoto: string; @@ -31,7 +36,7 @@ const NearbyDiveLog: React.FC = ({ Image.prefetch(profilePhoto), ]); - const { width, height } = await ImageSize.getSize(coverPhoto); + const { width, height } = await ReactNativeImage.getSize(coverPhoto); setAspectRatio(width / height); setIsLoading(false); } catch (error) { diff --git a/frontend/components/image-picker.tsx b/frontend/components/image-picker.tsx index 5e4f41a..b8bc997 100644 --- a/frontend/components/image-picker.tsx +++ b/frontend/components/image-picker.tsx @@ -4,7 +4,6 @@ import { TouchableOpacity, View, Text, - InputAccessoryView, } from 'react-native'; import * as ExpoImagePicker from 'expo-image-picker'; import { useFormContext } from 'react-hook-form'; diff --git a/frontend/hooks/user.ts b/frontend/hooks/user.ts index b9c38a0..ea60be8 100644 --- a/frontend/hooks/user.ts +++ b/frontend/hooks/user.ts @@ -14,7 +14,11 @@ import { useQueryBase, useQueryPagination, } from './base'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + keepPreviousData, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; export const useUserData = () => { return useQueryBase(['user'], getMe); diff --git a/frontend/package.json b/frontend/package.json index eab9578..8ca349f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,6 @@ "react-hook-form": "^7.53.0", "react-native": "0.76.3", "react-native-extra-dimensions-android": "^1.2.5", - "react-native-fast-image": "^8.6.3", "react-native-image-size": "^1.1.6", "react-native-maps": "1.18.0", "react-native-modal": "^13.0.1", @@ -54,13 +53,15 @@ "react-native-svg-transformer": "^0.20.0", "zod": "^3.23.8", "zustand": "^3.7.2", - "zustand-persist": "^0.4.0" + "zustand-persist": "^0.4.0", + "metro-react-native-babel-transformer": "^0.77.0" }, "devDependencies": { "@babel/core": "^7.20.0", "@tanstack/eslint-plugin-query": "^5.59.1", "@types/react": "~18.3.12", "@types/react-native-dotenv": "^0.2.2", + "metro-react-native-babel-transformer": "^0.77.0", "react-native-dotenv": "^3.4.11", "react-native-svg-transformer": "^1.5.0", "tailwindcss": "3.3.2", diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..ecc8e8d --- /dev/null +++ b/vercel.json @@ -0,0 +1,15 @@ +{ + "builds": [ + { + "src": "backend/build/server.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "backend/build/server.js" + } + ] + } + \ No newline at end of file