mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 05:54:08 +01:00
fix: make can like in forum page
This commit is contained in:
parent
5212429fb1
commit
7190731cdc
@ -1,23 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, Alert } 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 { Feather, FontAwesome } from '@expo/vector-icons';
|
||||||
import { router } from 'expo-router';
|
import { router, useFocusEffect } from 'expo-router';
|
||||||
import { useAuth } from '../../context/auth-context';
|
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 { supabase } from '../../services/supabase';
|
||||||
|
import {
|
||||||
|
useFoods,
|
||||||
|
useFoodStats,
|
||||||
|
useFoodCreators,
|
||||||
|
useUserInteractions,
|
||||||
|
useLikeMutation,
|
||||||
|
useSaveMutation
|
||||||
|
} from '../../hooks/use-foods';
|
||||||
|
|
||||||
// Categories for filtering
|
// Categories for filtering
|
||||||
const categories = [
|
const categories = [
|
||||||
@ -37,13 +31,8 @@ export default function ForumScreen() {
|
|||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [foods, setFoods] = useState<Food[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('');
|
const [selectedCategory, setSelectedCategory] = useState('');
|
||||||
const [selectedSort, setSelectedSort] = useState('rating');
|
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
|
// Get current user ID from Supabase session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -61,169 +50,53 @@ export default function ForumScreen() {
|
|||||||
getCurrentUser();
|
getCurrentUser();
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
// Set up real-time subscription for likes and saves
|
// Use React Query hooks
|
||||||
useEffect(() => {
|
const {
|
||||||
if (!isAuthenticated) return;
|
data: foods = [],
|
||||||
|
isLoading: isLoadingFoods,
|
||||||
|
refetch: refetchFoods
|
||||||
|
} = useFoods(selectedCategory, searchQuery, selectedSort);
|
||||||
|
|
||||||
const likesSubscription = supabase
|
const foodIds = foods.map(food => food.id);
|
||||||
.channel('food_likes_changes')
|
|
||||||
.on('postgres_changes', {
|
|
||||||
event: '*',
|
|
||||||
schema: 'public',
|
|
||||||
table: 'food_likes'
|
|
||||||
}, () => {
|
|
||||||
// Refresh stats when changes occur
|
|
||||||
loadFoods();
|
|
||||||
})
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
const savesSubscription = supabase
|
const {
|
||||||
.channel('food_saves_changes')
|
data: foodStats = {},
|
||||||
.on('postgres_changes', {
|
isLoading: isLoadingStats
|
||||||
event: '*',
|
} = useFoodStats(foodIds);
|
||||||
schema: 'public',
|
|
||||||
table: 'food_saves'
|
|
||||||
}, () => {
|
|
||||||
// Refresh stats when changes occur
|
|
||||||
loadFoods();
|
|
||||||
})
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
return () => {
|
const creatorIds = foods
|
||||||
supabase.removeChannel(likesSubscription);
|
|
||||||
supabase.removeChannel(savesSubscription);
|
|
||||||
};
|
|
||||||
}, [isAuthenticated]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadFoods();
|
|
||||||
}, [selectedCategory, selectedSort, currentUserId]);
|
|
||||||
|
|
||||||
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)
|
.filter(food => food.created_by)
|
||||||
.map(food => food.created_by as string);
|
.map(food => food.created_by as string);
|
||||||
|
|
||||||
const uniqueCreatorIds = [...new Set(creatorIds)];
|
const {
|
||||||
|
data: foodCreators = {},
|
||||||
|
isLoading: isLoadingCreators
|
||||||
|
} = useFoodCreators(creatorIds);
|
||||||
|
|
||||||
const creatorProfiles: {[key: string]: Profile} = {};
|
const {
|
||||||
|
data: userInteractions = {},
|
||||||
|
isLoading: isLoadingInteractions
|
||||||
|
} = useUserInteractions(foodIds, currentUserId);
|
||||||
|
|
||||||
for (const creatorId of uniqueCreatorIds) {
|
const likeMutation = useLikeMutation();
|
||||||
const { data: profile } = await getProfile(creatorId);
|
const saveMutation = useSaveMutation();
|
||||||
if (profile) {
|
|
||||||
creatorProfiles[creatorId] = profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFoodCreators(creatorProfiles);
|
// Refetch data when the screen comes into focus
|
||||||
|
useFocusEffect(
|
||||||
// Check user interactions if authenticated
|
React.useCallback(() => {
|
||||||
if (isAuthenticated && currentUserId) {
|
refetchFoods();
|
||||||
const interactionsPromises = sortedData.map(async (food) => {
|
}, [refetchFoods])
|
||||||
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 handleSearch = (text: string) => {
|
const handleSearch = (text: string) => {
|
||||||
setSearchQuery(text);
|
setSearchQuery(text);
|
||||||
// Debounce search for better performance
|
|
||||||
setTimeout(() => {
|
|
||||||
loadFoods();
|
|
||||||
}, 500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToPostDetail = (food: Food) => {
|
const navigateToPostDetail = (food: { id: string }) => {
|
||||||
router.push(`/post-detail/${food.id}`);
|
router.push(`/post-detail/${food.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLike = async (food: Food) => {
|
const handleLike = async (food: { id: string }) => {
|
||||||
if (!isAuthenticated || !currentUserId) {
|
if (!isAuthenticated || !currentUserId) {
|
||||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||||
return;
|
return;
|
||||||
@ -232,77 +105,18 @@ const navigateToPostDetail = (food: Food) => {
|
|||||||
try {
|
try {
|
||||||
const isLiked = userInteractions[food.id]?.liked || false;
|
const isLiked = userInteractions[food.id]?.liked || false;
|
||||||
|
|
||||||
// Optimistically update UI
|
likeMutation.mutate({
|
||||||
setUserInteractions(prev => ({
|
foodId: food.id,
|
||||||
...prev,
|
userId: currentUserId,
|
||||||
[food.id]: {
|
isLiked
|
||||||
...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) {
|
} catch (error) {
|
||||||
console.error('Error toggling like:', error);
|
console.error('Error toggling like:', error);
|
||||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (food: Food) => {
|
const handleSave = async (food: { id: string }) => {
|
||||||
if (!isAuthenticated || !currentUserId) {
|
if (!isAuthenticated || !currentUserId) {
|
||||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||||
return;
|
return;
|
||||||
@ -311,77 +125,18 @@ const navigateToPostDetail = (food: Food) => {
|
|||||||
try {
|
try {
|
||||||
const isSaved = userInteractions[food.id]?.saved || false;
|
const isSaved = userInteractions[food.id]?.saved || false;
|
||||||
|
|
||||||
// Optimistically update UI
|
saveMutation.mutate({
|
||||||
setUserInteractions(prev => ({
|
foodId: food.id,
|
||||||
...prev,
|
userId: currentUserId,
|
||||||
[food.id]: {
|
isSaved
|
||||||
...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) {
|
} catch (error) {
|
||||||
console.error('Error toggling save:', error);
|
console.error('Error toggling save:', error);
|
||||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFoodItem = ({ item }: { item: Food }) => {
|
const renderFoodItem = ({ item }: { item: any }) => {
|
||||||
// Get stats for this food
|
// Get stats for this food
|
||||||
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
|
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 (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white">
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
@ -537,7 +294,7 @@ const navigateToPostDetail = (food: Food) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Food Posts */}
|
{/* Food Posts */}
|
||||||
{loading ? (
|
{isLoading ? (
|
||||||
<View className="flex-1 items-center justify-center">
|
<View className="flex-1 items-center justify-center">
|
||||||
<ActivityIndicator size="large" color="#ffd60a" />
|
<ActivityIndicator size="large" color="#ffd60a" />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -140,7 +140,7 @@ export default function ProfileScreen() {
|
|||||||
refetchMyRecipes()
|
refetchMyRecipes()
|
||||||
} else if (tab === "Likes") {
|
} else if (tab === "Likes") {
|
||||||
refetchLikes()
|
refetchLikes()
|
||||||
} else if (tab === "Bookmark") {
|
} else if (tab === "Bookmarks") {
|
||||||
refetchBookmarks()
|
refetchBookmarks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ export default function ProfileScreen() {
|
|||||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||||
case "Likes":
|
case "Likes":
|
||||||
return { data: likesData, isLoading: isLikesLoading, error: likesError }
|
return { data: likesData, isLoading: isLikesLoading, error: likesError }
|
||||||
case "Bookmark":
|
case "Bookmarks":
|
||||||
return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError }
|
return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError }
|
||||||
default:
|
default:
|
||||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||||
@ -334,7 +334,7 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<View className="flex-row justify-around py-3 border-b border-gray-200">
|
<View className="flex-row justify-around py-3 border-b border-gray-200">
|
||||||
{["My Recipes", "Likes", "Bookmark"].map((tab) => (
|
{["My Recipes", "Likes", "Bookmarks"].map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`py-2 px-4 ${activeTab === tab ? "border-b-2 border-[#333]" : ""}`}
|
className={`py-2 px-4 ${activeTab === tab ? "border-b-2 border-[#333]" : ""}`}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, Activity
|
|||||||
import { Feather, FontAwesome } from '@expo/vector-icons';
|
import { Feather, FontAwesome } from '@expo/vector-icons';
|
||||||
import { useLocalSearchParams, router } from 'expo-router';
|
import { useLocalSearchParams, router } from 'expo-router';
|
||||||
import { useAuth } from '../../context/auth-context';
|
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 { getFoods, getIngredients, getNutrients } from '../../services/data/foods';
|
||||||
import {
|
import {
|
||||||
createLike,
|
createLike,
|
||||||
@ -19,32 +21,20 @@ import {
|
|||||||
} from '../../services/data/forum';
|
} from '../../services/data/forum';
|
||||||
import { getProfile } from '../../services/data/profile';
|
import { getProfile } from '../../services/data/profile';
|
||||||
import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index';
|
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() {
|
export default function PostDetailScreen() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const foodId = typeof id === 'string' ? id : '';
|
const foodId = typeof id === 'string' ? id : '';
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
console.log('Post detail screen - Food ID:', foodId);
|
console.log('Post detail screen - Food ID:', foodId);
|
||||||
|
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||||
const [food, setFood] = useState<Food | null>(null);
|
|
||||||
const [foodCreator, setFoodCreator] = useState<Profile | null>(null);
|
|
||||||
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
|
||||||
const [nutrients, setNutrients] = useState<Nutrient | null>(null);
|
|
||||||
const [comments, setComments] = useState<FoodComment[]>([]);
|
|
||||||
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 [commentText, setCommentText] = useState('');
|
||||||
const [submittingComment, setSubmittingComment] = useState(false);
|
const [submittingComment, setSubmittingComment] = useState(false);
|
||||||
const [stats, setStats] = useState({
|
const [showReviews, setShowReviews] = useState(true);
|
||||||
likes: 0,
|
|
||||||
saves: 0,
|
|
||||||
comments: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get current user ID from Supabase session
|
// Get current user ID from Supabase session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -62,14 +52,128 @@ export default function PostDetailScreen() {
|
|||||||
getCurrentUser();
|
getCurrentUser();
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Fetch food details
|
||||||
if (foodId) {
|
const {
|
||||||
console.log('Loading food details for ID:', foodId);
|
data: food,
|
||||||
loadFoodDetails();
|
isLoading: isLoadingFood,
|
||||||
} else {
|
error: foodError
|
||||||
console.error('No food ID provided');
|
} = useQuery({
|
||||||
}
|
queryKey: queryKeys.foodDetails(foodId),
|
||||||
}, [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
|
// Set up real-time subscription for comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -84,167 +188,55 @@ export default function PostDetailScreen() {
|
|||||||
schema: 'public',
|
schema: 'public',
|
||||||
table: 'food_comments',
|
table: 'food_comments',
|
||||||
filter: `food_id=eq.${foodId}`
|
filter: `food_id=eq.${foodId}`
|
||||||
}, (payload) => {
|
}, () => {
|
||||||
console.log('Comment change detected:', payload);
|
console.log('Comment change detected, refreshing comments');
|
||||||
// Refresh comments when changes occur
|
refetchComments();
|
||||||
refreshComments();
|
refetchStats();
|
||||||
})
|
})
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(subscription);
|
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(() => {
|
useEffect(() => {
|
||||||
if (foodId && currentUserId && food) {
|
if (!foodId) return;
|
||||||
checkUserInteractions();
|
|
||||||
}
|
|
||||||
}, [currentUserId, foodId, food]);
|
|
||||||
|
|
||||||
const checkUserInteractions = async () => {
|
const likesSubscription = supabase
|
||||||
if (!currentUserId || !foodId) return;
|
.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();
|
||||||
|
|
||||||
try {
|
const savesSubscription = supabase
|
||||||
console.log('Checking user interactions with user ID:', currentUserId);
|
.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();
|
||||||
|
|
||||||
const [likedRes, savedRes] = await Promise.all([
|
return () => {
|
||||||
checkUserLiked(foodId, currentUserId),
|
supabase.removeChannel(likesSubscription);
|
||||||
checkUserSaved(foodId, currentUserId)
|
supabase.removeChannel(savesSubscription);
|
||||||
]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}, [foodId, refetchStats, refetchInteractions]);
|
||||||
|
|
||||||
const handleLike = async () => {
|
const handleLike = async () => {
|
||||||
if (!isAuthenticated || !currentUserId || !food) {
|
if (!isAuthenticated || !currentUserId || !food) {
|
||||||
@ -253,42 +245,13 @@ export default function PostDetailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Toggling like with user ID:', currentUserId, 'and food ID:', foodId);
|
likeMutation.mutate({
|
||||||
|
foodId,
|
||||||
// Optimistically update UI
|
userId: currentUserId,
|
||||||
setIsLiked(!isLiked);
|
isLiked: interactions.liked
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error toggling like:', 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.');
|
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -300,42 +263,13 @@ export default function PostDetailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Toggling save with user ID:', currentUserId, 'and food ID:', foodId);
|
saveMutation.mutate({
|
||||||
|
foodId,
|
||||||
// Optimistically update UI
|
userId: currentUserId,
|
||||||
setIsSaved(!isSaved);
|
isSaved: interactions.saved
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error toggling save:', 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.');
|
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -350,23 +284,11 @@ export default function PostDetailScreen() {
|
|||||||
|
|
||||||
setSubmittingComment(true);
|
setSubmittingComment(true);
|
||||||
try {
|
try {
|
||||||
console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', foodId);
|
await commentMutation.mutateAsync({
|
||||||
|
foodId,
|
||||||
const { error } = await createComment(foodId, currentUserId, commentText.trim());
|
userId: currentUserId,
|
||||||
|
content: 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) {
|
} catch (error) {
|
||||||
console.error('Error submitting comment:', error);
|
console.error('Error submitting comment:', error);
|
||||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||||
@ -375,7 +297,9 @@ export default function PostDetailScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white">
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
<View className="flex-1 items-center justify-center">
|
<View className="flex-1 items-center justify-center">
|
||||||
@ -385,7 +309,7 @@ export default function PostDetailScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!food) {
|
if (foodError || !food) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white">
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
<View className="flex-1 items-center justify-center">
|
<View className="flex-1 items-center justify-center">
|
||||||
@ -436,13 +360,13 @@ export default function PostDetailScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
||||||
<Text className="text-lg font-bold text-gray-600">
|
<Text className="text-lg font-bold text-gray-600">
|
||||||
{foodCreator?.username?.charAt(0).toUpperCase() || '?'}
|
{foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || '?'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text className="ml-3 text-lg font-bold">
|
<Text className="ml-3 text-lg font-bold">
|
||||||
{foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'}
|
{foodCreator?.username || foodCreator?.full_name || 'Chef'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-row items-center">
|
<View className="flex-row items-center">
|
||||||
@ -471,9 +395,13 @@ export default function PostDetailScreen() {
|
|||||||
|
|
||||||
{/* Interaction buttons */}
|
{/* Interaction buttons */}
|
||||||
<View className="flex-row justify-between px-4 py-4 border-b border-gray-200">
|
<View className="flex-row justify-between px-4 py-4 border-b border-gray-200">
|
||||||
<TouchableOpacity className="flex-row items-center">
|
|
||||||
<Feather name="message-square" size={22} color="#333" />
|
<TouchableOpacity
|
||||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
className="flex-row items-center"
|
||||||
|
onPress={handleLike}
|
||||||
|
>
|
||||||
|
<Feather name="heart" size={22} color={interactions.liked ? "#E91E63" : "#333"} />
|
||||||
|
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity className="flex-row items-center">
|
<TouchableOpacity className="flex-row items-center">
|
||||||
@ -481,16 +409,9 @@ export default function PostDetailScreen() {
|
|||||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
className="flex-row items-center"
|
|
||||||
onPress={handleLike}
|
|
||||||
>
|
|
||||||
<Feather name="heart" size={22} color={isLiked ? "#E91E63" : "#333"} />
|
|
||||||
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity onPress={handleSave}>
|
<TouchableOpacity onPress={handleSave}>
|
||||||
<Feather name="bookmark" size={22} color={isSaved ? "#ffd60a" : "#333"} />
|
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -518,14 +439,14 @@ export default function PostDetailScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
||||||
<Text className="text-base font-bold text-gray-600">
|
<Text className="text-base font-bold text-gray-600">
|
||||||
{comment.user?.username?.charAt(0).toUpperCase() || '?'}
|
{comment.user?.username?.charAt(0).toUpperCase() || comment.user_id?.charAt(0).toUpperCase() || '?'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-row items-center justify-between flex-1">
|
<View className="flex-row items-center justify-between flex-1">
|
||||||
<Text className="font-bold">
|
<Text className="font-bold">
|
||||||
{comment.user?.username || comment.user?.full_name || 'Unknown User'}
|
{comment.user?.username || comment.user?.full_name || 'User'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-gray-500 text-xs">
|
<Text className="text-gray-500 text-xs">
|
||||||
{new Date(comment.created_at).toLocaleDateString()}
|
{new Date(comment.created_at).toLocaleDateString()}
|
||||||
|
|||||||
287
hooks/use-foods.ts
Normal file
287
hooks/use-foods.ts
Normal file
@ -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<string, { likes: number, saves: number, comments: number }>);
|
||||||
|
},
|
||||||
|
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<string, Profile> = {};
|
||||||
|
|
||||||
|
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<string, { liked: boolean, saved: boolean }>);
|
||||||
|
},
|
||||||
|
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] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -208,6 +208,7 @@ export const checkUserSaved = async (food_id: string, user_id: string) => {
|
|||||||
export const getComments = async (food_id: string) => {
|
export const getComments = async (food_id: string) => {
|
||||||
console.log('Getting comments for food_id:', food_id);
|
console.log('Getting comments for food_id:', food_id);
|
||||||
|
|
||||||
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("food_comments")
|
.from("food_comments")
|
||||||
.select(`
|
.select(`
|
||||||
@ -233,7 +234,7 @@ export const getComments = async (food_id: string) => {
|
|||||||
const { data: profiles } = await getProfiles(userIds);
|
const { data: profiles } = await getProfiles(userIds);
|
||||||
|
|
||||||
// Add user profiles to comments
|
// Add user profiles to comments
|
||||||
if (profiles) {
|
if (profiles && profiles.length > 0) {
|
||||||
const profileMap = profiles.reduce((acc, profile) => {
|
const profileMap = profiles.reduce((acc, profile) => {
|
||||||
acc[profile.id] = profile;
|
acc[profile.id] = profile;
|
||||||
return acc;
|
return acc;
|
||||||
@ -242,14 +243,19 @@ export const getComments = async (food_id: string) => {
|
|||||||
// Attach profiles to comments
|
// Attach profiles to comments
|
||||||
const commentsWithProfiles = data.map(comment => ({
|
const commentsWithProfiles = data.map(comment => ({
|
||||||
...comment,
|
...comment,
|
||||||
user: profileMap[comment.user_id]
|
user: profileMap[comment.user_id] || null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`);
|
console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`);
|
||||||
return { data: commentsWithProfiles, error };
|
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}`);
|
console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`);
|
||||||
return { data, error };
|
return { data: data?.map(comment => ({ ...comment, user: null })) || [], error: null };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getComments:', error);
|
||||||
|
return { data: [], error };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
@ -64,55 +64,3 @@ export interface Profile {
|
|||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
website?: 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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user