mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 05:54:08 +01:00
feat: show element in tabs profile
This commit is contained in:
parent
d35ae859e4
commit
5212429fb1
@ -1,13 +1,15 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useAuth } from "@/context/auth-context";
|
import { useAuth } from "@/context/auth-context"
|
||||||
import { getFoods } from "@/services/data/foods";
|
import { getFoods } from "@/services/data/foods"
|
||||||
import { getProfile, updateProfile } from "@/services/data/profile";
|
import { getBookmarkedPosts } from "@/services/data/bookmarks"
|
||||||
import { supabase } from "@/services/supabase";
|
import { getLikedPosts } from "@/services/data/likes"
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { getProfile, updateProfile } from "@/services/data/profile"
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { supabase } from "@/services/supabase"
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import { useIsFocused, useNavigation } from "@react-navigation/native"
|
||||||
import { useState } from "react";
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import * as ImagePicker from "expo-image-picker"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Image,
|
Image,
|
||||||
@ -18,15 +20,31 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native"
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context"
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid"
|
||||||
|
|
||||||
|
// Define the Food type based on your database structure
|
||||||
|
type Food = {
|
||||||
|
id: number
|
||||||
|
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
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const [activeTab, setActiveTab] = useState("My Recipes");
|
const [activeTab, setActiveTab] = useState("My Recipes")
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth()
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused()
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: userData,
|
data: userData,
|
||||||
@ -35,14 +53,14 @@ export default function ProfileScreen() {
|
|||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["auth-user"],
|
queryKey: ["auth-user"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const { data, error } = await supabase.auth.getUser()
|
||||||
if (error) throw error;
|
if (error) throw error
|
||||||
return data?.user;
|
return data?.user
|
||||||
},
|
},
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
})
|
||||||
const userId = userData?.id;
|
const userId = userData?.id
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@ -51,115 +69,186 @@ export default function ProfileScreen() {
|
|||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["profile", userId],
|
queryKey: ["profile", userId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!userId) throw new Error("No user id");
|
if (!userId) throw new Error("No user id")
|
||||||
return getProfile(userId);
|
return getProfile(userId)
|
||||||
},
|
},
|
||||||
enabled: !!userId,
|
enabled: !!userId,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
subscribed: isFocused,
|
subscribed: isFocused,
|
||||||
});
|
})
|
||||||
|
|
||||||
|
// My Recipes Query
|
||||||
const {
|
const {
|
||||||
data: foodsData,
|
data: myRecipesData,
|
||||||
isLoading: isFoodsLoading,
|
isLoading: isMyRecipesLoading,
|
||||||
error: foodsError,
|
error: myRecipesError,
|
||||||
|
refetch: refetchMyRecipes,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["my-recipes", userId],
|
queryKey: ["my-recipes", userId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!userId) throw new Error("No user id");
|
if (!userId) throw new Error("No user id")
|
||||||
return getFoods(userId);
|
return getFoods(userId)
|
||||||
},
|
},
|
||||||
enabled: !!userId && activeTab === "My Recipes",
|
enabled: !!userId,
|
||||||
staleTime: 0,
|
staleTime: 1000 * 60, // 1 minute
|
||||||
});
|
})
|
||||||
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
// Likes Query
|
||||||
const [editUsername, setEditUsername] = useState("");
|
const {
|
||||||
const [editImage, setEditImage] = useState<string | null>(null);
|
data: likesData,
|
||||||
const [editLoading, setEditLoading] = useState(false);
|
isLoading: isLikesLoading,
|
||||||
const [editError, setEditError] = useState<string | null>(null);
|
error: likesError,
|
||||||
|
refetch: refetchLikes,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["liked-posts", userId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!userId) throw new Error("No user id")
|
||||||
|
return getLikedPosts(userId)
|
||||||
|
},
|
||||||
|
enabled: !!userId,
|
||||||
|
staleTime: 1000 * 60, // 1 minute
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bookmarks Query
|
||||||
|
const {
|
||||||
|
data: bookmarksData,
|
||||||
|
isLoading: isBookmarksLoading,
|
||||||
|
error: bookmarksError,
|
||||||
|
refetch: refetchBookmarks,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["bookmarked-posts", userId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!userId) throw new Error("No user id")
|
||||||
|
return getBookmarkedPosts(userId)
|
||||||
|
},
|
||||||
|
enabled: !!userId,
|
||||||
|
staleTime: 1000 * 60, // 1 minute
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigate to post detail
|
||||||
|
const handleFoodPress = (foodId: number) => {
|
||||||
|
// @ts-ignore - Navigation typing might be different in your app
|
||||||
|
navigation.navigate("post-detail", { id: foodId })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch data when tab changes
|
||||||
|
const handleTabChange = (tab: string) => {
|
||||||
|
setActiveTab(tab)
|
||||||
|
|
||||||
|
// Refetch data for the selected tab
|
||||||
|
if (tab === "My Recipes") {
|
||||||
|
refetchMyRecipes()
|
||||||
|
} else if (tab === "Likes") {
|
||||||
|
refetchLikes()
|
||||||
|
} else if (tab === "Bookmark") {
|
||||||
|
refetchBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch all data when the screen comes into focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused && userId) {
|
||||||
|
refetchMyRecipes()
|
||||||
|
refetchLikes()
|
||||||
|
refetchBookmarks()
|
||||||
|
}
|
||||||
|
}, [isFocused, userId])
|
||||||
|
|
||||||
|
const [modalVisible, setModalVisible] = useState(false)
|
||||||
|
const [editUsername, setEditUsername] = useState("")
|
||||||
|
const [editImage, setEditImage] = useState<string | null>(null)
|
||||||
|
const [editLoading, setEditLoading] = useState(false)
|
||||||
|
const [editError, setEditError] = useState<string | null>(null)
|
||||||
|
|
||||||
const pickImage = async () => {
|
const pickImage = async () => {
|
||||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
||||||
if (status !== "granted") {
|
if (status !== "granted") {
|
||||||
setEditError("Permission to access media library is required.");
|
setEditError("Permission to access media library is required.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
mediaTypes: ["images"],
|
mediaTypes: ["images"],
|
||||||
quality: 0.7,
|
quality: 0.7,
|
||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
setEditImage(result.assets[0].uri);
|
setEditImage(result.assets[0].uri)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const uploadImageToSupabase = async (uri: string): Promise<string> => {
|
const uploadImageToSupabase = async (uri: string): Promise<string> => {
|
||||||
const fileName = `${userId}/${uuid.v4()}.jpg`;
|
const fileName = `${userId}/${uuid.v4()}.jpg`
|
||||||
const response = await fetch(uri);
|
const response = await fetch(uri)
|
||||||
const blob = await response.blob();
|
const blob = await response.blob()
|
||||||
|
|
||||||
const { error: uploadError } = await supabase.storage
|
const { error: uploadError } = await supabase.storage.from("avatars").upload(fileName, blob, {
|
||||||
.from("avatars")
|
contentType: "image/jpeg",
|
||||||
.upload(fileName, blob, {
|
upsert: true,
|
||||||
contentType: "image/jpeg",
|
})
|
||||||
upsert: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadError) throw uploadError;
|
if (uploadError) throw uploadError
|
||||||
|
|
||||||
const { data } = supabase.storage.from("avatars").getPublicUrl(fileName);
|
const { data } = supabase.storage.from("avatars").getPublicUrl(fileName)
|
||||||
return data.publicUrl;
|
return data.publicUrl
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
setEditLoading(true);
|
setEditLoading(true)
|
||||||
setEditError(null);
|
setEditError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!editUsername.trim()) throw new Error("Username cannot be empty");
|
if (!editUsername.trim()) throw new Error("Username cannot be empty")
|
||||||
|
|
||||||
let avatarUrl = profileData?.data?.avatar_url ?? null;
|
let avatarUrl = profileData?.data?.avatar_url ?? null
|
||||||
|
|
||||||
if (editImage && editImage !== avatarUrl) {
|
if (editImage && editImage !== avatarUrl) {
|
||||||
avatarUrl = await uploadImageToSupabase(editImage);
|
avatarUrl = await uploadImageToSupabase(editImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error: updateError } = await updateProfile(
|
const { error: updateError } = await updateProfile(userId!, editUsername.trim(), avatarUrl)
|
||||||
userId!,
|
if (updateError) throw updateError
|
||||||
editUsername.trim(),
|
|
||||||
avatarUrl
|
|
||||||
);
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
setModalVisible(false);
|
setModalVisible(false)
|
||||||
await queryClient.invalidateQueries({ queryKey: ["profile", userId] });
|
await queryClient.invalidateQueries({ queryKey: ["profile", userId] })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setEditError(err.message || "Failed to update profile");
|
setEditError(err.message || "Failed to update profile")
|
||||||
} finally {
|
} finally {
|
||||||
setEditLoading(false);
|
setEditLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Get the active data based on the current tab
|
||||||
|
const getActiveData = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case "My Recipes":
|
||||||
|
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||||
|
case "Likes":
|
||||||
|
return { data: likesData, isLoading: isLikesLoading, error: likesError }
|
||||||
|
case "Bookmark":
|
||||||
|
return { data: bookmarksData, isLoading: isBookmarksLoading, error: bookmarksError }
|
||||||
|
default:
|
||||||
|
return { data: myRecipesData, isLoading: isMyRecipesLoading, error: myRecipesError }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: activeData, isLoading: isActiveLoading, error: activeError } = getActiveData()
|
||||||
|
|
||||||
if (isUserLoading) {
|
if (isUserLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 justify-center items-center bg-white">
|
<SafeAreaView className="flex-1 justify-center items-center bg-white">
|
||||||
<ActivityIndicator size="large" color="#bb0718" />
|
<ActivityIndicator size="large" color="#bb0718" />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userError) {
|
if (userError) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 justify-center items-center bg-white px-4">
|
<SafeAreaView className="flex-1 justify-center items-center bg-white px-4">
|
||||||
<Text className="text-red-600 font-bold text-center">
|
<Text className="text-red-600 font-bold text-center">{userError.message || "Failed to load user data."}</Text>
|
||||||
{userError.message || "Failed to load user data."}
|
|
||||||
</Text>
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -179,46 +268,31 @@ export default function ProfileScreen() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator size="small" color="#bb0718" />
|
<ActivityIndicator size="small" color="#bb0718" />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Text className="text-red-600 font-bold mb-3">
|
<Text className="text-red-600 font-bold mb-3">{error.message || error.toString()}</Text>
|
||||||
{error.message || error.toString()}
|
|
||||||
</Text>
|
|
||||||
) : (
|
) : (
|
||||||
<Text className="text-xl font-bold mb-3">
|
<Text className="text-xl font-bold mb-3">{profileData?.data?.username ?? "-"}</Text>
|
||||||
{profileData?.data?.username ?? "-"}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="bg-red-600 py-2 px-10 rounded-lg"
|
className="bg-red-600 py-2 px-10 rounded-lg"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setEditUsername(profileData?.data?.username ?? "");
|
setEditUsername(profileData?.data?.username ?? "")
|
||||||
setEditImage(profileData?.data?.avatar_url ?? null);
|
setEditImage(profileData?.data?.avatar_url ?? null)
|
||||||
setEditError(null);
|
setEditError(null)
|
||||||
setModalVisible(true);
|
setModalVisible(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="text-white font-bold">Edit</Text>
|
<Text className="text-white font-bold">Edit</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
<Modal
|
<Modal visible={modalVisible} animationType="slide" transparent onRequestClose={() => setModalVisible(false)}>
|
||||||
visible={modalVisible}
|
|
||||||
animationType="slide"
|
|
||||||
transparent
|
|
||||||
onRequestClose={() => setModalVisible(false)}
|
|
||||||
>
|
|
||||||
<View className="flex-1 justify-center items-center bg-black bg-opacity-40">
|
<View className="flex-1 justify-center items-center bg-black bg-opacity-40">
|
||||||
<View className="bg-white rounded-xl p-6 w-11/12 max-w-md shadow-lg">
|
<View className="bg-white rounded-xl p-6 w-11/12 max-w-md shadow-lg">
|
||||||
<Text className="text-lg font-bold mb-4 text-center">
|
<Text className="text-lg font-bold mb-4 text-center">Edit Profile</Text>
|
||||||
Edit Profile
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Pressable className="items-center mb-4" onPress={pickImage}>
|
<Pressable className="items-center mb-4" onPress={pickImage}>
|
||||||
<Image
|
<Image
|
||||||
source={
|
source={editImage ? { uri: editImage } : require("@/assets/images/placeholder-food.jpg")}
|
||||||
editImage
|
|
||||||
? { uri: editImage }
|
|
||||||
: require("@/assets/images/placeholder-food.jpg")
|
|
||||||
}
|
|
||||||
className="w-24 h-24 rounded-full mb-2 bg-gray-200"
|
className="w-24 h-24 rounded-full mb-2 bg-gray-200"
|
||||||
/>
|
/>
|
||||||
<Text className="text-blue-600 underline">Change Photo</Text>
|
<Text className="text-blue-600 underline">Change Photo</Text>
|
||||||
@ -231,11 +305,7 @@ export default function ProfileScreen() {
|
|||||||
onChangeText={setEditUsername}
|
onChangeText={setEditUsername}
|
||||||
placeholder="Enter new username"
|
placeholder="Enter new username"
|
||||||
/>
|
/>
|
||||||
{editError && (
|
{editError && <Text className="text-red-600 mb-2 text-center">{editError}</Text>}
|
||||||
<Text className="text-red-600 mb-2 text-center">
|
|
||||||
{editError}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View className="flex-row justify-between mt-2">
|
<View className="flex-row justify-between mt-2">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -263,61 +333,52 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<View className="flex-row justify-around py-3">
|
<View className="flex-row justify-around py-3 border-b border-gray-200">
|
||||||
{["My Recipes", "Likes", "Saved"].map((tab) => (
|
{["My Recipes", "Likes", "Bookmark"].map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`py-2 px-4 ${
|
className={`py-2 px-4 ${activeTab === tab ? "border-b-2 border-[#333]" : ""}`}
|
||||||
activeTab === tab ? "border-b-2 border-[#333]" : ""
|
onPress={() => handleTabChange(tab)}
|
||||||
}`}
|
|
||||||
onPress={() => setActiveTab(tab)}
|
|
||||||
>
|
>
|
||||||
<Text className="font-medium">{tab}</Text>
|
<Text className={`font-medium ${activeTab === tab ? "font-bold" : ""}`}>{tab}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="h-px bg-[#EEEEEE] mx-4" />
|
{/* Tab Content */}
|
||||||
|
{isActiveLoading ? (
|
||||||
{/* Recipes */}
|
<View className="flex-1 items-center justify-center py-8">
|
||||||
{activeTab === "My Recipes" && (
|
<ActivityIndicator size="small" color="#bb0718" />
|
||||||
|
</View>
|
||||||
|
) : activeError ? (
|
||||||
|
<View className="flex-1 items-center justify-center py-8">
|
||||||
|
<Text className="text-red-600 font-bold text-center">{activeError.message || "Failed to load data"}</Text>
|
||||||
|
</View>
|
||||||
|
) : !activeData?.data?.length ? (
|
||||||
|
<View className="flex-1 items-center justify-center py-8">
|
||||||
|
<Text className="text-gray-400 font-medium text-center">No items found</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<View className="flex-row flex-wrap p-2">
|
<View className="flex-row flex-wrap p-2">
|
||||||
{isFoodsLoading ? (
|
{activeData.data.map((item: Food) => (
|
||||||
<ActivityIndicator
|
<TouchableOpacity
|
||||||
size="small"
|
key={item.id}
|
||||||
color="#bb0718"
|
className="w-1/2 p-2 relative"
|
||||||
style={{ marginTop: 20 }}
|
onPress={() => handleFoodPress(item.id)}
|
||||||
/>
|
activeOpacity={0.7}
|
||||||
) : foodsError ? (
|
>
|
||||||
<Text className="text-red-600 font-bold p-4">
|
<Image
|
||||||
{foodsError.message || foodsError.toString()}
|
source={item.image_url ? { uri: item.image_url } : require("@/assets/images/placeholder-food.jpg")}
|
||||||
</Text>
|
className="w-full h-[120px] rounded-lg"
|
||||||
) : foodsData?.data?.length ? (
|
/>
|
||||||
foodsData.data.map((item) => (
|
<View className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90 bg-white/80">
|
||||||
<View key={item.id} className="w-1/2 p-2 relative">
|
<Text className="text-[#333] font-bold text-xs">{item.name}</Text>
|
||||||
<Image
|
|
||||||
source={
|
|
||||||
item.image_url
|
|
||||||
? { uri: item.image_url }
|
|
||||||
: require("@/assets/images/placeholder-food.jpg")
|
|
||||||
}
|
|
||||||
className="w-full h-[120px] rounded-lg"
|
|
||||||
/>
|
|
||||||
<View className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90 bg-white/80">
|
|
||||||
<Text className="text-[#333] font-bold text-xs">
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
))
|
</TouchableOpacity>
|
||||||
) : (
|
))}
|
||||||
<Text className="text-gray-400 font-bold p-4">
|
|
||||||
No recipes found.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
48
services/data/bookmarks.ts
Normal file
48
services/data/bookmarks.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { supabase } from "@/services/supabase"
|
||||||
|
import type { PostgrestError } from "@supabase/supabase-js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves posts that a user has saved/bookmarked
|
||||||
|
*/
|
||||||
|
export async function getBookmarkedPosts(userId: string): Promise<{
|
||||||
|
data: any[] | null
|
||||||
|
error: PostgrestError | null
|
||||||
|
}> {
|
||||||
|
// First get all food_ids that the user has saved
|
||||||
|
const { data: savedFoodIds, error: saveError } = await supabase
|
||||||
|
.from("food_saves")
|
||||||
|
.select("food_id")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
|
||||||
|
if (saveError) {
|
||||||
|
return { data: null, error: saveError }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!savedFoodIds || savedFoodIds.length === 0) {
|
||||||
|
return { data: [], error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract just the IDs
|
||||||
|
const foodIds = savedFoodIds.map((item) => item.food_id)
|
||||||
|
|
||||||
|
// Then fetch the actual food items
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("foods")
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
time_to_cook_minutes,
|
||||||
|
skill_level,
|
||||||
|
ingredient_count,
|
||||||
|
calories,
|
||||||
|
image_url,
|
||||||
|
is_shared,
|
||||||
|
created_by,
|
||||||
|
created_at
|
||||||
|
`)
|
||||||
|
.in("id", foodIds)
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
|
||||||
|
return { data, error }
|
||||||
|
}
|
||||||
48
services/data/likes.ts
Normal file
48
services/data/likes.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { supabase } from "@/services/supabase"
|
||||||
|
import type { PostgrestError } from "@supabase/supabase-js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves posts that a user has liked
|
||||||
|
*/
|
||||||
|
export async function getLikedPosts(userId: string): Promise<{
|
||||||
|
data: any[] | null
|
||||||
|
error: PostgrestError | null
|
||||||
|
}> {
|
||||||
|
// First get all food_ids that the user has liked
|
||||||
|
const { data: likedFoodIds, error: likeError } = await supabase
|
||||||
|
.from("food_likes")
|
||||||
|
.select("food_id")
|
||||||
|
.eq("user_id", userId)
|
||||||
|
|
||||||
|
if (likeError) {
|
||||||
|
return { data: null, error: likeError }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!likedFoodIds || likedFoodIds.length === 0) {
|
||||||
|
return { data: [], error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract just the IDs
|
||||||
|
const foodIds = likedFoodIds.map((item) => item.food_id)
|
||||||
|
|
||||||
|
// Then fetch the actual food items
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("foods")
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
time_to_cook_minutes,
|
||||||
|
skill_level,
|
||||||
|
ingredient_count,
|
||||||
|
calories,
|
||||||
|
image_url,
|
||||||
|
is_shared,
|
||||||
|
created_by,
|
||||||
|
created_at
|
||||||
|
`)
|
||||||
|
.in("id", foodIds)
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
|
||||||
|
return { data, error }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user