From d35ae859e4c2e885fec7e165dbec50431eb7e418 Mon Sep 17 00:00:00 2001 From: Tantikon Phasanphaengsi Date: Sat, 10 May 2025 15:28:20 +0700 Subject: [PATCH] create forum id --- app/(tabs)/forum.tsx | 366 ++++++++++++++++++++++--- app/post-detail.tsx | 376 +++++++++++++++++++------ app/post-detail/[id].tsx | 575 +++++++++++++++++++++++++++++++++++++++ services/data/forum.ts | 242 ++++++++++++---- 4 files changed, 1396 insertions(+), 163 deletions(-) create mode 100644 app/post-detail/[id].tsx diff --git a/app/(tabs)/forum.tsx b/app/(tabs)/forum.tsx index 94ebb79..74cabae 100644 --- a/app/(tabs)/forum.tsx +++ b/app/(tabs)/forum.tsx @@ -1,11 +1,23 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator } from 'react-native'; +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 { useAuth } from '../../context/auth-context'; import { getFoods } from '../../services/data/foods'; -import { getLikesCount, getSavesCount, getCommentsCount } from '../../services/data/forum'; -import { Food } from '../../types/index'; +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'; // Categories for filtering const categories = [ @@ -23,16 +35,69 @@ const sortOptions = [ 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(() => { + 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); + } else { + setCurrentUserId(null); + } + } + + 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]); useEffect(() => { loadFoods(); - }, [selectedCategory, selectedSort]); + }, [selectedCategory, selectedSort, currentUserId]); const loadFoods = async () => { setLoading(true); @@ -59,12 +124,12 @@ export default function ForumScreen() { } 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 - }))); + ...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) => { @@ -93,6 +158,51 @@ export default function ForumScreen() { }, {} 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); @@ -109,24 +219,181 @@ export default function ForumScreen() { }, 500); }; - const navigateToPostDetail = (food: Food) => { - router.push({ - pathname: '/post-detail', - params: { id: food.id } - }); +const navigateToPostDetail = (food: Food) => { + router.push(`/post-detail/${food.id}`); +}; + + const handleLike = async (food: Food) => { + if (!isAuthenticated || !currentUserId) { + Alert.alert('Authentication Required', 'Please log in to like posts.'); + return; + } + + 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.'); + } + } + } catch (error) { + console.error('Error toggling like:', error); + Alert.alert('Error', 'Failed to update like. Please try again.'); + } + }; + + const handleSave = async (food: Food) => { + if (!isAuthenticated || !currentUserId) { + Alert.alert('Authentication Required', 'Please log in to save posts.'); + return; + } + + 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.'); + } + } + } catch (error) { + console.error('Error toggling save:', error); + Alert.alert('Error', 'Failed to update save. Please try again.'); + } }; const renderFoodItem = ({ item }: { item: Food }) => { // Get stats for this food const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 }; - // Mock data for UI elements not in the Food type - const username = 'Mr. Chef'; - const rating = 4.2; + // Get creator profile + const creator = item.created_by ? foodCreators[item.created_by] : null; + + // Get user interactions + const interactions = userInteractions[item.id] || { liked: false, saved: false }; return ( navigateToPostDetail(item)} > @@ -134,15 +401,25 @@ export default function ForumScreen() { - + {creator?.avatar_url ? ( + + ) : ( + + + {creator?.username?.charAt(0).toUpperCase() || '?'} + + + )} - {username} + + {creator?.username || creator?.full_name || 'Unknown Chef'} + - {rating} + 4.2 @@ -165,24 +442,41 @@ export default function ForumScreen() { {/* Interaction buttons */} - - - {stats.comments} + { + e.stopPropagation(); + handleLike(item); + }} + > + + {stats.likes} - + navigateToPostDetail(item)} + > {stats.comments} - - - - {stats.likes} - - - + { + e.stopPropagation(); + handleSave(item); + }} + > + diff --git a/app/post-detail.tsx b/app/post-detail.tsx index d925dbb..cdc9fb0 100644 --- a/app/post-detail.tsx +++ b/app/post-detail.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform } from 'react-native'; +import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform, Alert } from 'react-native'; import { Feather, FontAwesome } from '@expo/vector-icons'; import { useLocalSearchParams, router } from 'expo-router'; import { useAuth } from '../context/auth-context'; @@ -17,13 +17,17 @@ import { checkUserLiked, checkUserSaved } from '../services/data/forum'; -import { Food, Ingredient, Nutrient, FoodComment } from '../types/index'; +import { getProfile } from '../services/data/profile'; +import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../types/index'; +import { supabase } from '../services/supabase'; export default function PostDetailScreen() { const { id } = useLocalSearchParams(); - const authContext = useAuth(); - const { isAuthenticated } = authContext || {}; // Adjust based on the actual structure of AuthContextType + const foodId = Array.isArray(id) ? id[0] : id; + 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([]); @@ -39,19 +43,137 @@ export default function PostDetailScreen() { comments: 0 }); - // Mock data for UI elements - const username = 'Mr. Chef'; - const rating = 4.2; + // 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); + } else { + setCurrentUserId(null); + } + } + + getCurrentUser(); + }, [isAuthenticated]); useEffect(() => { - if (id) { + if (foodId) { loadFoodDetails(); } - }, [id]); + }, [foodId]); + + // Set up real-time subscription for comments + useEffect(() => { + if (!foodId) return; + + const subscription = supabase + .channel(`food_comments:${foodId}`) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'food_comments', + filter: `food_id=eq.${foodId}` + }, () => { + // Refresh comments when changes occur + refreshComments(); + }) + .subscribe(); + + return () => { + supabase.removeChannel(subscription); + }; + }, [foodId]); + + // Set up real-time subscription for likes + useEffect(() => { + if (!foodId) return; + + const subscription = supabase + .channel(`food_likes:${foodId}`) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'food_likes', + filter: `food_id=eq.${foodId}` + }, () => { + // Refresh likes when changes occur + refreshLikes(); + }) + .subscribe(); + + return () => { + supabase.removeChannel(subscription); + }; + }, [foodId]); + + // Check if user has liked/saved when user ID changes + useEffect(() => { + if (foodId && currentUserId && food) { + checkUserInteractions(); + } + }, [currentUserId, foodId, food]); + + const checkUserInteractions = async () => { + if (!currentUserId || !foodId) return; + + try { + console.log('Checking user interactions with user ID:', currentUserId); + + const [likedRes, savedRes] = await Promise.all([ + checkUserLiked(foodId, currentUserId), + checkUserSaved(foodId, currentUserId) + ]); + + 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) return; + + try { + const { data: commentsData } = await getComments(foodId); + if (commentsData) { + setComments(commentsData); + } + + const { count } = await getCommentsCount(foodId); + setStats(prev => ({ ...prev, comments: count || 0 })); + } catch (error) { + console.error('Error refreshing comments:', error); + } + }; + + const refreshLikes = async () => { + if (!foodId || !currentUserId) return; + + try { + const { count } = await getLikesCount(foodId); + setStats(prev => ({ ...prev, likes: count || 0 })); + + const { data } = await checkUserLiked(foodId, currentUserId); + setIsLiked(!!data); + } catch (error) { + console.error('Error refreshing likes:', error); + } + }; const loadFoodDetails = async () => { + if (!foodId) return; + setLoading(true); try { + console.log('Loading food details for ID:', foodId); + // Get food details const { data: foodData, error: foodError } = await getFoods(undefined, undefined, undefined, 1, 0); @@ -61,128 +183,199 @@ export default function PostDetailScreen() { } if (foodData && foodData.length > 0) { - setFood({ - ...foodData[0], - description: foodData[0].description || '', // Ensure description is always a string - ingredient_count: foodData[0].ingredient_count ?? 0, // Provide default value for ingredient_count - calories: foodData[0].calories ?? 0, // Provide default value for calories - image_url: foodData[0].image_url || '', // Provide default value for image_url - }); + const foodItem = { + ...foodData[0], + description: foodData[0].description || '', + ingredient_count: foodData[0].ingredient_count ?? 0, + calories: foodData[0].calories ?? 0, + image_url: foodData[0].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(foodData[0].id); + const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodItem.id); if (!ingredientsError && ingredientsData) { setIngredients(ingredientsData); } // Get nutrients - const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodData[0].id); + const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodItem.id); if (!nutrientsError && nutrientsData) { setNutrients(nutrientsData); } // Get comments - const { data: commentsData, error: commentsError } = await getComments(foodData[0].id); + const { data: commentsData, error: commentsError } = await getComments(foodItem.id); if (!commentsError && commentsData) { + console.log('Comments loaded:', commentsData.length); setComments(commentsData); } // Get stats const [likesRes, savesRes, commentsRes] = await Promise.all([ - getLikesCount(foodData[0].id), - getSavesCount(foodData[0].id), - getCommentsCount(foodData[0].id) + getLikesCount(foodItem.id), + getSavesCount(foodItem.id), + getCommentsCount(foodItem.id) ]); + 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 }); - - // Check if user has liked/saved - if (isAuthenticated) { - const userId = 'current-user-id'; // Replace with actual user ID - - const [likedRes, savedRes] = await Promise.all([ - checkUserLiked(foodData[0].id, userId), - checkUserSaved(foodData[0].id, userId) - ]); - - setIsLiked(!!likedRes.data); - setIsSaved(!!savedRes.data); - } } } catch (error) { - console.error('Error:', error); + console.error('Error loading food details:', error); + Alert.alert('Error', 'Failed to load food details. Please try again.'); } finally { setLoading(false); } }; const handleLike = async () => { - if (!authContext.isAuthenticated || !food) return; + if (!isAuthenticated || !currentUserId || !food) { + Alert.alert('Authentication Required', 'Please log in to like posts.'); + return; + } try { - const userId = 'current-user-id'; // Replace with actual user ID + console.log('Toggling like with user ID:', currentUserId, 'and food ID:', food.id); + + // Optimistically update UI + setIsLiked(!isLiked); + setStats(prev => ({ + ...prev, + likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1 + })); if (isLiked) { - await deleteLike(food.id, userId); - setIsLiked(false); - setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) })); + const { error } = await deleteLike(food.id, 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 { - await createLike(food.id, userId); - setIsLiked(true); - setStats(prev => ({ ...prev, likes: prev.likes + 1 })); + const { error } = await createLike(food.id, 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.'); + } } } 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.'); } }; const handleSave = async () => { - if (!authContext.isAuthenticated || !food) return; + if (!isAuthenticated || !currentUserId || !food) { + Alert.alert('Authentication Required', 'Please log in to save posts.'); + return; + } try { - const userId = 'current-user-id'; // Replace with actual user ID + console.log('Toggling save with user ID:', currentUserId, 'and food ID:', food.id); + + // Optimistically update UI + setIsSaved(!isSaved); + setStats(prev => ({ + ...prev, + saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1 + })); if (isSaved) { - await deleteSave(food.id, userId); - setIsSaved(false); - setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) })); + const { error } = await deleteSave(food.id, 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 { - await createSave(food.id, userId); - setIsSaved(true); - setStats(prev => ({ ...prev, saves: prev.saves + 1 })); + const { error } = await createSave(food.id, 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.'); + } } } 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.'); } }; const handleSubmitComment = async () => { - if (!authContext.isAuthenticated || !food || !commentText.trim()) return; + if (!isAuthenticated || !currentUserId || !food || !commentText.trim()) { + if (!isAuthenticated || !currentUserId) { + Alert.alert('Authentication Required', 'Please log in to comment.'); + } + return; + } setSubmittingComment(true); try { - const userId = 'current-user-id'; // Replace with actual user ID + console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', food.id); - await createComment(food.id, userId, commentText.trim()); + const { error } = await createComment(food.id, currentUserId, commentText.trim()); - // Refresh comments - const { data: commentsData } = await getComments(food.id); - - if (commentsData) { - setComments(commentsData); - setStats(prev => ({ ...prev, comments: prev.comments + 1 })); + 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'); } catch (error) { console.error('Error submitting comment:', error); + Alert.alert('Error', 'Failed to submit comment. Please try again.'); } finally { setSubmittingComment(false); } @@ -241,15 +434,25 @@ export default function PostDetailScreen() { - + {foodCreator?.avatar_url ? ( + + ) : ( + + + {foodCreator?.username?.charAt(0).toUpperCase() || '?'} + + + )} - {username} + + {foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'} + - {rating} + 4.2 @@ -267,7 +470,9 @@ export default function PostDetailScreen() { {food.name} {food.description} - 09:41 - 4/3/25 + + {new Date(food.created_at).toLocaleDateString()} - {new Date(food.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {/* Interaction buttons */} @@ -286,12 +491,12 @@ export default function PostDetailScreen() { className="flex-row items-center" onPress={handleLike} > - + {stats.likes} - + @@ -311,13 +516,23 @@ export default function PostDetailScreen() { - + {comment.user?.avatar_url ? ( + + ) : ( + + + {comment.user?.username?.charAt(0).toUpperCase() || '?'} + + + )} - {comment.user_id} + + {comment.user?.username || comment.user?.full_name || 'Unknown User'} + {new Date(comment.created_at).toLocaleDateString()} @@ -347,13 +562,18 @@ export default function PostDetailScreen() { multiline /> - + + {!isAuthenticated && ( + + Please log in to comment + + )} diff --git a/app/post-detail/[id].tsx b/app/post-detail/[id].tsx new file mode 100644 index 0000000..240e149 --- /dev/null +++ b/app/post-detail/[id].tsx @@ -0,0 +1,575 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform, Alert } from 'react-native'; +import { Feather, FontAwesome } from '@expo/vector-icons'; +import { useLocalSearchParams, router } from 'expo-router'; +import { useAuth } from '../../context/auth-context'; +import { getFoods, getIngredients, getNutrients } from '../../services/data/foods'; +import { + createLike, + deleteLike, + createSave, + deleteSave, + getComments, + createComment, + getLikesCount, + getSavesCount, + getCommentsCount, + checkUserLiked, + checkUserSaved +} from '../../services/data/forum'; +import { getProfile } from '../../services/data/profile'; +import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index'; +import { supabase } from '../../services/supabase'; + +export default function PostDetailScreen() { + const { id } = useLocalSearchParams(); + const foodId = typeof id === 'string' ? id : ''; + + 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 + }); + + // 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); + } else { + setCurrentUserId(null); + } + } + + getCurrentUser(); + }, [isAuthenticated]); + + useEffect(() => { + if (foodId) { + console.log('Loading food details for ID:', foodId); + loadFoodDetails(); + } else { + console.error('No food ID provided'); + } + }, [foodId]); + + // Set up real-time subscription for comments + useEffect(() => { + if (!foodId) return; + + console.log(`Setting up real-time subscription for comments on food_id: ${foodId}`); + + const subscription = supabase + .channel(`food_comments:${foodId}`) + .on('postgres_changes', { + event: '*', + schema: 'public', + table: 'food_comments', + filter: `food_id=eq.${foodId}` + }, (payload) => { + console.log('Comment change detected:', payload); + // Refresh comments when changes occur + refreshComments(); + }) + .subscribe(); + + return () => { + supabase.removeChannel(subscription); + }; + }, [foodId]); + + // Check if user has liked/saved when user ID changes + useEffect(() => { + if (foodId && currentUserId && food) { + checkUserInteractions(); + } + }, [currentUserId, foodId, food]); + + const checkUserInteractions = async () => { + if (!currentUserId || !foodId) return; + + try { + console.log('Checking user interactions with user ID:', currentUserId); + + const [likedRes, savedRes] = await Promise.all([ + checkUserLiked(foodId, currentUserId), + checkUserSaved(foodId, currentUserId) + ]); + + 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); + } + }; + + const handleLike = async () => { + if (!isAuthenticated || !currentUserId || !food) { + Alert.alert('Authentication Required', 'Please log in to like posts.'); + return; + } + + 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.'); + } + } + } 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.'); + } + }; + + const handleSave = async () => { + if (!isAuthenticated || !currentUserId || !food) { + Alert.alert('Authentication Required', 'Please log in to save posts.'); + return; + } + + 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.'); + } + } + } 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.'); + } + }; + + const handleSubmitComment = async () => { + if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) { + if (!isAuthenticated || !currentUserId) { + Alert.alert('Authentication Required', 'Please log in to comment.'); + } + return; + } + + 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'); + } catch (error) { + console.error('Error submitting comment:', error); + Alert.alert('Error', 'Failed to submit comment. Please try again.'); + } finally { + setSubmittingComment(false); + } + }; + + if (loading) { + return ( + + + + + + ); + } + + if (!food) { + return ( + + + Post not found + router.back()} + > + Go Back + + + + ); + } + + return ( + + + + {/* Header */} + + router.back()} + > + + + + Post + + + + + + + {/* User info and rating */} + + + + {foodCreator?.avatar_url ? ( + + ) : ( + + + {foodCreator?.username?.charAt(0).toUpperCase() || '?'} + + + )} + + + {foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'} + + + + 4.2 + + + + + {/* Food image */} + + + + + {/* Food title and description */} + + {food.name} + {food.description} + + {new Date(food.created_at).toLocaleDateString()} - {new Date(food.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + {/* Interaction buttons */} + + + + {stats.comments} + + + + + {stats.comments} + + + + + {stats.likes} + + + + + + + + {/* Reviews section */} + setShowReviews(!showReviews)} + > + Review + + + + {showReviews && ( + + {comments.length > 0 ? ( + comments.map((comment) => ( + + + + {comment.user?.avatar_url ? ( + + ) : ( + + + {comment.user?.username?.charAt(0).toUpperCase() || '?'} + + + )} + + + + {comment.user?.username || comment.user?.full_name || 'Unknown User'} + + + {new Date(comment.created_at).toLocaleDateString()} + + + + {comment.content} + + )) + ) : ( + No reviews yet. Be the first to comment! + )} + + )} + + {/* Bottom spacing */} + + + + {/* Comment input */} + + + + + + + + {!isAuthenticated && ( + + Please log in to comment + + )} + + + + ); +} \ No newline at end of file diff --git a/services/data/forum.ts b/services/data/forum.ts index bb4b641..d370523 100644 --- a/services/data/forum.ts +++ b/services/data/forum.ts @@ -2,31 +2,212 @@ import { supabase } from "@/services/supabase"; import { getProfile, getProfiles } from "./profile"; export const createLike = async (food_id: string, user_id: string) => { - const { data, error } = await supabase.from("food_likes").insert({ food_id, user_id }); + console.log('Creating like with food_id:', food_id, 'and user_id:', user_id); + + // Check if like already exists to prevent duplicates + const { data: existingLike } = await supabase + .from("food_likes") + .select("*") + .eq("food_id", food_id) + .eq("user_id", user_id) + .single(); + + if (existingLike) { + console.log('Like already exists'); + return { data: existingLike, error: null }; + } + + const { data, error } = await supabase + .from("food_likes") + .insert({ food_id, user_id }) + .select(); + + if (error) { + console.error('Error creating like:', error); + } else { + console.log('Like created successfully'); + } + return { data, error }; } export const createSave = async (food_id: string, user_id: string) => { - const { data, error } = await supabase.from("food_saves").insert({ food_id, user_id }); + console.log('Creating save with food_id:', food_id, 'and user_id:', user_id); + + // Check if save already exists to prevent duplicates + const { data: existingSave } = await supabase + .from("food_saves") + .select("*") + .eq("food_id", food_id) + .eq("user_id", user_id) + .single(); + + if (existingSave) { + console.log('Save already exists'); + return { data: existingSave, error: null }; + } + + const { data, error } = await supabase + .from("food_saves") + .insert({ food_id, user_id }) + .select(); + + if (error) { + console.error('Error creating save:', error); + } else { + console.log('Save created successfully'); + } + return { data, error }; } export const deleteLike = async (food_id: string, user_id: string) => { - const { data, error } = await supabase.from("food_likes").delete().eq("food_id", food_id).eq("user_id", user_id); + console.log('Deleting like with food_id:', food_id, 'and user_id:', user_id); + + const { data, error } = await supabase + .from("food_likes") + .delete() + .eq("food_id", food_id) + .eq("user_id", user_id) + .select(); + + if (error) { + console.error('Error deleting like:', error); + } else { + console.log('Like deleted successfully'); + } + return { data, error }; } export const deleteSave = async (food_id: string, user_id: string) => { - const { data, error } = await supabase.from("food_saves").delete().eq("food_id", food_id).eq("user_id", user_id); + console.log('Deleting save with food_id:', food_id, 'and user_id:', user_id); + + const { data, error } = await supabase + .from("food_saves") + .delete() + .eq("food_id", food_id) + .eq("user_id", user_id) + .select(); + + if (error) { + console.error('Error deleting save:', error); + } else { + console.log('Save deleted successfully'); + } + return { data, error }; } export const createComment = async (food_id: string, user_id: string, content: string) => { - const { data, error } = await supabase.from("food_comments").insert({ food_id, user_id, content }); + console.log('Creating comment with food_id:', food_id, 'user_id:', user_id, 'and content:', content); + + const { data, error } = await supabase + .from("food_comments") + .insert({ food_id, user_id, content }) + .select(); + + if (error) { + console.error('Error creating comment:', error); + } else { + console.log('Comment created successfully'); + } + return { data, error }; } +export const getLikesCount = async (food_id: string) => { + console.log('Getting likes count for food_id:', food_id); + + const { count, error } = await supabase + .from("food_likes") + .select("*", { count: "exact", head: true }) + .eq("food_id", food_id); + + if (error) { + console.error('Error getting likes count:', error); + } else { + console.log('Likes count:', count); + } + + return { count, error }; +} + +export const getSavesCount = async (food_id: string) => { + console.log('Getting saves count for food_id:', food_id); + + const { count, error } = await supabase + .from("food_saves") + .select("*", { count: "exact", head: true }) + .eq("food_id", food_id); + + if (error) { + console.error('Error getting saves count:', error); + } else { + console.log('Saves count:', count); + } + + return { count, error }; +} + +export const getCommentsCount = async (food_id: string) => { + console.log('Getting comments count for food_id:', food_id); + + const { count, error } = await supabase + .from("food_comments") + .select("*", { count: "exact", head: true }) + .eq("food_id", food_id); + + if (error) { + console.error('Error getting comments count:', error); + } else { + console.log('Comments count:', count); + } + + return { count, error }; +} + +export const checkUserLiked = async (food_id: string, user_id: string) => { + console.log('Checking if user liked with food_id:', food_id, 'and user_id:', user_id); + + const { data, error } = await supabase + .from("food_likes") + .select("*") + .eq("food_id", food_id) + .eq("user_id", user_id) + .single(); + + if (error && error.code !== 'PGRST116') { // PGRST116 is the "no rows returned" error code + console.error('Error checking if user liked:', error); + } else { + console.log('User liked:', !!data); + } + + return { data, error: error && error.code === 'PGRST116' ? null : error }; +} + +export const checkUserSaved = async (food_id: string, user_id: string) => { + console.log('Checking if user saved with food_id:', food_id, 'and user_id:', user_id); + + const { data, error } = await supabase + .from("food_saves") + .select("*") + .eq("food_id", food_id) + .eq("user_id", user_id) + .single(); + + if (error && error.code !== 'PGRST116') { // PGRST116 is the "no rows returned" error code + console.error('Error checking if user saved:', error); + } else { + console.log('User saved:', !!data); + } + + return { data, error: error && error.code === 'PGRST116' ? null : error }; +} + 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(` @@ -39,6 +220,11 @@ export const getComments = async (food_id: string) => { .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))]; @@ -59,53 +245,11 @@ export const getComments = async (food_id: string) => { user: profileMap[comment.user_id] })); + console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`); return { data: commentsWithProfiles, error }; } } - return { data, error }; -} - -export const getLikesCount = async (food_id: string) => { - const { count, error } = await supabase - .from("food_likes") - .select("*", { count: "exact", head: true }) - .eq("food_id", food_id); - return { count, error }; -} - -export const getSavesCount = async (food_id: string) => { - const { count, error } = await supabase - .from("food_saves") - .select("*", { count: "exact", head: true }) - .eq("food_id", food_id); - return { count, error }; -} - -export const getCommentsCount = async (food_id: string) => { - const { count, error } = await supabase - .from("food_comments") - .select("*", { count: "exact", head: true }) - .eq("food_id", food_id); - return { count, error }; -} - -export const checkUserLiked = async (food_id: string, user_id: string) => { - const { data, error } = await supabase - .from("food_likes") - .select("*") - .eq("food_id", food_id) - .eq("user_id", user_id) - .single(); - return { data, error }; -} - -export const checkUserSaved = async (food_id: string, user_id: string) => { - const { data, error } = await supabase - .from("food_saves") - .select("*") - .eq("food_id", food_id) - .eq("user_id", user_id) - .single(); + console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`); return { data, error }; } \ No newline at end of file