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