mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-18 21:44:09 +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";
|
||||
|
||||
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<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("My Recipes");
|
||||
const { isAuthenticated } = useAuth();
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const [userId, setUserId] = useState<string | null>(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 (
|
||||
<SafeAreaView className="flex-1 bg-white" edges={["top"]}>
|
||||
@ -115,16 +81,20 @@ export default function ProfileScreen() {
|
||||
<Text className="text-5xl">👨🍳</Text>
|
||||
</View>
|
||||
</View>
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color="#bb0718"
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
) : 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">
|
||||
<Text className="text-white font-bold">Edit</Text>
|
||||
@ -133,7 +103,7 @@ export default function ProfileScreen() {
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<View className="flex-row justify-around py-3">
|
||||
{["Repost", "Likes", "Bookmark"].map((tab) => (
|
||||
{["My Recipes", "Likes", "Saved"].map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab}
|
||||
className={`py-2 px-4 ${
|
||||
@ -148,26 +118,54 @@ export default function ProfileScreen() {
|
||||
|
||||
<View className="h-px bg-[#EEEEEE] mx-4" />
|
||||
|
||||
{/* Food Grid */}
|
||||
<View className="flex-row flex-wrap p-2">
|
||||
{foodItems.map((item, index) => (
|
||||
<View key={`${item.id}-${index}`} className="w-1/2 p-2 relative">
|
||||
<Image
|
||||
source={item.image}
|
||||
className="w-full h-[120px] rounded-lg"
|
||||
resizeMode="cover"
|
||||
{/* Food Grid / Tab Content */}
|
||||
{activeTab === "My Recipes" && (
|
||||
<View className="flex-row flex-wrap p-2">
|
||||
{isFoodsLoading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color="#bb0718"
|
||||
style={{ marginTop: 20 }}
|
||||
/>
|
||||
<View
|
||||
className="absolute bottom-4 left-4 py-1 px-2 rounded bg-opacity-90"
|
||||
style={{ backgroundColor: item.color }}
|
||||
>
|
||||
<Text className="text-[#333] font-bold text-xs">
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : foodsError ? (
|
||||
<Text className="text-red-600 font-bold p-4">
|
||||
{foodsError.message || foodsError.toString()}
|
||||
</Text>
|
||||
) : foodsData?.data?.length ? (
|
||||
foodsData.data.map((item, index) => (
|
||||
<View key={item.id} className="w-1/2 p-2 relative">
|
||||
<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>
|
||||
))
|
||||
) : (
|
||||
<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>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<AuthProvider>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="recipe-detail"
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "card",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="recipe-detail"
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "card",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
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