mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 14:04:08 +01:00
Merge branch 'main' of https://github.com/Sosokker/chefhai
This commit is contained in:
commit
6933392271
2
app.json
2
app.json
@ -28,7 +28,7 @@
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"image": "./assets/images/splash.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
|
||||
@ -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;
|
||||
|
||||
const likesSubscription = supabase
|
||||
.channel('food_likes_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
const savesSubscription = supabase
|
||||
.channel('food_saves_changes')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_saves'
|
||||
}, () => {
|
||||
// Refresh stats when changes occur
|
||||
loadFoods();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(likesSubscription);
|
||||
supabase.removeChannel(savesSubscription);
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
// Use React Query hooks
|
||||
const {
|
||||
data: foods = [],
|
||||
isLoading: isLoadingFoods,
|
||||
refetch: refetchFoods
|
||||
} = useFoods(selectedCategory, searchQuery, selectedSort);
|
||||
|
||||
useEffect(() => {
|
||||
loadFoods();
|
||||
}, [selectedCategory, selectedSort, currentUserId]);
|
||||
const foodIds = foods.map(food => food.id);
|
||||
|
||||
const loadFoods = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// In a real app, you would filter by category and sort accordingly
|
||||
const { data, error } = await getFoods(undefined, true, searchQuery);
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading foods:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
// Sort data based on selectedSort
|
||||
let sortedData = [...data];
|
||||
if (selectedSort === 'rating') {
|
||||
// Assuming higher calories means higher rating for demo purposes
|
||||
sortedData.sort((a, b) => (b.calories ?? 0) - (a.calories ?? 0));
|
||||
} else if (selectedSort === 'newest') {
|
||||
sortedData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
} else if (selectedSort === 'best') {
|
||||
// Assuming higher ingredient_count means better for demo purposes
|
||||
sortedData.sort((a, b) => (b.ingredient_count ?? 0) - (a.ingredient_count ?? 0));
|
||||
}
|
||||
|
||||
setFoods(sortedData.map(food => ({
|
||||
...food,
|
||||
description: food.description || '', // Ensure description is always a string
|
||||
ingredient_count: food.ingredient_count ?? 0, // Ensure ingredient_count is always a number
|
||||
calories: food.calories ?? 0, // Ensure calories is always a number
|
||||
image_url: food.image_url || '', // Ensure image_url is always a string
|
||||
})));
|
||||
|
||||
// Load stats for each food
|
||||
const statsPromises = sortedData.map(async (food) => {
|
||||
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||
getLikesCount(food.id),
|
||||
getSavesCount(food.id),
|
||||
getCommentsCount(food.id)
|
||||
]);
|
||||
|
||||
return {
|
||||
foodId: food.id,
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
};
|
||||
});
|
||||
|
||||
const stats = await Promise.all(statsPromises);
|
||||
const statsMap = stats.reduce((acc, stat) => {
|
||||
acc[stat.foodId] = {
|
||||
likes: stat.likes,
|
||||
saves: stat.saves,
|
||||
comments: stat.comments
|
||||
};
|
||||
return acc;
|
||||
}, {} as {[key: string]: {likes: number, saves: number, comments: number}});
|
||||
|
||||
setFoodStats(statsMap);
|
||||
|
||||
// Load creator profiles
|
||||
const creatorIds = sortedData
|
||||
.filter(food => food.created_by)
|
||||
.map(food => food.created_by as string);
|
||||
|
||||
const uniqueCreatorIds = [...new Set(creatorIds)];
|
||||
|
||||
const creatorProfiles: {[key: string]: Profile} = {};
|
||||
|
||||
for (const creatorId of uniqueCreatorIds) {
|
||||
const { data: profile } = await getProfile(creatorId);
|
||||
if (profile) {
|
||||
creatorProfiles[creatorId] = profile;
|
||||
}
|
||||
}
|
||||
|
||||
setFoodCreators(creatorProfiles);
|
||||
|
||||
// Check user interactions if authenticated
|
||||
if (isAuthenticated && currentUserId) {
|
||||
const interactionsPromises = sortedData.map(async (food) => {
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(food.id, currentUserId),
|
||||
checkUserSaved(food.id, currentUserId)
|
||||
]);
|
||||
|
||||
return {
|
||||
foodId: food.id,
|
||||
liked: !!likedRes.data,
|
||||
saved: !!savedRes.data
|
||||
};
|
||||
});
|
||||
|
||||
const interactions = await Promise.all(interactionsPromises);
|
||||
const interactionsMap = interactions.reduce((acc, interaction) => {
|
||||
acc[interaction.foodId] = {
|
||||
liked: interaction.liked,
|
||||
saved: interaction.saved
|
||||
};
|
||||
return acc;
|
||||
}, {} as {[key: string]: {liked: boolean, saved: boolean}});
|
||||
|
||||
setUserInteractions(interactionsMap);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
data: foodStats = {},
|
||||
isLoading: isLoadingStats
|
||||
} = useFoodStats(foodIds);
|
||||
|
||||
const creatorIds = foods
|
||||
.filter(food => food.created_by)
|
||||
.map(food => food.created_by as string);
|
||||
|
||||
const {
|
||||
data: foodCreators = {},
|
||||
isLoading: isLoadingCreators
|
||||
} = useFoodCreators(creatorIds);
|
||||
|
||||
const {
|
||||
data: userInteractions = {},
|
||||
isLoading: isLoadingInteractions
|
||||
} = useUserInteractions(foodIds, currentUserId);
|
||||
|
||||
const likeMutation = useLikeMutation();
|
||||
const saveMutation = useSaveMutation();
|
||||
|
||||
// Refetch data when the screen comes into focus
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
refetchFoods();
|
||||
}, [refetchFoods])
|
||||
);
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
setSearchQuery(text);
|
||||
// Debounce search for better performance
|
||||
setTimeout(() => {
|
||||
loadFoods();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const navigateToPostDetail = (food: Food) => {
|
||||
router.push(`/post-detail/${food.id}`);
|
||||
};
|
||||
const navigateToPostDetail = (food: { id: string }) => {
|
||||
router.push(`/post-detail/${food.id}`);
|
||||
};
|
||||
|
||||
const handleLike = async (food: Food) => {
|
||||
const handleLike = async (food: { id: string }) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to like posts.');
|
||||
return;
|
||||
@ -232,77 +105,18 @@ const navigateToPostDetail = (food: Food) => {
|
||||
try {
|
||||
const isLiked = userInteractions[food.id]?.liked || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: !isLiked
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: isLiked ? Math.max(0, prev[food.id].likes - 1) : prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: prev[food.id].likes + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
liked: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
likes: Math.max(0, prev[food.id].likes - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
likeMutation.mutate({
|
||||
foodId: food.id,
|
||||
userId: currentUserId,
|
||||
isLiked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (food: Food) => {
|
||||
const handleSave = async (food: { id: string }) => {
|
||||
if (!isAuthenticated || !currentUserId) {
|
||||
Alert.alert('Authentication Required', 'Please log in to save posts.');
|
||||
return;
|
||||
@ -311,77 +125,18 @@ const navigateToPostDetail = (food: Food) => {
|
||||
try {
|
||||
const isSaved = userInteractions[food.id]?.saved || false;
|
||||
|
||||
// Optimistically update UI
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: !isSaved
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: isSaved ? Math.max(0, prev[food.id].saves - 1) : prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: true
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: prev[food.id].saves + 1
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(food.id, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setUserInteractions(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saved: false
|
||||
}
|
||||
}));
|
||||
|
||||
setFoodStats(prev => ({
|
||||
...prev,
|
||||
[food.id]: {
|
||||
...prev[food.id],
|
||||
saves: Math.max(0, prev[food.id].saves - 1)
|
||||
}
|
||||
}));
|
||||
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
saveMutation.mutate({
|
||||
foodId: food.id,
|
||||
userId: currentUserId,
|
||||
isSaved
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const renderFoodItem = ({ item }: { item: Food }) => {
|
||||
const renderFoodItem = ({ item }: { item: any }) => {
|
||||
// Get stats for this food
|
||||
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
|
||||
|
||||
@ -484,6 +239,8 @@ const navigateToPostDetail = (food: Food) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingFoods || isLoadingStats || isLoadingCreators || isLoadingInteractions;
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { useAuth } from "@/context/auth-context";
|
||||
import { getFoods } from "@/services/data/foods";
|
||||
import { getProfile, updateProfile } from "@/services/data/profile";
|
||||
import { supabase } from "@/services/supabase";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/context/auth-context"
|
||||
import { getFoods } from "@/services/data/foods"
|
||||
import { getBookmarkedPosts } from "@/services/data/bookmarks"
|
||||
import { getLikedPosts } from "@/services/data/likes"
|
||||
import { getProfile, updateProfile } from "@/services/data/profile"
|
||||
import { supabase } from "@/services/supabase"
|
||||
import { useIsFocused, useNavigation } from "@react-navigation/native"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import * as ImagePicker from "expo-image-picker"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
@ -18,15 +20,31 @@ import {
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import uuid from "react-native-uuid";
|
||||
} from "react-native"
|
||||
import { SafeAreaView } from "react-native-safe-area-context"
|
||||
import uuid from "react-native-uuid"
|
||||
|
||||
// Define the Food type based on your database structure
|
||||
type Food = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
time_to_cook_minutes: number
|
||||
skill_level: string
|
||||
ingredient_count: number
|
||||
calories: number
|
||||
image_url: string
|
||||
is_shared: boolean
|
||||
created_by: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const [activeTab, setActiveTab] = useState("My Recipes");
|
||||
const { isAuthenticated } = useAuth();
|
||||
const isFocused = useIsFocused();
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState("My Recipes")
|
||||
const { isAuthenticated } = useAuth()
|
||||
const isFocused = useIsFocused()
|
||||
const queryClient = useQueryClient()
|
||||
const navigation = useNavigation()
|
||||
|
||||
const {
|
||||
data: userData,
|
||||
@ -35,14 +53,14 @@ export default function ProfileScreen() {
|
||||
} = useQuery({
|
||||
queryKey: ["auth-user"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error) throw error;
|
||||
return data?.user;
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
return data?.user
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 0,
|
||||
});
|
||||
const userId = userData?.id;
|
||||
})
|
||||
const userId = userData?.id
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
@ -51,115 +69,186 @@ export default function ProfileScreen() {
|
||||
} = useQuery({
|
||||
queryKey: ["profile", userId],
|
||||
queryFn: async () => {
|
||||
if (!userId) throw new Error("No user id");
|
||||
return getProfile(userId);
|
||||
if (!userId) throw new Error("No user id")
|
||||
return getProfile(userId)
|
||||
},
|
||||
enabled: !!userId,
|
||||
staleTime: 0,
|
||||
subscribed: isFocused,
|
||||
});
|
||||
})
|
||||
|
||||
// My Recipes Query
|
||||
const {
|
||||
data: foodsData,
|
||||
isLoading: isFoodsLoading,
|
||||
error: foodsError,
|
||||
data: myRecipesData,
|
||||
isLoading: isMyRecipesLoading,
|
||||
error: myRecipesError,
|
||||
refetch: refetchMyRecipes,
|
||||
} = useQuery({
|
||||
queryKey: ["my-recipes", userId],
|
||||
queryFn: async () => {
|
||||
if (!userId) throw new Error("No user id");
|
||||
return getFoods(userId);
|
||||
if (!userId) throw new Error("No user id")
|
||||
return getFoods(userId)
|
||||
},
|
||||
enabled: !!userId && activeTab === "My Recipes",
|
||||
staleTime: 0,
|
||||
});
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60, // 1 minute
|
||||
})
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editUsername, setEditUsername] = useState("");
|
||||
const [editImage, setEditImage] = useState<string | null>(null);
|
||||
const [editLoading, setEditLoading] = useState(false);
|
||||
const [editError, setEditError] = useState<string | null>(null);
|
||||
// Likes Query
|
||||
const {
|
||||
data: likesData,
|
||||
isLoading: isLikesLoading,
|
||||
error: likesError,
|
||||
refetch: refetchLikes,
|
||||
} = useQuery({
|
||||
queryKey: ["liked-posts", userId],
|
||||
queryFn: async () => {
|
||||
if (!userId) throw new Error("No user id")
|
||||
return getLikedPosts(userId)
|
||||
},
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60, // 1 minute
|
||||
})
|
||||
|
||||
// Bookmarks Query
|
||||
const {
|
||||
data: bookmarksData,
|
||||
isLoading: isBookmarksLoading,
|
||||
error: bookmarksError,
|
||||
refetch: refetchBookmarks,
|
||||
} = useQuery({
|
||||
queryKey: ["bookmarked-posts", userId],
|
||||
queryFn: async () => {
|
||||
if (!userId) throw new Error("No user id")
|
||||
return getBookmarkedPosts(userId)
|
||||
},
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60, // 1 minute
|
||||
})
|
||||
|
||||
// Navigate to post detail
|
||||
const handleFoodPress = (foodId: number) => {
|
||||
// @ts-ignore - Navigation typing might be different in your app
|
||||
navigation.navigate("post-detail", { id: foodId })
|
||||
}
|
||||
|
||||
// Refetch data when tab changes
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
|
||||
// Refetch data for the selected tab
|
||||
if (tab === "My Recipes") {
|
||||
refetchMyRecipes()
|
||||
} else if (tab === "Likes") {
|
||||
refetchLikes()
|
||||
} else if (tab === "Bookmarks") {
|
||||
refetchBookmarks()
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch all data when the screen comes into focus
|
||||
useEffect(() => {
|
||||
if (isFocused && userId) {
|
||||
refetchMyRecipes()
|
||||
refetchLikes()
|
||||
refetchBookmarks()
|
||||
}
|
||||
}, [isFocused, userId])
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editUsername, setEditUsername] = useState("")
|
||||
const [editImage, setEditImage] = useState<string | null>(null)
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
const [editError, setEditError] = useState<string | null>(null)
|
||||
|
||||
const pickImage = async () => {
|
||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
||||
if (status !== "granted") {
|
||||
setEditError("Permission to access media library is required.");
|
||||
return;
|
||||
setEditError("Permission to access media library is required.")
|
||||
return
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images"],
|
||||
quality: 0.7,
|
||||
allowsEditing: true,
|
||||
});
|
||||
})
|
||||
|
||||
if (!result.canceled) {
|
||||
setEditImage(result.assets[0].uri);
|
||||
setEditImage(result.assets[0].uri)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const uploadImageToSupabase = async (uri: string): Promise<string> => {
|
||||
const fileName = `${userId}/${uuid.v4()}.jpg`;
|
||||
const response = await fetch(uri);
|
||||
const blob = await response.blob();
|
||||
const fileName = `${userId}/${uuid.v4()}.jpg`
|
||||
const response = await fetch(uri)
|
||||
const blob = await response.blob()
|
||||
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(fileName, blob, {
|
||||
contentType: "image/jpeg",
|
||||
upsert: true,
|
||||
});
|
||||
const { error: uploadError } = await supabase.storage.from("avatars").upload(fileName, blob, {
|
||||
contentType: "image/jpeg",
|
||||
upsert: true,
|
||||
})
|
||||
|
||||
if (uploadError) throw uploadError;
|
||||
if (uploadError) throw uploadError
|
||||
|
||||
const { data } = supabase.storage.from("avatars").getPublicUrl(fileName);
|
||||
return data.publicUrl;
|
||||
};
|
||||
const { data } = supabase.storage.from("avatars").getPublicUrl(fileName)
|
||||
return data.publicUrl
|
||||
}
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
setEditLoading(true);
|
||||
setEditError(null);
|
||||
setEditLoading(true)
|
||||
setEditError(null)
|
||||
|
||||
try {
|
||||
if (!editUsername.trim()) throw new Error("Username cannot be empty");
|
||||
if (!editUsername.trim()) throw new Error("Username cannot be empty")
|
||||
|
||||
let avatarUrl = profileData?.data?.avatar_url ?? null;
|
||||
let avatarUrl = profileData?.data?.avatar_url ?? null
|
||||
|
||||
if (editImage && editImage !== avatarUrl) {
|
||||
avatarUrl = await uploadImageToSupabase(editImage);
|
||||
avatarUrl = await uploadImageToSupabase(editImage)
|
||||
}
|
||||
|
||||
const { error: updateError } = await updateProfile(
|
||||
userId!,
|
||||
editUsername.trim(),
|
||||
avatarUrl
|
||||
);
|
||||
if (updateError) throw updateError;
|
||||
const { error: updateError } = await updateProfile(userId!, editUsername.trim(), avatarUrl)
|
||||
if (updateError) throw updateError
|
||||
|
||||
setModalVisible(false);
|
||||
await queryClient.invalidateQueries({ queryKey: ["profile", userId] });
|
||||
setModalVisible(false)
|
||||
await queryClient.invalidateQueries({ queryKey: ["profile", userId] })
|
||||
} catch (err: any) {
|
||||
setEditError(err.message || "Failed to update profile");
|
||||
setEditError(err.message || "Failed to update profile")
|
||||
} finally {
|
||||
setEditLoading(false);
|
||||
setEditLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get the active data based on the current tab
|
||||
const getActiveData = () => {
|
||||
switch (activeTab) {
|
||||
case "My Recipes":
|
||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||
case "Likes":
|
||||
return { data: likesData, isLoading: isLikesLoading, error: likesError }
|
||||
case "Bookmarks":
|
||||
return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError }
|
||||
default:
|
||||
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||
}
|
||||
}
|
||||
|
||||
const { data: activeData, isLoading: isActiveLoading, error: activeError } = getActiveData()
|
||||
|
||||
if (isUserLoading) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 justify-center items-center bg-white">
|
||||
<ActivityIndicator size="large" color="#bb0718" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (userError) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 justify-center items-center bg-white px-4">
|
||||
<Text className="text-red-600 font-bold text-center">
|
||||
{userError.message || "Failed to load user data."}
|
||||
</Text>
|
||||
<Text className="text-red-600 font-bold text-center">{userError.message || "Failed to load user data."}</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -179,46 +268,31 @@ export default function ProfileScreen() {
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#bb0718" />
|
||||
) : error ? (
|
||||
<Text className="text-red-600 font-bold mb-3">
|
||||
{error.message || error.toString()}
|
||||
</Text>
|
||||
<Text className="text-red-600 font-bold mb-3">{error.message || error.toString()}</Text>
|
||||
) : (
|
||||
<Text className="text-xl font-bold mb-3">
|
||||
{profileData?.data?.username ?? "-"}
|
||||
</Text>
|
||||
<Text className="text-xl font-bold mb-3">{profileData?.data?.username ?? "-"}</Text>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className="bg-red-600 py-2 px-10 rounded-lg"
|
||||
onPress={() => {
|
||||
setEditUsername(profileData?.data?.username ?? "");
|
||||
setEditImage(profileData?.data?.avatar_url ?? null);
|
||||
setEditError(null);
|
||||
setModalVisible(true);
|
||||
setEditUsername(profileData?.data?.username ?? "")
|
||||
setEditImage(profileData?.data?.avatar_url ?? null)
|
||||
setEditError(null)
|
||||
setModalVisible(true)
|
||||
}}
|
||||
>
|
||||
<Text className="text-white font-bold">Edit</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<Modal visible={modalVisible} animationType="slide" transparent onRequestClose={() => setModalVisible(false)}>
|
||||
<View className="flex-1 justify-center items-center bg-black bg-opacity-40">
|
||||
<View className="bg-white rounded-xl p-6 w-11/12 max-w-md shadow-lg">
|
||||
<Text className="text-lg font-bold mb-4 text-center">
|
||||
Edit Profile
|
||||
</Text>
|
||||
<Text className="text-lg font-bold mb-4 text-center">Edit Profile</Text>
|
||||
|
||||
<Pressable className="items-center mb-4" onPress={pickImage}>
|
||||
<Image
|
||||
source={
|
||||
editImage
|
||||
? { uri: editImage }
|
||||
: require("@/assets/images/placeholder-food.jpg")
|
||||
}
|
||||
source={editImage ? { uri: editImage } : require("@/assets/images/placeholder-food.jpg")}
|
||||
className="w-24 h-24 rounded-full mb-2 bg-gray-200"
|
||||
/>
|
||||
<Text className="text-blue-600 underline">Change Photo</Text>
|
||||
@ -231,11 +305,7 @@ export default function ProfileScreen() {
|
||||
onChangeText={setEditUsername}
|
||||
placeholder="Enter new username"
|
||||
/>
|
||||
{editError && (
|
||||
<Text className="text-red-600 mb-2 text-center">
|
||||
{editError}
|
||||
</Text>
|
||||
)}
|
||||
{editError && <Text className="text-red-600 mb-2 text-center">{editError}</Text>}
|
||||
|
||||
<View className="flex-row justify-between mt-2">
|
||||
<TouchableOpacity
|
||||
@ -263,61 +333,52 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<View className="flex-row justify-around py-3">
|
||||
{["My Recipes", "Likes", "Saved"].map((tab) => (
|
||||
<View className="flex-row justify-around py-3 border-b border-gray-200">
|
||||
{["My Recipes", "Likes", "Bookmarks"].map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab}
|
||||
className={`py-2 px-4 ${
|
||||
activeTab === tab ? "border-b-2 border-[#333]" : ""
|
||||
}`}
|
||||
onPress={() => setActiveTab(tab)}
|
||||
className={`py-2 px-4 ${activeTab === tab ? "border-b-2 border-[#333]" : ""}`}
|
||||
onPress={() => handleTabChange(tab)}
|
||||
>
|
||||
<Text className="font-medium">{tab}</Text>
|
||||
<Text className={`font-medium ${activeTab === tab ? "font-bold" : ""}`}>{tab}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className="h-px bg-[#EEEEEE] mx-4" />
|
||||
|
||||
{/* Recipes */}
|
||||
{activeTab === "My Recipes" && (
|
||||
{/* Tab Content */}
|
||||
{isActiveLoading ? (
|
||||
<View className="flex-1 items-center justify-center py-8">
|
||||
<ActivityIndicator size="small" color="#bb0718" />
|
||||
</View>
|
||||
) : activeError ? (
|
||||
<View className="flex-1 items-center justify-center py-8">
|
||||
<Text className="text-red-600 font-bold text-center">{activeError.message || "Failed to load data"}</Text>
|
||||
</View>
|
||||
) : !activeData?.data?.length ? (
|
||||
<View className="flex-1 items-center justify-center py-8">
|
||||
<Text className="text-gray-400 font-medium text-center">No items found</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-row flex-wrap p-2">
|
||||
{isFoodsLoading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color="#bb0718"
|
||||
style={{ marginTop: 20 }}
|
||||
/>
|
||||
) : foodsError ? (
|
||||
<Text className="text-red-600 font-bold p-4">
|
||||
{foodsError.message || foodsError.toString()}
|
||||
</Text>
|
||||
) : foodsData?.data?.length ? (
|
||||
foodsData.data.map((item) => (
|
||||
<View key={item.id} className="w-1/2 p-2 relative">
|
||||
<Image
|
||||
source={
|
||||
item.image_url
|
||||
? { uri: item.image_url }
|
||||
: require("@/assets/images/placeholder-food.jpg")
|
||||
}
|
||||
className="w-full h-[120px] rounded-lg"
|
||||
/>
|
||||
<View className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90 bg-white/80">
|
||||
<Text className="text-[#333] font-bold text-xs">
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
{activeData.data.map((item: Food) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
className="w-1/2 p-2 relative"
|
||||
onPress={() => handleFoodPress(item.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Image
|
||||
source={item.image_url ? { uri: item.image_url } : require("@/assets/images/placeholder-food.jpg")}
|
||||
className="w-full h-[120px] rounded-lg"
|
||||
/>
|
||||
<View className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90 bg-white/80">
|
||||
<Text className="text-[#333] font-bold text-xs">{item.name}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-gray-400 font-bold p-4">
|
||||
No recipes found.
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
|
||||
const checkUserInteractions = async () => {
|
||||
if (!currentUserId || !foodId) return;
|
||||
if (!foodId) return;
|
||||
|
||||
try {
|
||||
console.log('Checking user interactions with user ID:', currentUserId);
|
||||
const likesSubscription = supabase
|
||||
.channel(`food_likes:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_likes',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
console.log('Like change detected, refreshing stats and interactions');
|
||||
refetchStats();
|
||||
refetchInteractions();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
const [likedRes, savedRes] = await Promise.all([
|
||||
checkUserLiked(foodId, currentUserId),
|
||||
checkUserSaved(foodId, currentUserId)
|
||||
]);
|
||||
const savesSubscription = supabase
|
||||
.channel(`food_saves:${foodId}`)
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'food_saves',
|
||||
filter: `food_id=eq.${foodId}`
|
||||
}, () => {
|
||||
console.log('Save change detected, refreshing stats and interactions');
|
||||
refetchStats();
|
||||
refetchInteractions();
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
console.log('User liked:', !!likedRes.data);
|
||||
console.log('User saved:', !!savedRes.data);
|
||||
|
||||
setIsLiked(!!likedRes.data);
|
||||
setIsSaved(!!savedRes.data);
|
||||
} catch (error) {
|
||||
console.error('Error checking user interactions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshComments = async () => {
|
||||
if (!foodId) {
|
||||
console.error('Cannot refresh comments: No food ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Refreshing comments for food_id: ${foodId}`);
|
||||
|
||||
const { data: commentsData, error } = await getComments(foodId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error refreshing comments:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (commentsData) {
|
||||
console.log(`Refreshed ${commentsData.length} comments for food_id: ${foodId}`);
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
const { count } = await getCommentsCount(foodId);
|
||||
setStats(prev => ({ ...prev, comments: count || 0 }));
|
||||
} catch (error) {
|
||||
console.error('Error refreshing comments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFoodDetails = async () => {
|
||||
if (!foodId) {
|
||||
console.error('Cannot load food details: No food ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('Loading food details for ID:', foodId);
|
||||
|
||||
// Get specific food by ID
|
||||
const { data: foodData, error: foodError } = await supabase
|
||||
.from('foods')
|
||||
.select('*')
|
||||
.eq('id', foodId)
|
||||
.single();
|
||||
|
||||
if (foodError) {
|
||||
console.error('Error loading food:', foodError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foodData) {
|
||||
const foodItem = {
|
||||
...foodData,
|
||||
description: foodData.description || '',
|
||||
ingredient_count: foodData.ingredient_count ?? 0,
|
||||
calories: foodData.calories ?? 0,
|
||||
image_url: foodData.image_url || '',
|
||||
};
|
||||
|
||||
console.log('Food loaded:', foodItem.name);
|
||||
setFood(foodItem);
|
||||
|
||||
// Get food creator profile
|
||||
if (foodItem.created_by) {
|
||||
console.log('Loading creator profile for:', foodItem.created_by);
|
||||
const { data: creatorProfile } = await getProfile(foodItem.created_by);
|
||||
if (creatorProfile) {
|
||||
setFoodCreator(creatorProfile);
|
||||
}
|
||||
}
|
||||
|
||||
// Get ingredients
|
||||
const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodId);
|
||||
|
||||
if (!ingredientsError && ingredientsData) {
|
||||
setIngredients(ingredientsData);
|
||||
}
|
||||
|
||||
// Get nutrients
|
||||
const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodId);
|
||||
|
||||
if (!nutrientsError && nutrientsData) {
|
||||
setNutrients(nutrientsData);
|
||||
}
|
||||
|
||||
// Get comments for this specific food ID
|
||||
const { data: commentsData, error: commentsError } = await getComments(foodId);
|
||||
|
||||
if (commentsError) {
|
||||
console.error('Error loading comments:', commentsError);
|
||||
} else if (commentsData) {
|
||||
console.log(`Loaded ${commentsData.length} comments for food_id: ${foodId}`);
|
||||
setComments(commentsData);
|
||||
}
|
||||
|
||||
// Get stats
|
||||
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||
getLikesCount(foodId),
|
||||
getSavesCount(foodId),
|
||||
getCommentsCount(foodId)
|
||||
]);
|
||||
|
||||
console.log('Stats loaded:', {
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
|
||||
setStats({
|
||||
likes: likesRes.count || 0,
|
||||
saves: savesRes.count || 0,
|
||||
comments: commentsRes.count || 0
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading food details:', error);
|
||||
Alert.alert('Error', 'Failed to load food details. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
supabase.removeChannel(likesSubscription);
|
||||
supabase.removeChannel(savesSubscription);
|
||||
};
|
||||
}, [foodId, refetchStats, refetchInteractions]);
|
||||
|
||||
const handleLike = async () => {
|
||||
if (!isAuthenticated || !currentUserId || !food) {
|
||||
@ -253,42 +245,13 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling like with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
|
||||
if (isLiked) {
|
||||
const { error } = await deleteLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(true);
|
||||
setStats(prev => ({ ...prev, likes: prev.likes + 1 }));
|
||||
Alert.alert('Error', 'Failed to unlike. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createLike(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(false);
|
||||
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||
Alert.alert('Error', 'Failed to like. Please try again.');
|
||||
}
|
||||
}
|
||||
likeMutation.mutate({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
isLiked: interactions.liked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling like:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsLiked(!isLiked);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
likes: !isLiked ? Math.max(0, prev.likes - 1) : prev.likes + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update like. Please try again.');
|
||||
}
|
||||
};
|
||||
@ -300,42 +263,13 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Toggling save with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
// Optimistically update UI
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
|
||||
if (isSaved) {
|
||||
const { error } = await deleteSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error deleting save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(true);
|
||||
setStats(prev => ({ ...prev, saves: prev.saves + 1 }));
|
||||
Alert.alert('Error', 'Failed to unsave. Please try again.');
|
||||
}
|
||||
} else {
|
||||
const { error } = await createSave(foodId, currentUserId);
|
||||
if (error) {
|
||||
console.error('Error creating save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(false);
|
||||
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||
Alert.alert('Error', 'Failed to save. Please try again.');
|
||||
}
|
||||
}
|
||||
saveMutation.mutate({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
isSaved: interactions.saved
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling save:', error);
|
||||
// Revert optimistic update if there's an error
|
||||
setIsSaved(!isSaved);
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
saves: !isSaved ? Math.max(0, prev.saves - 1) : prev.saves + 1
|
||||
}));
|
||||
Alert.alert('Error', 'Failed to update save. Please try again.');
|
||||
}
|
||||
};
|
||||
@ -350,23 +284,11 @@ export default function PostDetailScreen() {
|
||||
|
||||
setSubmittingComment(true);
|
||||
try {
|
||||
console.log('Submitting comment with user ID:', currentUserId, 'and food ID:', foodId);
|
||||
|
||||
const { error } = await createComment(foodId, currentUserId, commentText.trim());
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear comment text
|
||||
setCommentText('');
|
||||
|
||||
// Refresh comments
|
||||
await refreshComments();
|
||||
|
||||
console.log('Comment submitted successfully');
|
||||
await commentMutation.mutateAsync({
|
||||
foodId,
|
||||
userId: currentUserId,
|
||||
content: commentText.trim()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
Alert.alert('Error', 'Failed to submit comment. Please try again.');
|
||||
@ -375,7 +297,9 @@ export default function PostDetailScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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()}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
BIN
assets/images/splash.png
Normal file
BIN
assets/images/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
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] });
|
||||
},
|
||||
});
|
||||
}
|
||||
48
services/data/bookmarks.ts
Normal file
48
services/data/bookmarks.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { supabase } from "@/services/supabase"
|
||||
import type { PostgrestError } from "@supabase/supabase-js"
|
||||
|
||||
/**
|
||||
* Retrieves posts that a user has saved/bookmarked
|
||||
*/
|
||||
export async function getBookmarkedPosts(userId: string): Promise<{
|
||||
data: any[] | null
|
||||
error: PostgrestError | null
|
||||
}> {
|
||||
// First get all food_ids that the user has saved
|
||||
const { data: savedFoodIds, error: saveError } = await supabase
|
||||
.from("food_saves")
|
||||
.select("food_id")
|
||||
.eq("user_id", userId)
|
||||
|
||||
if (saveError) {
|
||||
return { data: null, error: saveError }
|
||||
}
|
||||
|
||||
if (!savedFoodIds || savedFoodIds.length === 0) {
|
||||
return { data: [], error: null }
|
||||
}
|
||||
|
||||
// Extract just the IDs
|
||||
const foodIds = savedFoodIds.map((item) => item.food_id)
|
||||
|
||||
// Then fetch the actual food items
|
||||
const { data, error } = await supabase
|
||||
.from("foods")
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
time_to_cook_minutes,
|
||||
skill_level,
|
||||
ingredient_count,
|
||||
calories,
|
||||
image_url,
|
||||
is_shared,
|
||||
created_by,
|
||||
created_at
|
||||
`)
|
||||
.in("id", foodIds)
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
return { data, error }
|
||||
}
|
||||
@ -208,48 +208,54 @@ export const checkUserSaved = async (food_id: string, user_id: string) => {
|
||||
export const getComments = async (food_id: string) => {
|
||||
console.log('Getting comments for food_id:', food_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select(`
|
||||
id,
|
||||
created_at,
|
||||
user_id,
|
||||
food_id,
|
||||
content
|
||||
`)
|
||||
.eq("food_id", food_id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting comments:', error);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("food_comments")
|
||||
.select(`
|
||||
id,
|
||||
created_at,
|
||||
user_id,
|
||||
food_id,
|
||||
content
|
||||
`)
|
||||
.eq("food_id", food_id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error getting comments:', error);
|
||||
return { data: [], error };
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Get unique user IDs from comments
|
||||
const userIds = [...new Set(data.map(comment => comment.user_id))];
|
||||
|
||||
// Fetch profiles for these users
|
||||
const { data: profiles } = await getProfiles(userIds);
|
||||
|
||||
// Add user profiles to comments
|
||||
if (profiles && profiles.length > 0) {
|
||||
const profileMap = profiles.reduce((acc, profile) => {
|
||||
acc[profile.id] = profile;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
// Attach profiles to comments
|
||||
const commentsWithProfiles = data.map(comment => ({
|
||||
...comment,
|
||||
user: profileMap[comment.user_id] || null
|
||||
}));
|
||||
|
||||
console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`);
|
||||
return { data: commentsWithProfiles, error: null };
|
||||
}
|
||||
}
|
||||
|
||||
// If no profiles were found or no comments exist, return the original data
|
||||
console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`);
|
||||
return { data: data?.map(comment => ({ ...comment, user: null })) || [], error: null };
|
||||
} catch (error) {
|
||||
console.error('Error in getComments:', error);
|
||||
return { data: [], error };
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Get unique user IDs from comments
|
||||
const userIds = [...new Set(data.map(comment => comment.user_id))];
|
||||
|
||||
// Fetch profiles for these users
|
||||
const { data: profiles } = await getProfiles(userIds);
|
||||
|
||||
// Add user profiles to comments
|
||||
if (profiles) {
|
||||
const profileMap = profiles.reduce((acc, profile) => {
|
||||
acc[profile.id] = profile;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
// Attach profiles to comments
|
||||
const commentsWithProfiles = data.map(comment => ({
|
||||
...comment,
|
||||
user: profileMap[comment.user_id]
|
||||
}));
|
||||
|
||||
console.log(`Found ${commentsWithProfiles.length} comments for food_id: ${food_id}`);
|
||||
return { data: commentsWithProfiles, error };
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${data?.length || 0} comments for food_id: ${food_id}`);
|
||||
return { data, error };
|
||||
}
|
||||
};
|
||||
48
services/data/likes.ts
Normal file
48
services/data/likes.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { supabase } from "@/services/supabase"
|
||||
import type { PostgrestError } from "@supabase/supabase-js"
|
||||
|
||||
/**
|
||||
* Retrieves posts that a user has liked
|
||||
*/
|
||||
export async function getLikedPosts(userId: string): Promise<{
|
||||
data: any[] | null
|
||||
error: PostgrestError | null
|
||||
}> {
|
||||
// First get all food_ids that the user has liked
|
||||
const { data: likedFoodIds, error: likeError } = await supabase
|
||||
.from("food_likes")
|
||||
.select("food_id")
|
||||
.eq("user_id", userId)
|
||||
|
||||
if (likeError) {
|
||||
return { data: null, error: likeError }
|
||||
}
|
||||
|
||||
if (!likedFoodIds || likedFoodIds.length === 0) {
|
||||
return { data: [], error: null }
|
||||
}
|
||||
|
||||
// Extract just the IDs
|
||||
const foodIds = likedFoodIds.map((item) => item.food_id)
|
||||
|
||||
// Then fetch the actual food items
|
||||
const { data, error } = await supabase
|
||||
.from("foods")
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
time_to_cook_minutes,
|
||||
skill_level,
|
||||
ingredient_count,
|
||||
calories,
|
||||
image_url,
|
||||
is_shared,
|
||||
created_by,
|
||||
created_at
|
||||
`)
|
||||
.in("id", foodIds)
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
return { data, error }
|
||||
}
|
||||
@ -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