mirror of
https://github.com/Sosokker/chefhai.git
synced 2025-12-19 05:54:08 +01:00
feat: add recipes page
This commit is contained in:
parent
8c6252b19f
commit
9125ca19ba
@ -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 || [];
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"]}>
|
||||||
<ScrollView className="flex-1">
|
<Text className="text-2xl font-bold text-[#bb0718] mx-4 mt-6 mb-4">
|
||||||
{/* Search Bar */}
|
All Recipes
|
||||||
<View className="flex-row items-center mx-4 mt-2 mb-4 px-3 h-10 bg-white rounded-full border border-gray-300">
|
|
||||||
<IconSymbol name="magnifyingglass" size={20} color="#FF0000" />
|
|
||||||
<TextInput
|
|
||||||
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>
|
</Text>
|
||||||
|
<ScrollView className="flex-1">
|
||||||
|
<View className="flex-row flex-wrap px-2 pb-8">
|
||||||
|
{recipes.length === 0 ? (
|
||||||
|
<View className="w-full items-center mt-10">
|
||||||
|
<Text className="text-gray-500">No recipes found.</Text>
|
||||||
</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>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
60
components/RecipeHighlightCard.tsx
Normal file
60
components/RecipeHighlightCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user