├── assets ├── icon.png ├── favicon.png ├── splash-icon.png └── adaptive-icon.png ├── tsconfig.json ├── index.ts ├── src ├── types.ts ├── components │ ├── CategoryBar.tsx │ └── RecipeCard.tsx ├── screens │ ├── FavoritesScreen.tsx │ ├── MainFeedScreen.tsx │ ├── RecipeDetailScreen.tsx │ ├── MyFoodScreen.tsx │ └── AddEditRecipeScreen.tsx ├── store │ └── RecipesContext.tsx ├── data │ └── seed.ts └── navigation │ └── RootNavigator.tsx ├── App.tsx ├── .gitignore ├── app.json ├── package.json └── README.md /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/Foodie/main/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/Foodie/main/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/Foodie/main/assets/splash-icon.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/Foodie/main/assets/adaptive-icon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Difficulty = "Easy" | "Medium" | "Hard"; 2 | 3 | export type Recipe = { 4 | id: string; 5 | name: string; 6 | category: string; 7 | imageUrl: string; 8 | ingredients: string[]; 9 | instructions: string[]; 10 | prepTimeMinutes: number; 11 | servings: number; 12 | calories: number; 13 | difficulty: Difficulty; 14 | isFavorite: boolean; 15 | isUserRecipe: boolean; 16 | }; 17 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavigationContainer } from "@react-navigation/native"; 3 | import { RecipesProvider } from "./src/store/RecipesContext"; 4 | import { RootNavigator } from "./src/navigation/RootNavigator"; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | .kotlin/ 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | # generated native folders 40 | /ios 41 | /android 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Foodie", 4 | "slug": "Foodie", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | }, 23 | "edgeToEdgeEnabled": true, 24 | "predictiveBackGestureEnabled": false 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodie", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@react-navigation/bottom-tabs": "^7.8.12", 13 | "@react-navigation/native": "^7.1.25", 14 | "@react-navigation/native-stack": "^7.8.6", 15 | "expo": "~54.0.29", 16 | "expo-status-bar": "~3.0.9", 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0", 19 | "react-native": "0.81.5", 20 | "react-native-safe-area-context": "~5.6.0", 21 | "react-native-screens": "~4.16.0", 22 | "react-native-web": "^0.21.0" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "~19.1.0", 26 | "typescript": "~5.9.2" 27 | }, 28 | "private": true 29 | } 30 | -------------------------------------------------------------------------------- /src/components/CategoryBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FlatList, Pressable, Text, View } from "react-native"; 3 | 4 | type Props = { 5 | categories: string[]; 6 | selected: string; 7 | onSelect: (category: string) => void; 8 | }; 9 | 10 | export function CategoryBar({ categories, selected, onSelect }: Props) { 11 | return ( 12 | 13 | c} 18 | contentContainerStyle={{ paddingHorizontal: 12, gap: 8 }} 19 | renderItem={({ item }) => { 20 | const active = item === selected; 21 | return ( 22 | onSelect(item)} 24 | style={{ 25 | paddingVertical: 8, 26 | paddingHorizontal: 12, 27 | borderRadius: 18, 28 | borderWidth: 1, 29 | borderColor: active ? "#111" : "#ccc", 30 | backgroundColor: active ? "#111" : "transparent", 31 | }} 32 | > 33 | 34 | {item} 35 | 36 | 37 | ); 38 | }} 39 | /> 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/RecipeCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Image, Pressable, Text, View } from "react-native"; 3 | import { Ionicons } from "@expo/vector-icons"; 4 | import { Recipe } from "../types"; 5 | 6 | type Props = { 7 | recipe: Recipe; 8 | onPress: () => void; 9 | onToggleFavorite: () => void; 10 | }; 11 | 12 | export function RecipeCard({ recipe, onPress, onToggleFavorite }: Props) { 13 | return ( 14 | 26 | 27 | 28 | 29 | {recipe.name} 30 | 31 | {recipe.category} • {recipe.prepTimeMinutes} min • {recipe.difficulty} 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/screens/FavoritesScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { FlatList, SafeAreaView, Text, View } from "react-native"; 3 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 4 | import { RecipesContext } from "../store/RecipesContext"; 5 | import { RecipeCard } from "../components/RecipeCard"; 6 | import { FavoritesStackParamList } from "../navigation/RootNavigator"; 7 | 8 | type Props = NativeStackScreenProps; 9 | 10 | export function FavoritesScreen({ navigation }: Props) { 11 | const { favorites, toggleFavorite } = useContext(RecipesContext); 12 | 13 | return ( 14 | 15 | 16 | Favorites 17 | Your saved recipes 18 | 19 | 20 | r.id} 23 | contentContainerStyle={{ paddingBottom: 24 }} 24 | ListEmptyComponent={ 25 | 26 | No favorites yet. Tap a heart on a recipe. 27 | 28 | } 29 | renderItem={({ item }) => ( 30 | navigation.navigate("RecipeDetail", { id: item.id })} 33 | onToggleFavorite={() => toggleFavorite(item.id)} 34 | /> 35 | )} 36 | /> 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 📱 Foodie App 3 | 4 | Foodie is a simple recipe browsing and management app built with **Expo**, **React Native**, and **TypeScript**. 5 | Users can explore recipes, filter by category, save favorites, and create their own dishes using a clean and intuitive interface. 6 | 7 | --- 8 | 9 | ## ✨ Features 10 | 11 | ### 🏠 Main Feed 12 | - Horizontal category bar with **10+ categories** 13 | - Scrollable recipe list 14 | - Category filtering 15 | 16 | ### 📄 Recipe Details 17 | - Ingredients 18 | - Instructions 19 | - Preparation time 20 | - Servings 21 | - Calories 22 | - Difficulty level 23 | 24 | ### ❤️ Favorites 25 | - Favorite/unfavorite recipes 26 | - Favorites screen with full details 27 | 28 | ### 🍳 My Food 29 | - Add new recipes 30 | - Edit recipes 31 | - Delete recipes 32 | - View full details of custom recipes 33 | 34 | ### 🔙 Navigation 35 | - Back navigation supported across all screens 36 | 37 | --- 38 | 39 | ## 🚀 Getting Started 40 | 41 | ```bash 42 | git clone https://github.com/your-username/Foodie.git 43 | cd Foodie 44 | npm install 45 | npx expo start 46 | ``` 47 | ## 🧱 Tech Stack 48 | 49 | - Expo / React Native 50 | - TypeScript 51 | - React Navigation 52 | - Context API 53 | 54 | --- 55 | 56 | ## 📂 Project Structure 57 | 58 | Foodie/ 59 | ├── App.tsx 60 | ├── package.json 61 | └── src/ 62 | ├── components/ 63 | ├── data/ 64 | ├── navigation/ 65 | ├── screens/ 66 | ├── store/ 67 | ├── theme.ts 68 | └── types.ts 69 | 70 | --- 71 | 72 | ## 🚧 Future Improvements 73 | 74 | - Persist data using AsyncStorage 75 | - Image upload (Expo ImagePicker) 76 | - Search bar 77 | - Dark mode 78 | - Cloud sync 79 | - Animations 80 | 81 | --- 82 | 83 | ## 📄 License 84 | 85 | Licensed under the **MIT License**. 86 | 87 | --- 88 | 89 | ## ▶️ Run Using (Expo) 90 | 91 | - **w** → Web 92 | - **i** → iOS Simulator 93 | - **a** → Android Emulator 94 | - **QR** → Expo Go 95 | If you want this merged into the full README.md, tell me and I’ll combine everything into one Markdown file. 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/store/RecipesContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useMemo, useReducer, PropsWithChildren } from "react"; 2 | import { Recipe } from "../types"; 3 | import { SEED_RECIPES } from "../data/seed"; 4 | 5 | type State = { recipes: Recipe[] }; 6 | 7 | type Action = 8 | | { type: "TOGGLE_FAVORITE"; id: string } 9 | | { type: "ADD_RECIPE"; recipe: Recipe } 10 | | { type: "UPDATE_RECIPE"; recipe: Recipe } 11 | | { type: "DELETE_RECIPE"; id: string }; 12 | 13 | const initialState: State = { recipes: SEED_RECIPES }; 14 | 15 | function reducer(state: State, action: Action): State { 16 | switch (action.type) { 17 | case "TOGGLE_FAVORITE": 18 | return { 19 | recipes: state.recipes.map((r) => 20 | r.id === action.id ? { ...r, isFavorite: !r.isFavorite } : r 21 | ), 22 | }; 23 | case "ADD_RECIPE": 24 | return { recipes: [action.recipe, ...state.recipes] }; 25 | case "UPDATE_RECIPE": 26 | return { 27 | recipes: state.recipes.map((r) => (r.id === action.recipe.id ? action.recipe : r)), 28 | }; 29 | case "DELETE_RECIPE": 30 | return { recipes: state.recipes.filter((r) => r.id !== action.id) }; 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | type Ctx = { 37 | recipes: Recipe[]; 38 | favorites: Recipe[]; 39 | myRecipes: Recipe[]; 40 | toggleFavorite: (id: string) => void; 41 | addRecipe: (recipe: Recipe) => void; 42 | updateRecipe: (recipe: Recipe) => void; 43 | deleteRecipe: (id: string) => void; 44 | }; 45 | 46 | export const RecipesContext = createContext({} as Ctx); 47 | 48 | export function RecipesProvider({ children }: PropsWithChildren) { 49 | const [state, dispatch] = useReducer(reducer, initialState); 50 | 51 | const value = useMemo(() => { 52 | const favorites = state.recipes.filter((r) => r.isFavorite); 53 | const myRecipes = state.recipes.filter((r) => r.isUserRecipe); 54 | 55 | return { 56 | recipes: state.recipes, 57 | favorites, 58 | myRecipes, 59 | toggleFavorite: (id) => dispatch({ type: "TOGGLE_FAVORITE", id }), 60 | addRecipe: (recipe) => dispatch({ type: "ADD_RECIPE", recipe }), 61 | updateRecipe: (recipe) => dispatch({ type: "UPDATE_RECIPE", recipe }), 62 | deleteRecipe: (id) => dispatch({ type: "DELETE_RECIPE", id }), 63 | }; 64 | }, [state.recipes]); 65 | 66 | return {children}; 67 | } 68 | -------------------------------------------------------------------------------- /src/data/seed.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from "../types"; 2 | 3 | export const CATEGORIES: string[] = [ 4 | "All", 5 | "Breakfast", 6 | "Lunch", 7 | "Dinner", 8 | "Dessert", 9 | "Vegan", 10 | "Pasta", 11 | "Salads", 12 | "Soups", 13 | "Snacks", 14 | "Drinks", 15 | "My Food", // required in categories bar 16 | ]; 17 | 18 | export const SEED_RECIPES: Recipe[] = [ 19 | { 20 | id: "r1", 21 | name: "Avocado Toast", 22 | category: "Breakfast", 23 | imageUrl: "https://picsum.photos/seed/avo/800/500", 24 | ingredients: ["2 slices bread", "1 avocado", "Salt", "Pepper", "Lemon"], 25 | instructions: [ 26 | "Toast the bread.", 27 | "Mash avocado with salt, pepper, lemon.", 28 | "Spread on toast and serve.", 29 | ], 30 | prepTimeMinutes: 10, 31 | servings: 1, 32 | calories: 320, 33 | difficulty: "Easy", 34 | isFavorite: false, 35 | isUserRecipe: false, 36 | }, 37 | { 38 | id: "r2", 39 | name: "Pasta Pomodoro", 40 | category: "Pasta", 41 | imageUrl: "https://picsum.photos/seed/pasta/800/500", 42 | ingredients: ["200g pasta", "Tomato sauce", "Olive oil", "Basil", "Salt"], 43 | instructions: [ 44 | "Boil pasta in salted water.", 45 | "Warm sauce with olive oil.", 46 | "Combine pasta and sauce. Top with basil.", 47 | ], 48 | prepTimeMinutes: 20, 49 | servings: 2, 50 | calories: 540, 51 | difficulty: "Easy", 52 | isFavorite: false, 53 | isUserRecipe: false, 54 | }, 55 | { 56 | id: "r3", 57 | name: "Chicken Salad", 58 | category: "Salads", 59 | imageUrl: "https://picsum.photos/seed/salad/800/500", 60 | ingredients: ["Chicken breast", "Lettuce", "Tomatoes", "Cucumber", "Dressing"], 61 | instructions: ["Cook chicken.", "Chop veggies.", "Mix everything and dress."], 62 | prepTimeMinutes: 25, 63 | servings: 2, 64 | calories: 410, 65 | difficulty: "Medium", 66 | isFavorite: false, 67 | isUserRecipe: false, 68 | }, 69 | { 70 | id: "r4", 71 | name: "Tomato Soup", 72 | category: "Soups", 73 | imageUrl: "https://picsum.photos/seed/soup/800/500", 74 | ingredients: ["Tomatoes", "Onion", "Garlic", "Broth", "Salt", "Pepper"], 75 | instructions: ["Sauté onion/garlic.", "Add tomatoes + broth.", "Simmer and blend."], 76 | prepTimeMinutes: 30, 77 | servings: 3, 78 | calories: 180, 79 | difficulty: "Medium", 80 | isFavorite: false, 81 | isUserRecipe: false, 82 | }, 83 | ]; 84 | -------------------------------------------------------------------------------- /src/screens/MainFeedScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo, useState } from "react"; 2 | import { FlatList, SafeAreaView, Text, View } from "react-native"; 3 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 4 | import { RecipesContext } from "../store/RecipesContext"; 5 | import { CategoryBar } from "../components/CategoryBar"; 6 | import { RecipeCard } from "../components/RecipeCard"; 7 | import { CATEGORIES } from "../data/seed"; 8 | import { FeedStackParamList, RootTabParamList } from "../navigation/RootNavigator"; 9 | 10 | type Props = NativeStackScreenProps & { 11 | navigation: any; 12 | }; 13 | 14 | export function MainFeedScreen({ navigation }: Props) { 15 | const { recipes, toggleFavorite } = useContext(RecipesContext); 16 | const [selected, setSelected] = useState("All"); 17 | 18 | const filtered = useMemo(() => { 19 | if (selected === "All") return recipes.filter((r) => r.category !== "My Food"); 20 | if (selected === "My Food") return []; // we route to My Food tab instead 21 | return recipes.filter((r) => r.category === selected); 22 | }, [recipes, selected]); 23 | 24 | const onSelect = (cat: string) => { 25 | setSelected(cat); 26 | if (cat === "My Food") { 27 | navigation.getParent()?.navigate("MyFoodTab" as keyof RootTabParamList); 28 | } 29 | }; 30 | 31 | return ( 32 | 33 | 34 | Foodie 35 | Browse recipes and save favorites 36 | 37 | 38 | 39 | 40 | r.id} 43 | contentContainerStyle={{ paddingTop: 4, paddingBottom: 24 }} 44 | ListEmptyComponent={ 45 | 46 | 47 | No recipes in this category yet. 48 | 49 | 50 | } 51 | renderItem={({ item }) => ( 52 | navigation.navigate("RecipeDetail", { id: item.id })} 55 | onToggleFavorite={() => toggleFavorite(item.id)} 56 | /> 57 | )} 58 | /> 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/screens/RecipeDetailScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from "react"; 2 | import { Image, SafeAreaView, ScrollView, Text, View } from "react-native"; 3 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 4 | import { Ionicons } from "@expo/vector-icons"; 5 | import { RecipesContext } from "../store/RecipesContext"; 6 | import { AnyStackParamList } from "../navigation/RootNavigator"; 7 | 8 | type Props = NativeStackScreenProps; 9 | 10 | export function RecipeDetailScreen({ route }: Props) { 11 | const { id } = route.params; 12 | const { recipes } = useContext(RecipesContext); 13 | 14 | const recipe = useMemo(() => recipes.find((r) => r.id === id), [recipes, id]); 15 | 16 | if (!recipe) { 17 | return ( 18 | 19 | Recipe not found. 20 | 21 | ); 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | {recipe.name} 31 | 32 | {/* Required fields */} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Ingredients 41 | {recipe.ingredients.map((x, i) => ( 42 | 43 | • {x} 44 | 45 | ))} 46 | 47 | Instructions 48 | {recipe.instructions.map((x, i) => ( 49 | 50 | {i + 1}. {x} 51 | 52 | ))} 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | function Meta({ icon, text }: { icon: any; text: string }) { 60 | return ( 61 | 62 | 63 | {text} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/navigation/RootNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 3 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 4 | import { Ionicons } from "@expo/vector-icons"; 5 | 6 | import { MainFeedScreen } from "../screens/MainFeedScreen"; 7 | import { FavoritesScreen } from "../screens/FavoritesScreen"; 8 | import { MyFoodScreen } from "../screens/MyFoodScreen"; 9 | import { RecipeDetailScreen } from "../screens/RecipeDetailScreen"; 10 | import { AddEditRecipeScreen } from "../screens/AddEditRecipeScreen"; 11 | 12 | export type AnyStackParamList = { 13 | RecipeDetail: { id: string }; 14 | }; 15 | 16 | export type FeedStackParamList = { 17 | MainFeed: undefined; 18 | RecipeDetail: { id: string }; 19 | }; 20 | 21 | export type FavoritesStackParamList = { 22 | Favorites: undefined; 23 | RecipeDetail: { id: string }; 24 | }; 25 | 26 | export type MyFoodStackParamList = { 27 | MyFood: undefined; 28 | AddEditRecipe: { id?: string } | undefined; 29 | RecipeDetail: { id: string }; 30 | }; 31 | 32 | export type RootTabParamList = { 33 | FeedTab: undefined; 34 | FavoritesTab: undefined; 35 | MyFoodTab: undefined; 36 | }; 37 | 38 | const Tab = createBottomTabNavigator(); 39 | const FeedStack = createNativeStackNavigator(); 40 | const FavoritesStack = createNativeStackNavigator(); 41 | const MyFoodStack = createNativeStackNavigator(); 42 | 43 | function FeedStackNav() { 44 | return ( 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | function FavoritesStackNav() { 53 | return ( 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | function MyFoodStackNav() { 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export function RootNavigator() { 72 | return ( 73 | ({ 75 | headerShown: false, 76 | tabBarIcon: ({ size, color }) => { 77 | const name = 78 | route.name === "FeedTab" 79 | ? "restaurant-outline" 80 | : route.name === "FavoritesTab" 81 | ? "heart-outline" 82 | : "create-outline"; 83 | return ; 84 | }, 85 | })} 86 | > 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/screens/MyFoodScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { FlatList, Pressable, SafeAreaView, Text, View } from "react-native"; 3 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 4 | import { RecipesContext } from "../store/RecipesContext"; 5 | import { MyFoodStackParamList } from "../navigation/RootNavigator"; 6 | 7 | type Props = NativeStackScreenProps; 8 | 9 | export function MyFoodScreen({ navigation }: Props) { 10 | const { myRecipes, deleteRecipe } = useContext(RecipesContext); 11 | 12 | return ( 13 | 14 | 15 | My Food 16 | Manage your recipes 17 | 18 | {/* Required: Add New Recipe option */} 19 | navigation.navigate("AddEditRecipe", {})} 21 | style={{ 22 | marginTop: 12, 23 | paddingVertical: 12, 24 | paddingHorizontal: 14, 25 | borderRadius: 12, 26 | backgroundColor: "#111", 27 | alignSelf: "flex-start", 28 | }} 29 | > 30 | Add New Recipe 31 | 32 | 33 | 34 | r.id} 37 | contentContainerStyle={{ paddingBottom: 24 }} 38 | ListEmptyComponent={ 39 | 40 | No recipes yet. Add your first one! 41 | 42 | } 43 | renderItem={({ item }) => ( 44 | 55 | navigation.navigate("RecipeDetail", { id: item.id })}> 56 | {item.name} 57 | 58 | {item.category} • {item.prepTimeMinutes} min • {item.difficulty} 59 | 60 | 61 | 62 | {/* Required: Edit + Delete functional */} 63 | 64 | navigation.navigate("AddEditRecipe", { id: item.id })} 66 | style={{ 67 | paddingVertical: 10, 68 | paddingHorizontal: 12, 69 | borderRadius: 10, 70 | borderWidth: 1, 71 | borderColor: "#111", 72 | }} 73 | > 74 | Edit 75 | 76 | 77 | deleteRecipe(item.id)} 79 | style={{ 80 | paddingVertical: 10, 81 | paddingHorizontal: 12, 82 | borderRadius: 10, 83 | borderWidth: 1, 84 | borderColor: "#dc2626", 85 | }} 86 | > 87 | Delete 88 | 89 | 90 | 91 | )} 92 | /> 93 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/screens/AddEditRecipeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo, useState } from "react"; 2 | import { Alert, Pressable, SafeAreaView, ScrollView, Text, TextInput, View } from "react-native"; 3 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 4 | import { RecipesContext } from "../store/RecipesContext"; 5 | import { Difficulty, Recipe } from "../types"; 6 | import { CATEGORIES } from "../data/seed"; 7 | import { MyFoodStackParamList } from "../navigation/RootNavigator"; 8 | 9 | type Props = NativeStackScreenProps; 10 | 11 | const DIFFICULTIES: Difficulty[] = ["Easy", "Medium", "Hard"]; 12 | 13 | export function AddEditRecipeScreen({ navigation, route }: Props) { 14 | const { id } = route.params ?? {}; 15 | const { recipes, addRecipe, updateRecipe } = useContext(RecipesContext); 16 | 17 | const editing = useMemo(() => recipes.find((r) => r.id === id), [recipes, id]); 18 | 19 | const [name, setName] = useState(editing?.name ?? ""); 20 | const [imageUrl, setImageUrl] = useState(editing?.imageUrl ?? "https://picsum.photos/seed/my/800/500"); 21 | const [category, setCategory] = useState(editing?.category ?? "Dinner"); 22 | const [ingredientsText, setIngredientsText] = useState((editing?.ingredients ?? []).join("\n")); 23 | const [instructionsText, setInstructionsText] = useState((editing?.instructions ?? []).join("\n")); 24 | const [prepTime, setPrepTime] = useState(String(editing?.prepTimeMinutes ?? 15)); 25 | const [servings, setServings] = useState(String(editing?.servings ?? 2)); 26 | const [calories, setCalories] = useState(String(editing?.calories ?? 400)); 27 | const [difficulty, setDifficulty] = useState(editing?.difficulty ?? "Easy"); 28 | 29 | const save = () => { 30 | if (!name.trim()) return Alert.alert("Missing name", "Please enter a recipe name."); 31 | if (!ingredientsText.trim()) return Alert.alert("Missing ingredients", "Add at least one ingredient."); 32 | if (!instructionsText.trim()) return Alert.alert("Missing instructions", "Add at least one instruction."); 33 | 34 | const recipe: Recipe = { 35 | id: editing?.id ?? `u_${Date.now()}`, 36 | name: name.trim(), 37 | imageUrl: imageUrl.trim(), 38 | category, 39 | ingredients: ingredientsText.split("\n").map((s) => s.trim()).filter(Boolean), 40 | instructions: instructionsText.split("\n").map((s) => s.trim()).filter(Boolean), 41 | prepTimeMinutes: Math.max(1, parseInt(prepTime || "0", 10) || 0), 42 | servings: Math.max(1, parseInt(servings || "0", 10) || 0), 43 | calories: Math.max(0, parseInt(calories || "0", 10) || 0), 44 | difficulty, 45 | isFavorite: editing?.isFavorite ?? false, 46 | isUserRecipe: true, 47 | }; 48 | 49 | if (editing) updateRecipe(recipe); 50 | else addRecipe(recipe); 51 | 52 | navigation.goBack(); 53 | }; 54 | 55 | return ( 56 | 57 | 58 | 59 | {editing ? "Edit Recipe" : "Add New Recipe"} 60 | 61 | 62 | {/* Required inputs */} 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | {CATEGORIES.filter((c) => c !== "My Food" && c !== "All").map((c) => { 80 | const active = c === category; 81 | return ( 82 | setCategory(c)} 85 | style={{ 86 | paddingVertical: 8, 87 | paddingHorizontal: 12, 88 | borderRadius: 18, 89 | borderWidth: 1, 90 | borderColor: active ? "#111" : "#ccc", 91 | backgroundColor: active ? "#111" : "transparent", 92 | }} 93 | > 94 | {c} 95 | 96 | ); 97 | })} 98 | 99 | 100 | 101 | 102 | 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | {DIFFICULTIES.map((d) => { 141 | const active = d === difficulty; 142 | return ( 143 | setDifficulty(d)} 146 | style={{ 147 | paddingVertical: 10, 148 | paddingHorizontal: 12, 149 | borderRadius: 12, 150 | borderWidth: 1, 151 | borderColor: active ? "#111" : "#ccc", 152 | backgroundColor: active ? "#111" : "transparent", 153 | }} 154 | > 155 | {d} 156 | 157 | ); 158 | })} 159 | 160 | 161 | 162 | {/* Required: Save Recipe button */} 163 | 167 | Save Recipe 168 | 169 | 170 | 171 | ); 172 | } 173 | 174 | function Field({ label, children }: { label: string; children: React.ReactNode }) { 175 | return ( 176 | 177 | {label} 178 | {children} 179 | 180 | ); 181 | } 182 | 183 | const input = { 184 | borderWidth: 1, 185 | borderColor: "#e5e5e5", 186 | borderRadius: 12, 187 | paddingHorizontal: 12, 188 | paddingVertical: 12, 189 | fontSize: 15, 190 | } as const; 191 | --------------------------------------------------------------------------------