mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 14:04:08 +01:00
refactor: use react-query to fetch profile and food data
This commit is contained in:
parent
c03746bb96
commit
4f465fd617
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Image,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@ -12,98 +12,64 @@ import {
|
|||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
import { useAuth } from "@/context/auth-context";
|
import { useAuth } from "@/context/auth-context";
|
||||||
|
import { getFoods } from "@/services/data/foods";
|
||||||
import { getProfile } from "@/services/data/profile";
|
import { getProfile } from "@/services/data/profile";
|
||||||
import { supabase } from "@/services/supabase";
|
import { supabase } from "@/services/supabase";
|
||||||
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const [activeTab, setActiveTab] = useState("Repost");
|
const [activeTab, setActiveTab] = useState("My Recipes");
|
||||||
const [username, setUsername] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
const isFocused = useIsFocused();
|
||||||
|
|
||||||
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProfile = async () => {
|
let ignore = false;
|
||||||
setLoading(true);
|
async function getUser() {
|
||||||
setError(null);
|
const { data } = await supabase.auth.getUser();
|
||||||
try {
|
if (!ignore) {
|
||||||
const { data: userData, error: userError } =
|
setUserId(data?.user?.id ?? null);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if (isAuthenticated) {
|
|
||||||
fetchProfile();
|
|
||||||
}
|
}
|
||||||
|
if (isAuthenticated) getUser();
|
||||||
|
else setUserId(null);
|
||||||
|
return () => {
|
||||||
|
ignore = true;
|
||||||
|
};
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const foodItems = [
|
const {
|
||||||
{
|
data: profileData,
|
||||||
id: 1,
|
error,
|
||||||
name: "Padthaipro",
|
isLoading,
|
||||||
image: require("@/assets/images/food/padthai.jpg"),
|
} = useQuery({
|
||||||
color: "#FFCC00",
|
queryKey: ["profile", userId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!userId) throw new Error("No user id");
|
||||||
|
return getProfile(userId);
|
||||||
},
|
},
|
||||||
{
|
enabled: !!userId,
|
||||||
id: 2,
|
subscribed: isFocused,
|
||||||
name: "Jjajangmyeon",
|
});
|
||||||
image: require("@/assets/images/food/jjajangmyeon.jpg"),
|
|
||||||
color: "#FFA500",
|
// 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);
|
||||||
},
|
},
|
||||||
{
|
enabled: !!userId && activeTab === "My Recipes",
|
||||||
id: 3,
|
subscribed: isFocused && activeTab === "My Recipes",
|
||||||
name: "Wingztab",
|
});
|
||||||
image: require("@/assets/images/food/wings.jpg"),
|
|
||||||
color: "#FFCC00",
|
// Remove static foodItems, use foodsData for My Recipes
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
|
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
|
||||||
@ -115,16 +81,20 @@ export default function ProfileScreen() {
|
|||||||
<Text className="text-5xl">👨🍳</Text>
|
<Text className="text-5xl">👨🍳</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{loading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
size="small"
|
size="small"
|
||||||
color="#bb0718"
|
color="#bb0718"
|
||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
/>
|
/>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Text className="text-xl font-bold mb-3 text-red-600">{error}</Text>
|
<Text className="text-xl font-bold mb-3 text-red-600">
|
||||||
|
{error.message || error.toString()}
|
||||||
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text className="text-xl font-bold mb-3">{username ?? "-"}</Text>
|
<Text className="text-xl font-bold mb-3">
|
||||||
|
{profileData?.data?.username ?? "-"}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity className="bg-red-600 py-2 px-10 rounded-lg">
|
<TouchableOpacity className="bg-red-600 py-2 px-10 rounded-lg">
|
||||||
<Text className="text-white font-bold">Edit</Text>
|
<Text className="text-white font-bold">Edit</Text>
|
||||||
@ -133,7 +103,7 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<View className="flex-row justify-around py-3">
|
<View className="flex-row justify-around py-3">
|
||||||
{["Repost", "Likes", "Bookmark"].map((tab) => (
|
{["My Recipes", "Likes", "Saved"].map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`py-2 px-4 ${
|
className={`py-2 px-4 ${
|
||||||
@ -148,26 +118,54 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
<View className="h-px bg-[#EEEEEE] mx-4" />
|
<View className="h-px bg-[#EEEEEE] mx-4" />
|
||||||
|
|
||||||
{/* Food Grid */}
|
{/* Food Grid / Tab Content */}
|
||||||
<View className="flex-row flex-wrap p-2">
|
{activeTab === "My Recipes" && (
|
||||||
{foodItems.map((item, index) => (
|
<View className="flex-row flex-wrap p-2">
|
||||||
<View key={`${item.id}-${index}`} className="w-1/2 p-2 relative">
|
{isFoodsLoading ? (
|
||||||
<Image
|
<ActivityIndicator
|
||||||
source={item.image}
|
size="small"
|
||||||
className="w-full h-[120px] rounded-lg"
|
color="#bb0718"
|
||||||
resizeMode="cover"
|
style={{ marginTop: 20 }}
|
||||||
/>
|
/>
|
||||||
<View
|
) : foodsError ? (
|
||||||
className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90"
|
<Text className="text-red-600 font-bold p-4">
|
||||||
style={{ backgroundColor: item.color }}
|
{foodsError.message || foodsError.toString()}
|
||||||
>
|
</Text>
|
||||||
<Text className="text-[#333] font-bold text-xs">
|
) : foodsData?.data?.length ? (
|
||||||
{item.name}
|
foodsData.data.map((item, index) => (
|
||||||
</Text>
|
<View key={item.id} className="w-1/2 p-2 relative">
|
||||||
</View>
|
<Image
|
||||||
</View>
|
source={
|
||||||
))}
|
item.image_url
|
||||||
</View>
|
? { 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>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className="text-gray-400 font-bold p-4">
|
||||||
|
No recipes found.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{activeTab === "Likes" && (
|
||||||
|
<Text className="text-gray-400 font-bold p-4">
|
||||||
|
Liked recipes will appear here.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{activeTab === "Saved" && (
|
||||||
|
<Text className="text-gray-400 font-bold p-4">
|
||||||
|
Saved recipes will appear here.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
import { AuthProvider } from "@/context/auth-context";
|
import { AuthProvider } from "@/context/auth-context";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<AuthProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<AuthProvider>
|
||||||
<Stack.Screen
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
name="(tabs)"
|
<Stack.Screen
|
||||||
options={{
|
name="(tabs)"
|
||||||
headerShown: false,
|
options={{
|
||||||
}}
|
headerShown: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="recipe-detail"
|
<Stack.Screen
|
||||||
options={{
|
name="recipe-detail"
|
||||||
headerShown: false,
|
options={{
|
||||||
presentation: "card",
|
headerShown: false,
|
||||||
}}
|
presentation: "card",
|
||||||
/>
|
}}
|
||||||
</Stack>
|
/>
|
||||||
</AuthProvider>
|
</Stack>
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
45
package-lock.json
generated
45
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "chefhai",
|
"name": "chefhai",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dev-plugins/react-query": "^0.3.1",
|
||||||
"@expo/ngrok": "^4.1.3",
|
"@expo/ngrok": "^4.1.3",
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "2.1.2",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
@ -1439,6 +1440,19 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@egjs/hammerjs": {
|
||||||
"version": "2.0.17",
|
"version": "2.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||||
@ -3280,6 +3294,34 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||||
@ -7041,8 +7083,7 @@
|
|||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/flow-enums-runtime": {
|
"node_modules/flow-enums-runtime": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dev-plugins/react-query": "^0.3.1",
|
||||||
"@expo/ngrok": "^4.1.3",
|
"@expo/ngrok": "^4.1.3",
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "2.1.2",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
|
|||||||
59
services/data/foods.ts
Normal file
59
services/data/foods.ts
Normal file
@ -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 };
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user