diff --git a/app/post-detail/[id].tsx b/app/post-detail/[id].tsx index 6a3dc80..f2f233c 100644 --- a/app/post-detail/[id].tsx +++ b/app/post-detail/[id].tsx @@ -1,339 +1,510 @@ -import React, { useState, useEffect } from 'react'; -import { View, Text, Image, TouchableOpacity, ScrollView, SafeAreaView, ActivityIndicator, TextInput, KeyboardAvoidingView, Platform, Alert } from 'react-native'; -import { Feather, FontAwesome } from '@expo/vector-icons'; -import { useLocalSearchParams, router } from 'expo-router'; -import { useAuth } from '../../context/auth-context'; -import { supabase } from '../../services/supabase'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { getFoods, getIngredients, getNutrients } from '../../services/data/foods'; -import { - createLike, - deleteLike, - createSave, - deleteSave, - getComments, +"use client" + +import { useState, useEffect, useRef } from "react" +import { + View, + Text, + Image, + TouchableOpacity, + ScrollView, + ActivityIndicator, + FlatList, + Alert, + TextInput, + KeyboardAvoidingView, + Platform, + Keyboard, +} from "react-native" +import { Feather, MaterialCommunityIcons, Ionicons } 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 { + getComments, createComment, getLikesCount, getSavesCount, getCommentsCount, checkUserLiked, - checkUserSaved -} from '../../services/data/forum'; -import { getProfile } from '../../services/data/profile'; -import { Food, Ingredient, Nutrient, FoodComment, Profile } from '../../types/index'; -import { queryKeys, useLikeMutation, useSaveMutation } from '../../hooks/use-foods'; + checkUserSaved, +} from "../../services/data/forum" +import { getProfile } from "../../services/data/profile" +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(null); - const [commentText, setCommentText] = useState(''); - const [submittingComment, setSubmittingComment] = useState(false); - const [showReviews, setShowReviews] = useState(true); - + const params = useLocalSearchParams() + const foodId = typeof params.id === "string" ? params.id : "" + const queryClient = useQueryClient() + const scrollViewRef = useRef(null) + + console.log("Post detail screen - Food ID:", foodId) + + const { isAuthenticated } = useAuth() + const [currentUserId, setCurrentUserId] = useState(null) + const [commentText, setCommentText] = useState("") + const [submittingComment, setSubmittingComment] = useState(false) + const [showReviews, setShowReviews] = useState(true) + const [keyboardVisible, setKeyboardVisible] = useState(false) + + // Listen for keyboard events + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", () => { + setKeyboardVisible(true) + }) + + const keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", () => { + setKeyboardVisible(false) + }) + + return () => { + keyboardDidShowListener.remove() + keyboardDidHideListener.remove() + } + }, []) + + // Recipe info cards data + const recipeInfoCards = [ + { + id: "cooking_time", + title: "Cooking Time", + icon: ( + + + + ), + value: (food: any) => food.time_to_cook_minutes, + unit: (food: any) => (food.time_to_cook_minutes === 1 ? "minute" : "minutes"), + gradient: ["#fff8e1", "#fffde7"], + valueColor: "#bb0718", + }, + { + id: "skill_level", + title: "Skill Level", + icon: ( + + + + ), + value: (food: any) => food.skill_level, + unit: () => "", + gradient: ["#e8f5e9", "#f1f8e9"], + valueColor: "", + customContent: (food: any) => ( + + + {food.skill_level} + + {renderSkillLevelDots(food.skill_level)} + + ), + }, + { + id: "ingredients", + title: "Ingredients", + icon: ( + + + + ), + value: (food: any) => food.ingredient_count, + unit: (food: any) => (food.ingredient_count === 1 ? "item" : "items"), + gradient: ["#e3f2fd", "#e8f5e9"], + valueColor: "#2196F3", + }, + { + id: "calories", + title: "Calories", + icon: ( + + + + ), + value: (food: any) => food.calories, + unit: () => "kcal", + gradient: ["#ffebee", "#fff8e1"], + valueColor: "#F44336", + }, + ] + // 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]) + // Fetch food details - const { + const { data: food, isLoading: isLoadingFood, - error: foodError + 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; - + const { data, error } = await supabase.from("foods").select("*").eq("id", foodId).single() + + if (error) throw error + return { ...data, - description: data.description || '', + description: data.description || "", ingredient_count: data.ingredient_count ?? 0, calories: data.calories ?? 0, - image_url: data.image_url || '', - }; + time_to_cook_minutes: data.time_to_cook_minutes ?? 0, + skill_level: data.skill_level || "Easy", + image_url: data.image_url || "", + } }, enabled: !!foodId, - }); - + }) + // Fetch food creator - const { - data: foodCreator, - isLoading: isLoadingCreator - } = useQuery({ - queryKey: ['food-creator', food?.created_by], + 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; + 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 { + const { data: stats = { likes: 0, saves: 0, comments: 0 }, isLoading: isLoadingStats, - refetch: refetchStats + refetch: refetchStats, } = useQuery({ - queryKey: ['food-stats', foodId], + queryKey: ["food-stats", foodId], queryFn: async () => { const [likesRes, savesRes, commentsRes] = await Promise.all([ getLikesCount(foodId), getSavesCount(foodId), - getCommentsCount(foodId) - ]); - + getCommentsCount(foodId), + ]) + return { likes: likesRes.count || 0, saves: savesRes.count || 0, - comments: commentsRes.count || 0 - }; + comments: commentsRes.count || 0, + } }, enabled: !!foodId, - }); - + }) + // Fetch user interactions - const { + const { data: interactions = { liked: false, saved: false }, isLoading: isLoadingInteractions, - refetch: refetchInteractions + refetch: refetchInteractions, } = useQuery({ - queryKey: ['user-interactions', foodId, currentUserId], + queryKey: ["user-interactions", foodId, currentUserId], queryFn: async () => { - if (!currentUserId) return { liked: false, saved: false }; - + if (!currentUserId) return { liked: false, saved: false } + const [likedRes, savedRes] = await Promise.all([ checkUserLiked(foodId, currentUserId), - checkUserSaved(foodId, currentUserId) - ]); - + checkUserSaved(foodId, currentUserId), + ]) + return { liked: !!likedRes.data, - saved: !!savedRes.data - }; + saved: !!savedRes.data, + } }, enabled: !!foodId && !!currentUserId, - }); - + }) + // Fetch comments - const { + const { data: comments = [], isLoading: isLoadingComments, - refetch: refetchComments + refetch: refetchComments, } = useQuery({ queryKey: queryKeys.foodComments(foodId), queryFn: async () => { - const { data, error } = await getComments(foodId); - - if (error) throw error; - - return data || []; + const { data, error } = await getComments(foodId) + + if (error) throw error + + return data || [] }, enabled: !!foodId, - }); - + }) + // Set up mutations - const likeMutation = useLikeMutation(); - const saveMutation = useSaveMutation(); - + 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); + 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(''); + queryClient.invalidateQueries({ queryKey: queryKeys.foodComments(foodId) }) + queryClient.invalidateQueries({ queryKey: ["food-stats", foodId] }) + setCommentText("") + Keyboard.dismiss() }, - }); - + }) + // Set up real-time subscription for comments useEffect(() => { - if (!foodId) return; - - console.log(`Setting up real-time subscription for comments on food_id: ${foodId}`); - + if (!foodId) return + + console.log(`Setting up real-time subscription for comments on food_id: ${foodId}`) + const subscription = supabase .channel(`food_comments:${foodId}`) - .on('postgres_changes', { - event: '*', - schema: 'public', - table: 'food_comments', - filter: `food_id=eq.${foodId}` - }, () => { - console.log('Comment change detected, refreshing comments'); - refetchComments(); - refetchStats(); - }) - .subscribe(); - + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "food_comments", + filter: `food_id=eq.${foodId}`, + }, + () => { + console.log("Comment change detected, refreshing comments") + refetchComments() + refetchStats() + }, + ) + .subscribe() + return () => { - supabase.removeChannel(subscription); - }; - }, [foodId, refetchComments, refetchStats]); - + supabase.removeChannel(subscription) + } + }, [foodId, refetchComments, refetchStats]) + // Set up real-time subscription for likes and saves useEffect(() => { - if (!foodId) return; - + if (!foodId) return + const likesSubscription = supabase .channel(`food_likes:${foodId}`) - .on('postgres_changes', { - event: '*', - schema: 'public', - table: 'food_likes', - filter: `food_id=eq.${foodId}` - }, () => { - console.log('Like change detected, refreshing stats and interactions'); - refetchStats(); - refetchInteractions(); - }) - .subscribe(); - + .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 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(); - + .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() + return () => { - supabase.removeChannel(likesSubscription); - supabase.removeChannel(savesSubscription); - }; - }, [foodId, refetchStats, refetchInteractions]); - + supabase.removeChannel(likesSubscription) + supabase.removeChannel(savesSubscription) + } + }, [foodId, refetchStats, refetchInteractions]) + const handleLike = async () => { if (!isAuthenticated || !currentUserId || !food) { - Alert.alert('Authentication Required', 'Please log in to like posts.'); - return; + Alert.alert("Authentication Required", "Please log in to like posts.") + return } - + try { likeMutation.mutate({ foodId, userId: currentUserId, - isLiked: interactions.liked - }); + isLiked: interactions.liked, + }) } 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 () => { if (!isAuthenticated || !currentUserId || !food) { - Alert.alert('Authentication Required', 'Please log in to save posts.'); - return; + Alert.alert("Authentication Required", "Please log in to save posts.") + return } - + try { saveMutation.mutate({ foodId, userId: currentUserId, - isSaved: interactions.saved - }); + isSaved: interactions.saved, + }) } 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 handleSubmitComment = async () => { if (!isAuthenticated || !currentUserId || !foodId || !commentText.trim()) { if (!isAuthenticated || !currentUserId) { - Alert.alert('Authentication Required', 'Please log in to comment.'); + Alert.alert("Authentication Required", "Please log in to comment.") } - return; + return } - - setSubmittingComment(true); + + setSubmittingComment(true) try { await commentMutation.mutateAsync({ foodId, userId: currentUserId, - content: commentText.trim() - }); + content: commentText.trim(), + }) } catch (error) { - console.error('Error submitting comment:', error); - Alert.alert('Error', 'Failed to submit comment. Please try again.'); + console.error("Error submitting comment:", error) + Alert.alert("Error", "Failed to submit comment. Please try again.") } finally { - setSubmittingComment(false); + setSubmittingComment(false) } - }; - - const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments; - + } + + // Helper function to get skill level color + const getSkillLevelColor = (level: string) => { + switch (level) { + case "Easy": + return "#4CAF50" // Green + case "Medium": + return "#FFC107" // Amber + case "Hard": + return "#F44336" // Red + default: + return "#4CAF50" // Default to green + } + } + + // Helper function to get skill level dots + const renderSkillLevelDots = (level: string) => { + const totalDots = 3 + let activeDots = 1 + + if (level === "Medium") activeDots = 2 + if (level === "Hard") activeDots = 3 + + return ( + + {[...Array(totalDots)].map((_, i) => ( + + ))} + + ) + } + + // Render recipe info card + const renderRecipeInfoCard = ({ item }: { item: any }) => { + if (!food) return null + + return ( + + + {item.icon} + {item.title} + + {item.customContent ? ( + item.customContent(food) + ) : ( + + {item.value(food)} + {item.unit(food)} + + )} + + ) + } + + const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments + if (isLoading) { return ( - - + + - - ); + + ) } - + if (foodError || !food) { return ( - - - Post not found - + + Post not found + router.back()} > - Go Back + Go Back - - ); + + ) } - + return ( - - - - {/* Header */} - + + {/* Fixed Header */} + router.back()} @@ -347,137 +518,250 @@ export default function PostDetailScreen() { - - {/* User info and rating */} - - - - {foodCreator?.avatar_url ? ( - - ) : ( - - - {foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || '?'} - - - )} - - - {foodCreator?.username || foodCreator?.full_name || 'Chef'} - - - - 4.2 - + + {/* Scrollable Content */} + + + {/* User info */} + + + {foodCreator?.avatar_url ? ( + + ) : ( + + + {foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || "?"} + + + )} + + {foodCreator?.username || foodCreator?.full_name || "Chef"} + - + {/* Food image */} - - + - + {/* Food title and description */} - - {food.name} - {food.description} - - {new Date(food.created_at).toLocaleDateString()} - {new Date(food.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + {food.name} + {food.description} + + {new Date(food.created_at).toLocaleDateString()} -{" "} + {new Date(food.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - - {/* Interaction buttons */} - - + + Recipe Details + + item.id} + contentContainerStyle={{ paddingLeft: 16, paddingRight: 8 }} + /> + + + {/* Interaction buttons */} + + - - {stats.likes} + > + + {stats.likes} - - - - {stats.comments} - - - - + + + Save - + {/* Reviews section */} - setShowReviews(!showReviews)} > - Review - + + Reviews + + {stats.comments} + + + - + {showReviews && ( - + {comments.length > 0 ? ( comments.map((comment) => ( - - - + + + {/* Profile picture */} + {comment.user?.avatar_url ? ( - + ) : ( - - - {comment.user?.username?.charAt(0).toUpperCase() || comment.user_id?.charAt(0).toUpperCase() || '?'} + + + {comment.user?.username?.charAt(0).toUpperCase() || + comment.user_id?.charAt(0).toUpperCase() || + "?"} )} - - - {comment.user?.username || comment.user?.full_name || 'User'} - - + + {/* Comment bubble with username inside */} + + + {/* Username inside bubble */} + + {comment.user?.username || comment.user?.full_name || "User"} + + + {/* Comment content */} + {comment.content} + + + {/* Date below bubble */} + {new Date(comment.created_at).toLocaleDateString()} - {comment.content} + + {/* Separator */} + )) ) : ( - No reviews yet. Be the first to comment! + + + No reviews yet. + Be the first to comment! + )} )} - - {/* Bottom spacing */} - - - {/* Comment input */} - - + + {/* Comment input - Positioned above keyboard */} + + - @@ -485,12 +769,12 @@ export default function PostDetailScreen() { {!isAuthenticated && ( - + Please log in to comment )} - - - ); -} \ No newline at end of file + + + ) +} diff --git a/components/comment-input.tsx b/components/comment-input.tsx new file mode 100644 index 0000000..7d3af81 --- /dev/null +++ b/components/comment-input.tsx @@ -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 + 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 ( + + + + + + + + {!isAuthenticated && Please log in to comment} + + ) +} diff --git a/package-lock.json b/package-lock.json index 8a6bc5c..cc4dc57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "expo-haptics": "~14.1.4", "expo-image": "~2.1.7", "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", @@ -6538,6 +6539,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", diff --git a/package.json b/package.json index dee44a8..7563029 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "expo-haptics": "~14.1.4", "expo-image": "~2.1.7", "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",