diff --git a/app.json b/app.json index 93fa4bc..433587f 100644 --- a/app.json +++ b/app.json @@ -28,7 +28,7 @@ [ "expo-splash-screen", { - "image": "./assets/images/splash-icon.png", + "image": "./assets/images/splash.png", "imageWidth": 200, "resizeMode": "contain", "backgroundColor": "#ffffff" diff --git a/app/(tabs)/forum.tsx b/app/(tabs)/forum.tsx index 74cabae..cbf25b5 100644 --- a/app/(tabs)/forum.tsx +++ b/app/(tabs)/forum.tsx @@ -1,23 +1,17 @@ import React, { useState, useEffect } from 'react'; import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, Alert } from 'react-native'; -import { Feather, FontAwesome, Ionicons } from '@expo/vector-icons'; -import { router } from 'expo-router'; +import { Feather, FontAwesome } from '@expo/vector-icons'; +import { router, useFocusEffect } from 'expo-router'; import { useAuth } from '../../context/auth-context'; -import { getFoods } from '../../services/data/foods'; -import { - getLikesCount, - getSavesCount, - getCommentsCount, - createLike, - deleteLike, - createSave, - deleteSave, - checkUserLiked, - checkUserSaved -} from '../../services/data/forum'; -import { getProfile } from '../../services/data/profile'; -import { Food, Profile } from '../../types/index'; import { supabase } from '../../services/supabase'; +import { + useFoods, + useFoodStats, + useFoodCreators, + useUserInteractions, + useLikeMutation, + useSaveMutation +} from '../../hooks/use-foods'; // Categories for filtering const categories = [ @@ -37,13 +31,8 @@ export default function ForumScreen() { const { isAuthenticated } = useAuth(); const [currentUserId, setCurrentUserId] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - const [foods, setFoods] = useState([]); - const [loading, setLoading] = useState(true); const [selectedCategory, setSelectedCategory] = useState(''); const [selectedSort, setSelectedSort] = useState('rating'); - const [foodStats, setFoodStats] = useState<{[key: string]: {likes: number, saves: number, comments: number}}>({}); - const [foodCreators, setFoodCreators] = useState<{[key: string]: Profile}>({}); - const [userInteractions, setUserInteractions] = useState<{[key: string]: {liked: boolean, saved: boolean}}>({}); // Get current user ID from Supabase session useEffect(() => { @@ -61,169 +50,53 @@ export default function ForumScreen() { getCurrentUser(); }, [isAuthenticated]); - // Set up real-time subscription for likes and saves - useEffect(() => { - if (!isAuthenticated) return; - - const likesSubscription = supabase - .channel('food_likes_changes') - .on('postgres_changes', { - event: '*', - schema: 'public', - table: 'food_likes' - }, () => { - // Refresh stats when changes occur - loadFoods(); - }) - .subscribe(); - - const savesSubscription = supabase - .channel('food_saves_changes') - .on('postgres_changes', { - event: '*', - schema: 'public', - table: 'food_saves' - }, () => { - // Refresh stats when changes occur - loadFoods(); - }) - .subscribe(); - - return () => { - supabase.removeChannel(likesSubscription); - supabase.removeChannel(savesSubscription); - }; - }, [isAuthenticated]); + // Use React Query hooks + const { + data: foods = [], + isLoading: isLoadingFoods, + refetch: refetchFoods + } = useFoods(selectedCategory, searchQuery, selectedSort); - useEffect(() => { - loadFoods(); - }, [selectedCategory, selectedSort, currentUserId]); + const foodIds = foods.map(food => food.id); - const loadFoods = async () => { - setLoading(true); - try { - // In a real app, you would filter by category and sort accordingly - const { data, error } = await getFoods(undefined, true, searchQuery); - - if (error) { - console.error('Error loading foods:', error); - return; - } - - if (data) { - // Sort data based on selectedSort - let sortedData = [...data]; - if (selectedSort === 'rating') { - // Assuming higher calories means higher rating for demo purposes - sortedData.sort((a, b) => (b.calories ?? 0) - (a.calories ?? 0)); - } else if (selectedSort === 'newest') { - sortedData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); - } else if (selectedSort === 'best') { - // Assuming higher ingredient_count means better for demo purposes - sortedData.sort((a, b) => (b.ingredient_count ?? 0) - (a.ingredient_count ?? 0)); - } - - setFoods(sortedData.map(food => ({ - ...food, - description: food.description || '', // Ensure description is always a string - ingredient_count: food.ingredient_count ?? 0, // Ensure ingredient_count is always a number - calories: food.calories ?? 0, // Ensure calories is always a number - image_url: food.image_url || '', // Ensure image_url is always a string - }))); - - // Load stats for each food - const statsPromises = sortedData.map(async (food) => { - const [likesRes, savesRes, commentsRes] = await Promise.all([ - getLikesCount(food.id), - getSavesCount(food.id), - getCommentsCount(food.id) - ]); - - return { - foodId: food.id, - likes: likesRes.count || 0, - saves: savesRes.count || 0, - comments: commentsRes.count || 0 - }; - }); - - const stats = await Promise.all(statsPromises); - const statsMap = stats.reduce((acc, stat) => { - acc[stat.foodId] = { - likes: stat.likes, - saves: stat.saves, - comments: stat.comments - }; - return acc; - }, {} as {[key: string]: {likes: number, saves: number, comments: number}}); - - setFoodStats(statsMap); - - // Load creator profiles - const creatorIds = sortedData - .filter(food => food.created_by) - .map(food => food.created_by as string); - - const uniqueCreatorIds = [...new Set(creatorIds)]; - - const creatorProfiles: {[key: string]: Profile} = {}; - - for (const creatorId of uniqueCreatorIds) { - const { data: profile } = await getProfile(creatorId); - if (profile) { - creatorProfiles[creatorId] = profile; - } - } - - setFoodCreators(creatorProfiles); - - // Check user interactions if authenticated - if (isAuthenticated && currentUserId) { - const interactionsPromises = sortedData.map(async (food) => { - const [likedRes, savedRes] = await Promise.all([ - checkUserLiked(food.id, currentUserId), - checkUserSaved(food.id, currentUserId) - ]); - - return { - foodId: food.id, - liked: !!likedRes.data, - saved: !!savedRes.data - }; - }); - - const interactions = await Promise.all(interactionsPromises); - const interactionsMap = interactions.reduce((acc, interaction) => { - acc[interaction.foodId] = { - liked: interaction.liked, - saved: interaction.saved - }; - return acc; - }, {} as {[key: string]: {liked: boolean, saved: boolean}}); - - setUserInteractions(interactionsMap); - } - } - } catch (error) { - console.error('Error:', error); - } finally { - setLoading(false); - } - }; + const { + data: foodStats = {}, + isLoading: isLoadingStats + } = useFoodStats(foodIds); + + const creatorIds = foods + .filter(food => food.created_by) + .map(food => food.created_by as string); + + const { + data: foodCreators = {}, + isLoading: isLoadingCreators + } = useFoodCreators(creatorIds); + + const { + data: userInteractions = {}, + isLoading: isLoadingInteractions + } = useUserInteractions(foodIds, currentUserId); + + const likeMutation = useLikeMutation(); + const saveMutation = useSaveMutation(); + + // Refetch data when the screen comes into focus + useFocusEffect( + React.useCallback(() => { + refetchFoods(); + }, [refetchFoods]) + ); const handleSearch = (text: string) => { setSearchQuery(text); - // Debounce search for better performance - setTimeout(() => { - loadFoods(); - }, 500); }; -const navigateToPostDetail = (food: Food) => { - router.push(`/post-detail/${food.id}`); -}; + const navigateToPostDetail = (food: { id: string }) => { + router.push(`/post-detail/${food.id}`); + }; - const handleLike = async (food: Food) => { + const handleLike = async (food: { id: string }) => { if (!isAuthenticated || !currentUserId) { Alert.alert('Authentication Required', 'Please log in to like posts.'); return; @@ -232,77 +105,18 @@ const navigateToPostDetail = (food: Food) => { try { const isLiked = userInteractions[food.id]?.liked || false; - // Optimistically update UI - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - liked: !isLiked - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - likes: isLiked ? Math.max(0, prev[food.id].likes - 1) : prev[food.id].likes + 1 - } - })); - - if (isLiked) { - const { error } = await deleteLike(food.id, currentUserId); - if (error) { - console.error('Error deleting like:', error); - // Revert optimistic update if there's an error - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - liked: true - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - likes: prev[food.id].likes + 1 - } - })); - - Alert.alert('Error', 'Failed to unlike. Please try again.'); - } - } else { - const { error } = await createLike(food.id, currentUserId); - if (error) { - console.error('Error creating like:', error); - // Revert optimistic update if there's an error - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - liked: false - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - likes: Math.max(0, prev[food.id].likes - 1) - } - })); - - Alert.alert('Error', 'Failed to like. Please try again.'); - } - } + likeMutation.mutate({ + foodId: food.id, + userId: currentUserId, + isLiked + }); } catch (error) { console.error('Error toggling like:', error); Alert.alert('Error', 'Failed to update like. Please try again.'); } }; - const handleSave = async (food: Food) => { + const handleSave = async (food: { id: string }) => { if (!isAuthenticated || !currentUserId) { Alert.alert('Authentication Required', 'Please log in to save posts.'); return; @@ -311,77 +125,18 @@ const navigateToPostDetail = (food: Food) => { try { const isSaved = userInteractions[food.id]?.saved || false; - // Optimistically update UI - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saved: !isSaved - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saves: isSaved ? Math.max(0, prev[food.id].saves - 1) : prev[food.id].saves + 1 - } - })); - - if (isSaved) { - const { error } = await deleteSave(food.id, currentUserId); - if (error) { - console.error('Error deleting save:', error); - // Revert optimistic update if there's an error - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saved: true - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saves: prev[food.id].saves + 1 - } - })); - - Alert.alert('Error', 'Failed to unsave. Please try again.'); - } - } else { - const { error } = await createSave(food.id, currentUserId); - if (error) { - console.error('Error creating save:', error); - // Revert optimistic update if there's an error - setUserInteractions(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saved: false - } - })); - - setFoodStats(prev => ({ - ...prev, - [food.id]: { - ...prev[food.id], - saves: Math.max(0, prev[food.id].saves - 1) - } - })); - - Alert.alert('Error', 'Failed to save. Please try again.'); - } - } + saveMutation.mutate({ + foodId: food.id, + userId: currentUserId, + isSaved + }); } catch (error) { console.error('Error toggling save:', error); Alert.alert('Error', 'Failed to update save. Please try again.'); } }; - const renderFoodItem = ({ item }: { item: Food }) => { + const renderFoodItem = ({ item }: { item: any }) => { // Get stats for this food const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 }; @@ -484,6 +239,8 @@ const navigateToPostDetail = (food: Food) => { ); }; + const isLoading = isLoadingFoods || isLoadingStats || isLoadingCreators || isLoadingInteractions; + return ( {/* Search Bar */} @@ -537,7 +294,7 @@ const navigateToPostDetail = (food: Food) => { {/* Food Posts */} - {loading ? ( + {isLoading ? ( diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index bc05f4d..5332c71 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,13 +1,15 @@ -"use client"; +"use client" -import { useAuth } from "@/context/auth-context"; -import { getFoods } from "@/services/data/foods"; -import { getProfile, updateProfile } from "@/services/data/profile"; -import { supabase } from "@/services/supabase"; -import { useIsFocused } from "@react-navigation/native"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import * as ImagePicker from "expo-image-picker"; -import { useState } from "react"; +import { useAuth } from "@/context/auth-context" +import { getFoods } from "@/services/data/foods" +import { getBookmarkedPosts } from "@/services/data/bookmarks" +import { getLikedPosts } from "@/services/data/likes" +import { getProfile, updateProfile } from "@/services/data/profile" +import { supabase } from "@/services/supabase" +import { useIsFocused, useNavigation } from "@react-navigation/native" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import * as ImagePicker from "expo-image-picker" +import { useEffect, useState } from "react" import { ActivityIndicator, Image, @@ -18,15 +20,31 @@ import { TextInput, TouchableOpacity, View, -} from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import uuid from "react-native-uuid"; +} from "react-native" +import { SafeAreaView } from "react-native-safe-area-context" +import uuid from "react-native-uuid" + +// Define the Food type based on your database structure +type Food = { + id: number + name: string + description: string + time_to_cook_minutes: number + skill_level: string + ingredient_count: number + calories: number + image_url: string + is_shared: boolean + created_by: string + created_at: string +} export default function ProfileScreen() { - const [activeTab, setActiveTab] = useState("My Recipes"); - const { isAuthenticated } = useAuth(); - const isFocused = useIsFocused(); - const queryClient = useQueryClient(); + const [activeTab, setActiveTab] = useState("My Recipes") + const { isAuthenticated } = useAuth() + const isFocused = useIsFocused() + const queryClient = useQueryClient() + const navigation = useNavigation() const { data: userData, @@ -35,14 +53,14 @@ export default function ProfileScreen() { } = useQuery({ queryKey: ["auth-user"], queryFn: async () => { - const { data, error } = await supabase.auth.getUser(); - if (error) throw error; - return data?.user; + const { data, error } = await supabase.auth.getUser() + if (error) throw error + return data?.user }, enabled: isAuthenticated, staleTime: 0, - }); - const userId = userData?.id; + }) + const userId = userData?.id const { data: profileData, @@ -51,115 +69,186 @@ export default function ProfileScreen() { } = 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: 0, subscribed: isFocused, - }); + }) + // My Recipes Query const { - data: foodsData, - isLoading: isFoodsLoading, - error: foodsError, + data: myRecipesData, + isLoading: isMyRecipesLoading, + error: myRecipesError, + refetch: refetchMyRecipes, } = useQuery({ queryKey: ["my-recipes", userId], queryFn: async () => { - if (!userId) throw new Error("No user id"); - return getFoods(userId); + if (!userId) throw new Error("No user id") + return getFoods(userId) }, - enabled: !!userId && activeTab === "My Recipes", - staleTime: 0, - }); + enabled: !!userId, + staleTime: 1000 * 60, // 1 minute + }) - const [modalVisible, setModalVisible] = useState(false); - const [editUsername, setEditUsername] = useState(""); - const [editImage, setEditImage] = useState(null); - const [editLoading, setEditLoading] = useState(false); - const [editError, setEditError] = useState(null); + // Likes Query + const { + data: likesData, + isLoading: isLikesLoading, + error: likesError, + refetch: refetchLikes, + } = useQuery({ + queryKey: ["liked-posts", userId], + queryFn: async () => { + if (!userId) throw new Error("No user id") + return getLikedPosts(userId) + }, + enabled: !!userId, + staleTime: 1000 * 60, // 1 minute + }) + + // Bookmarks Query + const { + data: bookmarksData, + isLoading: isBookmarksLoading, + error: bookmarksError, + refetch: refetchBookmarks, + } = useQuery({ + queryKey: ["bookmarked-posts", userId], + queryFn: async () => { + if (!userId) throw new Error("No user id") + return getBookmarkedPosts(userId) + }, + enabled: !!userId, + staleTime: 1000 * 60, // 1 minute + }) + + // Navigate to post detail + const handleFoodPress = (foodId: number) => { + // @ts-ignore - Navigation typing might be different in your app + navigation.navigate("post-detail", { id: foodId }) + } + + // Refetch data when tab changes + const handleTabChange = (tab: string) => { + setActiveTab(tab) + + // Refetch data for the selected tab + if (tab === "My Recipes") { + refetchMyRecipes() + } else if (tab === "Likes") { + refetchLikes() + } else if (tab === "Bookmarks") { + refetchBookmarks() + } + } + + // Refetch all data when the screen comes into focus + useEffect(() => { + if (isFocused && userId) { + refetchMyRecipes() + refetchLikes() + refetchBookmarks() + } + }, [isFocused, userId]) + + const [modalVisible, setModalVisible] = useState(false) + const [editUsername, setEditUsername] = useState("") + const [editImage, setEditImage] = useState(null) + const [editLoading, setEditLoading] = useState(false) + const [editError, setEditError] = useState(null) const pickImage = async () => { - const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync() if (status !== "granted") { - setEditError("Permission to access media library is required."); - return; + setEditError("Permission to access media library is required.") + return } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images"], quality: 0.7, allowsEditing: true, - }); + }) if (!result.canceled) { - setEditImage(result.assets[0].uri); + setEditImage(result.assets[0].uri) } - }; + } const uploadImageToSupabase = async (uri: string): Promise => { - const fileName = `${userId}/${uuid.v4()}.jpg`; - const response = await fetch(uri); - const blob = await response.blob(); + const fileName = `${userId}/${uuid.v4()}.jpg` + const response = await fetch(uri) + const blob = await response.blob() - const { error: uploadError } = await supabase.storage - .from("avatars") - .upload(fileName, blob, { - contentType: "image/jpeg", - upsert: true, - }); + const { error: uploadError } = await supabase.storage.from("avatars").upload(fileName, blob, { + contentType: "image/jpeg", + upsert: true, + }) - if (uploadError) throw uploadError; + if (uploadError) throw uploadError - const { data } = supabase.storage.from("avatars").getPublicUrl(fileName); - return data.publicUrl; - }; + const { data } = supabase.storage.from("avatars").getPublicUrl(fileName) + return data.publicUrl + } const handleSaveProfile = async () => { - setEditLoading(true); - setEditError(null); + setEditLoading(true) + setEditError(null) try { - if (!editUsername.trim()) throw new Error("Username cannot be empty"); + if (!editUsername.trim()) throw new Error("Username cannot be empty") - let avatarUrl = profileData?.data?.avatar_url ?? null; + let avatarUrl = profileData?.data?.avatar_url ?? null if (editImage && editImage !== avatarUrl) { - avatarUrl = await uploadImageToSupabase(editImage); + avatarUrl = await uploadImageToSupabase(editImage) } - const { error: updateError } = await updateProfile( - userId!, - editUsername.trim(), - avatarUrl - ); - if (updateError) throw updateError; + const { error: updateError } = await updateProfile(userId!, editUsername.trim(), avatarUrl) + if (updateError) throw updateError - setModalVisible(false); - await queryClient.invalidateQueries({ queryKey: ["profile", userId] }); + setModalVisible(false) + await queryClient.invalidateQueries({ queryKey: ["profile", userId] }) } catch (err: any) { - setEditError(err.message || "Failed to update profile"); + setEditError(err.message || "Failed to update profile") } finally { - setEditLoading(false); + setEditLoading(false) } - }; + } + + // Get the active data based on the current tab + const getActiveData = () => { + switch (activeTab) { + case "My Recipes": + return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError } + case "Likes": + return { data: likesData, isLoading: isLikesLoading, error: likesError } + case "Bookmarks": + return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError } + default: + return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError } + } + } + + const { data: activeData, isLoading: isActiveLoading, error: activeError } = getActiveData() if (isUserLoading) { return ( - ); + ) } if (userError) { return ( - - {userError.message || "Failed to load user data."} - + {userError.message || "Failed to load user data."} - ); + ) } return ( @@ -179,46 +268,31 @@ export default function ProfileScreen() { {isLoading ? ( ) : error ? ( - - {error.message || error.toString()} - + {error.message || error.toString()} ) : ( - - {profileData?.data?.username ?? "-"} - + {profileData?.data?.username ?? "-"} )} { - setEditUsername(profileData?.data?.username ?? ""); - setEditImage(profileData?.data?.avatar_url ?? null); - setEditError(null); - setModalVisible(true); + setEditUsername(profileData?.data?.username ?? "") + setEditImage(profileData?.data?.avatar_url ?? null) + setEditError(null) + setModalVisible(true) }} > Edit {/* Edit Modal */} - setModalVisible(false)} - > + setModalVisible(false)}> - - Edit Profile - + Edit Profile Change Photo @@ -231,11 +305,7 @@ export default function ProfileScreen() { onChangeText={setEditUsername} placeholder="Enter new username" /> - {editError && ( - - {editError} - - )} + {editError && {editError}} {/* Tab Navigation */} - - {["My Recipes", "Likes", "Saved"].map((tab) => ( + + {["My Recipes", "Likes", "Bookmarks"].map((tab) => ( setActiveTab(tab)} + className={`py-2 px-4 ${activeTab === tab ? "border-b-2 border-[#333]" : ""}`} + onPress={() => handleTabChange(tab)} > - {tab} + {tab} ))} - - - {/* Recipes */} - {activeTab === "My Recipes" && ( + {/* Tab Content */} + {isActiveLoading ? ( + + + + ) : activeError ? ( + + {activeError.message || "Failed to load data"} + + ) : !activeData?.data?.length ? ( + + No items found + + ) : ( - {isFoodsLoading ? ( - - ) : foodsError ? ( - - {foodsError.message || foodsError.toString()} - - ) : foodsData?.data?.length ? ( - foodsData.data.map((item) => ( - - - - - {item.name} - - + {activeData.data.map((item: Food) => ( + handleFoodPress(item.id)} + activeOpacity={0.7} + > + + + {item.name} - )) - ) : ( - - No recipes found. - - )} + + ))} )} - ); + ) } diff --git a/app/post-detail/[id].tsx b/app/post-detail/[id].tsx index 240e149..6a3dc80 100644 --- a/app/post-detail/[id].tsx +++ b/app/post-detail/[id].tsx @@ -3,6 +3,8 @@ import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, Activity import { Feather, FontAwesome } 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'; import { getFoods, getIngredients, getNutrients } from '../../services/data/foods'; import { createLike, @@ -19,32 +21,20 @@ import { } from '../../services/data/forum'; import { getProfile } from '../../services/data/profile'; import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index'; -import { supabase } from '../../services/supabase'; +import { queryKeys, useLikeMutation, useSaveMutation } from '../../hooks/use-foods'; export default function PostDetailScreen() { const { id } = useLocalSearchParams(); const foodId = typeof id === 'string' ? id : ''; + const queryClient = useQueryClient(); console.log('Post detail screen - Food ID:', foodId); const { isAuthenticated } = useAuth(); const [currentUserId, setCurrentUserId] = useState(null); - const [food, setFood] = useState(null); - const [foodCreator, setFoodCreator] = useState(null); - const [ingredients, setIngredients] = useState([]); - const [nutrients, setNutrients] = useState(null); - const [comments, setComments] = useState([]); - const [loading, setLoading] = useState(true); - const [isLiked, setIsLiked] = useState(false); - const [isSaved, setIsSaved] = useState(false); - const [showReviews, setShowReviews] = useState(true); const [commentText, setCommentText] = useState(''); const [submittingComment, setSubmittingComment] = useState(false); - const [stats, setStats] = useState({ - likes: 0, - saves: 0, - comments: 0 - }); + const [showReviews, setShowReviews] = useState(true); // Get current user ID from Supabase session useEffect(() => { @@ -62,14 +52,128 @@ export default function PostDetailScreen() { getCurrentUser(); }, [isAuthenticated]); - useEffect(() => { - if (foodId) { - console.log('Loading food details for ID:', foodId); - loadFoodDetails(); - } else { - console.error('No food ID provided'); - } - }, [foodId]); + // Fetch food details + const { + data: food, + isLoading: isLoadingFood, + error: foodError + } = useQuery({ + queryKey: queryKeys.foodDetails(foodId), + queryFn: async () => { + const { data, error } = await supabase + .from('foods') + .select('*') + .eq('id', foodId) + .single(); + + if (error) throw error; + + return { + ...data, + description: data.description || '', + ingredient_count: data.ingredient_count ?? 0, + calories: data.calories ?? 0, + 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; + + const { data, error } = await getProfile(food.created_by); + + if (error) throw error; + + return data; + }, + enabled: !!food?.created_by, + }); + + // Fetch food stats + const { + data: stats = { likes: 0, saves: 0, comments: 0 }, + isLoading: isLoadingStats, + refetch: refetchStats + } = useQuery({ + queryKey: ['food-stats', foodId], + queryFn: async () => { + const [likesRes, savesRes, commentsRes] = await Promise.all([ + 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 { + data: interactions = { liked: false, saved: false }, + isLoading: isLoadingInteractions, + refetch: refetchInteractions + } = useQuery({ + queryKey: ['user-interactions', foodId, currentUserId], + queryFn: async () => { + 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 { + data: comments = [], + isLoading: isLoadingComments, + refetch: refetchComments + } = useQuery({ + queryKey: queryKeys.foodComments(foodId), + queryFn: async () => { + const { data, error } = await getComments(foodId); + + if (error) throw error; + + return data || []; + }, + enabled: !!foodId, + }); + + // Set up mutations + 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); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.foodComments(foodId) }); + queryClient.invalidateQueries({ queryKey: ['food-stats', foodId] }); + setCommentText(''); + }, + }); // Set up real-time subscription for comments useEffect(() => { @@ -84,167 +188,55 @@ export default function PostDetailScreen() { schema: 'public', table: 'food_comments', filter: `food_id=eq.${foodId}` - }, (payload) => { - console.log('Comment change detected:', payload); - // Refresh comments when changes occur - refreshComments(); + }, () => { + console.log('Comment change detected, refreshing comments'); + refetchComments(); + refetchStats(); }) .subscribe(); return () => { supabase.removeChannel(subscription); }; - }, [foodId]); + }, [foodId, refetchComments, refetchStats]); - // Check if user has liked/saved when user ID changes + // Set up real-time subscription for likes and saves useEffect(() => { - if (foodId && currentUserId && food) { - checkUserInteractions(); - } - }, [currentUserId, foodId, food]); - - const checkUserInteractions = async () => { - if (!currentUserId || !foodId) return; + if (!foodId) return; - try { - console.log('Checking user interactions with user ID:', currentUserId); + const likesSubscription = supabase + .channel(`food_likes:${foodId}`) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'food_likes', + filter: `food_id=eq.${foodId}` + }, () => { + console.log('Like change detected, refreshing stats and interactions'); + refetchStats(); + refetchInteractions(); + }) + .subscribe(); - const [likedRes, savedRes] = await Promise.all([ - checkUserLiked(foodId, currentUserId), - checkUserSaved(foodId, currentUserId) - ]); + const savesSubscription = supabase + .channel(`food_saves:${foodId}`) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'food_saves', + filter: `food_id=eq.${foodId}` + }, () => { + console.log('Save change detected, refreshing stats and interactions'); + refetchStats(); + refetchInteractions(); + }) + .subscribe(); - console.log('User liked:', !!likedRes.data); - console.log('User saved:', !!savedRes.data); - - setIsLiked(!!likedRes.data); - setIsSaved(!!savedRes.data); - } catch (error) { - console.error('Error checking user interactions:', error); - } - }; - - const refreshComments = async () => { - if (!foodId) { - console.error('Cannot refresh comments: No food ID'); - return; - } - - try { - console.log(`Refreshing comments for food_id: ${foodId}`); - - const { data: commentsData, error } = await getComments(foodId); - - if (error) { - console.error('Error refreshing comments:', error); - return; - } - - if (commentsData) { - console.log(`Refreshed ${commentsData.length} comments for food_id: ${foodId}`); - setComments(commentsData); - } - - const { count } = await getCommentsCount(foodId); - setStats(prev => ({ ...prev, comments: count || 0 })); - } catch (error) { - console.error('Error refreshing comments:', error); - } - }; - - const loadFoodDetails = async () => { - if (!foodId) { - console.error('Cannot load food details: No food ID'); - return; - } - - setLoading(true); - try { - console.log('Loading food details for ID:', foodId); - - // Get specific food by ID - const { data: foodData, error: foodError } = await supabase - .from('foods') - .select('*') - .eq('id', foodId) - .single(); - - if (foodError) { - console.error('Error loading food:', foodError); - return; - } - - if (foodData) { - const foodItem = { - ...foodData, - description: foodData.description || '', - ingredient_count: foodData.ingredient_count ?? 0, - calories: foodData.calories ?? 0, - image_url: foodData.image_url || '', - }; - - console.log('Food loaded:', foodItem.name); - setFood(foodItem); - - // Get food creator profile - if (foodItem.created_by) { - console.log('Loading creator profile for:', foodItem.created_by); - const { data: creatorProfile } = await getProfile(foodItem.created_by); - if (creatorProfile) { - setFoodCreator(creatorProfile); - } - } - - // Get ingredients - const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodId); - - if (!ingredientsError && ingredientsData) { - setIngredients(ingredientsData); - } - - // Get nutrients - const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodId); - - if (!nutrientsError && nutrientsData) { - setNutrients(nutrientsData); - } - - // Get comments for this specific food ID - const { data: commentsData, error: commentsError } = await getComments(foodId); - - if (commentsError) { - console.error('Error loading comments:', commentsError); - } else if (commentsData) { - console.log(`Loaded ${commentsData.length} comments for food_id: ${foodId}`); - setComments(commentsData); - } - - // Get stats - const [likesRes, savesRes, commentsRes] = await Promise.all([ - getLikesCount(foodId), - getSavesCount(foodId), - getCommentsCount(foodId) - ]); - - console.log('Stats loaded:', { - likes: likesRes.count || 0, - saves: savesRes.count || 0, - comments: commentsRes.count || 0 - }); - - setStats({ - likes: likesRes.count || 0, - saves: savesRes.count || 0, - comments: commentsRes.count || 0 - }); - } - } catch (error) { - console.error('Error loading food details:', error); - Alert.alert('Error', 'Failed to load food details. Please try again.'); - } finally { - setLoading(false); - } - }; + return () => { + supabase.removeChannel(likesSubscription); + supabase.removeChannel(savesSubscription); + }; + }, [foodId, refetchStats, refetchInteractions]); const handleLike = async () => { if (!isAuthenticated || !currentUserId || !food) { @@ -253,42 +245,13 @@ export default function PostDetailScreen() { } try { - console.log('Toggling like with user ID:', currentUserId, 'and food ID:', foodId); - - // Optimistically update UI - setIsLiked(!isLiked); - setStats(prev => ({ - ...prev, - likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1 - })); - - if (isLiked) { - const { error } = await deleteLike(foodId, currentUserId); - if (error) { - console.error('Error deleting like:', error); - // Revert optimistic update if there's an error - setIsLiked(true); - setStats(prev => ({ ...prev, likes: prev.likes + 1 })); - Alert.alert('Error', 'Failed to unlike. Please try again.'); - } - } else { - const { error } = await createLike(foodId, currentUserId); - if (error) { - console.error('Error creating like:', error); - // Revert optimistic update if there's an error - setIsLiked(false); - setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) })); - Alert.alert('Error', 'Failed to like. Please try again.'); - } - } + likeMutation.mutate({ + foodId, + userId: currentUserId, + isLiked: interactions.liked + }); } catch (error) { console.error('Error toggling like:', error); - // Revert optimistic update if there's an error - setIsLiked(!isLiked); - setStats(prev => ({ - ...prev, - likes: !isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1 - })); Alert.alert('Error', 'Failed to update like. Please try again.'); } }; @@ -300,42 +263,13 @@ export default function PostDetailScreen() { } try { - console.log('Toggling save with user ID:', currentUserId, 'and food ID:', foodId); - - // Optimistically update UI - setIsSaved(!isSaved); - setStats(prev => ({ - ...prev, - saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1 - })); - - if (isSaved) { - const { error } = await deleteSave(foodId, currentUserId); - if (error) { - console.error('Error deleting save:', error); - // Revert optimistic update if there's an error - setIsSaved(true); - setStats(prev => ({ ...prev, saves: prev.saves + 1 })); - Alert.alert('Error', 'Failed to unsave. Please try again.'); - } - } else { - const { error } = await createSave(foodId, currentUserId); - if (error) { - console.error('Error creating save:', error); - // Revert optimistic update if there's an error - setIsSaved(false); - setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) })); - Alert.alert('Error', 'Failed to save. Please try again.'); - } - } + saveMutation.mutate({ + foodId, + userId: currentUserId, + isSaved: interactions.saved + }); } catch (error) { console.error('Error toggling save:', error); - // Revert optimistic update if there's an error - setIsSaved(!isSaved); - setStats(prev => ({ - ...prev, - saves: !isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1 - })); Alert.alert('Error', 'Failed to update save. Please try again.'); } }; @@ -350,23 +284,11 @@ export default function PostDetailScreen() { setSubmittingComment(true); try { - console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', foodId); - - const { error } = await createComment(foodId, currentUserId, commentText.trim()); - - if (error) { - console.error('Error creating comment:', error); - Alert.alert('Error', 'Failed to submit comment. Please try again.'); - return; - } - - // Clear comment text - setCommentText(''); - - // Refresh comments - await refreshComments(); - - console.log('Comment submitted successfully'); + 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.'); @@ -375,7 +297,9 @@ export default function PostDetailScreen() { } }; - if (loading) { + const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments; + + if (isLoading) { return ( @@ -385,7 +309,7 @@ export default function PostDetailScreen() { ); } - if (!food) { + if (foodError || !food) { return ( @@ -436,13 +360,13 @@ export default function PostDetailScreen() { ) : ( - {foodCreator?.username?.charAt(0).toUpperCase() || '?'} + {foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || '?'} )} - {foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'} + {foodCreator?.username || foodCreator?.full_name || 'Chef'} @@ -471,9 +395,13 @@ export default function PostDetailScreen() { {/* Interaction buttons */} - - - {stats.comments} + + + + {stats.likes} @@ -481,16 +409,9 @@ export default function PostDetailScreen() { {stats.comments} - - - {stats.likes} - - + @@ -518,14 +439,14 @@ export default function PostDetailScreen() { ) : ( - {comment.user?.username?.charAt(0).toUpperCase() || '?'} + {comment.user?.username?.charAt(0).toUpperCase() || comment.user_id?.charAt(0).toUpperCase() || '?'} )} - {comment.user?.username || comment.user?.full_name || 'Unknown User'} + {comment.user?.username || comment.user?.full_name || 'User'} {new Date(comment.created_at).toLocaleDateString()} diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png index 03d6f6b..b963238 100644 Binary files a/assets/images/adaptive-icon.png and b/assets/images/adaptive-icon.png differ diff --git a/assets/images/icon.png b/assets/images/icon.png index a0b1526..b963238 100644 Binary files a/assets/images/icon.png and b/assets/images/icon.png differ diff --git a/assets/images/notebook-orange.png b/assets/images/notebook-orange.png deleted file mode 100644 index aa87d4a..0000000 Binary files a/assets/images/notebook-orange.png and /dev/null differ diff --git a/assets/images/splash.png b/assets/images/splash.png new file mode 100644 index 0000000..f8d038d Binary files /dev/null and b/assets/images/splash.png differ diff --git a/hooks/use-foods.ts b/hooks/use-foods.ts new file mode 100644 index 0000000..2b36b2a --- /dev/null +++ b/hooks/use-foods.ts @@ -0,0 +1,287 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getFoods } from '../services/data/foods'; +import { + getLikesCount, + getSavesCount, + getCommentsCount, + createLike, + deleteLike, + createSave, + deleteSave, + checkUserLiked, + checkUserSaved +} from '../services/data/forum'; +import { getProfile } from '../services/data/profile'; +import { Food, Profile } from '../types/index'; + +// Query keys +export const queryKeys = { + foods: 'foods', + foodStats: 'food-stats', + foodCreators: 'food-creators', + userInteractions: 'user-interactions', + foodDetails: (id: string) => ['food-details', id], + foodComments: (id: string) => ['food-comments', id], +}; + +// Hook to fetch foods +export function useFoods(category?: string, search?: string, sort?: string) { + return useQuery({ + queryKey: [queryKeys.foods, category, search, sort], + queryFn: async () => { + const { data, error } = await getFoods(category, true, search); + + if (error) { + throw error; + } + + if (!data) { + return []; + } + + let sortedData = [...data]; + + if (sort === 'rating') { + sortedData.sort((a, b) => (b.calories ?? 0) - (a.calories ?? 0)); + } else if (sort === 'newest') { + sortedData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + } else if (sort === 'best') { + sortedData.sort((a, b) => (b.ingredient_count ?? 0) - (a.ingredient_count ?? 0)); + } + + return sortedData.map(food => ({ + ...food, + description: food.description || '', + ingredient_count: food.ingredient_count ?? 0, + calories: food.calories ?? 0, + image_url: food.image_url || '', + })); + }, + }); +} + +// Hook to fetch food stats +export function useFoodStats(foodIds: string[]) { + return useQuery({ + queryKey: [queryKeys.foodStats, foodIds], + queryFn: async () => { + if (!foodIds.length) return {}; + + const statsPromises = foodIds.map(async (foodId) => { + const [likesRes, savesRes, commentsRes] = await Promise.all([ + getLikesCount(foodId), + getSavesCount(foodId), + getCommentsCount(foodId) + ]); + + return { + foodId, + likes: likesRes.count || 0, + saves: savesRes.count || 0, + comments: commentsRes.count || 0 + }; + }); + + const stats = await Promise.all(statsPromises); + + return stats.reduce((acc, stat) => { + acc[stat.foodId] = { + likes: stat.likes, + saves: stat.saves, + comments: stat.comments + }; + return acc; + }, {} as Record); + }, + enabled: foodIds.length > 0, + }); +} + +// Hook to fetch food creators +export function useFoodCreators(creatorIds: string[]) { + return useQuery({ + queryKey: [queryKeys.foodCreators, creatorIds], + queryFn: async () => { + if (!creatorIds.length) return {}; + + const uniqueCreatorIds = [...new Set(creatorIds)]; + const creatorProfiles: Record = {}; + + for (const creatorId of uniqueCreatorIds) { + const { data: profile } = await getProfile(creatorId); + if (profile) { + creatorProfiles[creatorId] = profile; + } + } + + return creatorProfiles; + }, + enabled: creatorIds.length > 0, + }); +} + +// Hook to fetch user interactions +export function useUserInteractions(foodIds: string[], userId: string | null) { + return useQuery({ + queryKey: [queryKeys.userInteractions, foodIds, userId], + queryFn: async () => { + if (!foodIds.length || !userId) return {}; + + const interactionsPromises = foodIds.map(async (foodId) => { + const [likedRes, savedRes] = await Promise.all([ + checkUserLiked(foodId, userId), + checkUserSaved(foodId, userId) + ]); + + return { + foodId, + liked: !!likedRes.data, + saved: !!savedRes.data + }; + }); + + const interactions = await Promise.all(interactionsPromises); + + return interactions.reduce((acc, interaction) => { + acc[interaction.foodId] = { + liked: interaction.liked, + saved: interaction.saved + }; + return acc; + }, {} as Record); + }, + enabled: foodIds.length > 0 && !!userId, + }); +} + +// Hook to like/unlike a food +export function useLikeMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ foodId, userId, isLiked }: { foodId: string, userId: string, isLiked: boolean }) => { + if (isLiked) { + return deleteLike(foodId, userId); + } else { + return createLike(foodId, userId); + } + }, + onMutate: async ({ foodId, isLiked }) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: [queryKeys.foodStats] }); + await queryClient.cancelQueries({ queryKey: [queryKeys.userInteractions] }); + + // Snapshot the previous value + const previousStats = queryClient.getQueryData([queryKeys.foodStats]); + const previousInteractions = queryClient.getQueryData([queryKeys.userInteractions]); + + // Optimistically update + queryClient.setQueryData([queryKeys.foodStats], (old: any) => { + if (!old) return old; + + return { + ...old, + [foodId]: { + ...old[foodId], + likes: isLiked ? Math.max(0, old[foodId].likes - 1) : old[foodId].likes + 1 + } + }; + }); + + queryClient.setQueryData([queryKeys.userInteractions], (old: any) => { + if (!old) return old; + + return { + ...old, + [foodId]: { + ...old[foodId], + liked: !isLiked + } + }; + }); + + // Return a context object with the snapshotted value + return { previousStats, previousInteractions }; + }, + onError: (err, variables, context) => { + // If the mutation fails, use the context returned from onMutate to roll back + if (context?.previousStats) { + queryClient.setQueryData([queryKeys.foodStats], context.previousStats); + } + if (context?.previousInteractions) { + queryClient.setQueryData([queryKeys.userInteractions], context.previousInteractions); + } + }, + onSettled: () => { + // Always refetch after error or success + queryClient.invalidateQueries({ queryKey: [queryKeys.foodStats] }); + queryClient.invalidateQueries({ queryKey: [queryKeys.userInteractions] }); + }, + }); +} + +// Hook to save/unsave a food +export function useSaveMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ foodId, userId, isSaved }: { foodId: string, userId: string, isSaved: boolean }) => { + if (isSaved) { + return deleteSave(foodId, userId); + } else { + return createSave(foodId, userId); + } + }, + onMutate: async ({ foodId, isSaved }) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: [queryKeys.foodStats] }); + await queryClient.cancelQueries({ queryKey: [queryKeys.userInteractions] }); + + // Snapshot the previous value + const previousStats = queryClient.getQueryData([queryKeys.foodStats]); + const previousInteractions = queryClient.getQueryData([queryKeys.userInteractions]); + + // Optimistically update + queryClient.setQueryData([queryKeys.foodStats], (old: any) => { + if (!old) return old; + + return { + ...old, + [foodId]: { + ...old[foodId], + saves: isSaved ? Math.max(0, old[foodId].saves - 1) : old[foodId].saves + 1 + } + }; + }); + + queryClient.setQueryData([queryKeys.userInteractions], (old: any) => { + if (!old) return old; + + return { + ...old, + [foodId]: { + ...old[foodId], + saved: !isSaved + } + }; + }); + + // Return a context object with the snapshotted value + return { previousStats, previousInteractions }; + }, + onError: (err, variables, context) => { + // If the mutation fails, use the context returned from onMutate to roll back + if (context?.previousStats) { + queryClient.setQueryData([queryKeys.foodStats], context.previousStats); + } + if (context?.previousInteractions) { + queryClient.setQueryData([queryKeys.userInteractions], context.previousInteractions); + } + }, + onSettled: () => { + // Always refetch after error or success + queryClient.invalidateQueries({ queryKey: [queryKeys.foodStats] }); + queryClient.invalidateQueries({ queryKey: [queryKeys.userInteractions] }); + }, + }); +} \ No newline at end of file diff --git a/services/data/bookmarks.ts b/services/data/bookmarks.ts new file mode 100644 index 0000000..c6b7d47 --- /dev/null +++ b/services/data/bookmarks.ts @@ -0,0 +1,48 @@ +import { supabase } from "@/services/supabase" +import type { PostgrestError } from "@supabase/supabase-js" + +/** + * Retrieves posts that a user has saved/bookmarked + */ +export async function getBookmarkedPosts(userId: string): Promise<{ + data: any[] | null + error: PostgrestError | null +}> { + // First get all food_ids that the user has saved + const { data: savedFoodIds, error: saveError } = await supabase + .from("food_saves") + .select("food_id") + .eq("user_id", userId) + + if (saveError) { + return { data: null, error: saveError } + } + + if (!savedFoodIds || savedFoodIds.length === 0) { + return { data: [], error: null } + } + + // Extract just the IDs + const foodIds = savedFoodIds.map((item) => item.food_id) + + // Then fetch the actual food items + 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 + `) + .in("id", foodIds) + .order("created_at", { ascending: false }) + + return { data, error } +} diff --git a/services/data/forum.ts b/services/data/forum.ts index d370523..79724df 100644 --- a/services/data/forum.ts +++ b/services/data/forum.ts @@ -208,48 +208,54 @@ export const checkUserSaved = async (food_id: string, user_id: string) => { export const getComments = async (food_id: string) => { console.log('Getting comments for food_id:', food_id); - const { data, error } = await supabase - .from("food_comments") - .select(` - id, - created_at, - user_id, - food_id, - content - `) - .eq("food_id", food_id) - .order("created_at", { ascending: false }); - - if (error) { - console.error('Error getting comments:', error); + try { + const { data, error } = await supabase + .from("food_comments") + .select(` + id, + created_at, + user_id, + food_id, + content + `) + .eq("food_id", food_id) + .order("created_at", { ascending: false }); + + if (error) { + console.error('Error getting comments:', error); + return { data: [], error }; + } + + if (data && data.length > 0) { + // Get unique user IDs from comments + const userIds = [...new Set(data.map(comment => comment.user_id))]; + + // Fetch profiles for these users + const { data: profiles } = await getProfiles(userIds); + + // Add user profiles to comments + if (profiles && profiles.length > 0) { + const profileMap = profiles.reduce((acc, profile) => { + acc[profile.id] = profile; + return acc; + }, {} as Record); + + // Attach profiles to comments + const commentsWithProfiles = data.map(comment => ({ + ...comment, + user: profileMap[comment.user_id] || null + })); + + console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`); + return { data: commentsWithProfiles, error: null }; + } + } + + // If no profiles were found or no comments exist, return the original data + console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`); + return { data: data?.map(comment => ({ ...comment, user: null })) || [], error: null }; + } catch (error) { + console.error('Error in getComments:', error); return { data: [], error }; } - - if (data && data.length > 0) { - // Get unique user IDs from comments - const userIds = [...new Set(data.map(comment => comment.user_id))]; - - // Fetch profiles for these users - const { data: profiles } = await getProfiles(userIds); - - // Add user profiles to comments - if (profiles) { - const profileMap = profiles.reduce((acc, profile) => { - acc[profile.id] = profile; - return acc; - }, {} as Record); - - // Attach profiles to comments - const commentsWithProfiles = data.map(comment => ({ - ...comment, - user: profileMap[comment.user_id] - })); - - console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`); - return { data: commentsWithProfiles, error }; - } - } - - console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`); - return { data, error }; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/services/data/likes.ts b/services/data/likes.ts new file mode 100644 index 0000000..62d187c --- /dev/null +++ b/services/data/likes.ts @@ -0,0 +1,48 @@ +import { supabase } from "@/services/supabase" +import type { PostgrestError } from "@supabase/supabase-js" + +/** + * Retrieves posts that a user has liked + */ +export async function getLikedPosts(userId: string): Promise<{ + data: any[] | null + error: PostgrestError | null +}> { + // First get all food_ids that the user has liked + const { data: likedFoodIds, error: likeError } = await supabase + .from("food_likes") + .select("food_id") + .eq("user_id", userId) + + if (likeError) { + return { data: null, error: likeError } + } + + if (!likedFoodIds || likedFoodIds.length === 0) { + return { data: [], error: null } + } + + // Extract just the IDs + const foodIds = likedFoodIds.map((item) => item.food_id) + + // Then fetch the actual food items + 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 + `) + .in("id", foodIds) + .order("created_at", { ascending: false }) + + return { data, error } +} diff --git a/types/index.ts b/types/index.ts index e8db137..3e865a9 100644 --- a/types/index.ts +++ b/types/index.ts @@ -64,55 +64,3 @@ export interface Profile { avatar_url?: string; website?: string; } - -export interface Food { - id: string; - created_at: string; - name: string; - description: string; - time_to_cook_minutes: number; - skill_level: string; - ingredient_count: number; - calories: number; - image_url: string; - is_shared: boolean; - created_by: string; -} - -export interface FoodLike { - created_at: string; - user_id: string; - food_id: string; -} - -export interface FoodSave { - created_at: string; - user_id: string; - food_id: string; -} - -export interface FoodComment { - id: string; - created_at: string; - user_id: string; - food_id: string; - content: string; - user?: Profile; // Add user profile to comments -} - -export interface Nutrient { - food_id: string; - fat_g: number; - fiber_g: number; - protein_g: number; - carbs_g: number; - created_at: string; -} - -export interface Ingredient { - id: string; - food_id: string; - name: string; - emoji: string; - created_at: string; -} \ No newline at end of file