This commit is contained in:
Sosokker 2025-05-11 20:04:10 +07:00
commit 53610dd108
8 changed files with 964 additions and 577 deletions

View File

@ -1,150 +1,143 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, Alert } from 'react-native';
import { Feather, FontAwesome } from '@expo/vector-icons';
import { router, useFocusEffect } from 'expo-router';
import { useAuth } from '../../context/auth-context';
import { supabase } from '../../services/supabase';
"use client"
import React, { useState, useEffect } from "react"
import {
View,
Text,
Image,
TextInput,
TouchableOpacity,
FlatList,
SafeAreaView,
ActivityIndicator,
Alert,
} from "react-native"
import { Feather, FontAwesome } from "@expo/vector-icons"
import { router, useFocusEffect } from "expo-router"
import { useAuth } from "../../context/auth-context"
import { supabase } from "../../services/supabase"
import {
useFoods,
useFoodStats,
useFoodCreators,
useUserInteractions,
useLikeMutation,
useSaveMutation
} from '../../hooks/use-foods';
// Categories for filtering
const categories = [
{ id: 'main', name: 'Main dish' },
{ id: 'dessert', name: 'Dessert' },
{ id: 'appetizer', name: 'Appetite' },
];
useSaveMutation,
} from "../../hooks/use-foods"
// Sort options
const sortOptions = [
{ id: 'rating', name: 'Rating', icon: 'star' },
{ id: 'newest', name: 'Newest', icon: 'calendar' },
{ id: 'best', name: 'Best', icon: 'fire' },
];
{ id: "newest", name: "Newest", icon: "calendar" },
{ id: "like_desc", name: "Most Liked", icon: "heart" },
]
export default function ForumScreen() {
const { isAuthenticated } = useAuth();
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedSort, setSelectedSort] = useState('rating');
const { isAuthenticated } = useAuth()
const [currentUserId, setCurrentUserId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState("")
const [selectedSort, setSelectedSort] = useState("newest")
// 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);
const { data } = await supabase.auth.getSession()
const userId = data.session?.user?.id
console.log("Current user ID:", userId)
setCurrentUserId(userId || null)
} else {
setCurrentUserId(null);
setCurrentUserId(null)
}
}
getCurrentUser();
}, [isAuthenticated]);
getCurrentUser()
}, [isAuthenticated])
// Use React Query hooks
const {
data: foods = [],
isLoading: isLoadingFoods,
refetch: refetchFoods
} = useFoods(selectedCategory, searchQuery, selectedSort);
refetch: refetchFoods,
} = useFoods(selectedCategory, searchQuery, selectedSort)
const foodIds = foods.map(food => food.id);
const foodIds = foods.map((food) => food.id)
const {
data: foodStats = {},
isLoading: isLoadingStats
} = useFoodStats(foodIds);
const { data: foodStats = {}, isLoading: isLoadingStats } = useFoodStats(foodIds)
const creatorIds = foods
.filter(food => food.created_by)
.map(food => food.created_by as string);
const creatorIds = foods.filter((food) => food.created_by).map((food) => food.created_by as string)
const {
data: foodCreators = {},
isLoading: isLoadingCreators
} = useFoodCreators(creatorIds);
const { data: foodCreators = {}, isLoading: isLoadingCreators } = useFoodCreators(creatorIds)
const {
data: userInteractions = {},
isLoading: isLoadingInteractions
} = useUserInteractions(foodIds, currentUserId);
const { data: userInteractions = {}, isLoading: isLoadingInteractions } = useUserInteractions(foodIds, currentUserId)
const likeMutation = useLikeMutation();
const saveMutation = useSaveMutation();
const likeMutation = useLikeMutation()
const saveMutation = useSaveMutation()
// Refetch data when the screen comes into focus
useFocusEffect(
React.useCallback(() => {
refetchFoods();
}, [refetchFoods])
);
refetchFoods()
}, [refetchFoods]),
)
const handleSearch = (text: string) => {
setSearchQuery(text);
};
setSearchQuery(text)
}
const navigateToPostDetail = (food: { id: string }) => {
router.push(`/post-detail/${food.id}`);
};
router.push(`/post-detail/${food.id}`)
}
const handleLike = async (food: { id: string }) => {
if (!isAuthenticated || !currentUserId) {
Alert.alert('Authentication Required', 'Please log in to like posts.');
return;
Alert.alert("Authentication Required", "Please log in to like posts.")
return
}
try {
const isLiked = userInteractions[food.id]?.liked || false;
const isLiked = userInteractions[food.id]?.liked || false
likeMutation.mutate({
foodId: food.id,
userId: currentUserId,
isLiked
});
isLiked,
})
} catch (error) {
console.error('Error toggling like:', error);
Alert.alert('Error', 'Failed to update like. Please try again.');
console.error("Error toggling like:", error)
Alert.alert("Error", "Failed to update like. Please try again.")
}
};
}
const handleSave = async (food: { id: string }) => {
if (!isAuthenticated || !currentUserId) {
Alert.alert('Authentication Required', 'Please log in to save posts.');
return;
Alert.alert("Authentication Required", "Please log in to save posts.")
return
}
try {
const isSaved = userInteractions[food.id]?.saved || false;
const isSaved = userInteractions[food.id]?.saved || false
saveMutation.mutate({
foodId: food.id,
userId: currentUserId,
isSaved
});
isSaved,
})
} catch (error) {
console.error('Error toggling save:', error);
Alert.alert('Error', 'Failed to update save. Please try again.');
console.error("Error toggling save:", error)
Alert.alert("Error", "Failed to update save. Please try again.")
}
};
}
const renderFoodItem = ({ item }: { item: any }) => {
// Get stats for this food
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 }
// Get creator profile
const creator = item.created_by ? foodCreators[item.created_by] : null;
const creator = item.created_by ? foodCreators[item.created_by] : null
// Get user interactions
const interactions = userInteractions[item.id] || { liked: false, saved: false };
const interactions = userInteractions[item.id] || { liked: false, saved: false }
return (
<TouchableOpacity
@ -157,32 +150,25 @@ export default function ForumScreen() {
<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: creator.avatar_url }}
className="w-full h-full"
/>
<Image 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() || '?'}
{creator?.username?.charAt(0).toUpperCase() || "?"}
</Text>
</View>
)}
</View>
<Text className="ml-3 text-lg font-bold">
{creator?.username || creator?.full_name || 'Unknown Chef'}
{creator?.username || creator?.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="rounded-lg overflow-hidden mb-4">
<Image
source={{ uri: item.image_url || "/placeholder.svg?height=300&width=500&query=food dish" }}
source={{ uri: item.image_url }}
className="w-full h-48"
resizeMode="cover"
/>
@ -200,22 +186,15 @@ export default function ForumScreen() {
<TouchableOpacity
className="flex-row items-center mr-6"
onPress={(e) => {
e.stopPropagation();
handleLike(item);
e.stopPropagation()
handleLike(item)
}}
>
<Feather
name="heart"
size={22}
color={interactions.liked ? "#E91E63" : "#333"}
/>
<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"
onPress={() => navigateToPostDetail(item)}
>
<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>
@ -223,23 +202,19 @@ export default function ForumScreen() {
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
handleSave(item);
e.stopPropagation()
handleSave(item)
}}
>
<Feather
name="bookmark"
size={22}
color={interactions.saved ? "#ffd60a" : "#333"}
/>
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} />
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
);
};
)
}
const isLoading = isLoadingFoods || isLoadingStats || isLoadingCreators || isLoadingInteractions;
const isLoading = isLoadingFoods || isLoadingStats || isLoadingCreators || isLoadingInteractions
return (
<SafeAreaView className="flex-1 bg-white">
@ -256,23 +231,6 @@ export default function ForumScreen() {
</View>
</View>
{/* Categories */}
<View className="px-4 py-4">
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={categories}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
className={`mr-3 px-6 py-4 rounded-lg ${selectedCategory === item.id ? 'bg-[#ffd60a]' : 'bg-[#ffd60a]'}`}
onPress={() => setSelectedCategory(item.id === selectedCategory ? '' : item.id)}
>
<Text className="text-lg font-medium">{item.name}</Text>
</TouchableOpacity>
)}
/>
</View>
{/* Sort Options */}
<View className="px-4 pb-4">
@ -283,11 +241,15 @@ export default function ForumScreen() {
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
className={`mr-3 px-6 py-3 rounded-lg flex-row items-center ${selectedSort === item.id ? 'bg-[#bb0718]' : 'bg-[#bb0718]'}`}
className={`mr-3 px-6 py-3 rounded-lg flex-row items-center ${selectedSort === item.id ? "bg-[#bb0718]" : "bg-gray-200"}`}
onPress={() => setSelectedSort(item.id)}
>
<Text className="text-lg font-medium text-[#ffd60a] mr-2">{item.name}</Text>
<Feather name={item.icon as any} size={18} color="#ffd60a" />
<Text
className={`text-lg font-medium ${selectedSort === item.id ? "text-[#ffd60a]" : "text-gray-800"} mr-2`}
>
{item.name}
</Text>
<Feather name={item.icon as any} size={18} color={selectedSort === item.id ? "#ffd60a" : "#333"} />
</TouchableOpacity>
)}
/>
@ -308,5 +270,5 @@ export default function ForumScreen() {
/>
)}
</SafeAreaView>
);
)
}

View File

@ -1,6 +1,9 @@
"use client";
import { IconSymbol } from "@/components/ui/IconSymbol";
import { getFoods, insertGenAIResult } from "@/services/data/foods";
import { uploadImageToSupabase } from "@/services/data/imageUpload";
import { getProfile } from "@/services/data/profile";
import { callGenAIonImage } from "@/services/gemini";
import { supabase } from "@/services/supabase";
import { Feather, FontAwesome, Ionicons } from "@expo/vector-icons";
@ -8,7 +11,7 @@ import { useQuery } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import * as ImagePicker from "expo-image-picker";
import { router } from "expo-router";
import React, { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
Alert,
Image,
@ -33,6 +36,50 @@ const useFoodsQuery = () => {
});
};
const useUserProfile = () => {
const [userId, setUserId] = useState<string | null>(null);
const [isLoadingUserId, setIsLoadingUserId] = useState(true);
// Get current user ID
useEffect(() => {
const fetchUserId = async () => {
try {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
setUserId(data?.user?.id || null);
} catch (error) {
console.error("Error fetching user:", error);
} finally {
setIsLoadingUserId(false);
}
};
fetchUserId();
}, []);
// Fetch user profile data
const {
data: profileData,
isLoading: isLoadingProfile,
error: profileError,
} = useQuery({
queryKey: ["profile", userId],
queryFn: async () => {
if (!userId) throw new Error("No user id");
return getProfile(userId);
},
enabled: !!userId,
staleTime: 1000 * 60 * 5, // 5 minutes
});
return {
userId,
profileData: profileData?.data,
isLoading: isLoadingUserId || isLoadingProfile,
error: profileError,
};
};
const runImagePipeline = async (
imageBase64: string,
imageType: string,
@ -111,6 +158,10 @@ export default function HomeScreen() {
: foodsData;
}, [foodsData, searchQuery]);
// Get username or fallback to a default greeting
const username = profileData?.username || profileData?.full_name || "Chef";
const greeting = `Hi! ${username}`;
return (
<SafeAreaView className="flex-1 bg-white">
<StatusBar barStyle="dark-content" />
@ -166,83 +217,93 @@ export default function HomeScreen() {
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
>
<View className="px-6 mb-6">
<View className="flex-row items-center mb-4">
<Text className="text-2xl font-bold mr-2">Show your dishes</Text>
<Feather name="wifi" size={20} color="black" />
</View>
{/* Main content container with consistent padding */}
<View className="px-6">
{/* "Show your dishes" section */}
<View className="mb-6">
<View className="flex-row items-center mb-4">
<Text className="mr-2 text-2xl font-bold">Show your dishes</Text>
<Feather name="wifi" size={20} color="black" />
</View>
<View className="bg-white border border-gray-300 rounded-full mb-6 flex-row items-center px-4 py-3">
<TextInput
className="flex-1"
placeholder="Search..."
value={searchQuery}
onChangeText={setSearchQuery}
/>
<View className="bg-[#ffd60a] p-2 rounded-full">
<Feather name="send" size={20} color="black" />
<View className="flex-row items-center px-4 py-3 mb-6 bg-white border border-gray-300 rounded-full">
<TextInput
className="flex-1"
placeholder="Search..."
value={searchQuery}
onChangeText={setSearchQuery}
/>
<View className="bg-[#ffd60a] p-2 rounded-full">
<Feather name="send" size={20} color="black" />
</View>
</View>
</View>
<View className="flex-row justify-between">
<TouchableOpacity
className="bg-[#ffd60a] p-4 rounded-xl w-[48%]"
onPress={async () => {
const { status } =
await ImagePicker.requestCameraPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Permission needed",
"Please grant camera permissions."
{/* Upload feature section */}
<View className="mb-8">
<View className="flex-row justify-between">
<TouchableOpacity
className="bg-[#ffd60a] p-4 rounded-xl w-[48%]"
onPress={async () => {
const { status } =
await ImagePicker.requestCameraPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Permission needed",
"Please grant camera permissions."
);
return;
}
await handleImageSelection(ImagePicker.launchCameraAsync);
}}
>
<View className="items-center">
<FontAwesome name="camera" size={24} color="black" />
<Text className="mt-2 text-lg font-bold">From Camera</Text>
<Text className="text-sm text-gray-700">
Straight from Camera
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
className="bg-[#f9be25] p-4 rounded-xl w-[48%]"
onPress={async () => {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Permission needed",
"Please grant gallery permissions."
);
return;
}
await handleImageSelection(
ImagePicker.launchImageLibraryAsync
);
return;
}
await handleImageSelection(ImagePicker.launchCameraAsync);
}}
>
<View className="items-center">
<FontAwesome name="camera" size={24} color="black" />
<Text className="text-lg font-bold mt-2">From Camera</Text>
<Text className="text-sm text-gray-700">
Straight from Camera
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
className="bg-[#f9be25] p-4 rounded-xl w-[48%]"
onPress={async () => {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Permission needed",
"Please grant gallery permissions."
);
return;
}
await handleImageSelection(ImagePicker.launchImageLibraryAsync);
}}
>
<View className="items-center">
<Feather name="image" size={24} color="black" />
<Text className="text-lg font-bold mt-2">From Gallery</Text>
<Text className="text-sm text-gray-700">
Straight from Gallery
</Text>
</View>
</TouchableOpacity>
}}
>
<View className="items-center">
<Feather name="image" size={24} color="black" />
<Text className="mt-2 text-lg font-bold">From Gallery</Text>
<Text className="text-sm text-gray-700">
Straight from Gallery
</Text>
</View>
</TouchableOpacity>
</View>
</View>
<View className="px-6 mb-6">
{/* Highlights section */}
<View className="mb-8">
<View className="flex-row items-center mb-4">
<Text className="text-2xl font-bold mr-2">Highlights</Text>
<Text className="mr-2 text-2xl font-bold">Highlights</Text>
<Ionicons name="star-outline" size={20} color="#bb0718" />
</View>
{isLoading ? (
{isLoadingFoods ? (
<Text className="text-center text-gray-500">
Loading highlights...
</Text>
) : error ? (
) : foodsError ? (
<Text className="text-center text-red-600">
Failed to load highlights
</Text>
@ -255,7 +316,7 @@ export default function HomeScreen() {
{filteredFoods.map((food, idx) => (
<TouchableOpacity
key={food.id}
className="bg-white rounded-xl shadow-md flex-1 mr-4"
className="flex-1 mr-4 bg-white shadow-sm rounded-xl"
style={{
marginRight: idx === filteredFoods.length - 1 ? 0 : 12,
}}
@ -268,11 +329,11 @@ export default function HomeScreen() {
resizeMode="cover"
/>
) : (
<View className="w-full h-32 rounded-t-xl bg-gray-200 items-center justify-center">
<View className="items-center justify-center w-full h-32 bg-gray-200 rounded-t-xl">
<Text className="text-gray-400">No Image</Text>
</View>
)}
<View className="flex-1 p-3 justify-between">
<View className="justify-between flex-1 p-3">
<Text
className="text-base font-bold text-[#333] mb-1"
numberOfLines={1}
@ -302,6 +363,7 @@ export default function HomeScreen() {
)}
</View>
</View>
{/* Extra space at bottom */}
<View className="h-20"></View>
</ScrollView>

View File

@ -6,7 +6,7 @@ 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 { useIsFocused } from "@react-navigation/native"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import * as ImagePicker from "expo-image-picker"
import { useEffect, useState } from "react"
@ -23,6 +23,7 @@ import {
} from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import uuid from "react-native-uuid"
import { router } from "expo-router"
// Define the Food type based on your database structure
type Food = {
@ -44,7 +45,6 @@ export default function ProfileScreen() {
const { isAuthenticated } = useAuth()
const isFocused = useIsFocused()
const queryClient = useQueryClient()
const navigation = useNavigation()
const {
data: userData,
@ -125,10 +125,9 @@ export default function ProfileScreen() {
staleTime: 1000 * 60, // 1 minute
})
// Navigate to post detail
// Navigate to post detail using Expo Router instead of navigation API
const handleFoodPress = (foodId: number) => {
// @ts-ignore - Navigation typing might be different in your app
navigation.navigate("post-detail", { id: foodId })
router.push(`/post-detail/${foodId}`)
}
// Refetch data when tab changes
@ -237,7 +236,7 @@ export default function ProfileScreen() {
if (isUserLoading) {
return (
<SafeAreaView className="flex-1 justify-center items-center bg-white">
<SafeAreaView className="items-center justify-center flex-1 bg-white">
<ActivityIndicator size="large" color="#bb0718" />
</SafeAreaView>
)
@ -245,8 +244,8 @@ export default function ProfileScreen() {
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>
<SafeAreaView className="items-center justify-center flex-1 px-4 bg-white">
<Text className="font-bold text-center text-red-600">{userError.message || "Failed to load user data."}</Text>
</SafeAreaView>
)
}
@ -268,12 +267,12 @@ 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="mb-3 font-bold text-red-600">{error.message || error.toString()}</Text>
) : (
<Text className="text-xl font-bold mb-3">{profileData?.data?.username ?? "-"}</Text>
<Text className="mb-3 text-xl font-bold">{profileData?.data?.username ?? "-"}</Text>
)}
<TouchableOpacity
className="bg-red-600 py-2 px-10 rounded-lg"
className="px-10 py-2 bg-red-600 rounded-lg"
onPress={() => {
setEditUsername(profileData?.data?.username ?? "")
setEditImage(profileData?.data?.avatar_url ?? null)
@ -281,49 +280,49 @@ export default function ProfileScreen() {
setModalVisible(true)
}}
>
<Text className="text-white font-bold">Edit</Text>
<Text className="font-bold text-white">Edit</Text>
</TouchableOpacity>
{/* Edit Modal */}
<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>
<View className="items-center justify-center flex-1 bg-gray-50 bg-opacity-40">
<View className="w-11/12 max-w-md p-6 bg-white shadow-md rounded-xl">
<Text className="mb-4 text-lg font-bold text-center">Edit Profile</Text>
<Pressable className="items-center mb-4" onPress={pickImage}>
<Image
source={editImage ? { uri: editImage } : require("@/assets/images/placeholder-food.jpg")}
className="w-24 h-24 rounded-full mb-2 bg-gray-200"
className="w-24 h-24 mb-2 bg-gray-200 rounded-full"
/>
<Text className="text-blue-600 underline">Change Photo</Text>
</Pressable>
<Text className="mb-1 font-medium">Username</Text>
<TextInput
className="border border-gray-300 rounded px-3 py-2 mb-4"
className="px-3 py-2 mb-4 border border-gray-300 rounded"
value={editUsername}
onChangeText={setEditUsername}
placeholder="Enter new username"
/>
{editError && <Text className="text-red-600 mb-2 text-center">{editError}</Text>}
{editError && <Text className="mb-2 text-center text-red-600">{editError}</Text>}
<View className="flex-row justify-between mt-2">
<TouchableOpacity
className="bg-gray-300 py-2 px-6 rounded-lg"
className="px-6 py-2 bg-gray-300 rounded-lg"
onPress={() => setModalVisible(false)}
disabled={editLoading}
>
<Text className="text-gray-700 font-bold">Cancel</Text>
<Text className="font-bold text-gray-700">Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
className="bg-red-600 py-2 px-6 rounded-lg"
className="px-6 py-2 bg-red-600 rounded-lg"
onPress={handleSaveProfile}
disabled={editLoading}
>
{editLoading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text className="text-white font-bold">Save</Text>
<Text className="font-bold text-white">Save</Text>
)}
</TouchableOpacity>
</View>
@ -347,23 +346,23 @@ export default function ProfileScreen() {
{/* Tab Content */}
{isActiveLoading ? (
<View className="flex-1 items-center justify-center py-8">
<View className="items-center justify-center flex-1 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 className="items-center justify-center flex-1 py-8">
<Text className="font-bold text-center text-red-600">{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 className="items-center justify-center flex-1 py-8">
<Text className="font-medium text-center text-gray-400">No items found</Text>
</View>
) : (
<View className="flex-row flex-wrap p-2">
{activeData.data.map((item: Food) => (
<TouchableOpacity
key={item.id}
className="w-1/2 p-2 relative"
className="relative w-1/2 p-2"
onPress={() => handleFoodPress(item.id)}
activeOpacity={0.7}
>
@ -371,7 +370,7 @@ export default function ProfileScreen() {
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">
<View className="absolute px-2 py-1 rounded bottom-4 left-4 bg-opacity-90 bg-white/80">
<Text className="text-[#333] font-bold text-xs">{item.name}</Text>
</View>
</TouchableOpacity>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
"use client"
import { useState } from "react"
import { View, TextInput, TouchableOpacity, Text, Alert } from "react-native"
import { Feather } from "@expo/vector-icons"
interface CommentInputProps {
isAuthenticated: boolean
onSubmit: (text: string) => Promise<void>
isSubmitting: boolean
}
export default function CommentInput({ isAuthenticated, onSubmit, isSubmitting }: CommentInputProps) {
const [commentText, setCommentText] = useState("")
const handleSubmit = async () => {
if (!isAuthenticated || !commentText.trim()) {
if (!isAuthenticated) {
Alert.alert("Authentication Required", "Please log in to comment.")
}
return
}
try {
await onSubmit(commentText.trim())
setCommentText("")
} catch (error) {
console.error("Error submitting comment:", error)
}
}
return (
<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-3 mr-2"
placeholder="Add a comment..."
value={commentText}
onChangeText={setCommentText}
editable={!isSubmitting}
/>
<TouchableOpacity
className={`p-3 rounded-full ${
commentText.trim() && isAuthenticated ? "bg-gradient-to-r from-[#ffd60a] to-[#bb0718]" : "bg-gray-300"
}`}
onPress={handleSubmit}
disabled={isSubmitting || !commentText.trim() || !isAuthenticated}
>
<Feather name="send" size={20} color={commentText.trim() && isAuthenticated ? "white" : "#666"} />
</TouchableOpacity>
</View>
{!isAuthenticated && <Text className="text-center text-sm text-red-500 mt-1">Please log in to comment</Text>}
</View>
)
}

View File

@ -47,6 +47,21 @@ export function useFoods(category?: string, search?: string, sort?: string) {
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));
} else if (sort === 'like_desc') {
// First, we need to get likes count for each food
const likesPromises = sortedData.map(async (food) => {
const { count } = await getLikesCount(food.id);
return { foodId: food.id, likes: count || 0 };
});
const likesData = await Promise.all(likesPromises);
const likesMap = likesData.reduce((acc, item) => {
acc[item.foodId] = item.likes;
return acc;
}, {} as Record<string, number>);
// Sort by likes count (high to low)
sortedData.sort((a, b) => (likesMap[b.id] || 0) - (likesMap[a.id] || 0));
}
return sortedData.map(food => ({

12
package-lock.json generated
View File

@ -29,6 +29,7 @@
"expo-image": "~2.1.7",
"expo-image-manipulator": "~13.1.6",
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "^14.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.6",
"expo-secure-store": "~14.2.3",
@ -6620,6 +6621,17 @@
"react": "*"
}
},
"node_modules/expo-linear-gradient": {
"version": "14.1.4",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.4.tgz",
"integrity": "sha512-bImj2qqIjnl+VHYGnIwan9LxmGvb8e4hFqHpxsPzUiK7Ady7uERrXPhJcyTKTxRf4RL2sQRDpoOKzBYNdQDmuw==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-linking": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.4.tgz",

View File

@ -32,6 +32,7 @@
"expo-image": "~2.1.7",
"expo-image-manipulator": "~13.1.6",
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "^14.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.6",
"expo-secure-store": "~14.2.3",