mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 05:54:08 +01:00
create forum id
This commit is contained in:
parent
a4197987ed
commit
d35ae859e4
@ -1,11 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, Alert } from 'react-native';
|
||||
import { Feather, FontAwesome, Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { useAuth } from '../../context/auth-context';
|
||||
import { getFoods } from '../../services/data/foods';
|
||||
import { getLikesCount, getSavesCount, getCommentsCount } from '../../services/data/forum';
|
||||
import { Food } from '../../types/index';
|
||||
import {
|
||||
getLikesCount,
|
||||
getSavesCount,
|
||||
getCommentsCount,
|
||||
createLike,
|
||||
deleteLike,
|
||||
createSave,
|
||||
deleteSave,
|
||||
checkUserLiked,
|
||||
checkUserSaved
|
||||
} from '../../services/data/forum';
|
||||
import { getProfile } from '../../services/data/profile';
|
||||
import { Food, Profile } from '../../types/index';
|
||||
import { supabase } from '../../services/supabase';
|
||||
|
||||
// Categories for filtering
|
||||
const categories = [
|
||||
@ -23,16 +35,69 @@ const sortOptions = [
|
||||
|
||||
export default function ForumScreen() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [currentUserId, setCurrentUserId] = useState<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(() => {
|
||||
async function getCurrentUser() {
|
||||
if (isAuthenticated) {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const userId = data.session?.user?.id;
|
||||
console.log('Current user ID:', userId);
|
||||
setCurrentUserId(userId || null);
|
||||
} else {
|
||||
setCurrentUserId(null);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Set up real-time subscription for likes and saves
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
const likesSubscription = supabase
|
||||
.channel('food_likes_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
const savesSubscription = supabase
|
||||
.channel('food_saves_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_saves'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(likesSubscription);
|
||||
supabase.removeChannel(savesSubscription);
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
loadFoods();
|
||||
}, [selectedCategory, selectedSort]);
|
||||
}, [selectedCategory, selectedSort, currentUserId]);
|
||||
|
||||
const loadFoods = async () => {
|
||||
setLoading(true);
|
||||
@ -93,6 +158,51 @@ export default function ForumScreen() {
|
||||
}, {} as {[key: string]: {likes: number, saves: number, comments: number}});
|
||||
|
||||
setFoodStats(statsMap);
|
||||
|
||||
// Load creator profiles
|
||||
const creatorIds = sortedData
|
||||
.filter(food => food.created_by)
|
||||
.map(food => food.created_by as string);
|
||||
|
||||
const uniqueCreatorIds = [...new Set(creatorIds)];
|
||||
|
||||
const creatorProfiles: {[key: string]: Profile} = {};
|
||||
|
||||
for (const creatorId of uniqueCreatorIds) {
|
||||
const { data: profile } = await getProfile(creatorId);
|
||||
if (profile) {
|
||||
creatorProfiles[creatorId] = profile;
|
||||
}
|
||||
}
|
||||
|
||||
setFoodCreators(creatorProfiles);
|
||||
|
||||
// Check user interactions if authenticated
|
||||
if (isAuthenticated && currentUserId) {
|
||||
const interactionsPromises = sortedData.map(async (food) => {
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(food.id, currentUserId),
|
||||
checkUserSaved(food.id, currentUserId)
|
||||
]);
|
||||
|
||||
return {
|
||||
foodId: food.id,
|
||||
liked: !!likedRes.data,
|
||||
saved: !!savedRes.data
|
||||
};
|
||||
});
|
||||
|
||||
const interactions = await Promise.all(interactionsPromises);
|
||||
const interactionsMap = interactions.reduce((acc, interaction) => {
|
||||
acc[interaction.foodId] = {
|
||||
liked: interaction.liked,
|
||||
saved: interaction.saved
|
||||
};
|
||||
return acc;
|
||||
}, {} as {[key: string]: {liked: boolean, saved: boolean}});
|
||||
|
||||
setUserInteractions(interactionsMap);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@ -109,24 +219,181 @@ export default function ForumScreen() {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const navigateToPostDetail = (food: Food) => {
|
||||
router.push({
|
||||
pathname: '/post-detail',
|
||||
params: { id: food.id }
|
||||
});
|
||||
const navigateToPostDetail = (food: Food) => {
|
||||
router.push(`/post-detail/${food.id}`);
|
||||
};
|
||||
|
||||
const handleLike = async (food: Food) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isLiked = userInteractions[food.id]?.liked || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: !isLiked
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: isLiked ? Math.max(0, prev[food.id].likes - 1) : prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: Math.max(0, prev[food.id].likes - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (food: Food) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isSaved = userInteractions[food.id]?.saved || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: !isSaved
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: isSaved ? Math.max(0, prev[food.id].saves - 1) : prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: Math.max(0, prev[food.id].saves - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const renderFoodItem = ({ item }: { item: Food }) => {
|
||||
// Get stats for this food
|
||||
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
|
||||
|
||||
// Mock data for UI elements not in the Food type
|
||||
const username = 'Mr. Chef';
|
||||
const rating = 4.2;
|
||||
// Get creator profile
|
||||
const creator = item.created_by ? foodCreators[item.created_by] : null;
|
||||
|
||||
// Get user interactions
|
||||
const interactions = userInteractions[item.id] || { liked: false, saved: false };
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="mb-6 bg-white rounded-lg overflow-hidden"
|
||||
className="mb-6 bg-white rounded-lg overflow-hidden shadow-sm"
|
||||
onPress={() => navigateToPostDetail(item)}
|
||||
>
|
||||
<View className="p-4">
|
||||
@ -134,15 +401,25 @@ export default function ForumScreen() {
|
||||
<View className="flex-row justify-between items-center mb-4">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-12 h-12 bg-gray-200 rounded-full overflow-hidden">
|
||||
{creator?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: "/placeholder.svg?height=48&width=48&query=user avatar" }}
|
||||
source={{ uri: creator.avatar_url }}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<View className="w-full h-full bg-gray-300 items-center justify-center">
|
||||
<Text className="text-base font-bold text-gray-600">
|
||||
{creator?.username?.charAt(0).toUpperCase() || '?'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">{username}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">
|
||||
{creator?.username || creator?.full_name || 'Unknown Chef'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-lg font-bold mr-1">{rating}</Text>
|
||||
<Text className="text-lg font-bold mr-1">4.2</Text>
|
||||
<FontAwesome name="star" size={20} color="#ffd60a" />
|
||||
</View>
|
||||
</View>
|
||||
@ -165,24 +442,41 @@ export default function ForumScreen() {
|
||||
{/* Interaction buttons */}
|
||||
<View className="flex-row justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<TouchableOpacity className="flex-row items-center mr-6">
|
||||
<Feather name="message-square" size={22} color="#333" />
|
||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center mr-6"
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
handleLike(item);
|
||||
}}
|
||||
>
|
||||
<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 mr-6">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center mr-6"
|
||||
onPress={() => navigateToPostDetail(item)}
|
||||
>
|
||||
<Feather name="message-circle" size={22} color="#333" />
|
||||
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Feather name="heart" size={22} color="#333" />
|
||||
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity>
|
||||
<Feather name="bookmark" size={22} color="#333" />
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSave(item);
|
||||
}}
|
||||
>
|
||||
<Feather
|
||||
name="bookmark"
|
||||
size={22}
|
||||
color={interactions.saved ? "#ffd60a" : "#333"}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
||||
import { Feather, FontAwesome } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { useAuth } from '../context/auth-context';
|
||||
@ -17,13 +17,17 @@ import {
|
||||
checkUserLiked,
|
||||
checkUserSaved
|
||||
} from '../services/data/forum';
|
||||
import { Food, Ingredient, Nutrient, FoodComment } from '../types/index';
|
||||
import { getProfile } from '../services/data/profile';
|
||||
import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../types/index';
|
||||
import { supabase } from '../services/supabase';
|
||||
|
||||
export default function PostDetailScreen() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const authContext = useAuth();
|
||||
const { isAuthenticated } = authContext || {}; // Adjust based on the actual structure of AuthContextType
|
||||
const foodId = Array.isArray(id) ? id[0] : id;
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [currentUserId, setCurrentUserId] = useState<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[]>([]);
|
||||
@ -39,19 +43,137 @@ export default function PostDetailScreen() {
|
||||
comments: 0
|
||||
});
|
||||
|
||||
// Mock data for UI elements
|
||||
const username = 'Mr. Chef';
|
||||
const rating = 4.2;
|
||||
// Get current user ID from Supabase session
|
||||
useEffect(() => {
|
||||
async function getCurrentUser() {
|
||||
if (isAuthenticated) {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const userId = data.session?.user?.id;
|
||||
console.log('Current user ID:', userId);
|
||||
setCurrentUserId(userId || null);
|
||||
} else {
|
||||
setCurrentUserId(null);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
if (foodId) {
|
||||
loadFoodDetails();
|
||||
}
|
||||
}, [id]);
|
||||
}, [foodId]);
|
||||
|
||||
// Set up real-time subscription for comments
|
||||
useEffect(() => {
|
||||
if (!foodId) return;
|
||||
|
||||
const subscription = supabase
|
||||
.channel(`food_comments:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_comments',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
// Refresh comments when changes occur
|
||||
refreshComments();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(subscription);
|
||||
};
|
||||
}, [foodId]);
|
||||
|
||||
// Set up real-time subscription for likes
|
||||
useEffect(() => {
|
||||
if (!foodId) return;
|
||||
|
||||
const subscription = supabase
|
||||
.channel(`food_likes:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
// Refresh likes when changes occur
|
||||
refreshLikes();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(subscription);
|
||||
};
|
||||
}, [foodId]);
|
||||
|
||||
// Check if user has liked/saved when user ID changes
|
||||
useEffect(() => {
|
||||
if (foodId && currentUserId && food) {
|
||||
checkUserInteractions();
|
||||
}
|
||||
}, [currentUserId, foodId, food]);
|
||||
|
||||
const checkUserInteractions = async () => {
|
||||
if (!currentUserId || !foodId) return;
|
||||
|
||||
try {
|
||||
console.log('Checking user interactions with user ID:', currentUserId);
|
||||
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(foodId, currentUserId),
|
||||
checkUserSaved(foodId, currentUserId)
|
||||
]);
|
||||
|
||||
console.log('User liked:', !!likedRes.data);
|
||||
console.log('User saved:', !!savedRes.data);
|
||||
|
||||
setIsLiked(!!likedRes.data);
|
||||
setIsSaved(!!savedRes.data);
|
||||
} catch (error) {
|
||||
console.error('Error checking user interactions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshComments = async () => {
|
||||
if (!foodId) return;
|
||||
|
||||
try {
|
||||
const { data: commentsData } = await getComments(foodId);
|
||||
if (commentsData) {
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
const { count } = await getCommentsCount(foodId);
|
||||
setStats(prev => ({ ...prev, comments: count || 0 }));
|
||||
} catch (error) {
|
||||
console.error('Error refreshing comments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshLikes = async () => {
|
||||
if (!foodId || !currentUserId) return;
|
||||
|
||||
try {
|
||||
const { count } = await getLikesCount(foodId);
|
||||
setStats(prev => ({ ...prev, likes: count || 0 }));
|
||||
|
||||
const { data } = await checkUserLiked(foodId, currentUserId);
|
||||
setIsLiked(!!data);
|
||||
} catch (error) {
|
||||
console.error('Error refreshing likes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFoodDetails = async () => {
|
||||
if (!foodId) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('Loading food details for ID:', foodId);
|
||||
|
||||
// Get food details
|
||||
const { data: foodData, error: foodError } = await getFoods(undefined, undefined, undefined, 1, 0);
|
||||
|
||||
@ -61,128 +183,199 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
|
||||
if (foodData && foodData.length > 0) {
|
||||
setFood({
|
||||
const foodItem = {
|
||||
...foodData[0],
|
||||
description: foodData[0].description || '', // Ensure description is always a string
|
||||
ingredient_count: foodData[0].ingredient_count ?? 0, // Provide default value for ingredient_count
|
||||
calories: foodData[0].calories ?? 0, // Provide default value for calories
|
||||
image_url: foodData[0].image_url || '', // Provide default value for image_url
|
||||
});
|
||||
description: foodData[0].description || '',
|
||||
ingredient_count: foodData[0].ingredient_count ?? 0,
|
||||
calories: foodData[0].calories ?? 0,
|
||||
image_url: foodData[0].image_url || '',
|
||||
};
|
||||
|
||||
console.log('Food loaded:', foodItem.name);
|
||||
setFood(foodItem);
|
||||
|
||||
// Get food creator profile
|
||||
if (foodItem.created_by) {
|
||||
console.log('Loading creator profile for:', foodItem.created_by);
|
||||
const { data: creatorProfile } = await getProfile(foodItem.created_by);
|
||||
if (creatorProfile) {
|
||||
setFoodCreator(creatorProfile);
|
||||
}
|
||||
}
|
||||
|
||||
// Get ingredients
|
||||
const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodData[0].id);
|
||||
const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodItem.id);
|
||||
|
||||
if (!ingredientsError && ingredientsData) {
|
||||
setIngredients(ingredientsData);
|
||||
}
|
||||
|
||||
// Get nutrients
|
||||
const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodData[0].id);
|
||||
const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodItem.id);
|
||||
|
||||
if (!nutrientsError && nutrientsData) {
|
||||
setNutrients(nutrientsData);
|
||||
}
|
||||
|
||||
// Get comments
|
||||
const { data: commentsData, error: commentsError } = await getComments(foodData[0].id);
|
||||
const { data: commentsData, error: commentsError } = await getComments(foodItem.id);
|
||||
|
||||
if (!commentsError && commentsData) {
|
||||
console.log('Comments loaded:', commentsData.length);
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
// Get stats
|
||||
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||
getLikesCount(foodData[0].id),
|
||||
getSavesCount(foodData[0].id),
|
||||
getCommentsCount(foodData[0].id)
|
||||
getLikesCount(foodItem.id),
|
||||
getSavesCount(foodItem.id),
|
||||
getCommentsCount(foodItem.id)
|
||||
]);
|
||||
|
||||
console.log('Stats loaded:', {
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
|
||||
setStats({
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
|
||||
// Check if user has liked/saved
|
||||
if (isAuthenticated) {
|
||||
const userId = 'current-user-id'; // Replace with actual user ID
|
||||
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(foodData[0].id, userId),
|
||||
checkUserSaved(foodData[0].id, userId)
|
||||
]);
|
||||
|
||||
setIsLiked(!!likedRes.data);
|
||||
setIsSaved(!!savedRes.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error('Error loading food details:', error);
|
||||
Alert.alert('Error', 'Failed to load food details. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLike = async () => {
|
||||
if (!authContext.isAuthenticated || !food) return;
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = 'current-user-id'; // Replace with actual user ID
|
||||
console.log('Toggling like with user ID:', currentUserId, 'and food ID:', food.id);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
await deleteLike(food.id, userId);
|
||||
setIsLiked(false);
|
||||
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||
} else {
|
||||
await createLike(food.id, userId);
|
||||
const { error } = await deleteLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(true);
|
||||
setStats(prev => ({ ...prev, likes: prev.likes + 1 }));
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(false);
|
||||
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: !isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!authContext.isAuthenticated || !food) return;
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = 'current-user-id'; // Replace with actual user ID
|
||||
console.log('Toggling save with user ID:', currentUserId, 'and food ID:', food.id);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
await deleteSave(food.id, userId);
|
||||
setIsSaved(false);
|
||||
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||
} else {
|
||||
await createSave(food.id, userId);
|
||||
const { error } = await deleteSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(true);
|
||||
setStats(prev => ({ ...prev, saves: prev.saves + 1 }));
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(false);
|
||||
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: !isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitComment = async () => {
|
||||
if (!authContext.isAuthenticated || !food || !commentText.trim()) return;
|
||||
if (!isAuthenticated || !currentUserId || !food || !commentText.trim()) {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to comment.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingComment(true);
|
||||
try {
|
||||
const userId = 'current-user-id'; // Replace with actual user ID
|
||||
console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', food.id);
|
||||
|
||||
await createComment(food.id, userId, commentText.trim());
|
||||
const { error } = await createComment(food.id, currentUserId, commentText.trim());
|
||||
|
||||
// Refresh comments
|
||||
const { data: commentsData } = await getComments(food.id);
|
||||
|
||||
if (commentsData) {
|
||||
setComments(commentsData);
|
||||
setStats(prev => ({ ...prev, comments: prev.comments + 1 }));
|
||||
if (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear comment text
|
||||
setCommentText('');
|
||||
|
||||
// Refresh comments
|
||||
await refreshComments();
|
||||
|
||||
console.log('Comment submitted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
} finally {
|
||||
setSubmittingComment(false);
|
||||
}
|
||||
@ -241,15 +434,25 @@ export default function PostDetailScreen() {
|
||||
<View className="flex-row justify-between items-center px-4 py-3">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-12 h-12 bg-gray-200 rounded-full overflow-hidden">
|
||||
{foodCreator?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: "/placeholder.svg?height=48&width=48&query=user avatar" }}
|
||||
source={{ uri: foodCreator.avatar_url }}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<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() || '?'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">{username}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">
|
||||
{foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-lg font-bold mr-1">{rating}</Text>
|
||||
<Text className="text-lg font-bold mr-1">4.2</Text>
|
||||
<FontAwesome name="star" size={20} color="#ffd60a" />
|
||||
</View>
|
||||
</View>
|
||||
@ -267,7 +470,9 @@ export default function PostDetailScreen() {
|
||||
<View className="px-4 mb-2">
|
||||
<Text className="text-2xl font-bold mb-2">{food.name}</Text>
|
||||
<Text className="text-gray-700 mb-2">{food.description}</Text>
|
||||
<Text className="text-gray-500 text-sm">09:41 - 4/3/25</Text>
|
||||
<Text className="text-gray-500 text-sm">
|
||||
{new Date(food.created_at).toLocaleDateString()} - {new Date(food.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Interaction buttons */}
|
||||
@ -286,12 +491,12 @@ export default function PostDetailScreen() {
|
||||
className="flex-row items-center"
|
||||
onPress={handleLike}
|
||||
>
|
||||
<Feather name={isLiked ? "heart" : "heart"} size={22} color={isLiked ? "#E91E63" : "#333"} />
|
||||
<Feather name="heart" size={22} color={isLiked ? "#E91E63" : "#333"} />
|
||||
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={handleSave}>
|
||||
<Feather name={isSaved ? "bookmark" : "bookmark"} size={22} color={isSaved ? "#ffd60a" : "#333"} />
|
||||
<Feather name="bookmark" size={22} color={isSaved ? "#ffd60a" : "#333"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -311,13 +516,23 @@ export default function PostDetailScreen() {
|
||||
<View key={comment.id} className="py-4 border-b border-gray-100">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<View className="w-10 h-10 bg-gray-200 rounded-full overflow-hidden mr-3">
|
||||
{comment.user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: "/placeholder.svg?height=40&width=40&query=user avatar" }}
|
||||
source={{ uri: comment.user.avatar_url }}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<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() || '?'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-row items-center justify-between flex-1">
|
||||
<Text className="font-bold">{comment.user_id}</Text>
|
||||
<Text className="font-bold">
|
||||
{comment.user?.username || comment.user?.full_name || 'Unknown User'}
|
||||
</Text>
|
||||
<Text className="text-gray-500 text-xs">
|
||||
{new Date(comment.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
@ -347,13 +562,18 @@ export default function PostDetailScreen() {
|
||||
multiline
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="bg-[#ffd60a] p-2 rounded-full"
|
||||
className={`p-2 rounded-full ${commentText.trim() && isAuthenticated ? 'bg-[#ffd60a]' : 'bg-gray-300'}`}
|
||||
onPress={handleSubmitComment}
|
||||
disabled={submittingComment || !commentText.trim()}
|
||||
disabled={submittingComment || !commentText.trim() || !isAuthenticated}
|
||||
>
|
||||
<Feather name="send" size={20} color="#bb0718" />
|
||||
<Feather name="send" size={20} color={commentText.trim() && isAuthenticated ? "#bb0718" : "#666"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{!isAuthenticated && (
|
||||
<Text className="text-center text-sm text-red-500 mt-1">
|
||||
Please log in to comment
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
575
app/post-detail/[id].tsx
Normal file
575
app/post-detail/[id].tsx
Normal file
@ -0,0 +1,575 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
||||
import { Feather, FontAwesome } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { useAuth } from '../../context/auth-context';
|
||||
import { getFoods, getIngredients, getNutrients } from '../../services/data/foods';
|
||||
import {
|
||||
createLike,
|
||||
deleteLike,
|
||||
createSave,
|
||||
deleteSave,
|
||||
getComments,
|
||||
createComment,
|
||||
getLikesCount,
|
||||
getSavesCount,
|
||||
getCommentsCount,
|
||||
checkUserLiked,
|
||||
checkUserSaved
|
||||
} from '../../services/data/forum';
|
||||
import { getProfile } from '../../services/data/profile';
|
||||
import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index';
|
||||
import { supabase } from '../../services/supabase';
|
||||
|
||||
export default function PostDetailScreen() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const foodId = typeof id === 'string' ? id : '';
|
||||
|
||||
console.log('Post detail screen - Food ID:', foodId);
|
||||
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [currentUserId, setCurrentUserId] = useState<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
|
||||
});
|
||||
|
||||
// Get current user ID from Supabase session
|
||||
useEffect(() => {
|
||||
async function getCurrentUser() {
|
||||
if (isAuthenticated) {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const userId = data.session?.user?.id;
|
||||
console.log('Current user ID:', userId);
|
||||
setCurrentUserId(userId || null);
|
||||
} else {
|
||||
setCurrentUserId(null);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (foodId) {
|
||||
console.log('Loading food details for ID:', foodId);
|
||||
loadFoodDetails();
|
||||
} else {
|
||||
console.error('No food ID provided');
|
||||
}
|
||||
}, [foodId]);
|
||||
|
||||
// Set up real-time subscription for comments
|
||||
useEffect(() => {
|
||||
if (!foodId) return;
|
||||
|
||||
console.log(`Setting up real-time subscription for comments on food_id: ${foodId}`);
|
||||
|
||||
const subscription = supabase
|
||||
.channel(`food_comments:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_comments',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, (payload) => {
|
||||
console.log('Comment change detected:', payload);
|
||||
// Refresh comments when changes occur
|
||||
refreshComments();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(subscription);
|
||||
};
|
||||
}, [foodId]);
|
||||
|
||||
// Check if user has liked/saved when user ID changes
|
||||
useEffect(() => {
|
||||
if (foodId && currentUserId && food) {
|
||||
checkUserInteractions();
|
||||
}
|
||||
}, [currentUserId, foodId, food]);
|
||||
|
||||
const checkUserInteractions = async () => {
|
||||
if (!currentUserId || !foodId) return;
|
||||
|
||||
try {
|
||||
console.log('Checking user interactions with user ID:', currentUserId);
|
||||
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(foodId, currentUserId),
|
||||
checkUserSaved(foodId, currentUserId)
|
||||
]);
|
||||
|
||||
console.log('User liked:', !!likedRes.data);
|
||||
console.log('User saved:', !!savedRes.data);
|
||||
|
||||
setIsLiked(!!likedRes.data);
|
||||
setIsSaved(!!savedRes.data);
|
||||
} catch (error) {
|
||||
console.error('Error checking user interactions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshComments = async () => {
|
||||
if (!foodId) {
|
||||
console.error('Cannot refresh comments: No food ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Refreshing comments for food_id: ${foodId}`);
|
||||
|
||||
const { data: commentsData, error } = await getComments(foodId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error refreshing comments:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (commentsData) {
|
||||
console.log(`Refreshed ${commentsData.length} comments for food_id: ${foodId}`);
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
const { count } = await getCommentsCount(foodId);
|
||||
setStats(prev => ({ ...prev, comments: count || 0 }));
|
||||
} catch (error) {
|
||||
console.error('Error refreshing comments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFoodDetails = async () => {
|
||||
if (!foodId) {
|
||||
console.error('Cannot load food details: No food ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('Loading food details for ID:', foodId);
|
||||
|
||||
// Get specific food by ID
|
||||
const { data: foodData, error: foodError } = await supabase
|
||||
.from('foods')
|
||||
.select('*')
|
||||
.eq('id', foodId)
|
||||
.single();
|
||||
|
||||
if (foodError) {
|
||||
console.error('Error loading food:', foodError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foodData) {
|
||||
const foodItem = {
|
||||
...foodData,
|
||||
description: foodData.description || '',
|
||||
ingredient_count: foodData.ingredient_count ?? 0,
|
||||
calories: foodData.calories ?? 0,
|
||||
image_url: foodData.image_url || '',
|
||||
};
|
||||
|
||||
console.log('Food loaded:', foodItem.name);
|
||||
setFood(foodItem);
|
||||
|
||||
// Get food creator profile
|
||||
if (foodItem.created_by) {
|
||||
console.log('Loading creator profile for:', foodItem.created_by);
|
||||
const { data: creatorProfile } = await getProfile(foodItem.created_by);
|
||||
if (creatorProfile) {
|
||||
setFoodCreator(creatorProfile);
|
||||
}
|
||||
}
|
||||
|
||||
// Get ingredients
|
||||
const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodId);
|
||||
|
||||
if (!ingredientsError && ingredientsData) {
|
||||
setIngredients(ingredientsData);
|
||||
}
|
||||
|
||||
// Get nutrients
|
||||
const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodId);
|
||||
|
||||
if (!nutrientsError && nutrientsData) {
|
||||
setNutrients(nutrientsData);
|
||||
}
|
||||
|
||||
// Get comments for this specific food ID
|
||||
const { data: commentsData, error: commentsError } = await getComments(foodId);
|
||||
|
||||
if (commentsError) {
|
||||
console.error('Error loading comments:', commentsError);
|
||||
} else if (commentsData) {
|
||||
console.log(`Loaded ${commentsData.length} comments for food_id: ${foodId}`);
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
// Get stats
|
||||
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||
getLikesCount(foodId),
|
||||
getSavesCount(foodId),
|
||||
getCommentsCount(foodId)
|
||||
]);
|
||||
|
||||
console.log('Stats loaded:', {
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
|
||||
setStats({
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading food details:', error);
|
||||
Alert.alert('Error', 'Failed to load food details. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLike = async () => {
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling like with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(true);
|
||||
setStats(prev => ({ ...prev, likes: prev.likes + 1 }));
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(false);
|
||||
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: !isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling save with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(true);
|
||||
setStats(prev => ({ ...prev, saves: prev.saves + 1 }));
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(false);
|
||||
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: !isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitComment = async () => {
|
||||
if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to comment.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingComment(true);
|
||||
try {
|
||||
console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
const { error } = await createComment(foodId, currentUserId, commentText.trim());
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear comment text
|
||||
setCommentText('');
|
||||
|
||||
// Refresh comments
|
||||
await refreshComments();
|
||||
|
||||
console.log('Comment submitted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
} finally {
|
||||
setSubmittingComment(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#ffd60a" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (!food) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Text className="text-lg">Post not found</Text>
|
||||
<TouchableOpacity
|
||||
className="mt-4 bg-[#ffd60a] px-6 py-3 rounded-lg"
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Text className="font-bold">Go Back</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<ScrollView className="flex-1">
|
||||
{/* Header */}
|
||||
<View className="flex-row justify-between items-center px-4 py-3">
|
||||
<TouchableOpacity
|
||||
className="bg-[#ffd60a] p-3 rounded-lg"
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Feather name="arrow-left" size={24} color="#bb0718" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text className="text-2xl font-bold">Post</Text>
|
||||
|
||||
<TouchableOpacity>
|
||||
<Feather name="more-horizontal" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* User info and rating */}
|
||||
<View className="flex-row justify-between items-center px-4 py-3">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-12 h-12 bg-gray-200 rounded-full overflow-hidden">
|
||||
{foodCreator?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: foodCreator.avatar_url }}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<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() || '?'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className="ml-3 text-lg font-bold">
|
||||
{foodCreator?.username || foodCreator?.full_name || 'Unknown Chef'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="text-lg font-bold mr-1">4.2</Text>
|
||||
<FontAwesome name="star" size={20} color="#ffd60a" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Food image */}
|
||||
<View className="px-4 mb-4">
|
||||
<Image
|
||||
source={{ uri: food.image_url || "/placeholder.svg?height=300&width=500&query=food dish" }}
|
||||
className="w-full h-64 rounded-lg"
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Food title and description */}
|
||||
<View className="px-4 mb-2">
|
||||
<Text className="text-2xl font-bold mb-2">{food.name}</Text>
|
||||
<Text className="text-gray-700 mb-2">{food.description}</Text>
|
||||
<Text className="text-gray-500 text-sm">
|
||||
{new Date(food.created_at).toLocaleDateString()} - {new Date(food.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Feather name="message-circle" size={22} color="#333" />
|
||||
<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"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Reviews section */}
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center px-4 py-4 border-b border-gray-200"
|
||||
onPress={() => setShowReviews(!showReviews)}
|
||||
>
|
||||
<Text className="text-xl font-bold">Review</Text>
|
||||
<Feather name={showReviews ? "chevron-up" : "chevron-down"} size={20} color="#333" className="ml-2" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{showReviews && (
|
||||
<View className="px-4 py-2">
|
||||
{comments.length > 0 ? (
|
||||
comments.map((comment) => (
|
||||
<View key={comment.id} className="py-4 border-b border-gray-100">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<View className="w-10 h-10 bg-gray-200 rounded-full overflow-hidden mr-3">
|
||||
{comment.user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: comment.user.avatar_url }}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<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() || '?'}
|
||||
</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'}
|
||||
</Text>
|
||||
<Text className="text-gray-500 text-xs">
|
||||
{new Date(comment.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text>{comment.content}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text className="py-4 text-gray-500">No reviews yet. Be the first to comment!</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Bottom spacing */}
|
||||
<View className="h-24" />
|
||||
</ScrollView>
|
||||
|
||||
{/* Comment input */}
|
||||
<View className="px-4 py-3 border-t border-gray-200 bg-white">
|
||||
<View className="flex-row items-center">
|
||||
<TextInput
|
||||
className="flex-1 bg-gray-100 rounded-full px-4 py-2 mr-2"
|
||||
placeholder="Add a comment..."
|
||||
value={commentText}
|
||||
onChangeText={setCommentText}
|
||||
multiline
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className={`p-2 rounded-full ${commentText.trim() && isAuthenticated ? 'bg-[#ffd60a]' : 'bg-gray-300'}`}
|
||||
onPress={handleSubmitComment}
|
||||
disabled={submittingComment || !commentText.trim() || !isAuthenticated}
|
||||
>
|
||||
<Feather name="send" size={20} color={commentText.trim() && isAuthenticated ? "#bb0718" : "#666"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{!isAuthenticated && (
|
||||
<Text className="text-center text-sm text-red-500 mt-1">
|
||||
Please log in to comment
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
@ -2,31 +2,212 @@ import { supabase } from "@/services/supabase";
|
||||
import { getProfile, getProfiles } from "./profile";
|
||||
|
||||
export const createLike = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase.from("food_likes").insert({ food_id, user_id });
|
||||
console.log('Creating like with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
// Check if like already exists to prevent duplicates
|
||||
const { data: existingLike } = await supabase
|
||||
.from("food_likes")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
|
||||
if (existingLike) {
|
||||
console.log('Like already exists');
|
||||
return { data: existingLike, error: null };
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_likes")
|
||||
.insert({ food_id, user_id })
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
} else {
|
||||
console.log('Like created successfully');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const createSave = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase.from("food_saves").insert({ food_id, user_id });
|
||||
console.log('Creating save with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
// Check if save already exists to prevent duplicates
|
||||
const { data: existingSave } = await supabase
|
||||
.from("food_saves")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
|
||||
if (existingSave) {
|
||||
console.log('Save already exists');
|
||||
return { data: existingSave, error: null };
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_saves")
|
||||
.insert({ food_id, user_id })
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
} else {
|
||||
console.log('Save created successfully');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const deleteLike = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase.from("food_likes").delete().eq("food_id", food_id).eq("user_id", user_id);
|
||||
console.log('Deleting like with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_likes")
|
||||
.delete()
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
} else {
|
||||
console.log('Like deleted successfully');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const deleteSave = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase.from("food_saves").delete().eq("food_id", food_id).eq("user_id", user_id);
|
||||
console.log('Deleting save with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_saves")
|
||||
.delete()
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
} else {
|
||||
console.log('Save deleted successfully');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const createComment = async (food_id: string, user_id: string, content: string) => {
|
||||
const { data, error } = await supabase.from("food_comments").insert({ food_id, user_id, content });
|
||||
console.log('Creating comment with food_id:', food_id, 'user_id:', user_id, 'and content:', content);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_comments")
|
||||
.insert({ food_id, user_id, content })
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
} else {
|
||||
console.log('Comment created successfully');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const getLikesCount = async (food_id: string) => {
|
||||
console.log('Getting likes count for food_id:', food_id);
|
||||
|
||||
const { count, error } = await supabase
|
||||
.from("food_likes")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting likes count:', error);
|
||||
} else {
|
||||
console.log('Likes count:', count);
|
||||
}
|
||||
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const getSavesCount = async (food_id: string) => {
|
||||
console.log('Getting saves count for food_id:', food_id);
|
||||
|
||||
const { count, error } = await supabase
|
||||
.from("food_saves")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting saves count:', error);
|
||||
} else {
|
||||
console.log('Saves count:', count);
|
||||
}
|
||||
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const getCommentsCount = async (food_id: string) => {
|
||||
console.log('Getting comments count for food_id:', food_id);
|
||||
|
||||
const { count, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting comments count:', error);
|
||||
} else {
|
||||
console.log('Comments count:', count);
|
||||
}
|
||||
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const checkUserLiked = async (food_id: string, user_id: string) => {
|
||||
console.log('Checking if user liked with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_likes")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') { // PGRST116 is the "no rows returned" error code
|
||||
console.error('Error checking if user liked:', error);
|
||||
} else {
|
||||
console.log('User liked:', !!data);
|
||||
}
|
||||
|
||||
return { data, error: error && error.code === 'PGRST116' ? null : error };
|
||||
}
|
||||
|
||||
export const checkUserSaved = async (food_id: string, user_id: string) => {
|
||||
console.log('Checking if user saved with food_id:', food_id, 'and user_id:', user_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_saves")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') { // PGRST116 is the "no rows returned" error code
|
||||
console.error('Error checking if user saved:', error);
|
||||
} else {
|
||||
console.log('User saved:', !!data);
|
||||
}
|
||||
|
||||
return { data, error: error && error.code === 'PGRST116' ? null : error };
|
||||
}
|
||||
|
||||
export const getComments = async (food_id: string) => {
|
||||
console.log('Getting comments for food_id:', food_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select(`
|
||||
@ -39,6 +220,11 @@ export const getComments = async (food_id: string) => {
|
||||
.eq("food_id", food_id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting comments:', error);
|
||||
return { data: [], error };
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Get unique user IDs from comments
|
||||
const userIds = [...new Set(data.map(comment => comment.user_id))];
|
||||
@ -59,53 +245,11 @@ export const getComments = async (food_id: string) => {
|
||||
user: profileMap[comment.user_id]
|
||||
}));
|
||||
|
||||
console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`);
|
||||
return { data: commentsWithProfiles, error };
|
||||
}
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const getLikesCount = async (food_id: string) => {
|
||||
const { count, error } = await supabase
|
||||
.from("food_likes")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const getSavesCount = async (food_id: string) => {
|
||||
const { count, error } = await supabase
|
||||
.from("food_saves")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const getCommentsCount = async (food_id: string) => {
|
||||
const { count, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("food_id", food_id);
|
||||
return { count, error };
|
||||
}
|
||||
|
||||
export const checkUserLiked = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from("food_likes")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export const checkUserSaved = async (food_id: string, user_id: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from("food_saves")
|
||||
.select("*")
|
||||
.eq("food_id", food_id)
|
||||
.eq("user_id", user_id)
|
||||
.single();
|
||||
console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`);
|
||||
return { data, error };
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user