diff --git a/backend/internal/handlers/menu_items/service.go b/backend/internal/handlers/menu_items/service.go index 557d91f..26a596c 100644 --- a/backend/internal/handlers/menu_items/service.go +++ b/backend/internal/handlers/menu_items/service.go @@ -540,7 +540,7 @@ func (s *Service) GetPopularWithFriends(userID primitive.ObjectID, limit int) ([ "$match": bson.M{ "$expr": bson.M{ "$in": bson.A{ - bson.M{"$toObjectId": "$reviewer.id"}, + bson.M{"$toObjectId": "$reviewer._id"}, "$$friendIDs", }, }, diff --git a/backend/internal/handlers/menu_items/types.go b/backend/internal/handlers/menu_items/types.go index f1e017f..4215f71 100644 --- a/backend/internal/handlers/menu_items/types.go +++ b/backend/internal/handlers/menu_items/types.go @@ -17,6 +17,7 @@ type MenuItemRequest struct { Name string `json:"name"` Picture string `json:"picture"` Reviews []string `json:"reviews"` + AvgRating AvgRatingDocument `json:"avgRating"` Description string `json:"description"` Location []float64 `json:"location"` Tags []string `json:"tags"` @@ -86,11 +87,11 @@ type MenuItemDocument struct { } type AvgRatingDocument struct { - Portion float64 `bson:"portion"` - Taste float64 `bson:"taste"` - Value float64 `bson:"value"` - Overall float64 `bson:"overall"` - Return float64 `bson:"return"` // @TODO: figure out if boolean or number + Portion float64 `bson:"portion" json:"portion"` + Taste float64 `bson:"taste" json:"taste"` + Value float64 `bson:"value" json:"value"` + Overall float64 `bson:"overall" json:"overall"` + Return float64 `bson:"return" json:"return"` // @TODO: figure out if boolean or number } // MenuItemMetrics represents analytics data for a single menu item diff --git a/frontend/api/menu-items.ts b/frontend/api/menu-items.ts index a59f282..a201372 100644 --- a/frontend/api/menu-items.ts +++ b/frontend/api/menu-items.ts @@ -1,4 +1,5 @@ import { TMenuItem } from "@/types/menu-item"; +import { TReview } from "@/types/review"; import { makeRequest } from "@/api/base"; import { TRestaurantMenuItemsMetrics } from "@/types/restaurant"; @@ -41,3 +42,11 @@ export const getRestaurantMenuItemsMetrics = async (restaurantId: string): Promi export const getRandomMenuItems = async (limit: number): Promise => { return await makeRequest(`/api/v1/menu-items/random?limit=${limit}`, "GET"); }; + +export const getFriendMenuItems = async (id: string, limit: number): Promise => { + return await makeRequest(`/api/v1/menu-items/popular-with-friends?userId=${id}&limit=${limit}`, "GET"); +} + +export const getMenuItemReviews = async (menuItemId: string): Promise => { + return await makeRequest(`/api/v1/menu-items/${menuItemId}/reviews`, "GET"); +} \ No newline at end of file diff --git a/frontend/api/restaurant.ts b/frontend/api/restaurant.ts index 4321fdc..11af58d 100644 --- a/frontend/api/restaurant.ts +++ b/frontend/api/restaurant.ts @@ -1,7 +1,20 @@ import { TRestaurant } from "@/types/restaurant"; +import { FriendsFavInfo } from "@/types/restaurant"; import { makeRequest } from "@/api/base"; export const getRestaurant = async (id: string): Promise => { - const res = makeRequest("/api/v1/restaurant/" + id, "GET"); - return res; + return await makeRequest(`/api/v1/restaurant/${id}`, "GET"); }; + +export const getRestaurantFriendsFav = async (userId: string, restaurantId: string): Promise => { + const data = await makeRequest(`/api/v1/restaurant/${userId}/${restaurantId}`, "GET"); + const formattedFriendsFav: FriendsFavInfo = { + isFriendsFav: data.friends_fav, + numFriends: data.friends_reviewed + }; + return formattedFriendsFav; +} + +export const getRestaurantSuperStars = async (restaurantId: string): Promise => { + return await makeRequest(`/api/v1/restaurant/${restaurantId}/super-stars`, "GET"); +} \ No newline at end of file diff --git a/frontend/api/reviews.tsx b/frontend/api/reviews.ts similarity index 87% rename from frontend/api/reviews.tsx rename to frontend/api/reviews.ts index d3d2a3a..3494800 100644 --- a/frontend/api/reviews.tsx +++ b/frontend/api/reviews.ts @@ -10,5 +10,5 @@ export const getReviewById = async (id: string, userId: string): Promise => { - return await makeRequest(`/api/v1/item/${id}/followReviews`, "GET"); + return await makeRequest(`/api/v1/item/${id}/followingReviews`, "GET"); }; diff --git a/frontend/app/(menuItem)/[id].tsx b/frontend/app/(menuItem)/[id].tsx index ae76c1e..91133d5 100644 --- a/frontend/app/(menuItem)/[id].tsx +++ b/frontend/app/(menuItem)/[id].tsx @@ -2,24 +2,35 @@ import { ThemedView } from "@/components/themed/ThemedView"; import { ScrollView, StyleSheet, View, Image, Pressable, TouchableOpacity } from "react-native"; import { ThemedText } from "@/components/themed/ThemedText"; import { StarRating } from "@/components/ui/StarReview"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { Ionicons } from "@expo/vector-icons"; import ReviewPreview from "@/components/review/ReviewPreview"; import { ThemedTag } from "@/components/themed/ThemedTag"; import { ReviewButton } from "@/components/review/ReviewButton"; import HighlightCard from "@/components/restaurant/HighlightCard"; -import { PersonWavingIcon, RestaurantIcon, ThumbsUpIcon } from "@/components/icons/Icons"; +import { PersonWavingIcon, RestaurantIcon, SmileyIcon, ThumbsUpIcon } from "@/components/icons/Icons"; import { useLocalSearchParams, useNavigation, useRouter } from "expo-router"; -import { getMenuItemById } from "@/api/menu-items"; +import { getMenuItemById, getMenuItemReviews } from "@/api/menu-items"; import { TMenuItem } from "@/types/menu-item"; import ReviewFlow from "@/components/review/ReviewFlow"; import AddReviewButton from "@/components/AddReviewButton"; import { Skeleton } from "moti/skeleton"; +import { useUser } from "@/context/user-context"; +import { getRestaurantFriendsFav, getRestaurantSuperStars } from "@/api/restaurant"; +import { FriendsFavInfo } from "@/types/restaurant"; +import { TReview } from "@/types/review"; export default function Route() { const [selectedFilter, setSelectedFilter] = React.useState("My Reviews"); + const [friendsFav, setFriendsFav] = useState({ + isFriendsFav: false, + numFriends: 0, + }); + const [superStars, setSuperStars] = useState(0); + const [menuItemReviews, setMenuItemReviews] = useState([]) const { id } = useLocalSearchParams<{ id: string }>(); + const { user } = useUser(); const navigation = useNavigation(); const [menuItem, setMenuItem] = useState(null); @@ -29,13 +40,42 @@ export default function Route() { const router = useRouter(); + const fetchData = useCallback(async () => { + try { + if (!user || !menuItem) { + console.log("USER", user); + console.log("MENUITEM", menuItem); + throw new Error("User and/or menuItem are null. Cannot fetch associated menu item data."); + } + const [friendsFavData, superStarsData, menuItemReviewData] = await Promise.all([getRestaurantFriendsFav(user.id, menuItem.restaurantID), getRestaurantSuperStars(menuItem.restaurantID), getMenuItemReviews(menuItem.id)]); + console.log("MENUITEMID", menuItem); + console.log("MENUREVIEWDATA", menuItemReviewData); + console.log("FRIENDSFAVDATA", friendsFavData); + setFriendsFav(friendsFavData); + setSuperStars(superStarsData); + setMenuItemReviews(menuItemReviewData); + + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + }, [user, menuItem]); + useEffect(() => { navigation.setOptions({ headerShown: false }); getMenuItemById(id).then((data) => { setMenuItem(data); setLoading(false); }); - }, [navigation]); + }, [navigation, id]); + + useEffect(() => { + // Only call fetchData when menuItem is initialized + if (menuItem) { + fetchData(); + } + }, [menuItem, user, fetchData]); return ( <> @@ -119,15 +159,15 @@ export default function Route() { } /> } /> - + } /> @@ -138,8 +178,8 @@ export default function Route() { - 4/5 - + {menuItem?.avgRating.overall} + @@ -162,18 +202,22 @@ export default function Route() { ))} - - + {menuItemReviews.map((item: TReview, index: number) => ( + router.push(`/(review)/${item._id}`)}> + + + ))} diff --git a/frontend/app/(profile)/[id].tsx b/frontend/app/(profile)/[id].tsx index 9b5c9d3..95c6bb4 100644 --- a/frontend/app/(profile)/[id].tsx +++ b/frontend/app/(profile)/[id].tsx @@ -9,7 +9,7 @@ import ProfileAvatar from "@/components/profile/ProfileAvatar"; import ProfileIdentity from "@/components/profile/ProfileIdentity"; import ProfileMetrics from "@/components/profile/ProfileMetrics"; import ReviewPreview from "@/components/review/ReviewPreview"; -import { SearchBoxFilter } from "@/components/SearchBoxFilter"; +import { SearchBox } from "@/components/SearchBox"; import EditFriendSheet from "@/components/profile/followers/FriendProfileOptions"; import { FollowButton } from "@/components/profile/followers/FollowButton"; import { useLocalSearchParams } from "expo-router"; @@ -120,7 +120,7 @@ const ProfileScreen = () => { {user.name}'s Food Journal {/* Made a search box with a filter/sort component as its own component */} - console.log("submit")} diff --git a/frontend/app/(tabs)/index/index.tsx b/frontend/app/(tabs)/index/index.tsx index 1547ae2..38dc369 100644 --- a/frontend/app/(tabs)/index/index.tsx +++ b/frontend/app/(tabs)/index/index.tsx @@ -5,7 +5,7 @@ import { ThemedView } from "@/components/themed/ThemedView"; import FeedTabs from "@/components/Feed/FeedTabs"; import ReviewPreview from "@/components/review/ReviewPreview"; import MenuItemPreview from "@/components/Cards/MenuItemPreview"; -import { getMenuItems, getRandomMenuItems } from "@/api/menu-items"; +import { getMenuItems, getFriendMenuItems } from "@/api/menu-items"; import { TMenuItem } from "@/types/menu-item"; import { TReview } from "@/types/review"; import { getReviews } from "@/api/reviews"; @@ -15,6 +15,7 @@ import { SearchIcon } from "@/components/icons/Icons"; import { FilterContext } from "@/context/filter-context"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ThemedText } from "@/components/themed/ThemedText"; +import { useUser } from "@/context/user-context"; // Define a type for our feed items type FeedItem = { @@ -30,6 +31,7 @@ export default function Feed() { const [feedItems, setFeedItems] = useState([]); const [reviews, setReviews] = useState([]); const [menuItems, setMenuItems] = useState([]); + const { user } = useUser(); const insets = useSafeAreaInsets(); const router = useRouter(); @@ -47,11 +49,14 @@ export default function Feed() { const fetchData = useCallback(async () => { try { - const [reviewsData, menuItemsData] = await Promise.all([getReviews(2, 20), getRandomMenuItems(20)]); + if (!user) { + throw new Error("User is null. Cannot fetch friend menu items and reviews."); + } + console.log("user id:", user.id); + const [reviewsData, menuItemsData] = await Promise.all([getReviews(2, 20), getFriendMenuItems(user.id, 20)]); const fetchedReviews = reviewsData.data as TReview[]; const fetchedMenuItems = menuItemsData as TMenuItem[]; - setReviews(fetchedReviews); setMenuItems(fetchedMenuItems); @@ -74,12 +79,13 @@ export default function Feed() { setLoading(false); setRefreshing(false); } - }, []); + }, [user]); useEffect(() => { setLoading(true); fetchData(); - }, [fetchData]); + console.log("FMI", menuItems); + }, [fetchData, user]); const onRefresh = useCallback(() => { setRefreshing(true); @@ -186,6 +192,52 @@ export default function Feed() { filter={true} /> + + {reviews.length > 0 ? ( + + {reviews.map((item: TReview, index: number) => { + console.log(item); + return ( + router.push(`/(review)/${item._id}`)}> + + + ); + })} + + ) : ( + + No reviews available + + )} + + {menuItems.length > 0 && ( + + {/* {menuItems.map((item: TMenuItem, index: number) => ( + router.push(`/(menuItem)/${item.id}`)}> + + + ))} */} + + )} + { {user.name.split(" ")[0]}'s Food Journal - } /> } /> - + } /> {/* Reviews Section */} diff --git a/frontend/app/friend/[userId].tsx b/frontend/app/friend/[userId].tsx index 3109742..3095213 100644 --- a/frontend/app/friend/[userId].tsx +++ b/frontend/app/friend/[userId].tsx @@ -8,7 +8,7 @@ import ProfileAvatar from "@/components/profile/ProfileAvatar"; import ProfileIdentity from "@/components/profile/ProfileIdentity"; import ProfileMetrics from "@/components/profile/ProfileMetrics"; import ReviewPreview from "@/components/review/ReviewPreview"; -import { SearchBoxFilter } from "@/components/SearchBoxFilter"; +import { SearchBox } from "@/components/SearchBox"; import { FollowButton } from "@/components/profile/followers/FollowButton"; import { useLocalSearchParams } from "expo-router"; import type { User } from "@/context/user-context"; @@ -120,7 +120,7 @@ const ProfileScreen = () => { {user.name}'s Food Journal {/* Made a search box with a filter/sort component as its own component */} - console.log("submit")} diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 8934524..6f08c46 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/components/SearchBoxFilter.tsx b/frontend/components/SearchBoxFilter.tsx index 6d52144..e0a0187 100644 --- a/frontend/components/SearchBoxFilter.tsx +++ b/frontend/components/SearchBoxFilter.tsx @@ -1,71 +1,95 @@ -import React, { useRef } from "react"; -import { TextInput, StyleSheet, View, TouchableOpacity } from "react-native"; -import { useThemeColor } from "@/hooks/useThemeColor"; -import { SearchBoxProps } from "./SearchBox"; -import { SortIcon } from "./icons/Icons"; +// import React, { useRef, useState, useCallback} from "react"; +// import { TextInput, StyleSheet, View, TouchableOpacity } from "react-native"; +// import { useThemeColor } from "@/hooks/useThemeColor"; +// import { SearchBoxProps } from "./SearchBox"; +// import { SortIcon } from "./icons/Icons"; -export function SearchBoxFilter({ value, onChangeText, onSubmit, icon, recent, name, ...rest }: SearchBoxProps) { - const textColor = useThemeColor({ light: "#000", dark: "#fff" }, "text"); - const inputRef = useRef(null); +// export function SearchBoxFilter({ value, onChangeText, onSubmit, icon, recent, name, ...rest }: SearchBoxProps) { +// const textColor = useThemeColor({ light: "#000", dark: "#fff" }, "text"); +// const inputRef = useRef(null); +// const [recentItems, setRecentItems] = useState([]); - const handleSubmit = () => { - if (onSubmit) { - onSubmit(); - } - }; +// const fetchRecents = useCallback(async () => { +// const recents = await getRecents(); +// setRecentItems(recents); +// }, [getRecents]); - return ( - - - - - - {icon && icon} - - - - {}}> - - - - ); -} +// async function clearRecents() { +// setRecentItems([]); +// } -const styles = StyleSheet.create({ - container: { - flexDirection: "row", - alignItems: "center", - borderWidth: 1, - borderColor: "#DDD", - borderRadius: 12, - paddingHorizontal: 12, - paddingVertical: 8, - fontFamily: "Source Sans 3", - }, - input: { - flex: 1, - fontFamily: "Source Sans 3", - }, - icon: { - marginLeft: 8, - resizeMode: "contain", - }, - searchContainer: { - flexDirection: "row", - alignItems: "center", - width: "100%", - justifyContent: "space-between", - }, - searchBoxContainer: { - flex: 1, - marginRight: 10, - }, -}); +// useEffect(() => { +// if (inputRef.current) { +// inputRef.current?.measureInWindow((height) => { +// setInputHeight(height + Dimensions.get("window").height * 0.01); +// }); +// } +// }, [inputRef]); + +// useEffect(() => { +// fetchRecents(); +// }, [recent, fetchRecents]); + +// const onSubmitEditing = () => { +// if (recent) +// appendSearch(value).then(() => { +// fetchRecents(); +// }); +// onSubmit(); +// }; + +// return ( +// +// +// +// +// +// {icon && icon} +// +// +// +// {}}> +// +// +// +// ); +// } + +// const styles = StyleSheet.create({ +// container: { +// flexDirection: "row", +// alignItems: "center", +// borderWidth: 1, +// borderColor: "#DDD", +// borderRadius: 12, +// paddingHorizontal: 12, +// paddingVertical: 8, +// fontFamily: "Source Sans 3", +// }, +// input: { +// flex: 1, +// fontFamily: "Source Sans 3", +// }, +// icon: { +// marginLeft: 8, +// resizeMode: "contain", +// }, +// searchContainer: { +// flexDirection: "row", +// alignItems: "center", +// width: "100%", +// justifyContent: "space-between", +// }, +// searchBoxContainer: { +// flex: 1, +// marginRight: 10, +// }, +// }); diff --git a/frontend/components/restaurant/HighlightCard.tsx b/frontend/components/restaurant/HighlightCard.tsx index ab3777f..92b6d81 100644 --- a/frontend/components/restaurant/HighlightCard.tsx +++ b/frontend/components/restaurant/HighlightCard.tsx @@ -1,12 +1,15 @@ import { SmileyIcon } from "@/components/icons/Icons"; import { View, Text, StyleSheet } from "react-native"; -const HighlightCard = ({ - icon = , - title = "Super Stars", - subtitle = "200+ Five Stars", - backgroundColor = "#F7F9FC", -}) => { +interface HighlightCardProps { + title: string; + subtitle: string; + icon: React.JSX.Element; +} + +const backgroundColor = "#F7F9FC" + +const HighlightCard = ({ title, subtitle, icon }: HighlightCardProps) => { return ( {icon} diff --git a/frontend/components/review/ReviewPreview.tsx b/frontend/components/review/ReviewPreview.tsx index 733e809..ce5ed04 100644 --- a/frontend/components/review/ReviewPreview.tsx +++ b/frontend/components/review/ReviewPreview.tsx @@ -10,7 +10,7 @@ import Tag from "@/components/ui/Tag"; import { ThemedTag } from "@/components/themed/ThemedTag"; import { ThemedView } from "@/components/themed/ThemedView"; -type Props = { +type ReviewProps = { plateName: string; restaurantName: string; tags: string[]; @@ -32,7 +32,7 @@ const ReviewPreview = ({ authorUsername, authorAvatar, authorId, -}: Props) => { +}: ReviewProps) => { return ( - - - - router.push(`/(profile)/${authorId}`)} - right={null} - /> - - + + - + router.push(`/(profile)/${authorId}`)} + right={null} + /> + + + - {rating} - - + + {rating} + + + - - - - - - - {plateName} - - - {restaurantName} - + + + + + + {plateName} + + + {restaurantName} + + + + {tags.map((tag, index) => ( + + ))} + + + {content} + - - {tags.map((tag, index) => ( - - ))} - - - {content} - - - - - - - - 0 - - - - 0 - - - - - + + + + + + 0 + + + + 0 + + + + + + diff --git a/frontend/components/ui/StarReview.tsx b/frontend/components/ui/StarReview.tsx index 0d24659..49512b1 100644 --- a/frontend/components/ui/StarReview.tsx +++ b/frontend/components/ui/StarReview.tsx @@ -48,6 +48,11 @@ export function StarRating({ {showNumRatingsText ? " reviews" : ""}) )} + {numRatings == 0 && showNumRatings && ( + + ({"There are no reviews to be displayed"}) + + )} ); } diff --git a/frontend/package.json b/frontend/package.json index 44af6cf..5dc15b8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "react-native-svg-transformer": "^1.5.0", "react-native-web": "~0.19.13", "react-native-webview": "13.12.5", + "ws": "8", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/frontend/types/restaurant.ts b/frontend/types/restaurant.ts index 811a5ad..1dc26a4 100644 --- a/frontend/types/restaurant.ts +++ b/frontend/types/restaurant.ts @@ -29,3 +29,8 @@ export type TRestaurantMenuItemsMetrics = { total_reviews: number; menu_item_metrics: TMenuItemMetrics[]; }; + +export type FriendsFavInfo = { + isFriendsFav: boolean; + numFriends: number; +}