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) => { const navigateToFoodDetail = (foodId: string) => {
router.push({ pathname: "/recipe-detail", params: { id: foodId } }); router.push({ pathname: "/food/[id]", params: { id: foodId } });
}; };
export default function HomeScreen() { export default function HomeScreen() {
@ -147,11 +147,7 @@ export default function HomeScreen() {
setImageProcessing(false); setImageProcessing(false);
} }
router.push({ router.push({
pathname: "/recipe-detail", pathname: "/profile",
params: {
title: "My New Recipe",
image: result.assets[0].uri,
},
}); });
} }
}; };

View File

@ -1,454 +1,419 @@
"use client"; "use client";
import { IconSymbol } from "@/components/ui/IconSymbol"; 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 { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { import {
KeyboardAvoidingView,
Platform,
ScrollView, ScrollView,
StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, View,
} from "react-native"; } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; 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() { export default function FoodDetailScreen() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [activeTab, setActiveTab] = useState("Ingredients"); const [activeTab, setActiveTab] = useState("Ingredients");
// Mock data - in a real app, you would fetch this based on the ID const foodId = typeof id === "string" ? id : "";
const foodData = {
id: 1, const {
name: "Pad Kra Pao Moo Sab with Eggs", data: foodData,
image: require("@/assets/images/food/padkrapao.jpg"), isLoading,
description: error,
"Pad kra pao, also written as pad gaprao, is a popular Thai stir-fry of ground meat and holy basil.", } = useQuery<Foods, Error>({
time: "30 Mins", queryKey: ["food-detail", foodId],
skills: "Easy", queryFn: async () => {
ingredients: [ const { data, error } = await getFoodById(foodId);
{ name: "Ground pork", emoji: "🥩" }, if (error) throw error;
{ name: "Holy basil", emoji: "🌿" }, if (!data) throw new Error("Food not found");
{ name: "Garlic", emoji: "🧄" }, return data;
{ 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,
}, },
steps: [ enabled: !!foodId,
"Gather and prepare all ingredients", });
"Heat oil in a wok or large frying pan",
"Fry the eggs sunny side up and set aside", const {
"Stir-fry garlic and chilies until fragrant", data: nutrients,
"Add ground pork and cook until browned", isLoading: nutrientsLoading,
"Add sauces and basil, serve with rice and egg on top", 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 = () => { const startCookingSession = () => {
router.push(`/cooking/[id]`); // Corrected router push to use the actual foodId
router.push(`/cooking/${foodId}`);
}; };
return ( return (
<SafeAreaView style={styles.container} edges={["top"]}> <SafeAreaView className="flex-1 bg-white" edges={["top"]}>
<ScrollView style={styles.scrollView}> <KeyboardAvoidingView
{/* Header with back and share buttons */} behavior={Platform.OS === "ios" ? "padding" : "height"}
<View style={styles.header}> className="flex-1"
<TouchableOpacity >
style={styles.backButton} <ScrollView className="flex-1">
onPress={() => router.back()} {/* 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">
<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}>
<TouchableOpacity <TouchableOpacity
style={styles.tabItem} className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => setActiveTab("Skills")} onPress={() => router.back()}
> >
<Text style={styles.tabLabel}>Skills</Text> <Feather name="arrow-left" size={24} color="#bb0718" />
<Text style={styles.tabValue}>{foodData.skills}</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity className="w-10 h-10 rounded-full bg-white justify-center items-center">
style={styles.tabItem} <IconSymbol
onPress={() => setActiveTab("Time")} name="square.and.arrow.up"
> size={24}
<Text style={styles.tabLabel}>Time</Text> color="#FFCC00"
<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> </TouchableOpacity>
</View> </View>
{/* Ingredients Section */} {/* Food Image */}
<View style={styles.sectionContainer}> <View className="items-center mt-16 mb-5">
<Text style={styles.sectionTitle}>Ingredients</Text> <View
<View style={styles.ingredientsGrid}> style={{
{foodData.ingredients.map((ingredient, index) => ( width: 200,
<View key={index} style={styles.ingredientItem}> height: 200,
<View style={styles.ingredientIconContainer}> backgroundColor: "#e0e0e0",
<Text style={styles.ingredientEmoji}> borderRadius: 24,
{ingredient.emoji} 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> </Text>
</View> </View>
<Text style={styles.ingredientName}>{ingredient.name}</Text> <View className="items-center">
</View> <View
))} className="w-15 h-15 rounded-full justify-center items-center mb-2"
</View> style={{ backgroundColor: "#90EE90" }}
</View> >
<Text className="text-lg font-bold text-gray-800">
{/* Nutrition Section - Improved UI */} {nutrients.fiber_g ?? 0}
<View style={styles.nutritionSection}> </Text>
<Text style={styles.sectionTitle}>Nutrition Facts</Text> <Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
<View style={styles.nutritionContainer}> g
<View style={styles.nutritionItem}> </Text>
<View </View>
style={[ <Text className="text-sm font-medium text-gray-800">
styles.nutritionCircle, Fiber
{ backgroundColor: "#FFD700" }, </Text>
]} </View>
> <View className="items-center">
<Text style={styles.nutritionValue}> <View
{foodData.nutrition.fat} className="w-15 h-15 rounded-full justify-center items-center mb-2"
</Text> style={{ backgroundColor: "#ADD8E6" }}
<Text style={styles.nutritionUnit}>g</Text> >
</View> <Text className="text-lg font-bold text-gray-800">
<Text style={styles.nutritionLabel}>Fat</Text> {nutrients.protein_g ?? 0}
</View> </Text>
<View style={styles.nutritionItem}> <Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
<View g
style={[ </Text>
styles.nutritionCircle, </View>
{ backgroundColor: "#90EE90" }, <Text className="text-sm font-medium text-gray-800">
]} Protein
> </Text>
<Text style={styles.nutritionValue}> </View>
{foodData.nutrition.fiber} <View className="items-center">
</Text> <View
<Text style={styles.nutritionUnit}>g</Text> className="w-15 h-15 rounded-full justify-center items-center mb-2"
</View> style={{ backgroundColor: "#FFA07A" }}
<Text style={styles.nutritionLabel}>Fiber</Text> >
</View> <Text className="text-lg font-bold text-gray-800">
<View style={styles.nutritionItem}> {nutrients.carbs_g ?? 0}
<View </Text>
style={[ <Text className="text-xs text-gray-800 absolute bottom-2.5 right-2.5">
styles.nutritionCircle, g
{ backgroundColor: "#ADD8E6" }, </Text>
]} </View>
> <Text className="text-sm font-medium text-gray-800">
<Text style={styles.nutritionValue}> Carbs
{foodData.nutrition.protein} </Text>
</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> </View>
<Text style={styles.stepPreviewText}>{step}</Text>
</View> </View>
))} ) : (
<Text style={styles.moreStepsText}> <Text className="text-sm text-gray-500">
...and {foodData.steps.length - 2} more steps 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> </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> </View>
</View> </ScrollView>
</ScrollView>
{/* Cook Button */} {/* Cook Button */}
<TouchableOpacity style={styles.cookButton} onPress={startCookingSession}> <TouchableOpacity
<Text style={styles.cookButtonText}>Let&apos;s Cook!</Text> className="absolute bottom-0 left-0 right-0 bg-red-600 flex-row justify-center items-center py-4"
<IconSymbol name="fork.knife" size={20} color="#FFCC00" /> onPress={startCookingSession}
</TouchableOpacity> // 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> </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 { import {
View,
Text,
Image,
TouchableOpacity,
ScrollView,
ActivityIndicator, ActivityIndicator,
FlatList,
Alert, Alert,
TextInput, FlatList,
Image,
Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
Keyboard, ScrollView,
} from "react-native" Text,
import { Feather, MaterialCommunityIcons, Ionicons } from "@expo/vector-icons" TextInput,
import { useLocalSearchParams, router } from "expo-router" TouchableOpacity,
import { useAuth } from "../../context/auth-context" View,
import { supabase } from "../../services/supabase" } from "react-native";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useAuth } from "../../context/auth-context";
import {
queryKeys,
useLikeMutation,
useSaveMutation,
} from "../../hooks/use-foods";
import { import {
getComments,
createComment,
getLikesCount,
getSavesCount,
getCommentsCount,
checkUserLiked, checkUserLiked,
checkUserSaved, checkUserSaved,
} from "../../services/data/forum" createComment,
import { getProfile } from "../../services/data/profile" getComments,
import { queryKeys, useLikeMutation, useSaveMutation } from "../../hooks/use-foods" getCommentsCount,
getLikesCount,
getSavesCount,
} from "../../services/data/forum";
import { getProfile } from "../../services/data/profile";
import { supabase } from "../../services/supabase";
export default function PostDetailScreen() { export default function PostDetailScreen() {
const params = useLocalSearchParams() const params = useLocalSearchParams();
const foodId = typeof params.id === "string" ? params.id : "" const foodId = typeof params.id === "string" ? params.id : "";
const queryClient = useQueryClient() const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null) 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 { isAuthenticated } = useAuth();
const [currentUserId, setCurrentUserId] = useState<string | null>(null) const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [commentText, setCommentText] = useState("") const [commentText, setCommentText] = useState("");
const [submittingComment, setSubmittingComment] = useState(false) const [submittingComment, setSubmittingComment] = useState(false);
const [showReviews, setShowReviews] = useState(true) const [showReviews, setShowReviews] = useState(true);
const [keyboardVisible, setKeyboardVisible] = useState(false) const [keyboardVisible, setKeyboardVisible] = useState(false);
// Listen for keyboard events // Listen for keyboard events
useEffect(() => { useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", () => { const keyboardDidShowListener = Keyboard.addListener(
setKeyboardVisible(true) "keyboardDidShow",
}) () => {
setKeyboardVisible(true);
}
);
const keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", () => { const keyboardDidHideListener = Keyboard.addListener(
setKeyboardVisible(false) "keyboardDidHide",
}) () => {
setKeyboardVisible(false);
}
);
return () => { return () => {
keyboardDidShowListener.remove() keyboardDidShowListener.remove();
keyboardDidHideListener.remove() keyboardDidHideListener.remove();
} };
}, []) }, []);
// Recipe info cards data // Recipe info cards data
const recipeInfoCards = [ const recipeInfoCards = [
@ -69,12 +79,15 @@ export default function PostDetailScreen() {
id: "cooking_time", id: "cooking_time",
title: "Cooking Time", title: "Cooking Time",
icon: ( icon: (
<View style={{ backgroundColor: "#ffd60a", padding: 8, borderRadius: 16 }}> <View
style={{ backgroundColor: "#ffd60a", padding: 8, borderRadius: 16 }}
>
<Feather name="clock" size={18} color="#bb0718" /> <Feather name="clock" size={18} color="#bb0718" />
</View> </View>
), ),
value: (food: any) => food.time_to_cook_minutes, 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"], gradient: ["#fff8e1", "#fffde7"],
valueColor: "#bb0718", valueColor: "#bb0718",
}, },
@ -82,7 +95,9 @@ export default function PostDetailScreen() {
id: "skill_level", id: "skill_level",
title: "Skill Level", title: "Skill Level",
icon: ( 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" /> <MaterialCommunityIcons name="chef-hat" size={18} color="white" />
</View> </View>
), ),
@ -92,7 +107,13 @@ export default function PostDetailScreen() {
valueColor: "", valueColor: "",
customContent: (food: any) => ( customContent: (food: any) => (
<View> <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} {food.skill_level}
</Text> </Text>
{renderSkillLevelDots(food.skill_level)} {renderSkillLevelDots(food.skill_level)}
@ -103,7 +124,9 @@ export default function PostDetailScreen() {
id: "ingredients", id: "ingredients",
title: "Ingredients", title: "Ingredients",
icon: ( icon: (
<View style={{ backgroundColor: "#2196F3", padding: 8, borderRadius: 16 }}> <View
style={{ backgroundColor: "#2196F3", padding: 8, borderRadius: 16 }}
>
<Feather name="list" size={18} color="white" /> <Feather name="list" size={18} color="white" />
</View> </View>
), ),
@ -116,7 +139,9 @@ export default function PostDetailScreen() {
id: "calories", id: "calories",
title: "Calories", title: "Calories",
icon: ( icon: (
<View style={{ backgroundColor: "#F44336", padding: 8, borderRadius: 16 }}> <View
style={{ backgroundColor: "#F44336", padding: 8, borderRadius: 16 }}
>
<Ionicons name="flame" size={18} color="white" /> <Ionicons name="flame" size={18} color="white" />
</View> </View>
), ),
@ -125,23 +150,23 @@ export default function PostDetailScreen() {
gradient: ["#ffebee", "#fff8e1"], gradient: ["#ffebee", "#fff8e1"],
valueColor: "#F44336", valueColor: "#F44336",
}, },
] ];
// Get current user ID from Supabase session // Get current user ID from Supabase session
useEffect(() => { useEffect(() => {
async function getCurrentUser() { async function getCurrentUser() {
if (isAuthenticated) { if (isAuthenticated) {
const { data } = await supabase.auth.getSession() const { data } = await supabase.auth.getSession();
const userId = data.session?.user?.id const userId = data.session?.user?.id;
console.log("Current user ID:", userId) console.log("Current user ID:", userId);
setCurrentUserId(userId || null) setCurrentUserId(userId || null);
} else { } else {
setCurrentUserId(null) setCurrentUserId(null);
} }
} }
getCurrentUser() getCurrentUser();
}, [isAuthenticated]) }, [isAuthenticated]);
// Fetch food details // Fetch food details
const { const {
@ -151,9 +176,13 @@ export default function PostDetailScreen() {
} = useQuery({ } = useQuery({
queryKey: queryKeys.foodDetails(foodId), queryKey: queryKeys.foodDetails(foodId),
queryFn: async () => { 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 { return {
...data, ...data,
@ -163,25 +192,25 @@ export default function PostDetailScreen() {
time_to_cook_minutes: data.time_to_cook_minutes ?? 0, time_to_cook_minutes: data.time_to_cook_minutes ?? 0,
skill_level: data.skill_level || "Easy", skill_level: data.skill_level || "Easy",
image_url: data.image_url || "", image_url: data.image_url || "",
} };
}, },
enabled: !!foodId, enabled: !!foodId,
}) });
// Fetch food creator // Fetch food creator
const { data: foodCreator, isLoading: isLoadingCreator } = useQuery({ const { data: foodCreator, isLoading: isLoadingCreator } = useQuery({
queryKey: ["food-creator", food?.created_by], queryKey: ["food-creator", food?.created_by],
queryFn: async () => { 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, enabled: !!food?.created_by,
}) });
// Fetch food stats // Fetch food stats
const { const {
@ -195,16 +224,16 @@ export default function PostDetailScreen() {
getLikesCount(foodId), getLikesCount(foodId),
getSavesCount(foodId), getSavesCount(foodId),
getCommentsCount(foodId), getCommentsCount(foodId),
]) ]);
return { return {
likes: likesRes.count || 0, likes: likesRes.count || 0,
saves: savesRes.count || 0, saves: savesRes.count || 0,
comments: commentsRes.count || 0, comments: commentsRes.count || 0,
} };
}, },
enabled: !!foodId, enabled: !!foodId,
}) });
// Fetch user interactions // Fetch user interactions
const { const {
@ -214,20 +243,20 @@ export default function PostDetailScreen() {
} = useQuery({ } = useQuery({
queryKey: ["user-interactions", foodId, currentUserId], queryKey: ["user-interactions", foodId, currentUserId],
queryFn: async () => { queryFn: async () => {
if (!currentUserId) return { liked: false, saved: false } if (!currentUserId) return { liked: false, saved: false };
const [likedRes, savedRes] = await Promise.all([ const [likedRes, savedRes] = await Promise.all([
checkUserLiked(foodId, currentUserId), checkUserLiked(foodId, currentUserId),
checkUserSaved(foodId, currentUserId), checkUserSaved(foodId, currentUserId),
]) ]);
return { return {
liked: !!likedRes.data, liked: !!likedRes.data,
saved: !!savedRes.data, saved: !!savedRes.data,
} };
}, },
enabled: !!foodId && !!currentUserId, enabled: !!foodId && !!currentUserId,
}) });
// Fetch comments // Fetch comments
const { const {
@ -237,36 +266,48 @@ export default function PostDetailScreen() {
} = useQuery({ } = useQuery({
queryKey: queryKeys.foodComments(foodId), queryKey: queryKeys.foodComments(foodId),
queryFn: async () => { 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, enabled: !!foodId,
}) });
// Set up mutations // Set up mutations
const likeMutation = useLikeMutation() const likeMutation = useLikeMutation();
const saveMutation = useSaveMutation() const saveMutation = useSaveMutation();
const commentMutation = useMutation({ const commentMutation = useMutation({
mutationFn: async ({ foodId, userId, content }: { foodId: string; userId: string; content: string }) => { mutationFn: async ({
return createComment(foodId, userId, content) foodId,
userId,
content,
}: {
foodId: string;
userId: string;
content: string;
}) => {
return createComment(foodId, userId, content);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.foodComments(foodId) }) queryClient.invalidateQueries({
queryClient.invalidateQueries({ queryKey: ["food-stats", foodId] }) queryKey: queryKeys.foodComments(foodId),
setCommentText("") });
Keyboard.dismiss() queryClient.invalidateQueries({ queryKey: ["food-stats", foodId] });
setCommentText("");
Keyboard.dismiss();
}, },
}) });
// Set up real-time subscription for comments // Set up real-time subscription for comments
useEffect(() => { 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 const subscription = supabase
.channel(`food_comments:${foodId}`) .channel(`food_comments:${foodId}`)
@ -279,21 +320,21 @@ export default function PostDetailScreen() {
filter: `food_id=eq.${foodId}`, filter: `food_id=eq.${foodId}`,
}, },
() => { () => {
console.log("Comment change detected, refreshing comments") console.log("Comment change detected, refreshing comments");
refetchComments() refetchComments();
refetchStats() refetchStats();
}, }
) )
.subscribe() .subscribe();
return () => { return () => {
supabase.removeChannel(subscription) supabase.removeChannel(subscription);
} };
}, [foodId, refetchComments, refetchStats]) }, [foodId, refetchComments, refetchStats]);
// Set up real-time subscription for likes and saves // Set up real-time subscription for likes and saves
useEffect(() => { useEffect(() => {
if (!foodId) return if (!foodId) return;
const likesSubscription = supabase const likesSubscription = supabase
.channel(`food_likes:${foodId}`) .channel(`food_likes:${foodId}`)
@ -306,12 +347,14 @@ export default function PostDetailScreen() {
filter: `food_id=eq.${foodId}`, filter: `food_id=eq.${foodId}`,
}, },
() => { () => {
console.log("Like change detected, refreshing stats and interactions") console.log(
refetchStats() "Like change detected, refreshing stats and interactions"
refetchInteractions() );
}, refetchStats();
refetchInteractions();
}
) )
.subscribe() .subscribe();
const savesSubscription = supabase const savesSubscription = supabase
.channel(`food_saves:${foodId}`) .channel(`food_saves:${foodId}`)
@ -324,23 +367,25 @@ export default function PostDetailScreen() {
filter: `food_id=eq.${foodId}`, filter: `food_id=eq.${foodId}`,
}, },
() => { () => {
console.log("Save change detected, refreshing stats and interactions") console.log(
refetchStats() "Save change detected, refreshing stats and interactions"
refetchInteractions() );
}, refetchStats();
refetchInteractions();
}
) )
.subscribe() .subscribe();
return () => { return () => {
supabase.removeChannel(likesSubscription) supabase.removeChannel(likesSubscription);
supabase.removeChannel(savesSubscription) supabase.removeChannel(savesSubscription);
} };
}, [foodId, refetchStats, refetchInteractions]) }, [foodId, refetchStats, refetchInteractions]);
const handleLike = async () => { const handleLike = async () => {
if (!isAuthenticated || !currentUserId || !food) { if (!isAuthenticated || !currentUserId || !food) {
Alert.alert("Authentication Required", "Please log in to like posts.") Alert.alert("Authentication Required", "Please log in to like posts.");
return return;
} }
try { try {
@ -348,17 +393,17 @@ export default function PostDetailScreen() {
foodId, foodId,
userId: currentUserId, userId: currentUserId,
isLiked: interactions.liked, isLiked: interactions.liked,
}) });
} catch (error) { } catch (error) {
console.error("Error toggling like:", error) console.error("Error toggling like:", error);
Alert.alert("Error", "Failed to update like. Please try again.") Alert.alert("Error", "Failed to update like. Please try again.");
} }
} };
const handleSave = async () => { const handleSave = async () => {
if (!isAuthenticated || !currentUserId || !food) { if (!isAuthenticated || !currentUserId || !food) {
Alert.alert("Authentication Required", "Please log in to save posts.") Alert.alert("Authentication Required", "Please log in to save posts.");
return return;
} }
try { try {
@ -366,57 +411,57 @@ export default function PostDetailScreen() {
foodId, foodId,
userId: currentUserId, userId: currentUserId,
isSaved: interactions.saved, isSaved: interactions.saved,
}) });
} catch (error) { } catch (error) {
console.error("Error toggling save:", error) console.error("Error toggling save:", error);
Alert.alert("Error", "Failed to update save. Please try again.") Alert.alert("Error", "Failed to update save. Please try again.");
} }
} };
const handleSubmitComment = async () => { const handleSubmitComment = async () => {
if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) { if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) {
if (!isAuthenticated || !currentUserId) { 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 { try {
await commentMutation.mutateAsync({ await commentMutation.mutateAsync({
foodId, foodId,
userId: currentUserId, userId: currentUserId,
content: commentText.trim(), content: commentText.trim(),
}) });
} catch (error) { } catch (error) {
console.error("Error submitting comment:", error) console.error("Error submitting comment:", error);
Alert.alert("Error", "Failed to submit comment. Please try again.") Alert.alert("Error", "Failed to submit comment. Please try again.");
} finally { } finally {
setSubmittingComment(false) setSubmittingComment(false);
} }
} };
// Helper function to get skill level color // Helper function to get skill level color
const getSkillLevelColor = (level: string) => { const getSkillLevelColor = (level: string) => {
switch (level) { switch (level) {
case "Easy": case "Easy":
return "#4CAF50" // Green return "#4CAF50"; // Green
case "Medium": case "Medium":
return "#FFC107" // Amber return "#FFC107"; // Amber
case "Hard": case "Hard":
return "#F44336" // Red return "#F44336"; // Red
default: default:
return "#4CAF50" // Default to green return "#4CAF50"; // Default to green
} }
} };
// Helper function to get skill level dots // Helper function to get skill level dots
const renderSkillLevelDots = (level: string) => { const renderSkillLevelDots = (level: string) => {
const totalDots = 3 const totalDots = 3;
let activeDots = 1 let activeDots = 1;
if (level === "Medium") activeDots = 2 if (level === "Medium") activeDots = 2;
if (level === "Hard") activeDots = 3 if (level === "Hard") activeDots = 3;
return ( return (
<View style={{ flexDirection: "row", marginTop: 4 }}> <View style={{ flexDirection: "row", marginTop: 4 }}>
@ -434,12 +479,12 @@ export default function PostDetailScreen() {
/> />
))} ))}
</View> </View>
) );
} };
// Render recipe info card // Render recipe info card
const renderRecipeInfoCard = ({ item }: { item: any }) => { const renderRecipeInfoCard = ({ item }: { item: any }) => {
if (!food) return null if (!food) return null;
return ( return (
<View <View
@ -451,38 +496,72 @@ export default function PostDetailScreen() {
width: 160, width: 160,
}} }}
> >
<View style={{ flexDirection: "row", alignItems: "center", marginBottom: 8 }}> <View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
}}
>
{item.icon} {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> </View>
{item.customContent ? ( {item.customContent ? (
item.customContent(food) item.customContent(food)
) : ( ) : (
<View style={{ flexDirection: "row", alignItems: "baseline" }}> <View style={{ flexDirection: "row", alignItems: "baseline" }}>
<Text style={{ fontSize: 24, fontWeight: "bold", color: item.valueColor }}>{item.value(food)}</Text> <Text
<Text style={{ marginLeft: 4, fontSize: 14, fontWeight: "500", color: "#606060" }}>{item.unit(food)}</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>
)} )}
</View> </View>
) );
} };
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments const isLoading =
isLoadingFood ||
isLoadingCreator ||
isLoadingStats ||
isLoadingInteractions ||
isLoadingComments;
if (isLoading) { if (isLoading) {
return ( return (
<View style={{ flex: 1, backgroundColor: "white" }}> <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" /> <ActivityIndicator size="large" color="#ffd60a" />
</View> </View>
</View> </View>
) );
} }
if (foodError || !food) { if (foodError || !food) {
return ( return (
<View style={{ flex: 1, backgroundColor: "white" }}> <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> <Text style={{ fontSize: 18 }}>Post not found</Text>
<TouchableOpacity <TouchableOpacity
style={{ style={{
@ -498,38 +577,60 @@ export default function PostDetailScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
) );
} }
return ( return (
<View style={{ flex: 1, backgroundColor: "white" }}> <View style={{ flex: 1, backgroundColor: "white" }}>
{/* Fixed Header */} {/* Fixed Header */}
<View className="flex-row items-center justify-between px-4 py-3 mt-11"> <View className="flex-row items-center justify-between px-4 py-3 mt-11">
<TouchableOpacity <TouchableOpacity
className="bg-[#ffd60a] p-3 rounded-lg" className="bg-[#ffd60a] p-3 rounded-lg"
onPress={() => router.back()} onPress={() => router.back()}
> >
<Feather name="arrow-left" size={24} color="#bb0718" /> <Feather name="arrow-left" size={24} color="#bb0718" />
</TouchableOpacity> </TouchableOpacity>
<Text className="text-2xl font-bold">Post</Text> <Text className="text-2xl font-bold">Post</Text>
<TouchableOpacity> <TouchableOpacity onPress={() => router.push(`/food/${food.id}`)}>
<Feather name="more-horizontal" size={24} color="#000" /> <Feather name="external-link" size={24} color="#000" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Scrollable Content */} {/* Scrollable Content */}
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1" className="flex-1"
> >
<ScrollView ref={scrollViewRef} style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 20 }}> <ScrollView
ref={scrollViewRef}
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 20 }}
>
{/* User info */} {/* User info */}
<View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12 }}> <View
<View style={{ width: 48, height: 48, backgroundColor: "#e0e0e0", borderRadius: 24, overflow: "hidden" }}> style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
}}
>
<View
style={{
width: 48,
height: 48,
backgroundColor: "#e0e0e0",
borderRadius: 24,
overflow: "hidden",
}}
>
{foodCreator?.avatar_url ? ( {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 <View
style={{ style={{
@ -540,8 +641,16 @@ export default function PostDetailScreen() {
justifyContent: "center", justifyContent: "center",
}} }}
> >
<Text style={{ fontSize: 18, fontWeight: "bold", color: "#606060" }}> <Text
{foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || "?"} style={{
fontSize: 18,
fontWeight: "bold",
color: "#606060",
}}
>
{foodCreator?.username?.charAt(0).toUpperCase() ||
food.created_by?.charAt(0).toUpperCase() ||
"?"}
</Text> </Text>
</View> </View>
)} )}
@ -562,17 +671,38 @@ export default function PostDetailScreen() {
{/* Food title and description */} {/* Food title and description */}
<View style={{ paddingHorizontal: 16, marginBottom: 8 }}> <View style={{ paddingHorizontal: 16, marginBottom: 8 }}>
<Text style={{ fontSize: 30, fontWeight: "bold", marginBottom: 8 }}>{food.name}</Text> <Text style={{ fontSize: 30, fontWeight: "bold", marginBottom: 8 }}>
<Text style={{ color: "#505050", marginBottom: 8, fontSize: 16, lineHeight: 24 }}>{food.description}</Text> {food.name}
</Text>
<Text
style={{
color: "#505050",
marginBottom: 8,
fontSize: 16,
lineHeight: 24,
}}
>
{food.description}
</Text>
<Text style={{ color: "#808080", fontSize: 14 }}> <Text style={{ color: "#808080", fontSize: 14 }}>
{new Date(food.created_at).toLocaleDateString()} -{" "} {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> </Text>
</View> </View>
{/* Recipe Info Cards - Horizontal Scrollable */} {/* Recipe Info Cards - Horizontal Scrollable */}
<View style={{ paddingVertical: 16 }}> <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 Recipe Details
</Text> </Text>
<FlatList <FlatList
@ -611,7 +741,9 @@ export default function PostDetailScreen() {
size={22} size={22}
color={interactions.liked ? "#E91E63" : "#333"} 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>
<TouchableOpacity <TouchableOpacity
@ -626,8 +758,14 @@ export default function PostDetailScreen() {
}} }}
onPress={handleSave} onPress={handleSave}
> >
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} /> <Feather
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>Save</Text> name="bookmark"
size={22}
color={interactions.saved ? "#ffd60a" : "#333"}
/>
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>
Save
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -655,10 +793,18 @@ export default function PostDetailScreen() {
borderRadius: 12, 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>
</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> </TouchableOpacity>
{showReviews && ( {showReviews && (
@ -678,7 +824,10 @@ export default function PostDetailScreen() {
}} }}
> >
{comment.user?.avatar_url ? ( {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 <View
style={{ style={{
@ -689,8 +838,16 @@ export default function PostDetailScreen() {
justifyContent: "center", justifyContent: "center",
}} }}
> >
<Text style={{ fontSize: 16, fontWeight: "bold", color: "white" }}> <Text
{comment.user?.username?.charAt(0).toUpperCase() || style={{
fontSize: 16,
fontWeight: "bold",
color: "white",
}}
>
{comment.user?.username
?.charAt(0)
.toUpperCase() ||
comment.user_id?.charAt(0).toUpperCase() || comment.user_id?.charAt(0).toUpperCase() ||
"?"} "?"}
</Text> </Text>
@ -700,18 +857,41 @@ export default function PostDetailScreen() {
{/* Comment bubble with username inside */} {/* Comment bubble with username inside */}
<View style={{ flex: 1, marginLeft: 12 }}> <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 */} {/* Username inside bubble */}
<Text style={{ fontWeight: "bold", fontSize: 16, marginBottom: 4 }}> <Text
{comment.user?.username || comment.user?.full_name || "User"} style={{
fontWeight: "bold",
fontSize: 16,
marginBottom: 4,
}}
>
{comment.user?.username ||
comment.user?.full_name ||
"User"}
</Text> </Text>
{/* Comment content */} {/* Comment content */}
<Text style={{ color: "#303030", lineHeight: 20 }}>{comment.content}</Text> <Text style={{ color: "#303030", lineHeight: 20 }}>
{comment.content}
</Text>
</View> </View>
{/* Date below bubble */} {/* 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()} {new Date(comment.created_at).toLocaleDateString()}
</Text> </Text>
</View> </View>
@ -721,8 +901,18 @@ export default function PostDetailScreen() {
) : ( ) : (
<View style={{ paddingVertical: 32, alignItems: "center" }}> <View style={{ paddingVertical: 32, alignItems: "center" }}>
<Feather name="message-circle" size={40} color="#e0e0e0" /> <Feather name="message-circle" size={40} color="#e0e0e0" />
<Text style={{ marginTop: 8, color: "#808080", textAlign: "center" }}>No reviews yet.</Text> <Text
<Text style={{ color: "#808080", textAlign: "center" }}>Be the first to comment!</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>
)} )}
</View> </View>
@ -757,21 +947,37 @@ export default function PostDetailScreen() {
style={{ style={{
padding: 12, padding: 12,
borderRadius: 24, borderRadius: 24,
backgroundColor: commentText.trim() && isAuthenticated ? "#ffd60a" : "#e0e0e0", backgroundColor:
commentText.trim() && isAuthenticated ? "#ffd60a" : "#e0e0e0",
}} }}
onPress={handleSubmitComment} 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> </TouchableOpacity>
</View> </View>
{!isAuthenticated && ( {!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 Please log in to comment
</Text> </Text>
)} )}
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View> </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 }; 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. * 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 created_at
`) `)
.eq("food_id", food_id) .eq("food_id", food_id)
.single() .limit(1)
return { data, error };
return { data: data?.[0] || null, error };
} }
interface Ingredient { interface Ingredient {