mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 14:04:08 +01:00
add forum detail
This commit is contained in:
parent
428714e978
commit
a4197987ed
@ -1,120 +1,261 @@
|
|||||||
import { IconSymbol } from "@/components/ui/IconSymbol";
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Image } from "expo-image";
|
import { View, Text, Image, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator } from 'react-native';
|
||||||
import {
|
import { Feather, FontAwesome, Ionicons } from '@expo/vector-icons';
|
||||||
ScrollView,
|
import { router } from 'expo-router';
|
||||||
Text,
|
import { useAuth } from '../../context/auth-context';
|
||||||
TextInput,
|
import { getFoods } from '../../services/data/foods';
|
||||||
TouchableOpacity,
|
import { getLikesCount, getSavesCount, getCommentsCount } from '../../services/data/forum';
|
||||||
View,
|
import { Food } from '../../types/index';
|
||||||
} from "react-native";
|
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
// Categories for filtering
|
||||||
|
const categories = [
|
||||||
|
{ id: 'main', name: 'Main dish' },
|
||||||
|
{ id: 'dessert', name: 'Dessert' },
|
||||||
|
{ id: 'appetizer', name: 'Appetite' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort options
|
||||||
|
const sortOptions = [
|
||||||
|
{ id: 'rating', name: 'Rating', icon: 'star' },
|
||||||
|
{ id: 'newest', name: 'Newest', icon: 'calendar' },
|
||||||
|
{ id: 'best', name: 'Best', icon: 'fire' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function ForumScreen() {
|
export default function ForumScreen() {
|
||||||
return (
|
const { isAuthenticated } = useAuth();
|
||||||
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
<ScrollView className="flex-1">
|
const [foods, setFoods] = useState<Food[]>([]);
|
||||||
{/* Search Bar */}
|
const [loading, setLoading] = useState(true);
|
||||||
<View className="flex-row items-center mx-4 mt-2 mb-4 px-3 h-10 bg-white rounded-full border border-gray-300">
|
const [selectedCategory, setSelectedCategory] = useState('');
|
||||||
<IconSymbol name="magnifyingglass" size={20} color="#FF0000" />
|
const [selectedSort, setSelectedSort] = useState('rating');
|
||||||
<TextInput
|
const [foodStats, setFoodStats] = useState<{[key: string]: {likes: number, saves: number, comments: number}}>({});
|
||||||
className="flex-1 ml-2 text-[#333]"
|
|
||||||
placeholder="Search"
|
useEffect(() => {
|
||||||
placeholderTextColor="#FF0000"
|
loadFoods();
|
||||||
/>
|
}, [selectedCategory, selectedSort]);
|
||||||
</View>
|
|
||||||
|
const loadFoods = async () => {
|
||||||
{/* Category Filters */}
|
setLoading(true);
|
||||||
<View className="flex-row justify-between mx-4 mb-4">
|
try {
|
||||||
<TouchableOpacity className="bg-[#FFCC00] py-3 px-4 rounded-xl flex-1 mx-1 items-center">
|
// In a real app, you would filter by category and sort accordingly
|
||||||
<Text className="font-bold text-[#333]">Main dish</Text>
|
const { data, error } = await getFoods(undefined, true, searchQuery);
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity className="bg-[#FFCC00] py-3 px-4 rounded-xl flex-1 mx-1 items-center">
|
if (error) {
|
||||||
<Text className="font-bold text-[#333]">Dessert</Text>
|
console.error('Error loading foods:', error);
|
||||||
</TouchableOpacity>
|
return;
|
||||||
<TouchableOpacity className="bg-[#FFCC00] py-3 px-4 rounded-xl flex-1 mx-1 items-center">
|
}
|
||||||
<Text className="font-bold text-[#333]">Appetite</Text>
|
|
||||||
</TouchableOpacity>
|
if (data) {
|
||||||
</View>
|
// Sort data based on selectedSort
|
||||||
|
let sortedData = [...data];
|
||||||
{/* Filter Options */}
|
if (selectedSort === 'rating') {
|
||||||
<View className="flex-row mx-4 mb-4">
|
// Assuming higher calories means higher rating for demo purposes
|
||||||
<TouchableOpacity className="bg-red-600 py-2 px-3 rounded-full mr-2 flex-row items-center">
|
sortedData.sort((a, b) => (b.calories ?? 0) - (a.calories ?? 0));
|
||||||
<Text className="text-white font-bold mr-1">Rating</Text>
|
} else if (selectedSort === 'newest') {
|
||||||
<IconSymbol name="star.fill" size={16} color="#FFCC00" />
|
sortedData.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||||
</TouchableOpacity>
|
} else if (selectedSort === 'best') {
|
||||||
<TouchableOpacity className="bg-red-600 py-2 px-3 rounded-full mr-2 flex-row items-center">
|
// Assuming higher ingredient_count means better for demo purposes
|
||||||
<Text className="text-white font-bold mr-1">Newest</Text>
|
sortedData.sort((a, b) => (b.ingredient_count ?? 0) - (a.ingredient_count ?? 0));
|
||||||
<IconSymbol name="calendar" size={16} color="#FFFFFF" />
|
}
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity className="bg-red-600 py-2 px-3 rounded-full mr-2 flex-row items-center">
|
setFoods(sortedData.map(food => ({
|
||||||
<Text className="text-white font-bold mr-1">Best</Text>
|
...food,
|
||||||
<IconSymbol name="flame.fill" size={16} color="#FFCC00" />
|
description: food.description || '', // Ensure description is always a string
|
||||||
</TouchableOpacity>
|
ingredient_count: food.ingredient_count ?? 0, // Ensure ingredient_count is always a number
|
||||||
</View>
|
calories: food.calories ?? 0, // Ensure calories is always a number
|
||||||
|
image_url: food.image_url || '', // Ensure image_url is always a string
|
||||||
{/* Post */}
|
})));
|
||||||
<View className="mx-4 mb-4 bg-white rounded-xl overflow-hidden border border-[#EEEEEE]">
|
|
||||||
{/* User Info */}
|
// Load stats for each food
|
||||||
<View className="flex-row justify-between items-center px-3 py-2">
|
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);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (text: string) => {
|
||||||
|
setSearchQuery(text);
|
||||||
|
// Debounce search for better performance
|
||||||
|
setTimeout(() => {
|
||||||
|
loadFoods();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToPostDetail = (food: Food) => {
|
||||||
|
router.push({
|
||||||
|
pathname: '/post-detail',
|
||||||
|
params: { id: food.id }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFoodItem = ({ item }: { item: Food }) => {
|
||||||
|
// Get stats for this food
|
||||||
|
const stats = foodStats[item.id] || { likes: 0, saves: 0, comments: 0 };
|
||||||
|
|
||||||
|
// Mock data for UI elements not in the Food type
|
||||||
|
const username = 'Mr. Chef';
|
||||||
|
const rating = 4.2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
className="mb-6 bg-white rounded-lg overflow-hidden"
|
||||||
|
onPress={() => navigateToPostDetail(item)}
|
||||||
|
>
|
||||||
|
<View className="p-4">
|
||||||
|
{/* User info and rating */}
|
||||||
|
<View className="flex-row justify-between items-center mb-4">
|
||||||
<View className="flex-row items-center">
|
<View className="flex-row items-center">
|
||||||
<View className="w-8 h-8 rounded-full bg-gray-200 justify-center items-center mr-2">
|
<View className="w-12 h-12 bg-gray-200 rounded-full overflow-hidden">
|
||||||
<IconSymbol
|
<Image
|
||||||
name="person.circle.fill"
|
source={{ uri: "/placeholder.svg?height=48&width=48&query=user avatar" }}
|
||||||
size={24}
|
className="w-full h-full"
|
||||||
color="#888888"
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text className="font-bold text-[#333]">Mr. Chef</Text>
|
<Text className="ml-3 text-lg font-bold">{username}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-row items-center">
|
<View className="flex-row items-center">
|
||||||
<Text className="mr-1 font-bold text-[#333]">4.2</Text>
|
<Text className="text-lg font-bold mr-1">{rating}</Text>
|
||||||
<IconSymbol name="star.fill" size={16} color="#FFCC00" />
|
<FontAwesome name="star" size={20} color="#ffd60a" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Post Image */}
|
{/* Food image */}
|
||||||
<Image
|
<View className="rounded-lg overflow-hidden mb-4">
|
||||||
source={require("@/assets/images/placeholder-food.jpg")}
|
<Image
|
||||||
className="w-full h-[200px]"
|
source={{ uri: item.image_url || "/placeholder.svg?height=300&width=500&query=food dish" }}
|
||||||
resizeMode="cover"
|
className="w-full h-48"
|
||||||
/>
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
{/* Post Content */}
|
|
||||||
<View className="p-3">
|
|
||||||
<Text className="text-base font-bold mb-1 text-[#333]">
|
|
||||||
Kajjecaw
|
|
||||||
</Text>
|
|
||||||
<Text className="text-[#666] text-sm">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut at
|
|
||||||
hendrerit enim. Etiam lacinia mi nec nunc ornare, vitae tempus leo
|
|
||||||
aliquet...
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Post Actions */}
|
{/* Food title and description */}
|
||||||
<View className="flex-row border-t border-[#EEEEEE] py-2 px-3">
|
<View>
|
||||||
<TouchableOpacity className="flex-row items-center mr-4">
|
<Text className="text-2xl font-bold mb-2">{item.name}</Text>
|
||||||
<IconSymbol
|
<Text className="text-gray-700 mb-4">{item.description}</Text>
|
||||||
name="arrowshape.turn.up.left.fill"
|
</View>
|
||||||
size={16}
|
|
||||||
color="#888888"
|
{/* Interaction buttons */}
|
||||||
/>
|
<View className="flex-row justify-between">
|
||||||
<Text className="ml-1 text-[#888]">3</Text>
|
<View className="flex-row items-center">
|
||||||
</TouchableOpacity>
|
<TouchableOpacity className="flex-row items-center mr-6">
|
||||||
<TouchableOpacity className="flex-row items-center mr-4">
|
<Feather name="message-square" size={22} color="#333" />
|
||||||
<IconSymbol name="text.bubble.fill" size={16} color="#888888" />
|
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||||
<Text className="ml-1 text-[#888]">2</Text>
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity className="flex-row items-center mr-4">
|
<TouchableOpacity className="flex-row items-center mr-6">
|
||||||
<IconSymbol name="heart.fill" size={16} color="#888888" />
|
<Feather name="message-circle" size={22} color="#333" />
|
||||||
<Text className="ml-1 text-[#888]">2</Text>
|
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity className="flex-row items-center mr-4">
|
|
||||||
<IconSymbol name="bookmark.fill" size={16} color="#888888" />
|
<TouchableOpacity className="flex-row items-center">
|
||||||
|
<Feather name="heart" size={22} color="#333" />
|
||||||
|
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity>
|
||||||
|
<Feather name="bookmark" size={22} color="#333" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<View className="px-4 pt-4 pb-2">
|
||||||
|
<View className="flex-row items-center bg-gray-100 rounded-full px-4 py-2">
|
||||||
|
<Feather name="search" size={20} color="#E91E63" />
|
||||||
|
<TextInput
|
||||||
|
className="flex-1 ml-2 text-base"
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchQuery}
|
||||||
|
onChangeText={handleSearch}
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
data={sortOptions}
|
||||||
|
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]'}`}
|
||||||
|
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" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Food Posts */}
|
||||||
|
{loading ? (
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<ActivityIndicator size="large" color="#ffd60a" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={foods}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
renderItem={renderFoodItem}
|
||||||
|
contentContainerStyle={{ padding: 16 }}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
361
app/post-detail.tsx
Normal file
361
app/post-detail.tsx
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform } from 'react-native';
|
||||||
|
import { Feather, FontAwesome } from '@expo/vector-icons';
|
||||||
|
import { useLocalSearchParams, router } from 'expo-router';
|
||||||
|
import { useAuth } from '../context/auth-context';
|
||||||
|
import { getFoods, getIngredients, getNutrients } from '../services/data/foods';
|
||||||
|
import {
|
||||||
|
createLike,
|
||||||
|
deleteLike,
|
||||||
|
createSave,
|
||||||
|
deleteSave,
|
||||||
|
getComments,
|
||||||
|
createComment,
|
||||||
|
getLikesCount,
|
||||||
|
getSavesCount,
|
||||||
|
getCommentsCount,
|
||||||
|
checkUserLiked,
|
||||||
|
checkUserSaved
|
||||||
|
} from '../services/data/forum';
|
||||||
|
import { Food, Ingredient, Nutrient, FoodComment } from '../types/index';
|
||||||
|
|
||||||
|
export default function PostDetailScreen() {
|
||||||
|
const { id } = useLocalSearchParams();
|
||||||
|
const authContext = useAuth();
|
||||||
|
const { isAuthenticated } = authContext || {}; // Adjust based on the actual structure of AuthContextType
|
||||||
|
const [food, setFood] = useState<Food | 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock data for UI elements
|
||||||
|
const username = 'Mr. Chef';
|
||||||
|
const rating = 4.2;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadFoodDetails();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadFoodDetails = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Get food details
|
||||||
|
const { data: foodData, error: foodError } = await getFoods(undefined, undefined, undefined, 1, 0);
|
||||||
|
|
||||||
|
if (foodError) {
|
||||||
|
console.error('Error loading food:', foodError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foodData && foodData.length > 0) {
|
||||||
|
setFood({
|
||||||
|
...foodData[0],
|
||||||
|
description: foodData[0].description || '', // Ensure description is always a string
|
||||||
|
ingredient_count: foodData[0].ingredient_count ?? 0, // Provide default value for ingredient_count
|
||||||
|
calories: foodData[0].calories ?? 0, // Provide default value for calories
|
||||||
|
image_url: foodData[0].image_url || '', // Provide default value for image_url
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get ingredients
|
||||||
|
const { data: ingredientsData, error: ingredientsError } = await getIngredients(foodData[0].id);
|
||||||
|
|
||||||
|
if (!ingredientsError && ingredientsData) {
|
||||||
|
setIngredients(ingredientsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get nutrients
|
||||||
|
const { data: nutrientsData, error: nutrientsError } = await getNutrients(foodData[0].id);
|
||||||
|
|
||||||
|
if (!nutrientsError && nutrientsData) {
|
||||||
|
setNutrients(nutrientsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get comments
|
||||||
|
const { data: commentsData, error: commentsError } = await getComments(foodData[0].id);
|
||||||
|
|
||||||
|
if (!commentsError && commentsData) {
|
||||||
|
setComments(commentsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stats
|
||||||
|
const [likesRes, savesRes, commentsRes] = await Promise.all([
|
||||||
|
getLikesCount(foodData[0].id),
|
||||||
|
getSavesCount(foodData[0].id),
|
||||||
|
getCommentsCount(foodData[0].id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
likes: likesRes.count || 0,
|
||||||
|
saves: savesRes.count || 0,
|
||||||
|
comments: commentsRes.count || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if user has liked/saved
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const userId = 'current-user-id'; // Replace with actual user ID
|
||||||
|
|
||||||
|
const [likedRes, savedRes] = await Promise.all([
|
||||||
|
checkUserLiked(foodData[0].id, userId),
|
||||||
|
checkUserSaved(foodData[0].id, userId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setIsLiked(!!likedRes.data);
|
||||||
|
setIsSaved(!!savedRes.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
if (!authContext.isAuthenticated || !food) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = 'current-user-id'; // Replace with actual user ID
|
||||||
|
|
||||||
|
if (isLiked) {
|
||||||
|
await deleteLike(food.id, userId);
|
||||||
|
setIsLiked(false);
|
||||||
|
setStats(prev => ({ ...prev, likes: Math.max(0, prev.likes - 1) }));
|
||||||
|
} else {
|
||||||
|
await createLike(food.id, userId);
|
||||||
|
setIsLiked(true);
|
||||||
|
setStats(prev => ({ ...prev, likes: prev.likes + 1 }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling like:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!authContext.isAuthenticated || !food) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = 'current-user-id'; // Replace with actual user ID
|
||||||
|
|
||||||
|
if (isSaved) {
|
||||||
|
await deleteSave(food.id, userId);
|
||||||
|
setIsSaved(false);
|
||||||
|
setStats(prev => ({ ...prev, saves: Math.max(0, prev.saves - 1) }));
|
||||||
|
} else {
|
||||||
|
await createSave(food.id, userId);
|
||||||
|
setIsSaved(true);
|
||||||
|
setStats(prev => ({ ...prev, saves: prev.saves + 1 }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling save:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitComment = async () => {
|
||||||
|
if (!authContext.isAuthenticated || !food || !commentText.trim()) return;
|
||||||
|
|
||||||
|
setSubmittingComment(true);
|
||||||
|
try {
|
||||||
|
const userId = 'current-user-id'; // Replace with actual user ID
|
||||||
|
|
||||||
|
await createComment(food.id, userId, commentText.trim());
|
||||||
|
|
||||||
|
// Refresh comments
|
||||||
|
const { data: commentsData } = await getComments(food.id);
|
||||||
|
|
||||||
|
if (commentsData) {
|
||||||
|
setComments(commentsData);
|
||||||
|
setStats(prev => ({ ...prev, comments: prev.comments + 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommentText('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting comment:', error);
|
||||||
|
} finally {
|
||||||
|
setSubmittingComment(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<ActivityIndicator size="large" color="#ffd60a" />
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!food) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<Text className="text-lg">Post not found</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="mt-4 bg-[#ffd60a] px-6 py-3 rounded-lg"
|
||||||
|
onPress={() => router.back()}
|
||||||
|
>
|
||||||
|
<Text className="font-bold">Go Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
<ScrollView className="flex-1">
|
||||||
|
{/* Header */}
|
||||||
|
<View className="flex-row justify-between items-center px-4 py-3">
|
||||||
|
<TouchableOpacity
|
||||||
|
className="bg-[#ffd60a] p-3 rounded-lg"
|
||||||
|
onPress={() => router.back()}
|
||||||
|
>
|
||||||
|
<Feather name="arrow-left" size={24} color="#bb0718" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Text className="text-2xl font-bold">Post</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity>
|
||||||
|
<Feather name="more-horizontal" size={24} color="#000" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* User info and rating */}
|
||||||
|
<View className="flex-row justify-between items-center px-4 py-3">
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<View className="w-12 h-12 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<Image
|
||||||
|
source={{ uri: "/placeholder.svg?height=48&width=48&query=user avatar" }}
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text className="ml-3 text-lg font-bold">{username}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<Text className="text-lg font-bold mr-1">{rating}</Text>
|
||||||
|
<FontAwesome name="star" size={20} color="#ffd60a" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Food image */}
|
||||||
|
<View className="px-4 mb-4">
|
||||||
|
<Image
|
||||||
|
source={{ uri: food.image_url || "/placeholder.svg?height=300&width=500&query=food dish" }}
|
||||||
|
className="w-full h-64 rounded-lg"
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Food title and description */}
|
||||||
|
<View className="px-4 mb-2">
|
||||||
|
<Text className="text-2xl font-bold mb-2">{food.name}</Text>
|
||||||
|
<Text className="text-gray-700 mb-2">{food.description}</Text>
|
||||||
|
<Text className="text-gray-500 text-sm">09:41 - 4/3/25</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Interaction buttons */}
|
||||||
|
<View className="flex-row justify-between px-4 py-4 border-b border-gray-200">
|
||||||
|
<TouchableOpacity className="flex-row items-center">
|
||||||
|
<Feather name="message-square" size={22} color="#333" />
|
||||||
|
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity className="flex-row items-center">
|
||||||
|
<Feather name="message-circle" size={22} color="#333" />
|
||||||
|
<Text className="ml-2 text-lg">{stats.comments}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
className="flex-row items-center"
|
||||||
|
onPress={handleLike}
|
||||||
|
>
|
||||||
|
<Feather name={isLiked ? "heart" : "heart"} size={22} color={isLiked ? "#E91E63" : "#333"} />
|
||||||
|
<Text className="ml-2 text-lg">{stats.likes}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity onPress={handleSave}>
|
||||||
|
<Feather name={isSaved ? "bookmark" : "bookmark"} size={22} color={isSaved ? "#ffd60a" : "#333"} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Reviews section */}
|
||||||
|
<TouchableOpacity
|
||||||
|
className="flex-row items-center px-4 py-4 border-b border-gray-200"
|
||||||
|
onPress={() => setShowReviews(!showReviews)}
|
||||||
|
>
|
||||||
|
<Text className="text-xl font-bold">Review</Text>
|
||||||
|
<Feather name={showReviews ? "chevron-up" : "chevron-down"} size={20} color="#333" className="ml-2" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{showReviews && (
|
||||||
|
<View className="px-4 py-2">
|
||||||
|
{comments.length > 0 ? (
|
||||||
|
comments.map((comment) => (
|
||||||
|
<View key={comment.id} className="py-4 border-b border-gray-100">
|
||||||
|
<View className="flex-row items-center mb-2">
|
||||||
|
<View className="w-10 h-10 bg-gray-200 rounded-full overflow-hidden mr-3">
|
||||||
|
<Image
|
||||||
|
source={{ uri: "/placeholder.svg?height=40&width=40&query=user avatar" }}
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="flex-row items-center justify-between flex-1">
|
||||||
|
<Text className="font-bold">{comment.user_id}</Text>
|
||||||
|
<Text className="text-gray-500 text-xs">
|
||||||
|
{new Date(comment.created_at).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text>{comment.content}</Text>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className="py-4 text-gray-500">No reviews yet. Be the first to comment!</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bottom spacing */}
|
||||||
|
<View className="h-24" />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Comment input */}
|
||||||
|
<View className="px-4 py-3 border-t border-gray-200 bg-white">
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<TextInput
|
||||||
|
className="flex-1 bg-gray-100 rounded-full px-4 py-2 mr-2"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
value={commentText}
|
||||||
|
onChangeText={setCommentText}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="bg-[#ffd60a] p-2 rounded-full"
|
||||||
|
onPress={handleSubmitComment}
|
||||||
|
disabled={submittingComment || !commentText.trim()}
|
||||||
|
>
|
||||||
|
<Feather name="send" size={20} color="#bb0718" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -37,7 +37,7 @@ export default function SignupScreen() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Only pass email and password to signup, as per new auth-context
|
// Only pass email and password to signup, as per new auth-context
|
||||||
await signup(email, password);
|
await signup(name, email);
|
||||||
// Optionally, save name to profile after signup here in the future
|
// Optionally, save name to profile after signup here in the future
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { supabase } from "@/services/supabase";
|
import { supabase } from "@/services/supabase";
|
||||||
|
import { getProfile, getProfiles } from "./profile";
|
||||||
|
|
||||||
export const createLike = async (food_id: string, user_id: string) => {
|
export const createLike = async (food_id: string, user_id: string) => {
|
||||||
const { data, error } = await supabase.from("food_likes").insert({ food_id, user_id });
|
const { data, error } = await supabase.from("food_likes").insert({ food_id, user_id });
|
||||||
@ -19,3 +20,92 @@ export const deleteSave = async (food_id: string, user_id: string) => {
|
|||||||
const { data, error } = await supabase.from("food_saves").delete().eq("food_id", food_id).eq("user_id", user_id);
|
const { data, error } = await supabase.from("food_saves").delete().eq("food_id", food_id).eq("user_id", user_id);
|
||||||
return { data, error };
|
return { data, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createComment = async (food_id: string, user_id: string, content: string) => {
|
||||||
|
const { data, error } = await supabase.from("food_comments").insert({ food_id, user_id, content });
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComments = async (food_id: string) => {
|
||||||
|
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 (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]
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { data: commentsWithProfiles, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLikesCount = async (food_id: string) => {
|
||||||
|
const { count, error } = await supabase
|
||||||
|
.from("food_likes")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("food_id", food_id);
|
||||||
|
return { count, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSavesCount = async (food_id: string) => {
|
||||||
|
const { count, error } = await supabase
|
||||||
|
.from("food_saves")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("food_id", food_id);
|
||||||
|
return { count, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCommentsCount = async (food_id: string) => {
|
||||||
|
const { count, error } = await supabase
|
||||||
|
.from("food_comments")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("food_id", food_id);
|
||||||
|
return { count, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkUserLiked = async (food_id: string, user_id: string) => {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("food_likes")
|
||||||
|
.select("*")
|
||||||
|
.eq("food_id", food_id)
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.single();
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkUserSaved = async (food_id: string, user_id: string) => {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("food_saves")
|
||||||
|
.select("*")
|
||||||
|
.eq("food_id", food_id)
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.single();
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
@ -10,6 +10,8 @@ export async function getProfile(userId: string): Promise<{
|
|||||||
updated_at: any;
|
updated_at: any;
|
||||||
username: any;
|
username: any;
|
||||||
avatar_url: any;
|
avatar_url: any;
|
||||||
|
full_name?: any;
|
||||||
|
website?: any;
|
||||||
} | null;
|
} | null;
|
||||||
error: PostgrestError | null;
|
error: PostgrestError | null;
|
||||||
}> {
|
}> {
|
||||||
@ -19,7 +21,9 @@ export async function getProfile(userId: string): Promise<{
|
|||||||
id,
|
id,
|
||||||
updated_at,
|
updated_at,
|
||||||
username,
|
username,
|
||||||
avatar_url
|
full_name,
|
||||||
|
avatar_url,
|
||||||
|
website
|
||||||
`)
|
`)
|
||||||
.eq('id', userId)
|
.eq('id', userId)
|
||||||
.single()
|
.single()
|
||||||
@ -33,7 +37,9 @@ export async function getProfile(userId: string): Promise<{
|
|||||||
export async function updateProfile(
|
export async function updateProfile(
|
||||||
userId: string,
|
userId: string,
|
||||||
username?: string | null,
|
username?: string | null,
|
||||||
avatar_url?: string | null
|
avatar_url?: string | null,
|
||||||
|
full_name?: string | null,
|
||||||
|
website?: string | null
|
||||||
): Promise<{ data: any; error: PostgrestError | null }> {
|
): Promise<{ data: any; error: PostgrestError | null }> {
|
||||||
const updateData: Record<string, string | null> = {}
|
const updateData: Record<string, string | null> = {}
|
||||||
if (username !== undefined && username !== null) {
|
if (username !== undefined && username !== null) {
|
||||||
@ -42,6 +48,12 @@ export async function updateProfile(
|
|||||||
if (avatar_url !== undefined && avatar_url !== null) {
|
if (avatar_url !== undefined && avatar_url !== null) {
|
||||||
updateData.avatar_url = avatar_url
|
updateData.avatar_url = avatar_url
|
||||||
}
|
}
|
||||||
|
if (full_name !== undefined && full_name !== null) {
|
||||||
|
updateData.full_name = full_name
|
||||||
|
}
|
||||||
|
if (website !== undefined && website !== null) {
|
||||||
|
updateData.website = website
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
@ -52,3 +64,28 @@ export async function updateProfile(
|
|||||||
|
|
||||||
return { data, error }
|
return { data, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets multiple user profiles by their IDs
|
||||||
|
*/
|
||||||
|
export async function getProfiles(userIds: string[]): Promise<{
|
||||||
|
data: {
|
||||||
|
id: any;
|
||||||
|
username: any;
|
||||||
|
avatar_url: any;
|
||||||
|
full_name?: any;
|
||||||
|
}[] | null;
|
||||||
|
error: PostgrestError | null;
|
||||||
|
}> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
avatar_url
|
||||||
|
`)
|
||||||
|
.in('id', userIds)
|
||||||
|
|
||||||
|
return { data, error }
|
||||||
|
}
|
||||||
118
types/index.ts
Normal file
118
types/index.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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; // Adding content field for comments
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
rating?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Profile {
|
||||||
|
id: string;
|
||||||
|
updated_at: string;
|
||||||
|
username: string;
|
||||||
|
full_name?: string;
|
||||||
|
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