diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index bfa3d34..7224cac 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,9 +1,9 @@ "use client"; -import { Image } from "expo-image"; import { useEffect, useState } from "react"; import { ActivityIndicator, + Image, ScrollView, Text, TouchableOpacity, @@ -12,98 +12,64 @@ import { import { SafeAreaView } from "react-native-safe-area-context"; import { useAuth } from "@/context/auth-context"; +import { getFoods } from "@/services/data/foods"; import { getProfile } from "@/services/data/profile"; import { supabase } from "@/services/supabase"; +import { useIsFocused } from "@react-navigation/native"; +import { useQuery } from "@tanstack/react-query"; export default function ProfileScreen() { - const [activeTab, setActiveTab] = useState("Repost"); - const [username, setUsername] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState("My Recipes"); const { isAuthenticated } = useAuth(); + const isFocused = useIsFocused(); + + const [userId, setUserId] = useState(null); useEffect(() => { - const fetchProfile = async () => { - setLoading(true); - setError(null); - try { - const { data: userData, error: userError } = - await supabase.auth.getUser(); - if (userError || !userData?.user?.id) { - setError("Unable to get user info."); - setUsername(null); - setLoading(false); - return; - } - const { data, error } = await getProfile(userData.user.id); - if (error) { - setError(error.message || "Failed to fetch profile"); - setUsername(null); - } else { - setUsername(data?.username || null); - } - } catch (e: any) { - setError(e.message || "Unknown error"); - setUsername(null); - } finally { - setLoading(false); + let ignore = false; + async function getUser() { + const { data } = await supabase.auth.getUser(); + if (!ignore) { + setUserId(data?.user?.id ?? null); } - }; - if (isAuthenticated) { - fetchProfile(); } + if (isAuthenticated) getUser(); + else setUserId(null); + return () => { + ignore = true; + }; }, [isAuthenticated]); - const foodItems = [ - { - id: 1, - name: "Padthaipro", - image: require("@/assets/images/food/padthai.jpg"), - color: "#FFCC00", + const { + data: profileData, + error, + isLoading, + } = useQuery({ + queryKey: ["profile", userId], + queryFn: async () => { + if (!userId) throw new Error("No user id"); + return getProfile(userId); }, - { - id: 2, - name: "Jjajangmyeon", - image: require("@/assets/images/food/jjajangmyeon.jpg"), - color: "#FFA500", + enabled: !!userId, + subscribed: isFocused, + }); + + // Fetch user's foods for 'My Recipes' + const { + data: foodsData, + isLoading: isFoodsLoading, + error: foodsError, + } = useQuery({ + queryKey: ["my-recipes", userId], + queryFn: async () => { + if (!userId) throw new Error("No user id"); + return getFoods(userId); }, - { - id: 3, - name: "Wingztab", - image: require("@/assets/images/food/wings.jpg"), - color: "#FFCC00", - }, - { - id: 4, - name: "Ramen", - image: require("@/assets/images/food/ramen.jpg"), - color: "#FFA500", - }, - { - id: 5, - name: "Tiramisu", - image: require("@/assets/images/food/tiramisu.jpg"), - color: "#FFCC00", - }, - { - id: 6, - name: "Beef wellington", - image: require("@/assets/images/food/beef.jpg"), - color: "#FFA500", - }, - { - id: 7, - name: "Tiramisu", - image: require("@/assets/images/food/tiramisu.jpg"), - color: "#FFCC00", - }, - { - id: 8, - name: "Beef wellington", - image: require("@/assets/images/food/beef.jpg"), - color: "#FFA500", - }, - ]; + enabled: !!userId && activeTab === "My Recipes", + subscribed: isFocused && activeTab === "My Recipes", + }); + + // Remove static foodItems, use foodsData for My Recipes return ( @@ -115,16 +81,20 @@ export default function ProfileScreen() { 👨‍🍳 - {loading ? ( + {isLoading ? ( ) : error ? ( - {error} + + {error.message || error.toString()} + ) : ( - {username ?? "-"} + + {profileData?.data?.username ?? "-"} + )} Edit @@ -133,7 +103,7 @@ export default function ProfileScreen() { {/* Tab Navigation */} - {["Repost", "Likes", "Bookmark"].map((tab) => ( + {["My Recipes", "Likes", "Saved"].map((tab) => ( - {/* Food Grid */} - - {foodItems.map((item, index) => ( - - + {isFoodsLoading ? ( + - - - {item.name} - - - - ))} - + ) : foodsError ? ( + + {foodsError.message || foodsError.toString()} + + ) : foodsData?.data?.length ? ( + foodsData.data.map((item, index) => ( + + + + + {item.name} + + + + )) + ) : ( + + No recipes found. + + )} + + )} + {activeTab === "Likes" && ( + + Liked recipes will appear here. + + )} + {activeTab === "Saved" && ( + + Saved recipes will appear here. + + )} ); diff --git a/app/_layout.tsx b/app/_layout.tsx index ce04f52..b965d57 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,28 +1,33 @@ import { AuthProvider } from "@/context/auth-context"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Stack } from "expo-router"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "../global.css"; +const queryClient = new QueryClient(); + export default function RootLayout() { return ( - - - - - - + + + + + + + + ); } diff --git a/package-lock.json b/package-lock.json index e994d8c..f1f18be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "chefhai", "version": "1.0.0", "dependencies": { + "@dev-plugins/react-query": "^0.3.1", "@expo/ngrok": "^4.1.3", "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "2.1.2", @@ -1439,6 +1440,19 @@ "node": ">=6.9.0" } }, + "node_modules/@dev-plugins/react-query": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@dev-plugins/react-query/-/react-query-0.3.1.tgz", + "integrity": "sha512-xmNfMIwHLhigE/usbGma+W/2G8bHRAfrP83nqTtjPMc7Rt2zSv1Ju3EK3KhkAh3WuHrUMpBIbNJdLgSc+K4tMg==", + "license": "MIT", + "dependencies": { + "flatted": "^3.3.1" + }, + "peerDependencies": { + "@tanstack/react-query": "*", + "expo": "^53.0.5" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -3280,6 +3294,34 @@ "node": ">=10" } }, + "node_modules/@tanstack/query-core": { + "version": "5.75.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.7.tgz", + "integrity": "sha512-4BHu0qnxUHOSnTn3ow9fIoBKTelh0GY08yn1IO9cxjBTsGvnxz1ut42CHZqUE3Vl/8FAjcHsj8RNJMoXvjgHEA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.75.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.7.tgz", + "integrity": "sha512-JYcH1g5pNjKXNQcvvnCU/PueaYg05uKBDHlWIyApspv7r5C0BM12n6ysa2QF2T+1tlPnNXOob8vr8o96Nx0GxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.75.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -7041,8 +7083,7 @@ "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" }, "node_modules/flow-enums-runtime": { "version": "0.0.6", diff --git a/package.json b/package.json index 6658bba..5e86abc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "expo lint" }, "dependencies": { + "@dev-plugins/react-query": "^0.3.1", "@expo/ngrok": "^4.1.3", "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "2.1.2", diff --git a/services/data/foods.ts b/services/data/foods.ts new file mode 100644 index 0000000..9b888e0 --- /dev/null +++ b/services/data/foods.ts @@ -0,0 +1,59 @@ +import { supabase } from "@/services/supabase"; +import { PostgrestError } from "@supabase/supabase-js"; + +interface Foods { + id: string; + name: string; + description?: string; + time_to_cook_minutes: number; + skill_level: "Easy" | "Medium" | "Hard"; + ingredient_count?: number; + calories?: number; + image_url?: string; + is_shared: boolean; + created_by: string; + created_at: string; +} + +/** + * Retrieves a list of foods based on the provided filters. + * + * @param userId - The ID of the user to filter foods by. + * @param isShared - Whether to filter foods by shared status. + * @param search - The search query to filter foods by name or description. + * @param limit - The maximum number of foods to retrieve. + * @param offset - The offset to start retrieving foods from. + * @returns A promise that resolves to an object containing the list of foods and any error that occurred. + */ +export const getFoods = async ( + userId?: string, + isShared?: boolean, + search?: string, + limit?: number, + offset?: number +): Promise<{ data: Foods[] | null; error: PostgrestError | null }> => { + let query = supabase + .from("foods") + .select( + `id, name, description, time_to_cook_minutes, skill_level, ingredient_count, calories, image_url, is_shared, created_by, created_at` + ); + + if (userId) { + query = query.eq("created_by", userId); + } + if (typeof isShared === "boolean") { + query = query.eq("is_shared", isShared); + } + if (search) { + query = query.or(`name.ilike.%${search}%,description.ilike.%${search}%`); + } + if (typeof limit === "number") { + query = query.limit(limit); + } + if (typeof offset === "number") { + query = query.range(offset, offset + (limit ? limit - 1 : 9)); + } + + const { data, error } = await query; + return { data, error }; +};