From ae53b78ba6df51f8d8e8c488eb3a61aea1e48a09 Mon Sep 17 00:00:00 2001 From: Tantikon Phasanphaengsi Date: Mon, 12 May 2025 22:10:58 +0700 Subject: [PATCH] fix: add photo in recipes page --- app/(tabs)/home.tsx | 339 ++++++++++++++++------------------------- app/(tabs)/profile.tsx | 2 +- app/(tabs)/recipes.tsx | 162 ++++++++++++++------ 3 files changed, 248 insertions(+), 255 deletions(-) diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx index 127396e..8c798d8 100644 --- a/app/(tabs)/home.tsx +++ b/app/(tabs)/home.tsx @@ -1,61 +1,49 @@ -"use client"; - -import { IconSymbol } from "@/components/ui/IconSymbol"; -import { getFoods, insertGenAIResult } from "@/services/data/foods"; -import { uploadImageToSupabase } from "@/services/data/imageUpload"; -import { getProfile } from "@/services/data/profile"; -import { callGenAIonImage } from "@/services/gemini"; -import { supabase } from "@/services/supabase"; -import { Feather, FontAwesome, Ionicons } from "@expo/vector-icons"; -import { useQuery } from "@tanstack/react-query"; -import * as FileSystem from "expo-file-system"; -import * as ImagePicker from "expo-image-picker"; -import { router } from "expo-router"; -import { useEffect, useMemo, useState } from "react"; -import { - Alert, - Image, - SafeAreaView, - ScrollView, - StatusBar, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; +"use client" +import { getFoods, insertGenAIResult } from "@/services/data/foods" +import { uploadImageToSupabase } from "@/services/data/imageUpload" +import { getProfile } from "@/services/data/profile" +import { callGenAIonImage } from "@/services/gemini" +import { supabase } from "@/services/supabase" +import { Feather, FontAwesome, Ionicons } from "@expo/vector-icons" +import { useQuery } from "@tanstack/react-query" +import * as FileSystem from "expo-file-system" +import * as ImagePicker from "expo-image-picker" +import { router } from "expo-router" +import { useEffect, useState } from "react" +import { Alert, Image, SafeAreaView, ScrollView, StatusBar, Text, TouchableOpacity, View } from "react-native" const useFoodsQuery = () => { return useQuery({ queryKey: ["highlight-foods"], queryFn: async () => { - const { data, error } = await getFoods(undefined, true, undefined, 3); - if (error) throw error; - return data || []; + const { data, error } = await getFoods(undefined, true, undefined, 6) // Fetch 6 items for multiple rows + if (error) throw error + return data || [] }, staleTime: 1000 * 60 * 5, - }); -}; + }) +} const useUserProfile = () => { - const [userId, setUserId] = useState(null); - const [isLoadingUserId, setIsLoadingUserId] = useState(true); + const [userId, setUserId] = useState(null) + const [isLoadingUserId, setIsLoadingUserId] = useState(true) // Get current user ID useEffect(() => { const fetchUserId = async () => { try { - const { data, error } = await supabase.auth.getUser(); - if (error) throw error; - setUserId(data?.user?.id || null); + const { data, error } = await supabase.auth.getUser() + if (error) throw error + setUserId(data?.user?.id || null) } catch (error) { - console.error("Error fetching user:", error); + console.error("Error fetching user:", error) } finally { - setIsLoadingUserId(false); + setIsLoadingUserId(false) } - }; + } - fetchUserId(); - }, []); + fetchUserId() + }, []) // Fetch user profile data const { @@ -65,104 +53,78 @@ const useUserProfile = () => { } = useQuery({ queryKey: ["profile", userId], queryFn: async () => { - if (!userId) throw new Error("No user id"); - return getProfile(userId); + if (!userId) throw new Error("No user id") + return getProfile(userId) }, enabled: !!userId, staleTime: 1000 * 60 * 5, // 5 minutes - }); + }) return { userId, profileData: profileData?.data, isLoading: isLoadingUserId || isLoadingProfile, error: profileError, - }; -}; + } +} -const runImagePipeline = async ( - imageBase64: string, - imageType: string, - userId: string -) => { - const imageUri = await uploadImageToSupabase(imageBase64, imageType, userId); - const genAIResult = await callGenAIonImage(imageUri); - if (genAIResult.error) throw genAIResult.error; - const { data: genAIResultData } = genAIResult; - if (!genAIResultData) throw new Error("GenAI result is null"); - await insertGenAIResult(genAIResultData, userId, imageUri); -}; +const runImagePipeline = async (imageBase64: string, imageType: string, userId: string) => { + const imageUri = await uploadImageToSupabase(imageBase64, imageType, userId) + const genAIResult = await callGenAIonImage(imageUri) + if (genAIResult.error) throw genAIResult.error + const { data: genAIResultData } = genAIResult + if (!genAIResultData) throw new Error("GenAI result is null") + await insertGenAIResult(genAIResultData, userId, imageUri) +} -const processImage = async ( - asset: ImagePicker.ImagePickerAsset, - userId: string -) => { +const processImage = async (asset: ImagePicker.ImagePickerAsset, userId: string) => { const base64 = await FileSystem.readAsStringAsync(asset.uri, { encoding: "base64", - }); - const imageType = asset.mimeType || "image/jpeg"; - await runImagePipeline(base64, imageType, userId); -}; + }) + const imageType = asset.mimeType || "image/jpeg" + await runImagePipeline(base64, imageType, userId) +} const navigateToFoodDetail = (foodId: string) => { - router.push({ pathname: "/food/[id]", params: { id: foodId } }); -}; + router.push({ pathname: "/food/[id]", params: { id: foodId } }) +} export default function HomeScreen() { - const [imageProcessing, setImageProcessing] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - - const { profileData } = useUserProfile(); - const { - data: foodsData = [], - isLoading: isLoadingFoods, - error: foodsError, - } = useFoodsQuery(); + const [imageProcessing, setImageProcessing] = useState(false) + const { profileData } = useUserProfile() + const { data: foodsData = [], isLoading: isLoadingFoods, error: foodsError } = useFoodsQuery() const handleImageSelection = async ( - pickerFn: - | typeof ImagePicker.launchCameraAsync - | typeof ImagePicker.launchImageLibraryAsync + pickerFn: typeof ImagePicker.launchCameraAsync | typeof ImagePicker.launchImageLibraryAsync, ) => { const result = await pickerFn({ - mediaTypes: ["images"], + mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 1, - }); + }) if (!result.canceled) { - setImageProcessing(true); + setImageProcessing(true) try { - const { data, error } = await supabase.auth.getUser(); - if (error || !data?.user?.id) throw new Error("Cannot get user id"); - const userId = data.user.id; - await processImage(result.assets[0], userId); + const { data, error } = await supabase.auth.getUser() + if (error || !data?.user?.id) throw new Error("Cannot get user id") + const userId = data.user.id + await processImage(result.assets[0], userId) } catch (err) { - Alert.alert( - "Image Processing Failed", - (err as Error).message || "Unknown error" - ); + Alert.alert("Image Processing Failed", (err as Error).message || "Unknown error") } finally { - setImageProcessing(false); + setImageProcessing(false) } router.push({ pathname: "/profile", - }); + }) } - }; - - const filteredFoods = useMemo(() => { - return searchQuery - ? foodsData.filter((food) => - food.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) - : foodsData; - }, [foodsData, searchQuery]); + } // Get username or fallback to a default greeting - const username = profileData?.username || profileData?.full_name || "Chef"; - const greeting = `Hi! ${username}`; + const username = profileData?.username || profileData?.full_name || "Chef" + const greeting = `Hi! ${username}` return ( @@ -189,29 +151,18 @@ export default function HomeScreen() { alignItems: "center", }} > - - Processing image... - + Processing image... - + Please wait )} - + {/* Header with greeting only (settings button removed) */} + {greeting} - - - {/* Main content container with consistent padding */} - {/* "Show your dishes" section */} + {/* "Show your dishes" section - Search bar removed */} Show your dishes - - - - - - - {/* Upload feature section */} @@ -247,49 +186,35 @@ export default function HomeScreen() { { - const { status } = - await ImagePicker.requestCameraPermissionsAsync(); + const { status } = await ImagePicker.requestCameraPermissionsAsync() if (status !== "granted") { - Alert.alert( - "Permission needed", - "Please grant camera permissions." - ); - return; + Alert.alert("Permission needed", "Please grant camera permissions.") + return } - await handleImageSelection(ImagePicker.launchCameraAsync); + await handleImageSelection(ImagePicker.launchCameraAsync) }} > From Camera - - Straight from Camera - + Straight from Camera { - const { status } = - await ImagePicker.requestMediaLibraryPermissionsAsync(); + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync() if (status !== "granted") { - Alert.alert( - "Permission needed", - "Please grant gallery permissions." - ); - return; + Alert.alert("Permission needed", "Please grant gallery permissions.") + return } - await handleImageSelection( - ImagePicker.launchImageLibraryAsync - ); + await handleImageSelection(ImagePicker.launchImageLibraryAsync) }} > From Gallery - - Straight from Gallery - + Straight from Gallery @@ -297,66 +222,68 @@ export default function HomeScreen() { {/* Highlights section */} - - Highlights - + + + Highlights + + + router.push("/recipes")}> + See All + + {isLoadingFoods ? ( - - Loading highlights... - + + + Loading highlights... + ) : foodsError ? ( - - Failed to load highlights - - ) : filteredFoods.length === 0 ? ( - - No highlights available - + + + Failed to load highlights + + ) : foodsData.length === 0 ? ( + + + No highlights available + ) : ( - - {filteredFoods.map((food, idx) => ( + + {foodsData.map((food, idx) => ( navigateToFoodDetail(food.id)} > - {food.image_url ? ( - - ) : ( - - No Image - - )} - - + + {food.image_url ? ( + + ) : ( + + + + )} + {food.name} - - {food.description || "No description"} - - - - - - {food.time_to_cook_minutes - ? `${food.time_to_cook_minutes} min` - : "-"} - - + + + + {food.time_to_cook_minutes ? `${food.time_to_cook_minutes} min` : "-"} + @@ -370,5 +297,5 @@ export default function HomeScreen() { - ); + ) } diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 9c3be35..5880974 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -343,7 +343,7 @@ export default function ProfileScreen() { ))} - + {/* Tab Content */} {isActiveLoading ? ( diff --git a/app/(tabs)/recipes.tsx b/app/(tabs)/recipes.tsx index 23ab5d3..cfed6da 100644 --- a/app/(tabs)/recipes.tsx +++ b/app/(tabs)/recipes.tsx @@ -1,23 +1,29 @@ -"use client"; - -import RecipeHighlightCard from "@/components/RecipeHighlightCard"; -import { supabase } from "@/services/supabase"; -import { useQuery } from "@tanstack/react-query"; -import { router } from "expo-router"; -import { ActivityIndicator, ScrollView, Text, View } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; +"use client" +import { supabase } from "@/services/supabase" +import { Feather } from "@expo/vector-icons" +import { useQuery } from "@tanstack/react-query" +import { router } from "expo-router" +import { useState, useEffect } from "react" +import { ActivityIndicator, ScrollView, Text, View, TextInput, TouchableOpacity, Image, Dimensions } from "react-native" +import { SafeAreaView } from "react-native-safe-area-context" interface Recipe { - id: number; - name: string; - description: string; - image_url: string; - created_by: string; - is_shared: boolean; - time_to_cook_minutes?: number; + id: number + name: string + description: string + image_url: string + created_by: string + is_shared: boolean + time_to_cook_minutes?: number } +const { width } = Dimensions.get("window") +const cardWidth = (width - 32) / 2 // 2 cards per row with 16px padding on each side + export default function RecipesScreen() { + const [searchQuery, setSearchQuery] = useState("") + const [filteredRecipes, setFilteredRecipes] = useState([]) + const { data: allRecipes, isLoading: isAllLoading, @@ -29,63 +35,123 @@ export default function RecipesScreen() { .from("foods") .select("*") .eq("is_shared", true) - .order("created_at", { ascending: false }); - if (error) throw error; - return data ?? []; + .order("created_at", { ascending: false }) + if (error) throw error + return data ?? [] }, staleTime: 1000 * 60, - }); + }) - const recipes: Recipe[] = allRecipes || []; - const loading = isAllLoading; - const error = allError; + // Filter recipes based on search query + useEffect(() => { + if (!allRecipes) return + + if (!searchQuery.trim()) { + setFilteredRecipes(allRecipes) + return + } + + const filtered = allRecipes.filter((recipe) => recipe.name.toLowerCase().includes(searchQuery.toLowerCase())) + setFilteredRecipes(filtered) + }, [searchQuery, allRecipes]) + + const recipes: Recipe[] = filteredRecipes || [] + const loading = isAllLoading + const error = allError if (loading) { return ( - + + Loading recipes... - ); + ) } + if (error) { return ( - - Failed to load recipes + + + Failed to load recipes + router.push("/home")}> + Go back to home + - ); + ) } return ( - - All Recipes - - - + + All Recipes + + {/* Search Bar */} + + + + {searchQuery.length > 0 && ( + setSearchQuery("")}> + + + )} + + + + + {recipes.length === 0 ? ( - - No recipes found. + + + + {searchQuery.trim() ? `No recipes found for "${searchQuery}"` : "No recipes available."} + ) : ( - recipes.map((item, idx) => ( + recipes.map((item) => ( - { - router.push(`/food/${item.id}`); - }} - /> + router.push(`/food/${item.id}`)} + activeOpacity={0.7} + > + + {item.image_url ? ( + + ) : ( + + + + )} + + + + {item.name} + + + {item.description || "No description available"} + + {item.time_to_cook_minutes !== undefined && ( + + + {item.time_to_cook_minutes} min + + )} + + )) )} - ); + ) }