├── 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 |
--------------------------------------------------------------------------------