From a26cc0498b080fc8d75bb19cde839b99ddcace9d Mon Sep 17 00:00:00 2001 From: David Yu Date: Thu, 10 Apr 2025 17:47:25 -0400 Subject: [PATCH 1/7] connect menu item reviews with backend --- .expo/settings.json | 8 ++++ frontend/api/menu-items.ts | 20 ++++++++ frontend/app/MenuItemView.tsx | 89 ++++++++++++++++++++++++++++++++++- frontend/hooks/useLogin.tsx | 2 +- frontend/metro.config.js | 6 +++ frontend/package.json | 10 ++-- 6 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 .expo/settings.json diff --git a/.expo/settings.json b/.expo/settings.json new file mode 100644 index 00000000..92bc513b --- /dev/null +++ b/.expo/settings.json @@ -0,0 +1,8 @@ +{ + "hostType": "lan", + "lanType": "ip", + "dev": true, + "minify": false, + "urlRandomness": null, + "https": false +} diff --git a/frontend/api/menu-items.ts b/frontend/api/menu-items.ts index a59f2820..50498a09 100644 --- a/frontend/api/menu-items.ts +++ b/frontend/api/menu-items.ts @@ -2,6 +2,11 @@ import { TMenuItem } from "@/types/menu-item"; import { makeRequest } from "@/api/base"; import { TRestaurantMenuItemsMetrics } from "@/types/restaurant"; +interface ReviewQueryParams { + userID?: string; + sortBy?: "portion" | "taste" | "value" | "overall" | "timestamp"; +} + export const getMenuItems = async ({ name, tags, @@ -41,3 +46,18 @@ 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 getMenuItemReviews = async (menuItemId: string, params?: ReviewQueryParams) => { + const queryParams = new URLSearchParams(); + if (params?.userID) { + queryParams.append("userID", params.userID); + } + if (params?.sortBy) { + queryParams.append("sortBy", params.sortBy); + } + + const queryString = queryParams.toString(); + const path = `/api/v1/menu-items/${menuItemId}/reviews${queryString ? `?${queryString}` : ""}`; + + return await makeRequest(path, "GET"); +}; diff --git a/frontend/app/MenuItemView.tsx b/frontend/app/MenuItemView.tsx index 51087308..4f1114e1 100644 --- a/frontend/app/MenuItemView.tsx +++ b/frontend/app/MenuItemView.tsx @@ -2,7 +2,7 @@ import { ThemedView } from "@/components/themed/ThemedView"; import { ScrollView, StyleSheet, View, Image, Pressable } from "react-native"; import { ThemedText } from "@/components/themed/ThemedText"; import { StarRating } from "@/components/ui/StarReview"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Ionicons } from "@expo/vector-icons"; import ReviewPreview from "@/components/review/ReviewPreview"; import { ThemedTag } from "@/components/themed/ThemedTag"; @@ -10,10 +10,36 @@ import { ReviewButton } from "@/components/review/ReviewButton"; import { ReviewFlow } from "@/components/review/ReviewFlow"; import HighlightCard from "@/components/restaurant/HighlightCard"; import { PersonWavingIcon, ThumbsUpIcon } from "@/components/icons/Icons"; +import { getMenuItemReviews } from "@/api/menu-items"; + +interface Review { + _id: string; + rating: { + portion: number; + taste: number; + value: number; + overall: number; + return: boolean; + }; + picture: string; + content: string; + reviewer: { + _id: string; + pfp: string; + username: string; + }; + timestamp: string; + comments: any[]; + menuItem: string; + restaurantId: string; +} export default function MenuItemView() { const [selectedFilter, setSelectedFilter] = React.useState("My Reviews"); const [isReviewModalVisible, setIsReviewModalVisible] = useState(false); + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const dishTags = [ { title: "Gluten-free", @@ -32,6 +58,32 @@ export default function MenuItemView() { }, ]; + // Assuming we get the menuItemId from route params or props + const menuItemId = "your-menu-item-id"; // Replace this with actual menu item ID + + useEffect(() => { + const fetchReviews = async () => { + try { + setLoading(true); + let sortBy = "timestamp"; + if (selectedFilter === "My Reviews") { + // You might want to pass the current user's ID here + const userReviews = await getMenuItemReviews(menuItemId, { userID: "current-user-id" }); + setReviews(userReviews); + } else { + const allReviews = await getMenuItemReviews(menuItemId); + setReviews(allReviews); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch reviews"); + } finally { + setLoading(false); + } + }; + + fetchReviews(); + }, [menuItemId, selectedFilter]); + return ( <> @@ -116,7 +168,7 @@ export default function MenuItemView() { 4/5 - + @@ -148,6 +200,29 @@ export default function MenuItemView() { authorAvatar={"https://placehold.co/600x400/png?text=P"} reviewId="64f5a95cc7330b78d33265f1" /> + {/* Reviews List */} + {loading ? ( + Loading reviews... + ) : error ? ( + {error} + ) : reviews.length === 0 ? ( + No reviews yet + ) : ( + reviews.map((review) => ( + + )) + )} { config.resolver.unstable_conditionNames = ["browser", "require", "react-native"]; + // Set custom port + config.server = { + ...config.server, + port: 8082, // Change this to any available port you prefer + }; + return config; })(); diff --git a/frontend/package.json b/frontend/package.json index 44af6cf5..5a0f1001 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,20 +23,20 @@ "axios": "^1.8.4", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", - "expo": "^52.0.40", + "expo": "~52.0.42", "expo-blur": "^14.0.3", "expo-constants": "~17.0.8", "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.5", - "expo-location": "^18.0.8", - "expo-router": "~4.0.19", + "expo-location": "~18.0.10", + "expo-router": "~4.0.20", "expo-secure-store": "^14.0.1", "expo-splash-screen": "^0.29.22", "expo-status-bar": "~2.0.1", "expo-symbols": "~0.2.2", - "expo-system-ui": "~4.0.8", + "expo-system-ui": "~4.0.9", "expo-web-browser": "~14.0.2", "metro-config": "^0.82.0", "moti": "^0.30.0", @@ -44,7 +44,7 @@ "prettier": "3.4.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-native": "0.76.7", + "react-native": "^0.76.9", "react-native-gesture-handler": "~2.20.2", "react-native-portalize": "^1.0.7", "react-native-reanimated": "~3.16.7", From ae1c5af466ac3adba7a0ded482358ba165e659fa Mon Sep 17 00:00:00 2001 From: David Yu Date: Thu, 10 Apr 2025 17:55:12 -0400 Subject: [PATCH 2/7] chore: revert testing changes --- .expo/settings.json | 8 -------- frontend/hooks/useLogin.tsx | 2 +- frontend/metro.config.js | 6 ------ frontend/package.json | 10 +++++----- 4 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 .expo/settings.json diff --git a/.expo/settings.json b/.expo/settings.json deleted file mode 100644 index 92bc513b..00000000 --- a/.expo/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "hostType": "lan", - "lanType": "ip", - "dev": true, - "minify": false, - "urlRandomness": null, - "https": false -} diff --git a/frontend/hooks/useLogin.tsx b/frontend/hooks/useLogin.tsx index 9d026eda..fa6c1b67 100644 --- a/frontend/hooks/useLogin.tsx +++ b/frontend/hooks/useLogin.tsx @@ -1,6 +1,6 @@ import * as SecureStore from "expo-secure-store"; -const baseUrl = process.env.EXPO_PUBLIC_BASE_URL; +const baseUrl = "https://57a7-2601-19b-480-4dc0-f909-8c1c-d184-ab76.ngrok-free.app"; // will need to be changed to actual URl and store in .env async function useLogin() { return { login: login, register: register }; diff --git a/frontend/metro.config.js b/frontend/metro.config.js index c2ae5d52..aee682fb 100644 --- a/frontend/metro.config.js +++ b/frontend/metro.config.js @@ -17,11 +17,5 @@ module.exports = (() => { config.resolver.unstable_conditionNames = ["browser", "require", "react-native"]; - // Set custom port - config.server = { - ...config.server, - port: 8082, // Change this to any available port you prefer - }; - return config; })(); diff --git a/frontend/package.json b/frontend/package.json index 5a0f1001..bb68d69c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,20 +23,20 @@ "axios": "^1.8.4", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", - "expo": "~52.0.42", + "expo": "^52.0.40", "expo-blur": "^14.0.3", "expo-constants": "~17.0.8", "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", "expo-linear-gradient": "~14.0.2", "expo-linking": "~7.0.5", - "expo-location": "~18.0.10", - "expo-router": "~4.0.20", + "expo-location": "^18.0.8", + "expo-router": "~4.0.19", "expo-secure-store": "^14.0.1", "expo-splash-screen": "^0.29.22", "expo-status-bar": "~2.0.1", "expo-symbols": "~0.2.2", - "expo-system-ui": "~4.0.9", + "expo-system-ui": "~4.0.8", "expo-web-browser": "~14.0.2", "metro-config": "^0.82.0", "moti": "^0.30.0", @@ -44,7 +44,7 @@ "prettier": "3.4.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-native": "^0.76.9", + "react-native": "^0.76.7", "react-native-gesture-handler": "~2.20.2", "react-native-portalize": "^1.0.7", "react-native-reanimated": "~3.16.7", From 89e606dba229d288d353c2f97bbcbb1afc81d6ef Mon Sep 17 00:00:00 2001 From: David Yu Date: Thu, 10 Apr 2025 19:44:27 -0400 Subject: [PATCH 3/7] feat: connect all reviews to database --- frontend/api/reviews.tsx | 4 + frontend/components/review/AllReviews.tsx | 92 ++++++++++++++++++++--- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/frontend/api/reviews.tsx b/frontend/api/reviews.tsx index 84397169..cebc69f7 100644 --- a/frontend/api/reviews.tsx +++ b/frontend/api/reviews.tsx @@ -12,3 +12,7 @@ export const getReviewById = async (id: string, userId: string): Promise => { return await makeRequest(`/api/v1/reviews/${id}/friendReviews`, "GET"); }; + +export const getUserReviews = async (userId: string): Promise => { + return await makeRequest(`/api/v1/review/user/${userId}`, "GET"); +}; diff --git a/frontend/components/review/AllReviews.tsx b/frontend/components/review/AllReviews.tsx index 57786624..2b52981f 100644 --- a/frontend/components/review/AllReviews.tsx +++ b/frontend/components/review/AllReviews.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { View, StyleSheet, ScrollView, TouchableOpacity } from "react-native"; import { ThemedView } from "@/components/themed/ThemedView"; import { ThemedText } from "@/components/themed/ThemedText"; @@ -6,17 +6,65 @@ import { Ionicons } from "@expo/vector-icons"; import ReviewPreview from "./ReviewPreview"; import { ReviewButton } from "@/components/review/ReviewButton"; import { ReviewFlow } from "@/components/review/ReviewFlow"; +import { getUserReviews, getFriendsReviews, getReviews } from "@/api/reviews"; +import { TReview } from "@/types/review"; export default function AllReviews() { const [selectedMainFilter, setSelectedMainFilter] = React.useState("My Reviews"); const [selectedSubFilter, setSelectedSubFilter] = React.useState("Portion"); const [isReviewModalVisible, setIsReviewModalVisible] = useState(false); + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // TODO: Get the actual user ID from your auth context/state + const currentUserId = "your-user-id"; + + useEffect(() => { + const fetchReviews = async () => { + try { + setLoading(true); + setError(null); + + if (selectedMainFilter === "My Reviews") { + const userReviews = await getUserReviews(currentUserId); + setReviews(userReviews); + } else if (selectedMainFilter === "Friends") { + // TODO: Implement friends reviews fetch + const friendReviews = await getFriendsReviews(currentUserId); + setReviews(friendReviews); + } else { + const allReviews = await getReviews(); + setReviews(allReviews.data); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch reviews"); + } finally { + setLoading(false); + } + }; + + fetchReviews(); + }, [selectedMainFilter, currentUserId]); const handleBack = () => { // Navigation logic here console.log("Go back"); }; + // Sort reviews based on selectedSubFilter + const sortedReviews = [...reviews].sort((a, b) => { + if (selectedSubFilter === "Portion") { + return b.rating.portion - a.rating.portion; + } else if (selectedSubFilter === "Taste") { + return b.rating.taste - a.rating.taste; + } else if (selectedSubFilter === "Value") { + return b.rating.value - a.rating.value; + } else { + return b.rating.overall - a.rating.overall; + } + }); + return ( <> @@ -68,14 +116,30 @@ export default function AllReviews() { ))} - {/* Sample Review Preview */} - {/* */} + {/* Reviews List */} + {loading ? ( + Loading reviews... + ) : error ? ( + {error} + ) : sortedReviews.length === 0 ? ( + No reviews found + ) : ( + sortedReviews.map((review) => ( + + )) + )} Date: Thu, 10 Apr 2025 19:51:23 -0400 Subject: [PATCH 4/7] chore: styling --- frontend/app/MenuItemView.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/app/MenuItemView.tsx b/frontend/app/MenuItemView.tsx index 4f1114e1..3b6602f5 100644 --- a/frontend/app/MenuItemView.tsx +++ b/frontend/app/MenuItemView.tsx @@ -213,13 +213,14 @@ export default function MenuItemView() { key={review._id} plateName={review.menuItem} restaurantName="Pad Thai Kitchen" - tags={[]} // You might want to add tags based on the review + tags={[]} rating={review.rating.overall} content={review.content} authorName={review.reviewer.username} authorId={review.reviewer._id} authorUsername={review.reviewer.username} authorAvatar={review.reviewer.pfp || "https://placehold.co/600x400/png?text=P"} + reviewId={""} /> )) )} @@ -400,13 +401,13 @@ const styles = StyleSheet.create({ flexDirection: "row", }, errorText: { - color: 'red', - textAlign: 'center', + color: "red", + textAlign: "center", marginVertical: 16, }, noReviewsText: { - textAlign: 'center', + textAlign: "center", marginVertical: 16, - color: '#666', + color: "#666", }, }); From d2ccd2e2ae0eb48f474101751f921851cd1d6525 Mon Sep 17 00:00:00 2001 From: Ben Petrillo Date: Thu, 10 Apr 2025 22:09:56 -0400 Subject: [PATCH 5/7] chore: linesize fix --- frontend/components/Cards/MenuItemPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/Cards/MenuItemPreview.tsx b/frontend/components/Cards/MenuItemPreview.tsx index 86f83f2b..4c36b4e5 100644 --- a/frontend/components/Cards/MenuItemPreview.tsx +++ b/frontend/components/Cards/MenuItemPreview.tsx @@ -74,11 +74,11 @@ const MenuItemPreview = ({ plateName, restaurantName, tags, rating, content, pic - Overall Rating {rating} + Overall Rating: {rating} - + {restaurantName} From 32f3df612caf0d971adaf548b50057d499232b50 Mon Sep 17 00:00:00 2001 From: Ben Petrillo Date: Thu, 10 Apr 2025 22:17:06 -0400 Subject: [PATCH 6/7] chore: user actual user ID --- frontend/app/MenuItemView.tsx | 6 ++++-- frontend/components/review/AllReviews.tsx | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/app/MenuItemView.tsx b/frontend/app/MenuItemView.tsx index 3b6602f5..23a014a8 100644 --- a/frontend/app/MenuItemView.tsx +++ b/frontend/app/MenuItemView.tsx @@ -11,6 +11,7 @@ import { ReviewFlow } from "@/components/review/ReviewFlow"; import HighlightCard from "@/components/restaurant/HighlightCard"; import { PersonWavingIcon, ThumbsUpIcon } from "@/components/icons/Icons"; import { getMenuItemReviews } from "@/api/menu-items"; +import useAuthStore from "@/auth/store"; interface Review { _id: string; @@ -40,6 +41,7 @@ export default function MenuItemView() { const [reviews, setReviews] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { userId } = useAuthStore(); const dishTags = [ { title: "Gluten-free", @@ -59,7 +61,7 @@ export default function MenuItemView() { ]; // Assuming we get the menuItemId from route params or props - const menuItemId = "your-menu-item-id"; // Replace this with actual menu item ID + const menuItemId = "67e331d0f958ba76a112cc26 "; // Replace this with actual menu item ID useEffect(() => { const fetchReviews = async () => { @@ -68,7 +70,7 @@ export default function MenuItemView() { let sortBy = "timestamp"; if (selectedFilter === "My Reviews") { // You might want to pass the current user's ID here - const userReviews = await getMenuItemReviews(menuItemId, { userID: "current-user-id" }); + const userReviews = await getMenuItemReviews(menuItemId, { userID: userId || "" }); setReviews(userReviews); } else { const allReviews = await getMenuItemReviews(menuItemId); diff --git a/frontend/components/review/AllReviews.tsx b/frontend/components/review/AllReviews.tsx index 2b52981f..6c89e6c0 100644 --- a/frontend/components/review/AllReviews.tsx +++ b/frontend/components/review/AllReviews.tsx @@ -8,6 +8,7 @@ import { ReviewButton } from "@/components/review/ReviewButton"; import { ReviewFlow } from "@/components/review/ReviewFlow"; import { getUserReviews, getFriendsReviews, getReviews } from "@/api/reviews"; import { TReview } from "@/types/review"; +import useAuthStore from "@/auth/store"; export default function AllReviews() { const [selectedMainFilter, setSelectedMainFilter] = React.useState("My Reviews"); @@ -17,8 +18,10 @@ export default function AllReviews() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { userId } = useAuthStore(); + // TODO: Get the actual user ID from your auth context/state - const currentUserId = "your-user-id"; + const currentUserId = userId || ""; useEffect(() => { const fetchReviews = async () => { From f6889e884ac7d3df698ddbd21ca7bb8e9ffc6900 Mon Sep 17 00:00:00 2001 From: Abhik Ray Date: Fri, 11 Apr 2025 17:03:15 -0400 Subject: [PATCH 7/7] feat: menu on restaurant --- frontend/api/menu-items.ts | 4 ++++ frontend/app/(restaurant)/[id].tsx | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/api/menu-items.ts b/frontend/api/menu-items.ts index aa4eb0c5..b1e6d662 100644 --- a/frontend/api/menu-items.ts +++ b/frontend/api/menu-items.ts @@ -65,3 +65,7 @@ export const getMenuItemReviews = async (menuItemId: string, params?: ReviewQuer export const getRecommendations = async (userId: string): Promise => { return await makeRequest(`:4000/reccomendation?user_id=${userId}`, "GET"); }; + +export const getMenuItemsByRestaurant = async (restaurantId: string): Promise => { + return await makeRequest(`/api/v1/menu-items/restaurant/${restaurantId}`, "GET"); +}; diff --git a/frontend/app/(restaurant)/[id].tsx b/frontend/app/(restaurant)/[id].tsx index d9c27806..6ad17cde 100644 --- a/frontend/app/(restaurant)/[id].tsx +++ b/frontend/app/(restaurant)/[id].tsx @@ -20,6 +20,8 @@ import { Skeleton } from "moti/skeleton"; import { getRestaurantReviews, getRestaurantReviewsByUser, getReviews } from "@/api/reviews"; import { TReview } from "@/types/review"; import { useUser } from "@/context/user-context"; +import { TMenuItem } from "@/types/menu-item"; +import { getMenuItemsByRestaurant } from "@/api/menu-items"; export default function Route() { const restaurantTags = ["Fast Food", "Fried Chicken", "Chicken Sandwiches", "Order Online"]; @@ -39,6 +41,7 @@ export default function Route() { const { user } = useUser(); const [restaurant, setRestaurant] = React.useState(null); + const [menuItems, setMenuItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const navigation = useNavigation(); @@ -57,6 +60,10 @@ export default function Route() { setReviews(res); }); + getMenuItemsByRestaurant(id).then((res) => { + setMenuItems(res); + }); + getRestaurantReviewsByUser(id, user.id).then((res) => { console.log(res); setMyReviews(res); @@ -200,7 +207,24 @@ export default function Route() { ))} - {filterTab == 1 && <>{/* TODO MENU ITEM PREVIEW */}} + {filterTab == 1 && ( + <> + {menuItems.map((item, index) => { + return ( + + ); + })} + + )}