From 44a2f89a594789e06460446947d29132584af451 Mon Sep 17 00:00:00 2001 From: Sosokker Date: Sun, 11 May 2025 22:44:21 +0700 Subject: [PATCH] feat: add foods detail page --- app/(tabs)/home.tsx | 8 +- app/food/[id].tsx | 791 +++++++++++++++++++-------------------- app/post-detail/[id].tsx | 614 ++++++++++++++++++++---------- app/recipe-detail.tsx | 140 ------- services/data/foods.ts | 18 +- 5 files changed, 806 insertions(+), 765 deletions(-) delete mode 100644 app/recipe-detail.tsx diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx index 55de514..ef56af1 100644 --- a/app/(tabs)/home.tsx +++ b/app/(tabs)/home.tsx @@ -105,7 +105,7 @@ const processImage = async ( }; const navigateToFoodDetail = (foodId: string) => { - router.push({ pathname: "/recipe-detail", params: { id: foodId } }); + router.push({ pathname: "/food/[id]", params: { id: foodId } }); }; export default function HomeScreen() { @@ -147,11 +147,7 @@ export default function HomeScreen() { setImageProcessing(false); } router.push({ - pathname: "/recipe-detail", - params: { - title: "My New Recipe", - image: result.assets[0].uri, - }, + pathname: "/profile", }); } }; diff --git a/app/food/[id].tsx b/app/food/[id].tsx index c582278..726b3da 100644 --- a/app/food/[id].tsx +++ b/app/food/[id].tsx @@ -1,454 +1,419 @@ "use client"; import { IconSymbol } from "@/components/ui/IconSymbol"; +import { + getFoodById, + getIngredients, + getNutrients, +} from "@/services/data/foods"; +import { supabase } from "@/services/supabase"; +import { Foods } from "@/types"; +import { Ingredient, Nutrient } from "@/types/index"; +import { Feather } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { router, useLocalSearchParams } from "expo-router"; import { useState } from "react"; import { + KeyboardAvoidingView, + Platform, ScrollView, - StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +interface Step { + id: string; + food_id: string; + title: string; + step_order: number; + description: string; + created_at: string; +} + export default function FoodDetailScreen() { const { id } = useLocalSearchParams(); const [activeTab, setActiveTab] = useState("Ingredients"); - // Mock data - in a real app, you would fetch this based on the ID - const foodData = { - id: 1, - name: "Pad Kra Pao Moo Sab with Eggs", - image: require("@/assets/images/food/padkrapao.jpg"), - description: - "Pad kra pao, also written as pad gaprao, is a popular Thai stir-fry of ground meat and holy basil.", - time: "30 Mins", - skills: "Easy", - ingredients: [ - { name: "Ground pork", emoji: "🥩" }, - { name: "Holy basil", emoji: "🌿" }, - { name: "Garlic", emoji: "🧄" }, - { name: "Thai chili", emoji: "🌶️" }, - { name: "Soy sauce", emoji: "🍶" }, - { name: "Oyster sauce", emoji: "🦪" }, - { name: "Sugar", emoji: "🧂" }, - { name: "Eggs", emoji: "🥚" }, - ], - calories: "520 kcal", - nutrition: { - fat: 15, - fiber: 3, - protein: 25, - carbs: 40, + const foodId = typeof id === "string" ? id : ""; + + const { + data: foodData, + isLoading, + error, + } = useQuery({ + queryKey: ["food-detail", foodId], + queryFn: async () => { + const { data, error } = await getFoodById(foodId); + if (error) throw error; + if (!data) throw new Error("Food not found"); + return data; }, - steps: [ - "Gather and prepare all ingredients", - "Heat oil in a wok or large frying pan", - "Fry the eggs sunny side up and set aside", - "Stir-fry garlic and chilies until fragrant", - "Add ground pork and cook until browned", - "Add sauces and basil, serve with rice and egg on top", - ], - }; + enabled: !!foodId, + }); + + const { + data: nutrients, + isLoading: nutrientsLoading, + error: nutrientsError, + } = useQuery({ + queryKey: ["food-nutrients", foodId], + queryFn: async () => { + const { data, error } = await getNutrients(foodId); + if (error) throw error; + return data; + }, + enabled: !!foodId && !!foodData, + }); + + const { + data: ingredients, + error: ingredientsError, + isLoading: ingredientsLoading, + } = useQuery({ + queryKey: ["food-ingredients", foodId], + queryFn: async () => { + const { data, error } = await getIngredients(foodId); + if (error) throw error; + return data ?? []; + }, + enabled: !!foodId && !!foodData, + }); + + const { + data: steps, + isLoading: stepsLoading, + error: stepsError, + } = useQuery({ + queryKey: ["food-steps", foodId], + queryFn: async () => { + const { data, error } = await supabase + .from("cooking_steps") + .select( + ` + id, + food_id, + title, + step_order, + description, + created_at + ` + ) + .eq("food_id", foodId) + .order("step_order", { ascending: true }); + if (error) throw error; + return data ?? []; + }, + enabled: !!foodId && !!foodData, + }); + + if (isLoading || stepsLoading || nutrientsLoading || ingredientsLoading) { + return ( + + + Loading... + + + ); + } + + if (error || !foodData || ingredientsError || stepsError || nutrientsError) { + return ( + + + Error loading food details + router.push("/home")} + > + + Go back to home page + + + + + ); + } const startCookingSession = () => { - router.push(`/cooking/[id]`); + // Corrected router push to use the actual foodId + router.push(`/cooking/${foodId}`); }; return ( - - - {/* Header with back and share buttons */} - - router.back()} - > - - - - - - - - {/* Food Image */} - - - - - {/* Food Title and Description */} - - {foodData.name} - {foodData.description} - - {/* Info Tabs */} - + + + + {/* Header with back and share buttons */} + setActiveTab("Skills")} + className="bg-[#ffd60a] p-3 rounded-lg" + onPress={() => router.back()} > - Skills - {foodData.skills} + - setActiveTab("Time")} - > - Time - {foodData.time} - - setActiveTab("Ingredients")} - > - Ingredients - {foodData.ingredients.length} - - setActiveTab("Calories")} - > - Calories - {foodData.calories} + + - {/* Ingredients Section */} - - Ingredients - - {foodData.ingredients.map((ingredient, index) => ( - - - - {ingredient.emoji} + {/* Food Image */} + + + {foodData.image_url ? ( + + ) : ( + + Image not available + + )} + + + + {/* Food Title and Description */} + + + {foodData.name} + + + {foodData.description} + + + {/* Info Tabs */} + + setActiveTab("Skills")} + > + Skills + + {foodData.skill_level} + + + setActiveTab("Time")} + > + Time + + {foodData.time_to_cook_minutes} + + + setActiveTab("Ingredients")} + > + Ingredients + + {/* Use ingredient_count from foodData or length of the fetched ingredients array */} + {foodData.ingredient_count ?? ingredients?.length ?? 0} + + + setActiveTab("Calories")} + > + Calories + + {foodData.calories} + + + + + {/* Ingredients Section */} + + + Ingredients + + + {(ingredients ?? []).map( + // Use the 'ingredients' state variable + ( + ingredient: Ingredient, + index: number // Added type for ingredient + ) => ( + + + {ingredient.emoji} + + + {ingredient.name} + + + ) + )} + {/* You might want to show a loading/empty state for ingredients here too */} + {/*!ingredientsLoading && ingredients?.length === 0 && ( + No ingredients listed. + )*/} + + + + {/* Nutrition Section - Improved UI */} + + + Nutrition Facts + + {/* Conditionally render nutrients or show placeholder/loading */} + {nutrients ? ( + + + + + {nutrients.fat_g ?? 0} + + + g + + + + Fat - {ingredient.name} - - ))} - - - - {/* Nutrition Section - Improved UI */} - - Nutrition Facts - - - - - {foodData.nutrition.fat} - - g - - Fat - - - - - {foodData.nutrition.fiber} - - g - - Fiber - - - - - {foodData.nutrition.protein} - - g - - Protein - - - - - {foodData.nutrition.carbs} - - g - - Carbs - - - - - {/* Steps Preview */} - - Cooking Steps - - {foodData.steps.slice(0, 2).map((step, index) => ( - - - {index + 1} + + + + {nutrients.fiber_g ?? 0} + + + g + + + + Fiber + + + + + + {nutrients.protein_g ?? 0} + + + g + + + + Protein + + + + + + {nutrients.carbs_g ?? 0} + + + g + + + + Carbs + - {step} - ))} - - ...and {foodData.steps.length - 2} more steps + ) : ( + + Nutrition facts not available. + + )} + + + {/* Steps Preview */} + + + Cooking Steps + + {steps && steps.length > 0 ? ( + steps.slice(0, 2).map( + ( + step: Step, + index: number // Added type for step + ) => ( + + + + {step.step_order ?? index + 1}{" "} + {/* Use step_order or fallback to index */} + + + + {step.description || step.title}{" "} + {/* Display description or title */} + + + ) + ) + ) : ( + + No cooking steps listed + + )} + {steps && steps.length > 2 && ( + + ...and {steps.length - 2} more steps + + )} + - - + - {/* Cook Button */} - - Let's Cook! - - + {/* Cook Button */} + + + Let's Cook! + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#FFFFFF", - }, - scrollView: { - flex: 1, - }, - header: { - flexDirection: "row", - justifyContent: "space-between", - paddingHorizontal: 16, - paddingVertical: 12, - position: "absolute", - top: 0, - left: 0, - right: 0, - zIndex: 10, - }, - backButton: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: "#FFCC00", - justifyContent: "center", - alignItems: "center", - }, - shareButton: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: "#FFFFFF", - justifyContent: "center", - alignItems: "center", - }, - imageContainer: { - alignItems: "center", - marginTop: 60, - marginBottom: 20, - }, - foodImage: { - width: 200, - height: 200, - borderRadius: 100, - borderWidth: 5, - borderColor: "#FFFFFF", - }, - contentContainer: { - paddingHorizontal: 16, - }, - foodTitle: { - fontSize: 24, - fontWeight: "bold", - color: "#333333", - marginBottom: 8, - }, - foodDescription: { - fontSize: 16, - color: "#666666", - marginBottom: 20, - lineHeight: 22, - }, - tabsContainer: { - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 20, - }, - tabItem: { - alignItems: "center", - }, - activeTabItem: { - borderBottomWidth: 2, - borderBottomColor: "#333333", - }, - tabLabel: { - fontSize: 14, - color: "#666666", - }, - tabValue: { - fontSize: 16, - fontWeight: "bold", - color: "#333333", - marginTop: 4, - }, - sectionContainer: { - marginBottom: 20, - }, - sectionTitle: { - fontSize: 20, - fontWeight: "bold", - color: "#333333", - marginBottom: 16, - }, - ingredientsGrid: { - flexDirection: "row", - flexWrap: "wrap", - }, - ingredientItem: { - width: "25%", - alignItems: "center", - marginBottom: 16, - }, - ingredientIconContainer: { - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: "#F8F8F8", - justifyContent: "center", - alignItems: "center", - marginBottom: 8, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - }, - ingredientEmoji: { - fontSize: 30, - }, - ingredientName: { - fontSize: 12, - textAlign: "center", - color: "#333333", - }, - nutritionSection: { - marginBottom: 20, - }, - nutritionContainer: { - flexDirection: "row", - justifyContent: "space-between", - backgroundColor: "#FFFFFF", - borderRadius: 12, - padding: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - }, - nutritionItem: { - alignItems: "center", - }, - nutritionCircle: { - width: 60, - height: 60, - borderRadius: 30, - justifyContent: "center", - alignItems: "center", - marginBottom: 8, - }, - nutritionValue: { - fontSize: 18, - fontWeight: "bold", - color: "#333333", - }, - nutritionUnit: { - fontSize: 12, - color: "#333333", - position: "absolute", - bottom: 10, - right: 10, - }, - nutritionLabel: { - fontSize: 14, - fontWeight: "500", - color: "#333333", - }, - stepsPreviewContainer: { - backgroundColor: "#F8F8F8", - borderRadius: 12, - padding: 16, - }, - stepPreviewItem: { - flexDirection: "row", - alignItems: "center", - marginBottom: 12, - }, - stepNumberCircle: { - width: 30, - height: 30, - borderRadius: 15, - backgroundColor: "#FFCC00", - justifyContent: "center", - alignItems: "center", - marginRight: 12, - }, - stepNumber: { - fontSize: 16, - fontWeight: "bold", - color: "#333333", - }, - stepPreviewText: { - fontSize: 16, - color: "#333333", - flex: 1, - }, - moreStepsText: { - fontSize: 14, - color: "#666666", - fontStyle: "italic", - textAlign: "center", - marginTop: 8, - }, - cookButton: { - position: "absolute", - bottom: 0, - left: 0, - right: 0, - backgroundColor: "#FF0000", - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - paddingVertical: 16, - }, - cookButtonText: { - fontSize: 18, - fontWeight: "bold", - color: "#FFCC00", - marginRight: 8, - }, -}); diff --git a/app/post-detail/[id].tsx b/app/post-detail/[id].tsx index 152c5b2..fbfe008 100644 --- a/app/post-detail/[id].tsx +++ b/app/post-detail/[id].tsx @@ -1,67 +1,77 @@ -"use client" +"use client"; -import { useState, useEffect, useRef } from "react" +import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useRef, useState } from "react"; import { - View, - Text, - Image, - TouchableOpacity, - ScrollView, ActivityIndicator, - FlatList, Alert, - TextInput, + FlatList, + Image, + Keyboard, KeyboardAvoidingView, Platform, - Keyboard, -} from "react-native" -import { Feather, MaterialCommunityIcons, Ionicons } from "@expo/vector-icons" -import { useLocalSearchParams, router } from "expo-router" -import { useAuth } from "../../context/auth-context" -import { supabase } from "../../services/supabase" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" + ScrollView, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useAuth } from "../../context/auth-context"; +import { + queryKeys, + useLikeMutation, + useSaveMutation, +} from "../../hooks/use-foods"; import { - getComments, - createComment, - getLikesCount, - getSavesCount, - getCommentsCount, checkUserLiked, checkUserSaved, -} from "../../services/data/forum" -import { getProfile } from "../../services/data/profile" -import { queryKeys, useLikeMutation, useSaveMutation } from "../../hooks/use-foods" + createComment, + getComments, + getCommentsCount, + getLikesCount, + getSavesCount, +} from "../../services/data/forum"; +import { getProfile } from "../../services/data/profile"; +import { supabase } from "../../services/supabase"; export default function PostDetailScreen() { - const params = useLocalSearchParams() - const foodId = typeof params.id === "string" ? params.id : "" - const queryClient = useQueryClient() - const scrollViewRef = useRef(null) + const params = useLocalSearchParams(); + const foodId = typeof params.id === "string" ? params.id : ""; + const queryClient = useQueryClient(); + const scrollViewRef = useRef(null); - console.log("Post detail screen - Food ID:", foodId) + console.log("Post detail screen - Food ID:", foodId); - const { isAuthenticated } = useAuth() - const [currentUserId, setCurrentUserId] = useState(null) - const [commentText, setCommentText] = useState("") - const [submittingComment, setSubmittingComment] = useState(false) - const [showReviews, setShowReviews] = useState(true) - const [keyboardVisible, setKeyboardVisible] = useState(false) + const { isAuthenticated } = useAuth(); + const [currentUserId, setCurrentUserId] = useState(null); + const [commentText, setCommentText] = useState(""); + const [submittingComment, setSubmittingComment] = useState(false); + const [showReviews, setShowReviews] = useState(true); + const [keyboardVisible, setKeyboardVisible] = useState(false); // Listen for keyboard events useEffect(() => { - const keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", () => { - setKeyboardVisible(true) - }) + const keyboardDidShowListener = Keyboard.addListener( + "keyboardDidShow", + () => { + setKeyboardVisible(true); + } + ); - const keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", () => { - setKeyboardVisible(false) - }) + const keyboardDidHideListener = Keyboard.addListener( + "keyboardDidHide", + () => { + setKeyboardVisible(false); + } + ); return () => { - keyboardDidShowListener.remove() - keyboardDidHideListener.remove() - } - }, []) + keyboardDidShowListener.remove(); + keyboardDidHideListener.remove(); + }; + }, []); // Recipe info cards data const recipeInfoCards = [ @@ -69,12 +79,15 @@ export default function PostDetailScreen() { id: "cooking_time", title: "Cooking Time", icon: ( - + ), value: (food: any) => food.time_to_cook_minutes, - unit: (food: any) => (food.time_to_cook_minutes === 1 ? "minute" : "minutes"), + unit: (food: any) => + food.time_to_cook_minutes === 1 ? "minute" : "minutes", gradient: ["#fff8e1", "#fffde7"], valueColor: "#bb0718", }, @@ -82,7 +95,9 @@ export default function PostDetailScreen() { id: "skill_level", title: "Skill Level", icon: ( - + ), @@ -92,7 +107,13 @@ export default function PostDetailScreen() { valueColor: "", customContent: (food: any) => ( - + {food.skill_level} {renderSkillLevelDots(food.skill_level)} @@ -103,7 +124,9 @@ export default function PostDetailScreen() { id: "ingredients", title: "Ingredients", icon: ( - + ), @@ -116,7 +139,9 @@ export default function PostDetailScreen() { id: "calories", title: "Calories", icon: ( - + ), @@ -125,23 +150,23 @@ export default function PostDetailScreen() { gradient: ["#ffebee", "#fff8e1"], valueColor: "#F44336", }, - ] + ]; // Get current user ID from Supabase session useEffect(() => { async function getCurrentUser() { if (isAuthenticated) { - const { data } = await supabase.auth.getSession() - const userId = data.session?.user?.id - console.log("Current user ID:", userId) - setCurrentUserId(userId || null) + const { data } = await supabase.auth.getSession(); + const userId = data.session?.user?.id; + console.log("Current user ID:", userId); + setCurrentUserId(userId || null); } else { - setCurrentUserId(null) + setCurrentUserId(null); } } - getCurrentUser() - }, [isAuthenticated]) + getCurrentUser(); + }, [isAuthenticated]); // Fetch food details const { @@ -151,9 +176,13 @@ export default function PostDetailScreen() { } = useQuery({ queryKey: queryKeys.foodDetails(foodId), queryFn: async () => { - const { data, error } = await supabase.from("foods").select("*").eq("id", foodId).single() + const { data, error } = await supabase + .from("foods") + .select("*") + .eq("id", foodId) + .single(); - if (error) throw error + if (error) throw error; return { ...data, @@ -163,25 +192,25 @@ export default function PostDetailScreen() { time_to_cook_minutes: data.time_to_cook_minutes ?? 0, skill_level: data.skill_level || "Easy", image_url: data.image_url || "", - } + }; }, enabled: !!foodId, - }) + }); // Fetch food creator const { data: foodCreator, isLoading: isLoadingCreator } = useQuery({ queryKey: ["food-creator", food?.created_by], queryFn: async () => { - if (!food?.created_by) return null + if (!food?.created_by) return null; - const { data, error } = await getProfile(food.created_by) + const { data, error } = await getProfile(food.created_by); - if (error) throw error + if (error) throw error; - return data + return data; }, enabled: !!food?.created_by, - }) + }); // Fetch food stats const { @@ -195,16 +224,16 @@ export default function PostDetailScreen() { getLikesCount(foodId), getSavesCount(foodId), getCommentsCount(foodId), - ]) + ]); return { likes: likesRes.count || 0, saves: savesRes.count || 0, comments: commentsRes.count || 0, - } + }; }, enabled: !!foodId, - }) + }); // Fetch user interactions const { @@ -214,20 +243,20 @@ export default function PostDetailScreen() { } = useQuery({ queryKey: ["user-interactions", foodId, currentUserId], queryFn: async () => { - if (!currentUserId) return { liked: false, saved: false } + if (!currentUserId) return { liked: false, saved: false }; const [likedRes, savedRes] = await Promise.all([ checkUserLiked(foodId, currentUserId), checkUserSaved(foodId, currentUserId), - ]) + ]); return { liked: !!likedRes.data, saved: !!savedRes.data, - } + }; }, enabled: !!foodId && !!currentUserId, - }) + }); // Fetch comments const { @@ -237,36 +266,48 @@ export default function PostDetailScreen() { } = useQuery({ queryKey: queryKeys.foodComments(foodId), queryFn: async () => { - const { data, error } = await getComments(foodId) + const { data, error } = await getComments(foodId); - if (error) throw error + if (error) throw error; - return data || [] + return data || []; }, enabled: !!foodId, - }) + }); // Set up mutations - const likeMutation = useLikeMutation() - const saveMutation = useSaveMutation() + const likeMutation = useLikeMutation(); + const saveMutation = useSaveMutation(); const commentMutation = useMutation({ - mutationFn: async ({ foodId, userId, content }: { foodId: string; userId: string; content: string }) => { - return createComment(foodId, userId, content) + mutationFn: async ({ + foodId, + userId, + content, + }: { + foodId: string; + userId: string; + content: string; + }) => { + return createComment(foodId, userId, content); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: queryKeys.foodComments(foodId) }) - queryClient.invalidateQueries({ queryKey: ["food-stats", foodId] }) - setCommentText("") - Keyboard.dismiss() + queryClient.invalidateQueries({ + queryKey: queryKeys.foodComments(foodId), + }); + queryClient.invalidateQueries({ queryKey: ["food-stats", foodId] }); + setCommentText(""); + Keyboard.dismiss(); }, - }) + }); // Set up real-time subscription for comments useEffect(() => { - if (!foodId) return + if (!foodId) return; - console.log(`Setting up real-time subscription for comments on food_id: ${foodId}`) + console.log( + `Setting up real-time subscription for comments on food_id: ${foodId}` + ); const subscription = supabase .channel(`food_comments:${foodId}`) @@ -279,21 +320,21 @@ export default function PostDetailScreen() { filter: `food_id=eq.${foodId}`, }, () => { - console.log("Comment change detected, refreshing comments") - refetchComments() - refetchStats() - }, + console.log("Comment change detected, refreshing comments"); + refetchComments(); + refetchStats(); + } ) - .subscribe() + .subscribe(); return () => { - supabase.removeChannel(subscription) - } - }, [foodId, refetchComments, refetchStats]) + supabase.removeChannel(subscription); + }; + }, [foodId, refetchComments, refetchStats]); // Set up real-time subscription for likes and saves useEffect(() => { - if (!foodId) return + if (!foodId) return; const likesSubscription = supabase .channel(`food_likes:${foodId}`) @@ -306,12 +347,14 @@ export default function PostDetailScreen() { filter: `food_id=eq.${foodId}`, }, () => { - console.log("Like change detected, refreshing stats and interactions") - refetchStats() - refetchInteractions() - }, + console.log( + "Like change detected, refreshing stats and interactions" + ); + refetchStats(); + refetchInteractions(); + } ) - .subscribe() + .subscribe(); const savesSubscription = supabase .channel(`food_saves:${foodId}`) @@ -324,23 +367,25 @@ export default function PostDetailScreen() { filter: `food_id=eq.${foodId}`, }, () => { - console.log("Save change detected, refreshing stats and interactions") - refetchStats() - refetchInteractions() - }, + console.log( + "Save change detected, refreshing stats and interactions" + ); + refetchStats(); + refetchInteractions(); + } ) - .subscribe() + .subscribe(); return () => { - supabase.removeChannel(likesSubscription) - supabase.removeChannel(savesSubscription) - } - }, [foodId, refetchStats, refetchInteractions]) + supabase.removeChannel(likesSubscription); + supabase.removeChannel(savesSubscription); + }; + }, [foodId, refetchStats, refetchInteractions]); const handleLike = async () => { if (!isAuthenticated || !currentUserId || !food) { - Alert.alert("Authentication Required", "Please log in to like posts.") - return + Alert.alert("Authentication Required", "Please log in to like posts."); + return; } try { @@ -348,17 +393,17 @@ export default function PostDetailScreen() { foodId, userId: currentUserId, isLiked: interactions.liked, - }) + }); } catch (error) { - console.error("Error toggling like:", error) - Alert.alert("Error", "Failed to update like. Please try again.") + console.error("Error toggling like:", error); + Alert.alert("Error", "Failed to update like. Please try again."); } - } + }; const handleSave = async () => { if (!isAuthenticated || !currentUserId || !food) { - Alert.alert("Authentication Required", "Please log in to save posts.") - return + Alert.alert("Authentication Required", "Please log in to save posts."); + return; } try { @@ -366,57 +411,57 @@ export default function PostDetailScreen() { foodId, userId: currentUserId, isSaved: interactions.saved, - }) + }); } catch (error) { - console.error("Error toggling save:", error) - Alert.alert("Error", "Failed to update save. Please try again.") + console.error("Error toggling save:", error); + Alert.alert("Error", "Failed to update save. Please try again."); } - } + }; const handleSubmitComment = async () => { if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) { if (!isAuthenticated || !currentUserId) { - Alert.alert("Authentication Required", "Please log in to comment.") + Alert.alert("Authentication Required", "Please log in to comment."); } - return + return; } - setSubmittingComment(true) + setSubmittingComment(true); try { await commentMutation.mutateAsync({ foodId, userId: currentUserId, content: commentText.trim(), - }) + }); } catch (error) { - console.error("Error submitting comment:", error) - Alert.alert("Error", "Failed to submit comment. Please try again.") + console.error("Error submitting comment:", error); + Alert.alert("Error", "Failed to submit comment. Please try again."); } finally { - setSubmittingComment(false) + setSubmittingComment(false); } - } + }; // Helper function to get skill level color const getSkillLevelColor = (level: string) => { switch (level) { case "Easy": - return "#4CAF50" // Green + return "#4CAF50"; // Green case "Medium": - return "#FFC107" // Amber + return "#FFC107"; // Amber case "Hard": - return "#F44336" // Red + return "#F44336"; // Red default: - return "#4CAF50" // Default to green + return "#4CAF50"; // Default to green } - } + }; // Helper function to get skill level dots const renderSkillLevelDots = (level: string) => { - const totalDots = 3 - let activeDots = 1 + const totalDots = 3; + let activeDots = 1; - if (level === "Medium") activeDots = 2 - if (level === "Hard") activeDots = 3 + if (level === "Medium") activeDots = 2; + if (level === "Hard") activeDots = 3; return ( @@ -434,12 +479,12 @@ export default function PostDetailScreen() { /> ))} - ) - } + ); + }; // Render recipe info card const renderRecipeInfoCard = ({ item }: { item: any }) => { - if (!food) return null + if (!food) return null; return ( - + {item.icon} - {item.title} + + {item.title} + {item.customContent ? ( item.customContent(food) ) : ( - {item.value(food)} - {item.unit(food)} + + {item.value(food)} + + + {item.unit(food)} + )} - ) - } + ); + }; - const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments + const isLoading = + isLoadingFood || + isLoadingCreator || + isLoadingStats || + isLoadingInteractions || + isLoadingComments; if (isLoading) { return ( - + - ) + ); } if (foodError || !food) { return ( - + Post not found - ) + ); } return ( {/* Fixed Header */} - - router.back()} - > - - - - Post - - - - - + + router.back()} + > + + + + Post + + router.push(`/food/${food.id}`)}> + + + {/* Scrollable Content */} - - + + {/* User info */} - - + + {foodCreator?.avatar_url ? ( - + ) : ( - - {foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || "?"} + + {foodCreator?.username?.charAt(0).toUpperCase() || + food.created_by?.charAt(0).toUpperCase() || + "?"} )} @@ -562,17 +671,38 @@ export default function PostDetailScreen() { {/* Food title and description */} - {food.name} - {food.description} + + {food.name} + + + {food.description} + {new Date(food.created_at).toLocaleDateString()} -{" "} - {new Date(food.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} + {new Date(food.created_at).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} {/* Recipe Info Cards - Horizontal Scrollable */} - + Recipe Details - {stats.likes} + + {stats.likes} + - - Save + + + Save + @@ -655,10 +793,18 @@ export default function PostDetailScreen() { borderRadius: 12, }} > - {stats.comments} + + {stats.comments} + - + {showReviews && ( @@ -678,7 +824,10 @@ export default function PostDetailScreen() { }} > {comment.user?.avatar_url ? ( - + ) : ( - - {comment.user?.username?.charAt(0).toUpperCase() || + + {comment.user?.username + ?.charAt(0) + .toUpperCase() || comment.user_id?.charAt(0).toUpperCase() || "?"} @@ -700,18 +857,41 @@ export default function PostDetailScreen() { {/* Comment bubble with username inside */} - + {/* Username inside bubble */} - - {comment.user?.username || comment.user?.full_name || "User"} + + {comment.user?.username || + comment.user?.full_name || + "User"} {/* Comment content */} - {comment.content} + + {comment.content} + {/* Date below bubble */} - + {new Date(comment.created_at).toLocaleDateString()} @@ -721,8 +901,18 @@ export default function PostDetailScreen() { ) : ( - No reviews yet. - Be the first to comment! + + No reviews yet. + + + Be the first to comment! + )} @@ -757,21 +947,37 @@ export default function PostDetailScreen() { style={{ padding: 12, borderRadius: 24, - backgroundColor: commentText.trim() && isAuthenticated ? "#ffd60a" : "#e0e0e0", + backgroundColor: + commentText.trim() && isAuthenticated ? "#ffd60a" : "#e0e0e0", }} onPress={handleSubmitComment} - disabled={submittingComment || !commentText.trim() || !isAuthenticated} + disabled={ + submittingComment || !commentText.trim() || !isAuthenticated + } > - + {!isAuthenticated && ( - + Please log in to comment )} - ) + ); } diff --git a/app/recipe-detail.tsx b/app/recipe-detail.tsx deleted file mode 100644 index 19ddc47..0000000 --- a/app/recipe-detail.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, StatusBar } from 'react-native'; -import { Feather, FontAwesome5 } from '@expo/vector-icons'; -import { useLocalSearchParams, router } from 'expo-router'; - -export default function RecipeDetailScreen() { - const { title, image } = useLocalSearchParams(); - - const recipeTitle = title || "Pad Kra Pao Moo Sab with Eggs"; - const recipeImage = typeof image === 'string' ? image : "/placeholder.svg?height=400&width=400&query=thai basil stir fry with egg and rice"; - - return ( - - - - - {/* Header with back and share buttons */} - - router.back()} - > - - - - - - - - - {/* Recipe Image */} - - - - - {/* Recipe Title and Description */} - - {recipeTitle} - - Pad kra pao, also written as pad gaprao, is a popular Thai stir fry of ground meat and holy basil. - - - {/* Recipe Info */} - - - Skills - Easy - - - - Time - 30 Mins - - - - Ingredients - 10+ - - - - Calories - 300 kCal - - - - {/* Ingredients */} - Ingredients - - - - - - - - - - - {/* Nutrition Info */} - - - - - 0 - /32g - - Fat - - - - - 0 - /32g - - Fiber - - - - - 0 - /32g - - Protein - - - - - 0 - /32g - - Carbs - - - - - - {/* Bottom Spacing */} - - - - {/* Cook Button */} - - - - Let's Cook! - - - - - - - ); -} \ No newline at end of file diff --git a/services/data/foods.ts b/services/data/foods.ts index 0f68eb5..b7cd004 100644 --- a/services/data/foods.ts +++ b/services/data/foods.ts @@ -45,6 +45,19 @@ export const getFoods = async ( return { data, error }; }; +/** + * Retrieves a list of foods based on the provided filters. + */ +export const getFoodById = async (foodId: string): Promise<{ data: Foods | null; error: PostgrestError | null }> => { + const { data, error } = await supabase.from("foods") + .select( + `id, name, description, time_to_cook_minutes, skill_level, ingredient_count, calories, image_url, is_shared, created_by, created_at` + ) + .eq("id", foodId) + .single(); + return { data, error }; +} + /** * Retrieves a list of saved foods for a specific user. * @@ -97,8 +110,9 @@ export const getNutrients = async (food_id: string): Promise<{ data: Nutrient | created_at `) .eq("food_id", food_id) - .single() - return { data, error }; + .limit(1) + + return { data: data?.[0] || null, error }; } interface Ingredient {