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 { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, Alert } from 'react-native';
|
||||
import { Feather, FontAwesome, Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { Feather, FontAwesome } from '@expo/vector-icons';
|
||||
import { router, useFocusEffect } from 'expo-router';
|
||||
import { useAuth } from '../../context/auth-context';
|
||||
import { getFoods } from '../../services/data/foods';
|
||||
import {
|
||||
getLikesCount,
|
||||
getSavesCount,
|
||||
getCommentsCount,
|
||||
createLike,
|
||||
deleteLike,
|
||||
createSave,
|
||||
deleteSave,
|
||||
checkUserLiked,
|
||||
checkUserSaved
|
||||
} from '../../services/data/forum';
|
||||
import { getProfile } from '../../services/data/profile';
|
||||
import { Food, Profile } from '../../types/index';
|
||||
import { supabase } from '../../services/supabase';
|
||||
import {
|
||||
useFoods,
|
||||
useFoodStats,
|
||||
useFoodCreators,
|
||||
useUserInteractions,
|
||||
useLikeMutation,
|
||||
useSaveMutation
|
||||
} from '../../hooks/use-foods';
|
||||
|
||||
// Categories for filtering
|
||||
const categories = [
|
||||
@ -37,13 +31,8 @@ export default function ForumScreen() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [foods, setFoods] = useState<Food[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [selectedSort, setSelectedSort] = useState('rating');
|
||||
const [foodStats, setFoodStats] = useState<{[key: string]: {likes: number, saves: number, comments: number}}>({});
|
||||
const [foodCreators, setFoodCreators] = useState<{[key: string]: Profile}>({});
|
||||
const [userInteractions, setUserInteractions] = useState<{[key: string]: {liked: boolean, saved: boolean}}>({});
|
||||
|
||||
// Get current user ID from Supabase session
|
||||
useEffect(() => {
|
||||
@ -61,169 +50,53 @@ export default function ForumScreen() {
|
||||
getCurrentUser();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Set up real-time subscription for likes and saves
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
// Use React Query hooks
|
||||
const {
|
||||
data: foods = [],
|
||||
isLoading: isLoadingFoods,
|
||||
refetch: refetchFoods
|
||||
} = useFoods(selectedCategory, searchQuery, selectedSort);
|
||||
|
||||
const likesSubscription = supabase
|
||||
.channel('food_likes_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
const foodIds = foods.map(food => food.id);
|
||||
|
||||
const savesSubscription = supabase
|
||||
.channel('food_saves_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_saves'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
const {
|
||||
data: foodStats = {},
|
||||
isLoading: isLoadingStats
|
||||
} = useFoodStats(foodIds);
|
||||
|
||||
return () => {
|
||||
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
|
||||
const creatorIds = foods
|
||||
.filter(food => food.created_by)
|
||||
.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 { data: profile } = await getProfile(creatorId);
|
||||
if (profile) {
|
||||
creatorProfiles[creatorId] = profile;
|
||||
}
|
||||
}
|
||||
const likeMutation = useLikeMutation();
|
||||
const saveMutation = useSaveMutation();
|
||||
|
||||
setFoodCreators(creatorProfiles);
|
||||
|
||||
// Check user interactions if authenticated
|
||||
if (isAuthenticated && currentUserId) {
|
||||
const interactionsPromises = sortedData.map(async (food) => {
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(food.id, currentUserId),
|
||||
checkUserSaved(food.id, currentUserId)
|
||||
]);
|
||||
|
||||
return {
|
||||
foodId: food.id,
|
||||
liked: !!likedRes.data,
|
||||
saved: !!savedRes.data
|
||||
};
|
||||
});
|
||||
|
||||
const interactions = await Promise.all(interactionsPromises);
|
||||
const interactionsMap = interactions.reduce((acc, interaction) => {
|
||||
acc[interaction.foodId] = {
|
||||
liked: interaction.liked,
|
||||
saved: interaction.saved
|
||||
};
|
||||
return acc;
|
||||
}, {} as {[key: string]: {liked: boolean, saved: boolean}});
|
||||
|
||||
setUserInteractions(interactionsMap);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
// Refetch data when the screen comes into focus
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
refetchFoods();
|
||||
}, [refetchFoods])
|
||||
);
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
setSearchQuery(text);
|
||||
// Debounce search for better performance
|
||||
setTimeout(() => {
|
||||
loadFoods();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const navigateToPostDetail = (food: Food) => {
|
||||
const navigateToPostDetail = (food: { id: string }) => {
|
||||
router.push(`/post-detail/${food.id}`);
|
||||
};
|
||||
};
|
||||
|
||||
const handleLike = async (food: Food) => {
|
||||
const handleLike = async (food: { id: string }) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||
return;
|
||||
@ -232,77 +105,18 @@ const navigateToPostDetail = (food: Food) => {
|
||||
try {
|
||||
const isLiked = userInteractions[food.id]?.liked || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: !isLiked
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: isLiked ? Math.max(0, prev[food.id].likes - 1) : prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: Math.max(0, prev[food.id].likes - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
likeMutation.mutate({
|
||||
foodId: food.id,
|
||||
userId: currentUserId,
|
||||
isLiked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (food: Food) => {
|
||||
const handleSave = async (food: { id: string }) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||
return;
|
||||
@ -311,77 +125,18 @@ const navigateToPostDetail = (food: Food) => {
|
||||
try {
|
||||
const isSaved = userInteractions[food.id]?.saved || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: !isSaved
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: isSaved ? Math.max(0, prev[food.id].saves - 1) : prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: Math.max(0, prev[food.id].saves - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
saveMutation.mutate({
|
||||
foodId: food.id,
|
||||
userId: currentUserId,
|
||||
isSaved
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const renderFoodItem = ({ item }: { item: Food }) => {
|
||||
const renderFoodItem = ({ item }: { item: any }) => {
|
||||
// Get stats for this food
|
||||
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
|
||||
|
||||
@ -484,6 +239,8 @@ const navigateToPostDetail = (food: Food) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingFoods || isLoadingStats || isLoadingCreators || isLoadingInteractions;
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
{/* Search Bar */}
|
||||
@ -537,7 +294,7 @@ const navigateToPostDetail = (food: Food) => {
|
||||
</View>
|
||||
|
||||
{/* Food Posts */}
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#ffd60a" />
|
||||
</View>
|
||||
|
||||
@ -140,7 +140,7 @@ export default function ProfileScreen() {
|
||||
refetchMyRecipes()
|
||||
} else if (tab === "Likes") {
|
||||
refetchLikes()
|
||||
} else if (tab === "Bookmark") {
|
||||
} else if (tab === "Bookmarks") {
|
||||
refetchBookmarks()
|
||||
}
|
||||
}
|
||||
@ -226,7 +226,7 @@ export default function ProfileScreen() {
|
||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||
case "Likes":
|
||||
return { data: likesData, isLoading: isLikesLoading, error: likesError }
|
||||
case "Bookmark":
|
||||
case "Bookmarks":
|
||||
return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError }
|
||||
default:
|
||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||
@ -334,7 +334,7 @@ export default function ProfileScreen() {
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<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
|
||||
key={tab}
|
||||
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 { useLocalSearchParams, router } from 'expo-router';
|
||||
import { useAuth } from '../../context/auth-context';
|
||||
import { supabase } from '../../services/supabase';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getFoods, getIngredients, getNutrients } from '../../services/data/foods';
|
||||
import {
|
||||
createLike,
|
||||
@ -19,32 +21,20 @@ import {
|
||||
} from '../../services/data/forum';
|
||||
import { getProfile } from '../../services/data/profile';
|
||||
import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index';
|
||||
import { supabase } from '../../services/supabase';
|
||||
import { queryKeys, useLikeMutation, useSaveMutation } from '../../hooks/use-foods';
|
||||
|
||||
export default function PostDetailScreen() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const foodId = typeof id === 'string' ? id : '';
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
console.log('Post detail screen - Food ID:', foodId);
|
||||
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [currentUserId, setCurrentUserId] = useState<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 [submittingComment, setSubmittingComment] = useState(false);
|
||||
const [stats, setStats] = useState({
|
||||
likes: 0,
|
||||
saves: 0,
|
||||
comments: 0
|
||||
});
|
||||
const [showReviews, setShowReviews] = useState(true);
|
||||
|
||||
// Get current user ID from Supabase session
|
||||
useEffect(() => {
|
||||
@ -62,14 +52,128 @@ export default function PostDetailScreen() {
|
||||
getCurrentUser();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (foodId) {
|
||||
console.log('Loading food details for ID:', foodId);
|
||||
loadFoodDetails();
|
||||
} else {
|
||||
console.error('No food ID provided');
|
||||
}
|
||||
}, [foodId]);
|
||||
// Fetch food details
|
||||
const {
|
||||
data: food,
|
||||
isLoading: isLoadingFood,
|
||||
error: foodError
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.foodDetails(foodId),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('foods')
|
||||
.select('*')
|
||||
.eq('id', foodId)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
...data,
|
||||
description: data.description || '',
|
||||
ingredient_count: data.ingredient_count ?? 0,
|
||||
calories: data.calories ?? 0,
|
||||
image_url: data.image_url || '',
|
||||
};
|
||||
},
|
||||
enabled: !!foodId,
|
||||
});
|
||||
|
||||
// Fetch food creator
|
||||
const {
|
||||
data: foodCreator,
|
||||
isLoading: isLoadingCreator
|
||||
} = useQuery({
|
||||
queryKey: ['food-creator', food?.created_by],
|
||||
queryFn: async () => {
|
||||
if (!food?.created_by) return null;
|
||||
|
||||
const { data, error } = await getProfile(food.created_by);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: !!food?.created_by,
|
||||
});
|
||||
|
||||
// Fetch food stats
|
||||
const {
|
||||
data: stats = { likes: 0, saves: 0, comments: 0 },
|
||||
isLoading: isLoadingStats,
|
||||
refetch: refetchStats
|
||||
} = useQuery({
|
||||
queryKey: ['food-stats', foodId],
|
||||
queryFn: async () => {
|
||||
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||
getLikesCount(foodId),
|
||||
getSavesCount(foodId),
|
||||
getCommentsCount(foodId)
|
||||
]);
|
||||
|
||||
return {
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
};
|
||||
},
|
||||
enabled: !!foodId,
|
||||
});
|
||||
|
||||
// Fetch user interactions
|
||||
const {
|
||||
data: interactions = { liked: false, saved: false },
|
||||
isLoading: isLoadingInteractions,
|
||||
refetch: refetchInteractions
|
||||
} = useQuery({
|
||||
queryKey: ['user-interactions', foodId, currentUserId],
|
||||
queryFn: async () => {
|
||||
if (!currentUserId) return { liked: false, saved: false };
|
||||
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(foodId, currentUserId),
|
||||
checkUserSaved(foodId, currentUserId)
|
||||
]);
|
||||
|
||||
return {
|
||||
liked: !!likedRes.data,
|
||||
saved: !!savedRes.data
|
||||
};
|
||||
},
|
||||
enabled: !!foodId && !!currentUserId,
|
||||
});
|
||||
|
||||
// Fetch comments
|
||||
const {
|
||||
data: comments = [],
|
||||
isLoading: isLoadingComments,
|
||||
refetch: refetchComments
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.foodComments(foodId),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await getComments(foodId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return data || [];
|
||||
},
|
||||
enabled: !!foodId,
|
||||
});
|
||||
|
||||
// Set up mutations
|
||||
const likeMutation = useLikeMutation();
|
||||
const saveMutation = useSaveMutation();
|
||||
|
||||
const commentMutation = useMutation({
|
||||
mutationFn: async ({ foodId, userId, content }: { foodId: string, userId: string, content: string }) => {
|
||||
return createComment(foodId, userId, content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.foodComments(foodId) });
|
||||
queryClient.invalidateQueries({ queryKey: ['food-stats', foodId] });
|
||||
setCommentText('');
|
||||
},
|
||||
});
|
||||
|
||||
// Set up real-time subscription for comments
|
||||
useEffect(() => {
|
||||
@ -84,167 +188,55 @@ export default function PostDetailScreen() {
|
||||
schema: 'public',
|
||||
table: 'food_comments',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, (payload) => {
|
||||
console.log('Comment change detected:', payload);
|
||||
// Refresh comments when changes occur
|
||||
refreshComments();
|
||||
}, () => {
|
||||
console.log('Comment change detected, refreshing comments');
|
||||
refetchComments();
|
||||
refetchStats();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(subscription);
|
||||
};
|
||||
}, [foodId]);
|
||||
}, [foodId, refetchComments, refetchStats]);
|
||||
|
||||
// Check if user has liked/saved when user ID changes
|
||||
// Set up real-time subscription for likes and saves
|
||||
useEffect(() => {
|
||||
if (foodId && currentUserId && food) {
|
||||
checkUserInteractions();
|
||||
}
|
||||
}, [currentUserId, foodId, food]);
|
||||
if (!foodId) return;
|
||||
|
||||
const checkUserInteractions = async () => {
|
||||
if (!currentUserId || !foodId) return;
|
||||
const likesSubscription = supabase
|
||||
.channel(`food_likes:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
console.log('Like change detected, refreshing stats and interactions');
|
||||
refetchStats();
|
||||
refetchInteractions();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
try {
|
||||
console.log('Checking user interactions with user ID:', currentUserId);
|
||||
const savesSubscription = supabase
|
||||
.channel(`food_saves:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_saves',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
console.log('Save change detected, refreshing stats and interactions');
|
||||
refetchStats();
|
||||
refetchInteractions();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
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);
|
||||
}
|
||||
return () => {
|
||||
supabase.removeChannel(likesSubscription);
|
||||
supabase.removeChannel(savesSubscription);
|
||||
};
|
||||
}, [foodId, refetchStats, refetchInteractions]);
|
||||
|
||||
const handleLike = async () => {
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
@ -253,42 +245,13 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling like with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(true);
|
||||
setStats(prev => ({ ...prev, likes: prev.likes + 1 }));
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(false);
|
||||
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
likeMutation.mutate({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
isLiked: interactions.liked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: !isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
@ -300,42 +263,13 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling save with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(true);
|
||||
setStats(prev => ({ ...prev, saves: prev.saves + 1 }));
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(false);
|
||||
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
saveMutation.mutate({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
isSaved: interactions.saved
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: !isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
@ -350,23 +284,11 @@ export default function PostDetailScreen() {
|
||||
|
||||
setSubmittingComment(true);
|
||||
try {
|
||||
console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
const { error } = await createComment(foodId, currentUserId, commentText.trim());
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear comment text
|
||||
setCommentText('');
|
||||
|
||||
// Refresh comments
|
||||
await refreshComments();
|
||||
|
||||
console.log('Comment submitted successfully');
|
||||
await commentMutation.mutateAsync({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
content: commentText.trim()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
@ -375,7 +297,9 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<View className="flex-1 items-center justify-center">
|
||||
@ -385,7 +309,7 @@ export default function PostDetailScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!food) {
|
||||
if (foodError || !food) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<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">
|
||||
<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>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">
|
||||
{foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'}
|
||||
{foodCreator?.username || foodCreator?.full_name || 'Chef'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
@ -471,9 +395,13 @@ export default function PostDetailScreen() {
|
||||
|
||||
{/* Interaction buttons */}
|
||||
<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" />
|
||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
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 className="flex-row items-center">
|
||||
@ -481,16 +409,9 @@ export default function PostDetailScreen() {
|
||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||
</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}>
|
||||
<Feather name="bookmark" size={22} color={isSaved ? "#ffd60a" : "#333"} />
|
||||
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -518,14 +439,14 @@ export default function PostDetailScreen() {
|
||||
) : (
|
||||
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
||||
<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>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-row items-center justify-between flex-1">
|
||||
<Text className="font-bold">
|
||||
{comment.user?.username || comment.user?.full_name || 'Unknown User'}
|
||||
{comment.user?.username || comment.user?.full_name || 'User'}
|
||||
</Text>
|
||||
<Text className="text-gray-500 text-xs">
|
||||
{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) => {
|
||||
console.log('Getting comments for food_id:', food_id);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select(`
|
||||
@ -233,7 +234,7 @@ export const getComments = async (food_id: string) => {
|
||||
const { data: profiles } = await getProfiles(userIds);
|
||||
|
||||
// Add user profiles to comments
|
||||
if (profiles) {
|
||||
if (profiles && profiles.length > 0) {
|
||||
const profileMap = profiles.reduce((acc, profile) => {
|
||||
acc[profile.id] = profile;
|
||||
return acc;
|
||||
@ -242,14 +243,19 @@ export const getComments = async (food_id: string) => {
|
||||
// Attach profiles to comments
|
||||
const commentsWithProfiles = data.map(comment => ({
|
||||
...comment,
|
||||
user: profileMap[comment.user_id]
|
||||
user: profileMap[comment.user_id] || null
|
||||
}));
|
||||
|
||||
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}`);
|
||||
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;
|
||||
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