chefhai/app/post-detail/[id].tsx

984 lines
28 KiB
TypeScript

"use client";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
Alert,
FlatList,
Image,
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { useAuth } from "../../context/auth-context";
import {
queryKeys,
useLikeMutation,
useSaveMutation,
} from "../../hooks/use-foods";
import {
checkUserLiked,
checkUserSaved,
createComment,
getComments,
getCommentsCount,
getLikesCount,
getSavesCount,
} from "../../services/data/forum";
import { getProfile } from "../../services/data/profile";
import { supabase } from "../../services/supabase";
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 items-center justify-between 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 onPress={() => router.push(`/food/${food.id}`)}>
<Feather name="external-link" 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>
</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>
);
}