chefhai/app/post-detail/[id].tsx
Tantikon Phasanphaengsi e5b6a23cf1 fix: fix keyboard error
2025-05-11 04:26:19 +07:00

781 lines
25 KiB
TypeScript

"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 { queryKeys, useLikeMutation, useSaveMutation } from "../../hooks/use-foods"
export default function PostDetailScreen() {
const params = useLocalSearchParams()
const foodId = typeof params.id === "string" ? params.id : ""
const queryClient = useQueryClient()
const scrollViewRef = useRef<ScrollView>(null)
console.log("Post detail screen - Food ID:", foodId)
const { isAuthenticated } = useAuth()
const [currentUserId, setCurrentUserId] = useState<string | null>(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: (
<View style={{ backgroundColor: "#ffd60a", padding: 8, borderRadius: 16 }}>
<Feather name="clock" size={18} color="#bb0718" />
</View>
),
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: (
<View style={{ backgroundColor: "#4CAF50", padding: 8, borderRadius: 16 }}>
<MaterialCommunityIcons name="chef-hat" size={18} color="white" />
</View>
),
value: (food: any) => food.skill_level,
unit: () => "",
gradient: ["#e8f5e9", "#f1f8e9"],
valueColor: "",
customContent: (food: any) => (
<View>
<Text style={{ fontSize: 20, fontWeight: "bold", color: getSkillLevelColor(food.skill_level) }}>
{food.skill_level}
</Text>
{renderSkillLevelDots(food.skill_level)}
</View>
),
},
{
id: "ingredients",
title: "Ingredients",
icon: (
<View style={{ backgroundColor: "#2196F3", padding: 8, borderRadius: 16 }}>
<Feather name="list" size={18} color="white" />
</View>
),
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: (
<View style={{ backgroundColor: "#F44336", padding: 8, borderRadius: 16 }}>
<Ionicons name="flame" size={18} color="white" />
</View>
),
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)
} else {
setCurrentUserId(null)
}
}
getCurrentUser()
}, [isAuthenticated])
// 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,
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],
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("")
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}`)
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()
return () => {
supabase.removeChannel(subscription)
}
}, [foodId, refetchComments, refetchStats])
// Set up real-time subscription for likes and saves
useEffect(() => {
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()
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()
return () => {
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
}
try {
likeMutation.mutate({
foodId,
userId: currentUserId,
isLiked: interactions.liked,
})
} catch (error) {
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
}
try {
saveMutation.mutate({
foodId,
userId: currentUserId,
isSaved: interactions.saved,
})
} catch (error) {
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.")
}
return
}
setSubmittingComment(true)
try {
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.")
} finally {
setSubmittingComment(false)
}
}
// 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 (
<View style={{ flexDirection: "row", marginTop: 4 }}>
{[...Array(totalDots)].map((_, i) => (
<View
key={i}
style={{
height: 8,
width: 8,
borderRadius: 4,
marginRight: 4,
backgroundColor: getSkillLevelColor(level),
opacity: i < activeDots ? 1 : 0.3,
}}
/>
))}
</View>
)
}
// Render recipe info card
const renderRecipeInfoCard = ({ item }: { item: any }) => {
if (!food) return null
return (
<View
style={{
backgroundColor: "#f8f8f8",
borderRadius: 16,
padding: 16,
marginRight: 16,
width: 160,
}}
>
<View style={{ flexDirection: "row", alignItems: "center", marginBottom: 8 }}>
{item.icon}
<Text style={{ marginLeft: 8, fontWeight: "bold", color: "#505050" }}>{item.title}</Text>
</View>
{item.customContent ? (
item.customContent(food)
) : (
<View style={{ flexDirection: "row", alignItems: "baseline" }}>
<Text style={{ fontSize: 24, fontWeight: "bold", color: item.valueColor }}>{item.value(food)}</Text>
<Text style={{ marginLeft: 4, fontSize: 14, fontWeight: "500", color: "#606060" }}>{item.unit(food)}</Text>
</View>
)}
</View>
)
}
const isLoading = isLoadingFood || isLoadingCreator || isLoadingStats || isLoadingInteractions || isLoadingComments
if (isLoading) {
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<ActivityIndicator size="large" color="#ffd60a" />
</View>
</View>
)
}
if (foodError || !food) {
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text style={{ fontSize: 18 }}>Post not found</Text>
<TouchableOpacity
style={{
marginTop: 16,
backgroundColor: "#ffd60a",
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
}}
onPress={() => router.back()}
>
<Text style={{ fontWeight: "bold" }}>Go Back</Text>
</TouchableOpacity>
</View>
</View>
)
}
return (
<View style={{ flex: 1, backgroundColor: "white" }}>
{/* Fixed Header */}
<View className="flex-row justify-between items-center px-4 py-3 mt-11">
<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>
{/* Scrollable Content */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
>
<ScrollView ref={scrollViewRef} style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 20 }}>
{/* User info */}
<View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12 }}>
<View style={{ width: 48, height: 48, backgroundColor: "#e0e0e0", borderRadius: 24, overflow: "hidden" }}>
{foodCreator?.avatar_url ? (
<Image source={{ uri: foodCreator.avatar_url }} style={{ width: "100%", height: "100%" }} />
) : (
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "#d0d0d0",
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ fontSize: 18, fontWeight: "bold", color: "#606060" }}>
{foodCreator?.username?.charAt(0).toUpperCase() || food.created_by?.charAt(0).toUpperCase() || "?"}
</Text>
</View>
)}
</View>
<Text style={{ marginLeft: 12, fontSize: 18, fontWeight: "bold" }}>
{foodCreator?.username || foodCreator?.full_name || "Chef"}
</Text>
</View>
{/* Food image */}
<View style={{ paddingHorizontal: 16, marginBottom: 16 }}>
<Image
source={{ uri: food.image_url || "/vibrant-food-dish.png" }}
style={{ width: "100%", height: 256, borderRadius: 16 }}
resizeMode="cover"
/>
</View>
{/* Food title and description */}
<View style={{ paddingHorizontal: 16, marginBottom: 8 }}>
<Text style={{ fontSize: 30, fontWeight: "bold", marginBottom: 8 }}>{food.name}</Text>
<Text style={{ color: "#505050", marginBottom: 8, fontSize: 16, lineHeight: 24 }}>{food.description}</Text>
<Text style={{ color: "#808080", fontSize: 14 }}>
{new Date(food.created_at).toLocaleDateString()} -{" "}
{new Date(food.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Text>
</View>
{/* Recipe Info Cards - Horizontal Scrollable */}
<View style={{ paddingVertical: 16 }}>
<Text style={{ paddingHorizontal: 16, fontSize: 20, fontWeight: "bold", marginBottom: 12 }}>
Recipe Details
</Text>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={recipeInfoCards}
renderItem={renderRecipeInfoCard}
keyExtractor={(item) => item.id}
contentContainerStyle={{ paddingLeft: 16, paddingRight: 8 }}
/>
</View>
{/* Interaction buttons */}
<View
style={{
flexDirection: "row",
paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: "#e0e0e0",
}}
>
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f0f0f0",
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 24,
}}
onPress={handleLike}
>
<Feather
name={interactions.liked ? "heart" : "heart"}
size={22}
color={interactions.liked ? "#E91E63" : "#333"}
/>
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>{stats.likes}</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f0f0f0",
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 24,
marginLeft: 16,
}}
onPress={handleSave}
>
<Feather name="bookmark" size={22} color={interactions.saved ? "#ffd60a" : "#333"} />
<Text style={{ marginLeft: 8, fontSize: 18, fontWeight: "500" }}>Save</Text>
</TouchableOpacity>
</View>
{/* Reviews section */}
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: "#e0e0e0",
}}
onPress={() => setShowReviews(!showReviews)}
>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text style={{ fontSize: 20, fontWeight: "bold" }}>Reviews</Text>
<View
style={{
marginLeft: 8,
backgroundColor: "#ffd60a",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
}}
>
<Text style={{ fontSize: 12, fontWeight: "bold", color: "#bb0718" }}>{stats.comments}</Text>
</View>
</View>
<Feather name={showReviews ? "chevron-up" : "chevron-down"} size={20} color="#333" />
</TouchableOpacity>
{showReviews && (
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
{comments.length > 0 ? (
comments.map((comment) => (
<View key={comment.id} style={{ paddingVertical: 16 }}>
<View style={{ flexDirection: "row" }}>
{/* Profile picture */}
<View
style={{
width: 40,
height: 40,
backgroundColor: "#e0e0e0",
borderRadius: 20,
overflow: "hidden",
}}
>
{comment.user?.avatar_url ? (
<Image source={{ uri: comment.user.avatar_url }} style={{ width: "100%", height: "100%" }} />
) : (
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "#ffd60a",
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ fontSize: 16, fontWeight: "bold", color: "white" }}>
{comment.user?.username?.charAt(0).toUpperCase() ||
comment.user_id?.charAt(0).toUpperCase() ||
"?"}
</Text>
</View>
)}
</View>
{/* Comment bubble with username inside */}
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ backgroundColor: "#f0f0f0", padding: 12, borderRadius: 16 }}>
{/* Username inside bubble */}
<Text style={{ fontWeight: "bold", fontSize: 16, marginBottom: 4 }}>
{comment.user?.username || comment.user?.full_name || "User"}
</Text>
{/* Comment content */}
<Text style={{ color: "#303030", lineHeight: 20 }}>{comment.content}</Text>
</View>
{/* Date below bubble */}
<Text style={{ color: "#808080", fontSize: 12, marginTop: 4, marginLeft: 8 }}>
{new Date(comment.created_at).toLocaleDateString()}
</Text>
</View>
</View>
{/* Separator */}
<View style={{ height: 1, backgroundColor: "#e0e0e0", marginTop: 16 }} />
</View>
))
) : (
<View style={{ paddingVertical: 32, alignItems: "center" }}>
<Feather name="message-circle" size={40} color="#e0e0e0" />
<Text style={{ marginTop: 8, color: "#808080", textAlign: "center" }}>No reviews yet.</Text>
<Text style={{ color: "#808080", textAlign: "center" }}>Be the first to comment!</Text>
</View>
)}
</View>
)}
</ScrollView>
{/* Comment input - Positioned above keyboard */}
<View
style={{
paddingHorizontal: 16,
paddingVertical: 12,
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
backgroundColor: "white",
}}
>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextInput
style={{
flex: 1,
backgroundColor: "#f0f0f0",
borderRadius: 24,
paddingHorizontal: 16,
paddingVertical: 12,
marginRight: 8,
}}
placeholder="Add a comment..."
value={commentText}
onChangeText={setCommentText}
/>
<TouchableOpacity
style={{
padding: 12,
borderRadius: 24,
backgroundColor: commentText.trim() && isAuthenticated ? "#ffd60a" : "#e0e0e0",
}}
onPress={handleSubmitComment}
disabled={submittingComment || !commentText.trim() || !isAuthenticated}
>
<Feather name="send" size={20} color={commentText.trim() && isAuthenticated ? "#bb0718" : "#666"} />
</TouchableOpacity>
</View>
{!isAuthenticated && (
<Text style={{ textAlign: "center", fontSize: 14, color: "#E91E63", marginTop: 4 }}>
Please log in to comment
</Text>
)}
</View>
</KeyboardAvoidingView>
</View>
)
}