mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 14:04:08 +01:00
feat: add foods detail page
This commit is contained in:
parent
233d08d700
commit
44a2f89a59
@ -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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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'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'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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user