feat: add recipes page

This commit is contained in:
Sosokker 2025-05-12 00:34:27 +07:00
parent 8c6252b19f
commit 9125ca19ba
4 changed files with 138 additions and 95 deletions

View File

@ -28,7 +28,7 @@ const useFoodsQuery = () => {
return useQuery({ return useQuery({
queryKey: ["highlight-foods"], queryKey: ["highlight-foods"],
queryFn: async () => { queryFn: async () => {
const { data, error } = await getFoods(undefined, true, undefined, 4); const { data, error } = await getFoods(undefined, true, undefined, 3);
if (error) throw error; if (error) throw error;
return data || []; return data || [];
}, },

View File

@ -1,99 +1,89 @@
import { IconSymbol } from "@/components/ui/IconSymbol"; "use client";
import { Image } from "expo-image";
import { import RecipeHighlightCard from "@/components/RecipeHighlightCard";
ScrollView, import { supabase } from "@/services/supabase";
Text, import { useQuery } from "@tanstack/react-query";
TextInput, import { router } from "expo-router";
TouchableOpacity, import { ActivityIndicator, ScrollView, Text, View } from "react-native";
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
interface Recipe {
id: number;
name: string;
description: string;
image_url: string;
created_by: string;
is_shared: boolean;
time_to_cook_minutes?: number;
}
export default function RecipesScreen() { export default function RecipesScreen() {
const foodItems = [ const {
{ data: allRecipes,
id: 1, isLoading: isAllLoading,
name: "Padthaipro", error: allError,
image: require("@/assets/images/food/padthai.jpg"), } = useQuery<Recipe[], Error>({
color: "#FFCC00", queryKey: ["all-recipes"],
queryFn: async () => {
const { data, error } = await supabase
.from("foods")
.select("*")
.eq("is_shared", true)
.order("created_at", { ascending: false });
if (error) throw error;
return data ?? [];
}, },
{ staleTime: 1000 * 60,
id: 2, });
name: "Jjajangmyeon",
image: require("@/assets/images/food/jjajangmyeon.jpg"), const recipes: Recipe[] = allRecipes || [];
color: "#FFA500", const loading = isAllLoading;
}, const error = allError;
{
id: 3, if (loading) {
name: "Wingztab", return (
image: require("@/assets/images/food/wings.jpg"), <SafeAreaView className="flex-1 bg-white items-center justify-center">
color: "#FFCC00", <ActivityIndicator size="large" color="#FFCC00" />
}, </SafeAreaView>
{ );
id: 4, }
name: "Ramen", if (error) {
image: require("@/assets/images/food/ramen.jpg"), return (
color: "#FFA500", <SafeAreaView className="flex-1 bg-white items-center justify-center">
}, <Text className="text-lg text-red-600">Failed to load recipes</Text>
{ </SafeAreaView>
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",
},
];
return ( return (
<SafeAreaView className="flex-1 bg-white" edges={["top"]}> <SafeAreaView className="flex-1 bg-white" edges={["top"]}>
<Text className="text-2xl font-bold text-[#bb0718] mx-4 mt-6 mb-4">
All Recipes
</Text>
<ScrollView className="flex-1"> <ScrollView className="flex-1">
{/* Search Bar */} <View className="flex-row flex-wrap px-2 pb-8">
<View className="flex-row items-center mx-4 mt-2 mb-4 px-3 h-10 bg-white rounded-full border border-gray-300"> {recipes.length === 0 ? (
<IconSymbol name="magnifyingglass" size={20} color="#FF0000" /> <View className="w-full items-center mt-10">
<TextInput <Text className="text-gray-500">No recipes found.</Text>
className="flex-1 ml-2 text-[#333]"
placeholder="Search"
placeholderTextColor="#FF0000"
/>
</View>
{/* Filter Buttons */}
<View className="flex-row mx-4 mb-4">
<TouchableOpacity className="flex-1 bg-[#FFCC00] py-3 rounded-lg mr-2 items-center">
<Text className="font-bold text-[#333]">All Recipes</Text>
</TouchableOpacity>
<TouchableOpacity className="flex-1 bg-red-600 py-3 rounded-lg items-center">
<Text className="font-bold text-white">My Recipes</Text>
</TouchableOpacity>
</View>
{/* Divider */}
<View className="h-px bg-[#EEEEEE] mx-4 mb-4" />
{/* Food Grid */}
<View className="flex-row flex-wrap p-2">
{foodItems.map((item) => (
<View key={item.id} className="w-1/2 p-2 relative">
<Image
source={item.image}
className="w-full h-[120px] rounded-lg"
resizeMode="cover"
/>
<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>
))} ) : (
recipes.map((item, idx) => (
<View key={item.id} className="w-1/2 p-2">
<RecipeHighlightCard
recipe={{
id: item.id,
name: item.name,
description: item.description,
image_url: item.image_url,
time_to_cook_minutes: item.time_to_cook_minutes,
}}
onPress={() => {
router.push(`/food/${item.id}`);
}}
/>
</View>
))
)}
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>

View File

@ -18,13 +18,6 @@ export default function RootLayout() {
headerShown: false, headerShown: false,
}} }}
/> />
<Stack.Screen
name="recipe-detail"
options={{
headerShown: false,
presentation: "card",
}}
/>
</Stack> </Stack>
</AuthProvider> </AuthProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@ -0,0 +1,60 @@
import { IconSymbol } from "@/components/ui/IconSymbol";
import { Image } from "expo-image";
import { TouchableOpacity, View, Text } from "react-native";
interface RecipeHighlightCardProps {
recipe: {
id: number;
name: string;
description?: string;
image_url?: string;
time_to_cook_minutes?: number;
calories?: number;
};
onPress?: () => void;
}
export default function RecipeHighlightCard({ recipe, onPress }: RecipeHighlightCardProps) {
return (
<TouchableOpacity
className="flex-1 bg-white rounded-2xl shadow-lg mb-4 overflow-hidden"
style={{ marginRight: 12, elevation: 4 }}
onPress={onPress}
activeOpacity={0.85}
>
<View className="relative">
{recipe.image_url ? (
<Image
source={{ uri: recipe.image_url }}
className="w-full h-36"
contentFit="cover"
/>
) : (
<View className="items-center justify-center w-full h-36 bg-gray-200">
<Text className="text-gray-400">No Image</Text>
</View>
)}
{/* Calories badge */}
{recipe.calories !== undefined && (
<View className="absolute top-2 right-2 bg-[#ffd60a] px-2 py-1 rounded-full shadow">
<Text className="text-xs font-bold text-[#bb0718]">{recipe.calories} kcal</Text>
</View>
)}
</View>
<View className="p-4">
<Text className="text-lg font-bold text-[#222] mb-1" numberOfLines={1}>
{recipe.name}
</Text>
<Text className="text-sm text-[#666] mb-2" numberOfLines={2}>
{recipe.description || "No description"}
</Text>
<View className="flex-row items-center mt-1">
<IconSymbol name="clock" size={14} color="#bb0718" />
<Text className="text-xs text-[#bb0718] ml-1 font-semibold">
{recipe.time_to_cook_minutes ? `${recipe.time_to_cook_minutes} min` : "-"}
</Text>
</View>
</View>
</TouchableOpacity>
);
}