feat: add foods detail page

This commit is contained in:
Sosokker 2025-05-11 22:44:21 +07:00
parent 233d08d700
commit 44a2f89a59
5 changed files with 806 additions and 765 deletions

View File

@ -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",
});
}
};

View File

@ -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<Foods, Error>({
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<Nutrient | null, Error>({
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<Ingredient[], Error>({
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<Step[], Error>({
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 (
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
<View className="flex-1 justify-center items-center">
<Text>Loading...</Text>
</View>
</SafeAreaView>
);
}
if (error || !foodData || ingredientsError || stepsError || nutrientsError) {
return (
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
<View className="flex-1 justify-center items-center">
<Text>Error loading food details</Text>
<TouchableOpacity
className="px-4 py-2 bg-yellow-400 rounded-full mt-4"
onPress={() => router.push("/home")}
>
<Text className="text-lg font-bold text-white">
Go back to home page
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const startCookingSession = () => {
router.push(`/cooking/[id]`);
// Corrected router push to use the actual foodId
router.push(`/cooking/${foodId}`);
};
return (
<SafeAreaView style={styles.container} edges={["top"]}>
<ScrollView style={styles.scrollView}>
{/* Header with back and share buttons */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<IconSymbol name="chevron.left" size={24} color="#333333" />
</TouchableOpacity>
<TouchableOpacity style={styles.shareButton}>
<IconSymbol name="square.and.arrow.up" size={24} color="#FFCC00" />
</TouchableOpacity>
</View>
{/* Food Image */}
<View style={styles.imageContainer}>
<Image
source={foodData.image}
style={styles.foodImage}
contentFit="cover"
/>
</View>
{/* Food Title and Description */}
<View style={styles.contentContainer}>
<Text style={styles.foodTitle}>{foodData.name}</Text>
<Text style={styles.foodDescription}>{foodData.description}</Text>
{/* Info Tabs */}
<View style={styles.tabsContainer}>
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1"
>
<ScrollView className="flex-1">
{/* Header with back and share buttons */}
<View className="flex-row justify-between px-4 py-3 absolute top-0 left-0 right-0 z-10">
<TouchableOpacity
style={styles.tabItem}
onPress={() => setActiveTab("Skills")}
className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => router.back()}
>
<Text style={styles.tabLabel}>Skills</Text>
<Text style={styles.tabValue}>{foodData.skills}</Text>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
<TouchableOpacity
style={styles.tabItem}
onPress={() => setActiveTab("Time")}
>
<Text style={styles.tabLabel}>Time</Text>
<Text style={styles.tabValue}>{foodData.time}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tabItem,
activeTab === "Ingredients" && styles.activeTabItem,
]}
onPress={() => setActiveTab("Ingredients")}
>
<Text style={styles.tabLabel}>Ingredients</Text>
<Text style={styles.tabValue}>{foodData.ingredients.length}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.tabItem}
onPress={() => setActiveTab("Calories")}
>
<Text style={styles.tabLabel}>Calories</Text>
<Text style={styles.tabValue}>{foodData.calories}</Text>
<TouchableOpacity className="w-10 h-10 rounded-full bg-white justify-center items-center">
<IconSymbol
name="square.and.arrow.up"
size={24}
color="#FFCC00"
/>
</TouchableOpacity>
</View>
{/* Ingredients Section */}
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Ingredients</Text>
<View style={styles.ingredientsGrid}>
{foodData.ingredients.map((ingredient, index) => (
<View key={index} style={styles.ingredientItem}>
<View style={styles.ingredientIconContainer}>
<Text style={styles.ingredientEmoji}>
{ingredient.emoji}
{/* Food Image */}
<View className="items-center mt-16 mb-5">
<View
style={{
width: 200,
height: 200,
backgroundColor: "#e0e0e0",
borderRadius: 24,
overflow: "hidden",
}}
>
{foodData.image_url ? (
<Image
source={{ uri: foodData.image_url }}
className="w-52 h-52 rounded-full border-4 border-white"
style={{ width: "100%", height: "100%" }}
/>
) : (
<Text className="text-lg font-bold text-gray-500">
Image not available
</Text>
)}
</View>
</View>
{/* Food Title and Description */}
<View className="px-4">
<Text className="text-2xl font-bold text-gray-800 mb-2">
{foodData.name}
</Text>
<Text className="text-base text-gray-500 mb-5 leading-6">
{foodData.description}
</Text>
{/* Info Tabs */}
<View className="flex-row justify-between mb-5">
<TouchableOpacity
className="items-center"
onPress={() => setActiveTab("Skills")}
>
<Text className="text-sm text-gray-500">Skills</Text>
<Text className="text-base font-bold text-gray-800 mt-1">
{foodData.skill_level}
</Text>
</TouchableOpacity>
<TouchableOpacity
className="items-center"
onPress={() => setActiveTab("Time")}
>
<Text className="text-sm text-gray-500">Time</Text>
<Text className="text-base font-bold text-gray-800 mt-1">
{foodData.time_to_cook_minutes}
</Text>
</TouchableOpacity>
<TouchableOpacity
className={`items-center ${
activeTab === "Ingredients"
? "border-b-2 border-gray-800"
: ""
}`}
onPress={() => setActiveTab("Ingredients")}
>
<Text className="text-sm text-gray-500">Ingredients</Text>
<Text className="text-base font-bold text-gray-800 mt-1">
{/* Use ingredient_count from foodData or length of the fetched ingredients array */}
{foodData.ingredient_count ?? ingredients?.length ?? 0}
</Text>
</TouchableOpacity>
<TouchableOpacity
className="items-center"
onPress={() => setActiveTab("Calories")}
>
<Text className="text-sm text-gray-500">Calories</Text>
<Text className="text-base font-bold text-gray-800 mt-1">
{foodData.calories}
</Text>
</TouchableOpacity>
</View>
{/* Ingredients Section */}
<View className="mb-5">
<Text className="text-xl font-bold text-gray-800 mb-4">
Ingredients
</Text>
<View className="flex-row flex-wrap">
{(ingredients ?? []).map(
// Use the 'ingredients' state variable
(
ingredient: Ingredient,
index: number // Added type for ingredient
) => (
<View
key={ingredient.id || index}
className="w-1/4 items-center mb-4"
>
<View className="w-15 h-15 rounded-full bg-gray-100 justify-center items-center mb-2 shadow">
<Text className="text-2xl">{ingredient.emoji}</Text>
</View>
<Text className="text-xs text-center text-gray-800">
{ingredient.name}
</Text>
</View>
)
)}
{/* You might want to show a loading/empty state for ingredients here too */}
{/*!ingredientsLoading && ingredients?.length === 0 && (
<Text className="text-sm text-gray-500">No ingredients listed.</Text>
)*/}
</View>
</View>
{/* Nutrition Section - Improved UI */}
<View className="mb-5">
<Text className="text-xl font-bold text-gray-800 mb-4">
Nutrition Facts
</Text>
{/* Conditionally render nutrients or show placeholder/loading */}
{nutrients ? (
<View className="flex-row justify-between bg-white rounded-xl p-4 shadow">
<View className="items-center">
<View
className="w-15 h-15 rounded-full justify-center items-center mb-2"
style={{ backgroundColor: "#FFD700" }}
>
<Text className="text-lg font-bold text-gray-800">
{nutrients.fat_g ?? 0}
</Text>
<Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
g
</Text>
</View>
<Text className="text-sm font-medium text-gray-800">
Fat
</Text>
</View>
<Text style={styles.ingredientName}>{ingredient.name}</Text>
</View>
))}
</View>
</View>
{/* Nutrition Section - Improved UI */}
<View style={styles.nutritionSection}>
<Text style={styles.sectionTitle}>Nutrition Facts</Text>
<View style={styles.nutritionContainer}>
<View style={styles.nutritionItem}>
<View
style={[
styles.nutritionCircle,
{ backgroundColor: "#FFD700" },
]}
>
<Text style={styles.nutritionValue}>
{foodData.nutrition.fat}
</Text>
<Text style={styles.nutritionUnit}>g</Text>
</View>
<Text style={styles.nutritionLabel}>Fat</Text>
</View>
<View style={styles.nutritionItem}>
<View
style={[
styles.nutritionCircle,
{ backgroundColor: "#90EE90" },
]}
>
<Text style={styles.nutritionValue}>
{foodData.nutrition.fiber}
</Text>
<Text style={styles.nutritionUnit}>g</Text>
</View>
<Text style={styles.nutritionLabel}>Fiber</Text>
</View>
<View style={styles.nutritionItem}>
<View
style={[
styles.nutritionCircle,
{ backgroundColor: "#ADD8E6" },
]}
>
<Text style={styles.nutritionValue}>
{foodData.nutrition.protein}
</Text>
<Text style={styles.nutritionUnit}>g</Text>
</View>
<Text style={styles.nutritionLabel}>Protein</Text>
</View>
<View style={styles.nutritionItem}>
<View
style={[
styles.nutritionCircle,
{ backgroundColor: "#FFA07A" },
]}
>
<Text style={styles.nutritionValue}>
{foodData.nutrition.carbs}
</Text>
<Text style={styles.nutritionUnit}>g</Text>
</View>
<Text style={styles.nutritionLabel}>Carbs</Text>
</View>
</View>
</View>
{/* Steps Preview */}
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Cooking Steps</Text>
<View style={styles.stepsPreviewContainer}>
{foodData.steps.slice(0, 2).map((step, index) => (
<View key={index} style={styles.stepPreviewItem}>
<View style={styles.stepNumberCircle}>
<Text style={styles.stepNumber}>{index + 1}</Text>
<View className="items-center">
<View
className="w-15 h-15 rounded-full justify-center items-center mb-2"
style={{ backgroundColor: "#90EE90" }}
>
<Text className="text-lg font-bold text-gray-800">
{nutrients.fiber_g ?? 0}
</Text>
<Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
g
</Text>
</View>
<Text className="text-sm font-medium text-gray-800">
Fiber
</Text>
</View>
<View className="items-center">
<View
className="w-15 h-15 rounded-full justify-center items-center mb-2"
style={{ backgroundColor: "#ADD8E6" }}
>
<Text className="text-lg font-bold text-gray-800">
{nutrients.protein_g ?? 0}
</Text>
<Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
g
</Text>
</View>
<Text className="text-sm font-medium text-gray-800">
Protein
</Text>
</View>
<View className="items-center">
<View
className="w-15 h-15 rounded-full justify-center items-center mb-2"
style={{ backgroundColor: "#FFA07A" }}
>
<Text className="text-lg font-bold text-gray-800">
{nutrients.carbs_g ?? 0}
</Text>
<Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
g
</Text>
</View>
<Text className="text-sm font-medium text-gray-800">
Carbs
</Text>
</View>
<Text style={styles.stepPreviewText}>{step}</Text>
</View>
))}
<Text style={styles.moreStepsText}>
...and {foodData.steps.length - 2} more steps
) : (
<Text className="text-sm text-gray-500">
Nutrition facts not available.
</Text>
)}
</View>
{/* Steps Preview */}
<View className="mb-5">
<Text className="text-xl font-bold text-gray-800 mb-4">
Cooking Steps
</Text>
<View className="bg-gray-100 rounded-xl p-4">
{steps && steps.length > 0 ? (
steps.slice(0, 2).map(
(
step: Step,
index: number // Added type for step
) => (
<View
key={step.id || index}
className="flex-row items-center mb-3"
>
<View className="w-7.5 h-7.5 rounded-full bg-yellow-400 justify-center items-center mr-3">
<Text className="text-base font-bold text-gray-800">
{step.step_order ?? index + 1}{" "}
{/* Use step_order or fallback to index */}
</Text>
</View>
<Text className="text-base text-gray-800 flex-1">
{step.description || step.title}{" "}
{/* Display description or title */}
</Text>
</View>
)
)
) : (
<Text className="text-sm text-gray-500 italic text-center mt-2">
No cooking steps listed
</Text>
)}
{steps && steps.length > 2 && (
<Text className="text-sm text-gray-500 italic text-center mt-2">
...and {steps.length - 2} more steps
</Text>
)}
</View>
</View>
</View>
</View>
</ScrollView>
</ScrollView>
{/* Cook Button */}
<TouchableOpacity style={styles.cookButton} onPress={startCookingSession}>
<Text style={styles.cookButtonText}>Let&apos;s Cook!</Text>
<IconSymbol name="fork.knife" size={20} color="#FFCC00" />
</TouchableOpacity>
{/* Cook Button */}
<TouchableOpacity
className="absolute bottom-0 left-0 right-0 bg-red-600 flex-row justify-center items-center py-4"
onPress={startCookingSession}
// Disable button if essential data is missing or still loading
// disabled={isLoading || ingredientsLoading || stepsLoading || !ingredients || !steps}
>
<Text className="text-lg font-bold text-yellow-400 mr-2">
Let&apos;s Cook!
</Text>
<IconSymbol name="fork.knife" size={20} color="#FFCC00" />
</TouchableOpacity>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
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,
},
});

View File

@ -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<ScrollView>(null)
const params = useLocalSearchParams();
const foodId = typeof params.id === "string" ? params.id : "";
const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null);
console.log("Post detail screen - Food ID:", foodId)
console.log("Post detail screen - Food ID:", foodId);
const { isAuthenticated } = useAuth()
const [currentUserId, setCurrentUserId] = useState<string | null>(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<string | null>(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: (
<View style={{ backgroundColor: "#ffd60a", padding: 8, borderRadius: 16 }}>
<View
style={{ backgroundColor: "#ffd60a", padding: 8, borderRadius: 16 }}
>
<Feather name="clock" size={18} color="#bb0718" />
</View>
),
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: (
<View style={{ backgroundColor: "#4CAF50", padding: 8, borderRadius: 16 }}>
<View
style={{ backgroundColor: "#4CAF50", padding: 8, borderRadius: 16 }}
>
<MaterialCommunityIcons name="chef-hat" size={18} color="white" />
</View>
),
@ -92,7 +107,13 @@ export default function PostDetailScreen() {
valueColor: "",
customContent: (food: any) => (
<View>
<Text style={{ fontSize: 20, fontWeight: "bold", color: getSkillLevelColor(food.skill_level) }}>
<Text
style={{
fontSize: 20,
fontWeight: "bold",
color: getSkillLevelColor(food.skill_level),
}}
>
{food.skill_level}
</Text>
{renderSkillLevelDots(food.skill_level)}
@ -103,7 +124,9 @@ export default function PostDetailScreen() {
id: "ingredients",
title: "Ingredients",
icon: (
<View style={{ backgroundColor: "#2196F3", padding: 8, borderRadius: 16 }}>
<View
style={{ backgroundColor: "#2196F3", padding: 8, borderRadius: 16 }}
>
<Feather name="list" size={18} color="white" />
</View>
),
@ -116,7 +139,9 @@ export default function PostDetailScreen() {
id: "calories",
title: "Calories",
icon: (
<View style={{ backgroundColor: "#F44336", padding: 8, borderRadius: 16 }}>
<View
style={{ backgroundColor: "#F44336", padding: 8, borderRadius: 16 }}
>
<Ionicons name="flame" size={18} color="white" />
</View>
),
@ -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 (
<View style={{ flexDirection: "row", marginTop: 4 }}>
@ -434,12 +479,12 @@ export default function PostDetailScreen() {
/>
))}
</View>
)
}
);
};
// Render recipe info card
const renderRecipeInfoCard = ({ item }: { item: any }) => {
if (!food) return null
if (!food) return null;
return (
<View
@ -451,38 +496,72 @@ export default function PostDetailScreen() {
width: 160,
}}
>
<View style={{ flexDirection: "row", alignItems: "center", marginBottom: 8 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
}}
>
{item.icon}
<Text style={{ marginLeft: 8, fontWeight: "bold", color: "#505050" }}>{item.title}</Text>
<Text style={{ marginLeft: 8, fontWeight: "bold", color: "#505050" }}>
{item.title}
</Text>
</View>
{item.customContent ? (
item.customContent(food)
) : (
<View style={{ flexDirection: "row", alignItems: "baseline" }}>
<Text style={{ fontSize: 24, fontWeight: "bold", color: item.valueColor }}>{item.value(food)}</Text>
<Text style={{ marginLeft: 4, fontSize: 14, fontWeight: "500", color: "#606060" }}>{item.unit(food)}</Text>
<Text
style={{
fontSize: 24,
fontWeight: "bold",
color: item.valueColor,
}}
>
{item.value(food)}
</Text>
<Text
style={{
marginLeft: 4,
fontSize: 14,
fontWeight: "500",
color: "#606060",
}}
>
{item.unit(food)}
</Text>
</View>
)}
</View>
)
}
);
};
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments
const isLoading =
isLoadingFood ||
isLoadingCreator ||
isLoadingStats ||
isLoadingInteractions ||
isLoadingComments;
if (isLoading) {
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<ActivityIndicator size="large" color="#ffd60a" />
</View>
</View>
)
);
}
if (foodError || !food) {
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text style={{ fontSize: 18 }}>Post not found</Text>
<TouchableOpacity
style={{
@ -498,38 +577,60 @@ export default function PostDetailScreen() {
</TouchableOpacity>
</View>
</View>
)
);
}
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
{/* Fixed Header */}
<View className="flex-row items-center justify-between px-4 py-3 mt-11">
<TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => router.back()}
>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
<Text className="text-2xl font-bold">Post</Text>
<TouchableOpacity>
<Feather name="more-horizontal" size={24} color="#000" />
</TouchableOpacity>
</View>
<View className="flex-row items-center justify-between px-4 py-3 mt-11">
<TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => router.back()}
>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
<Text className="text-2xl font-bold">Post</Text>
<TouchableOpacity onPress={() => router.push(`/food/${food.id}`)}>
<Feather name="external-link" size={24} color="#000" />
</TouchableOpacity>
</View>
{/* Scrollable Content */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
>
<ScrollView ref={scrollViewRef} style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 20 }}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1"
>
<ScrollView
ref={scrollViewRef}
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 20 }}
>
{/* User info */}
<View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12 }}>
<View style={{ width: 48, height: 48, backgroundColor: "#e0e0e0", borderRadius: 24, overflow: "hidden" }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
}}
>
<View
style={{
width: 48,
height: 48,
backgroundColor: "#e0e0e0",
borderRadius: 24,
overflow: "hidden",
}}
>
{foodCreator?.avatar_url ? (
<Image source={{ uri: foodCreator.avatar_url }} style={{ width: "100%", height: "100%" }} />
<Image
source={{ uri: foodCreator.avatar_url }}
style={{ width: "100%", height: "100%" }}
/>
) : (
<View
style={{
@ -540,8 +641,16 @@ export default function PostDetailScreen() {
justifyContent: "center",
}}
>
<Text style={{ fontSize: 18, fontWeight: "bold", color: "#606060" }}>
{foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || "?"}
<Text
style={{
fontSize: 18,
fontWeight: "bold",
color: "#606060",
}}
>
{foodCreator?.username?.charAt(0).toUpperCase() ||
food.created_by?.charAt(0).toUpperCase() ||
"?"}
</Text>
</View>
)}
@ -562,17 +671,38 @@ export default function PostDetailScreen() {
{/* Food title and description */}
<View style={{ paddingHorizontal: 16, marginBottom: 8 }}>
<Text style={{ fontSize: 30, fontWeight: "bold", marginBottom: 8 }}>{food.name}</Text>
<Text style={{ color: "#505050", marginBottom: 8, fontSize: 16, lineHeight: 24 }}>{food.description}</Text>
<Text style={{ fontSize: 30, fontWeight: "bold", marginBottom: 8 }}>
{food.name}
</Text>
<Text
style={{
color: "#505050",
marginBottom: 8,
fontSize: 16,
lineHeight: 24,
}}
>
{food.description}
</Text>
<Text style={{ color: "#808080", fontSize: 14 }}>
{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",
})}
</Text>
</View>
{/* Recipe Info Cards - Horizontal Scrollable */}
<View style={{ paddingVertical: 16 }}>
<Text style={{ paddingHorizontal: 16, fontSize: 20, fontWeight: "bold", marginBottom: 12 }}>
<Text
style={{
paddingHorizontal: 16,
fontSize: 20,
fontWeight: "bold",
marginBottom: 12,
}}
>
Recipe Details
</Text>
<FlatList
@ -611,7 +741,9 @@ export default function PostDetailScreen() {
size={22}
color={interactions.liked ? "#E91E63" : "#333"}
/>
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>{stats.likes}</Text>
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>
{stats.likes}
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -626,8 +758,14 @@ export default function PostDetailScreen() {
}}
onPress={handleSave}
>
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} />
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>Save</Text>
<Feather
name="bookmark"
size={22}
color={interactions.saved ? "#ffd60a" : "#333"}
/>
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>
Save
</Text>
</TouchableOpacity>
</View>
@ -655,10 +793,18 @@ export default function PostDetailScreen() {
borderRadius: 12,
}}
>
<Text style={{ fontSize: 12, fontWeight: "bold", color: "#bb0718" }}>{stats.comments}</Text>
<Text
style={{ fontSize: 12, fontWeight: "bold", color: "#bb0718" }}
>
{stats.comments}
</Text>
</View>
</View>
<Feather name={showReviews ? "chevron-up" : "chevron-down"} size={20} color="#333" />
<Feather
name={showReviews ? "chevron-up" : "chevron-down"}
size={20}
color="#333"
/>
</TouchableOpacity>
{showReviews && (
@ -678,7 +824,10 @@ export default function PostDetailScreen() {
}}
>
{comment.user?.avatar_url ? (
<Image source={{ uri: comment.user.avatar_url }} style={{ width: "100%", height: "100%" }} />
<Image
source={{ uri: comment.user.avatar_url }}
style={{ width: "100%", height: "100%" }}
/>
) : (
<View
style={{
@ -689,8 +838,16 @@ export default function PostDetailScreen() {
justifyContent: "center",
}}
>
<Text style={{ fontSize: 16, fontWeight: "bold", color: "white" }}>
{comment.user?.username?.charAt(0).toUpperCase() ||
<Text
style={{
fontSize: 16,
fontWeight: "bold",
color: "white",
}}
>
{comment.user?.username
?.charAt(0)
.toUpperCase() ||
comment.user_id?.charAt(0).toUpperCase() ||
"?"}
</Text>
@ -700,18 +857,41 @@ export default function PostDetailScreen() {
{/* Comment bubble with username inside */}
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ backgroundColor: "#f0f0f0", padding: 12, borderRadius: 16 }}>
<View
style={{
backgroundColor: "#f0f0f0",
padding: 12,
borderRadius: 16,
}}
>
{/* Username inside bubble */}
<Text style={{ fontWeight: "bold", fontSize: 16, marginBottom: 4 }}>
{comment.user?.username || comment.user?.full_name || "User"}
<Text
style={{
fontWeight: "bold",
fontSize: 16,
marginBottom: 4,
}}
>
{comment.user?.username ||
comment.user?.full_name ||
"User"}
</Text>
{/* Comment content */}
<Text style={{ color: "#303030", lineHeight: 20 }}>{comment.content}</Text>
<Text style={{ color: "#303030", lineHeight: 20 }}>
{comment.content}
</Text>
</View>
{/* Date below bubble */}
<Text style={{ color: "#808080", fontSize: 12, marginTop: 4, marginLeft: 8 }}>
<Text
style={{
color: "#808080",
fontSize: 12,
marginTop: 4,
marginLeft: 8,
}}
>
{new Date(comment.created_at).toLocaleDateString()}
</Text>
</View>
@ -721,8 +901,18 @@ export default function PostDetailScreen() {
) : (
<View style={{ paddingVertical: 32, alignItems: "center" }}>
<Feather name="message-circle" size={40} color="#e0e0e0" />
<Text style={{ marginTop: 8, color: "#808080", textAlign: "center" }}>No reviews yet.</Text>
<Text style={{ color: "#808080", textAlign: "center" }}>Be the first to comment!</Text>
<Text
style={{
marginTop: 8,
color: "#808080",
textAlign: "center",
}}
>
No reviews yet.
</Text>
<Text style={{ color: "#808080", textAlign: "center" }}>
Be the first to comment!
</Text>
</View>
)}
</View>
@ -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
}
>
<Feather name="send" size={20} color={commentText.trim() && isAuthenticated ? "#bb0718" : "#666"} />
<Feather
name="send"
size={20}
color={
commentText.trim() && isAuthenticated ? "#bb0718" : "#666"
}
/>
</TouchableOpacity>
</View>
{!isAuthenticated && (
<Text style={{ textAlign: "center", fontSize: 14, color: "#E91E63", marginTop: 4 }}>
<Text
style={{
textAlign: "center",
fontSize: 14,
color: "#E91E63",
marginTop: 4,
}}
>
Please log in to comment
</Text>
)}
</View>
</KeyboardAvoidingView>
</View>
)
);
}

View File

@ -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 (
<SafeAreaView className="flex-1 bg-white">
<StatusBar barStyle="dark-content" />
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
{/* Header with back and share buttons */}
<View className="flex-row justify-between items-center px-4 pt-4 absolute z-10 w-full">
<TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => router.back()}
>
<Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity>
<TouchableOpacity className="bg-white p-3 rounded-lg">
<Feather name="send" size={24} color="#ffd60a" />
</TouchableOpacity>
</View>
{/* Recipe Image */}
<View className="items-center justify-center pt-16 pb-6">
<Image
source={{ uri: recipeImage }}
className="w-72 h-72 rounded-full"
/>
</View>
{/* Recipe Title and Description */}
<View className="px-6">
<Text className="text-4xl font-bold">{recipeTitle}</Text>
<Text className="text-gray-600 mt-2 text-lg">
Pad kra pao, also written as pad gaprao, is a popular Thai stir fry of ground meat and holy basil.
</Text>
{/* Recipe Info */}
<View className="flex-row justify-between mt-8">
<View>
<Text className="text-2xl font-bold">Skills</Text>
<Text className="text-gray-600 mt-1">Easy</Text>
</View>
<View>
<Text className="text-2xl font-bold">Time</Text>
<Text className="text-gray-600 mt-1">30 Mins</Text>
</View>
<View>
<Text className="text-2xl font-bold">Ingredients</Text>
<Text className="text-gray-600 mt-1">10+</Text>
</View>
<View>
<Text className="text-2xl font-bold">Calories</Text>
<Text className="text-gray-600 mt-1">300 kCal</Text>
</View>
</View>
{/* Ingredients */}
<Text className="text-3xl font-bold mt-12">Ingredients</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
className="mt-6 mb-4"
contentContainerStyle={{ paddingLeft: 4, paddingRight: 20 }}
>
<View className="flex-row space-x-6">
<View className="w-24 h-24 bg-gray-300 rounded-lg" />
<View className="w-24 h-24 bg-gray-300 rounded-lg" />
<View className="w-24 h-24 bg-gray-300 rounded-lg" />
<View className="w-24 h-24 bg-gray-300 rounded-lg" />
<View className="w-24 h-24 bg-gray-300 rounded-lg" />
</View>
</ScrollView>
{/* Nutrition Info */}
<View className="bg-[#ffd60a] rounded-lg p-6 mt-10">
<View className="flex-row justify-between">
<View className="items-center">
<View className="w-16 h-16 rounded-full border-4 border-[#397e36] items-center justify-center">
<Text className="text-xl font-bold">0</Text>
<Text className="text-xs">/32g</Text>
</View>
<Text className="mt-2 font-semibold">Fat</Text>
</View>
<View className="items-center">
<View className="w-16 h-16 rounded-full border-4 border-[#397e36] items-center justify-center">
<Text className="text-xl font-bold">0</Text>
<Text className="text-xs">/32g</Text>
</View>
<Text className="mt-2 font-semibold">Fiber</Text>
</View>
<View className="items-center">
<View className="w-16 h-16 rounded-full border-4 border-[#a07d1a] items-center justify-center">
<Text className="text-xl font-bold">0</Text>
<Text className="text-xs">/32g</Text>
</View>
<Text className="mt-2 font-semibold">Protein</Text>
</View>
<View className="items-center">
<View className="w-16 h-16 rounded-full border-4 border-[#c87a20] items-center justify-center">
<Text className="text-xl font-bold">0</Text>
<Text className="text-xs">/32g</Text>
</View>
<Text className="mt-2 font-semibold">Carbs</Text>
</View>
</View>
</View>
</View>
{/* Bottom Spacing */}
<View className="h-28" />
</ScrollView>
{/* Cook Button */}
<View className="absolute bottom-0 left-0 right-0">
<View className="bg-[#bb0718] py-4 items-center">
<View className="flex-row items-center">
<Text className="text-[#ffd60a] text-2xl font-bold mr-2">Let's Cook!</Text>
<FontAwesome5 name="utensils" size={24} color="#ffd60a" />
</View>
</View>
<View className="bg-[#ffd60a] h-16" style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, borderBottomLeftRadius: 50, borderBottomRightRadius: 50 }} />
</View>
</SafeAreaView>
);
}

View File

@ -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 {