diff --git a/.idea/misc.xml b/.idea/misc.xml index 3827c7d..374f537 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.idea/o-reilly-course-meals-app-flutter.iml b/.idea/o-reilly-course-meals-app-flutter.iml index 0daf8a9..f6cab43 100644 --- a/.idea/o-reilly-course-meals-app-flutter.iml +++ b/.idea/o-reilly-course-meals-app-flutter.iml @@ -3,10 +3,14 @@ + + + + \ No newline at end of file diff --git a/lib/category_meals_screen.dart b/lib/category_meals_screen.dart deleted file mode 100644 index 4580ae2..0000000 --- a/lib/category_meals_screen.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class CategoryMealsScreen extends StatelessWidget { - static const routeName = '/category-meals'; - - @override - Widget build(BuildContext context) { - final routeArgs = ModalRoute.of(context)?.settings.arguments as Map; - final categoryTitle = routeArgs['title']; - final categoryId = routeArgs['id']; - final theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - backgroundColor: theme.primaryColor, - title: Text(categoryTitle!), - ), - - ); - } -} diff --git a/lib/dummy_data.dart b/lib/dummy_data.dart index add0d22..ce1d435 100644 --- a/lib/dummy_data.dart +++ b/lib/dummy_data.dart @@ -1,56 +1,408 @@ import 'package:flutter/material.dart'; import './models/category.dart'; +import './models/meal.dart'; const DUMMY_CATEGORIES = [ Category( - 'c1', - 'Italian', - Colors.purple, + id: 'c1', + title: 'Italian', + color: Colors.purple, ), Category( - 'c2', - 'Quick & Easy', - Colors.red, + id: 'c2', + title: 'Quick & Easy', + color: Colors.red, ), Category( - 'c3', - 'Hamburgers', - Colors.orange, + id: 'c3', + title: 'Hamburgers', + color: Colors.orange, ), Category( - 'c4', - 'German', - Colors.amber, + id: 'c4', + title: 'German', + color: Colors.amber, ), Category( - 'c5', - 'Light & Lovely', - Colors.blue, + id: 'c5', + title: 'Light & Lovely', + color: Colors.blue, ), Category( - 'c6', - 'Exotic', - Colors.green, + id: 'c6', + title: 'Exotic', + color: Colors.green, ), Category( - 'c7', - 'Breakfast', - Colors.lightBlue, + id: 'c7', + title: 'Breakfast', + color: Colors.lightBlue, ), Category( - 'c8', - 'Asian', - Colors.lightGreen, + id: 'c8', + title: 'Asian', + color: Colors.lightGreen, ), Category( - 'c9', - 'French', - Colors.pink, + id: 'c9', + title: 'French', + color: Colors.pink, ), Category( - 'c10', - 'Summer', - Colors.teal, + id: 'c10', + title: 'Summer', + color: Colors.teal, + ), +]; + +const DUMMY_MEALS = [ + Meal( + id: 'm1', + categories: [ + 'c1', + 'c2', + ], + title: 'Spaghetti with Tomato Sauce', + affordability: Affordability.affordable, + complexity: Complexity.simple, + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg/800px-Spaghetti_Bolognese_mit_Parmesan_oder_Grana_Padano.jpg', + duration: 20, + ingredients: [ + '4 Tomatoes', + '1 Tablespoon of Olive Oil', + '1 Onion', + '250g Spaghetti', + 'Spices', + 'Cheese (optional)' + ], + steps: [ + 'Cut the tomatoes and the onion into small pieces.', + 'Boil some water - add salt to it once it boils.', + 'Put the spaghetti into the boiling water - they should be done in about 10 to 12 minutes.', + 'In the meantime, heaten up some olive oil and add the cut onion.', + 'After 2 minutes, add the tomato pieces, salt, pepper and your other spices.', + 'The sauce will be done once the spaghetti are.', + 'Feel free to add some cheese on top of the finished dish.' + ], + isGlutenFree: false, + isVegan: true, + isVegetarian: true, + isLactoseFree: true, + ), + Meal( + id: 'm2', + categories: [ + 'c2', + ], + title: 'Toast Hawaii', + affordability: Affordability.affordable, + complexity: Complexity.simple, + imageUrl: + 'https://cdn.pixabay.com/photo/2018/07/11/21/51/toast-3532016_1280.jpg', + duration: 10, + ingredients: [ + '1 Slice White Bread', + '1 Slice Ham', + '1 Slice Pineapple', + '1-2 Slices of Cheese', + 'Butter' + ], + steps: [ + 'Butter one side of the white bread', + 'Layer ham, the pineapple and cheese on the white bread', + 'Bake the toast for round about 10 minutes in the oven at 200°C' + ], + isGlutenFree: false, + isVegan: false, + isVegetarian: false, + isLactoseFree: false, + ), + Meal( + id: 'm3', + categories: [ + 'c2', + 'c3', + ], + title: 'Classic Hamburger', + affordability: Affordability.pricey, + complexity: Complexity.simple, + imageUrl: + 'https://cdn.pixabay.com/photo/2014/10/23/18/05/burger-500054_1280.jpg', + duration: 45, + ingredients: [ + '300g Cattle Hack', + '1 Tomato', + '1 Cucumber', + '1 Onion', + 'Ketchup', + '2 Burger Buns' + ], + steps: [ + 'Form 2 patties', + 'Fry the patties for c. 4 minutes on each side', + 'Quickly fry the buns for c. 1 minute on each side', + 'Bruch buns with ketchup', + 'Serve burger with tomato, cucumber and onion' + ], + isGlutenFree: false, + isVegan: false, + isVegetarian: false, + isLactoseFree: true, + ), + Meal( + id: 'm4', + categories: [ + 'c4', + ], + title: 'Wiener Schnitzel', + affordability: Affordability.luxurious, + complexity: Complexity.challenging, + imageUrl: + 'https://cdn.pixabay.com/photo/2018/03/31/19/29/schnitzel-3279045_1280.jpg', + duration: 60, + ingredients: [ + '8 Veal Cutlets', + '4 Eggs', + '200g Bread Crumbs', + '100g Flour', + '300ml Butter', + '100g Vegetable Oil', + 'Salt', + 'Lemon Slices' + ], + steps: [ + 'Tenderize the veal to about 2–4mm, and salt on both sides.', + 'On a flat plate, stir the eggs briefly with a fork.', + 'Lightly coat the cutlets in flour then dip into the egg, and finally, coat in breadcrumbs.', + 'Heat the butter and oil in a large pan (allow the fat to get very hot) and fry the schnitzels until golden brown on both sides.', + 'Make sure to toss the pan regularly so that the schnitzels are surrounded by oil and the crumbing becomes ‘fluffy’.', + 'Remove, and drain on kitchen paper. Fry the parsley in the remaining oil and drain.', + 'Place the schnitzels on awarmed plate and serve garnishedwith parsley and slices of lemon.' + ], + isGlutenFree: false, + isVegan: false, + isVegetarian: false, + isLactoseFree: false, + ), + Meal( + id: 'm5', + categories: [ + 'c2' + 'c5', + 'c10', + ], + title: 'Salad with Smoked Salmon', + affordability: Affordability.luxurious, + complexity: Complexity.simple, + imageUrl: + 'https://cdn.pixabay.com/photo/2016/10/25/13/29/smoked-salmon-salad-1768890_1280.jpg', + duration: 15, + ingredients: [ + 'Arugula', + 'Lamb\'s Lettuce', + 'Parsley', + 'Fennel', + '200g Smoked Salmon', + 'Mustard', + 'Balsamic Vinegar', + 'Olive Oil', + 'Salt and Pepper' + ], + steps: [ + 'Wash and cut salad and herbs', + 'Dice the salmon', + 'Process mustard, vinegar and olive oil into a dessing', + 'Prepare the salad', + 'Add salmon cubes and dressing' + ], + isGlutenFree: true, + isVegan: false, + isVegetarian: true, + isLactoseFree: true, + ), + Meal( + id: 'm6', + categories: [ + 'c6', + 'c10', + ], + title: 'Delicious Orange Mousse', + affordability: Affordability.affordable, + complexity: Complexity.hard, + imageUrl: + 'https://cdn.pixabay.com/photo/2017/05/01/05/18/pastry-2274750_1280.jpg', + duration: 240, + ingredients: [ + '4 Sheets of Gelatine', + '150ml Orange Juice', + '80g Sugar', + '300g Yoghurt', + '200g Cream', + 'Orange Peel', + ], + steps: [ + 'Dissolve gelatine in pot', + 'Add orange juice and sugar', + 'Take pot off the stove', + 'Add 2 tablespoons of yoghurt', + 'Stir gelatin under remaining yoghurt', + 'Cool everything down in the refrigerator', + 'Whip the cream and lift it under die orange mass', + 'Cool down again for at least 4 hours', + 'Serve with orange peel', + ], + isGlutenFree: true, + isVegan: false, + isVegetarian: true, + isLactoseFree: false, + ), + Meal( + id: 'm7', + categories: [ + 'c7', + ], + title: 'Pancakes', + affordability: Affordability.affordable, + complexity: Complexity.simple, + imageUrl: + 'https://cdn.pixabay.com/photo/2018/07/10/21/23/pancake-3529653_1280.jpg', + duration: 20, + ingredients: [ + '1 1/2 Cups all-purpose Flour', + '3 1/2 Teaspoons Baking Powder', + '1 Teaspoon Salt', + '1 Tablespoon White Sugar', + '1 1/4 cups Milk', + '1 Egg', + '3 Tablespoons Butter, melted', + ], + steps: [ + 'In a large bowl, sift together the flour, baking powder, salt and sugar.', + 'Make a well in the center and pour in the milk, egg and melted butter; mix until smooth.', + 'Heat a lightly oiled griddle or frying pan over medium high heat.', + 'Pour or scoop the batter onto the griddle, using approximately 1/4 cup for each pancake. Brown on both sides and serve hot.' + ], + isGlutenFree: true, + isVegan: false, + isVegetarian: true, + isLactoseFree: false, + ), + Meal( + id: 'm8', + categories: [ + 'c8', + ], + title: 'Creamy Indian Chicken Curry', + affordability: Affordability.pricey, + complexity: Complexity.challenging, + imageUrl: + 'https://cdn.pixabay.com/photo/2018/06/18/16/05/indian-food-3482749_1280.jpg', + duration: 35, + ingredients: [ + '4 Chicken Breasts', + '1 Onion', + '2 Cloves of Garlic', + '1 Piece of Ginger', + '4 Tablespoons Almonds', + '1 Teaspoon Cayenne Pepper', + '500ml Coconut Milk', + ], + steps: [ + 'Slice and fry the chicken breast', + 'Process onion, garlic and ginger into paste and sauté everything', + 'Add spices and stir fry', + 'Add chicken breast + 250ml of water and cook everything for 10 minutes', + 'Add coconut milk', + 'Serve with rice' + ], + isGlutenFree: true, + isVegan: false, + isVegetarian: false, + isLactoseFree: true, + ), + Meal( + id: 'm9', + categories: [ + 'c9', + ], + title: 'Chocolate Souffle', + affordability: Affordability.affordable, + complexity: Complexity.hard, + imageUrl: + 'https://cdn.pixabay.com/photo/2014/08/07/21/07/souffle-412785_1280.jpg', + duration: 45, + ingredients: [ + '1 Teaspoon melted Butter', + '2 Tablespoons white Sugar', + '2 Ounces 70% dark Chocolate, broken into pieces', + '1 Tablespoon Butter', + '1 Tablespoon all-purpose Flour', + '4 1/3 tablespoons cold Milk', + '1 Pinch Salt', + '1 Pinch Cayenne Pepper', + '1 Large Egg Yolk', + '2 Large Egg Whites', + '1 Pinch Cream of Tartar', + '1 Tablespoon white Sugar', + ], + steps: [ + 'Preheat oven to 190°C. Line a rimmed baking sheet with parchment paper.', + 'Brush bottom and sides of 2 ramekins lightly with 1 teaspoon melted butter; cover bottom and sides right up to the rim.', + 'Add 1 tablespoon white sugar to ramekins. Rotate ramekins until sugar coats all surfaces.', + 'Place chocolate pieces in a metal mixing bowl.', + 'Place bowl over a pan of about 3 cups hot water over low heat.', + 'Melt 1 tablespoon butter in a skillet over medium heat. Sprinkle in flour. Whisk until flour is incorporated into butter and mixture thickens.', + 'Whisk in cold milk until mixture becomes smooth and thickens. Transfer mixture to bowl with melted chocolate.', + 'Add salt and cayenne pepper. Mix together thoroughly. Add egg yolk and mix to combine.', + 'Leave bowl above the hot (not simmering) water to keep chocolate warm while you whip the egg whites.', + 'Place 2 egg whites in a mixing bowl; add cream of tartar. Whisk until mixture begins to thicken and a drizzle from the whisk stays on the surface about 1 second before disappearing into the mix.', + 'Add 1/3 of sugar and whisk in. Whisk in a bit more sugar about 15 seconds.', + 'whisk in the rest of the sugar. Continue whisking until mixture is about as thick as shaving cream and holds soft peaks, 3 to 5 minutes.', + 'Transfer a little less than half of egg whites to chocolate.', + 'Mix until egg whites are thoroughly incorporated into the chocolate.', + 'Add the rest of the egg whites; gently fold into the chocolate with a spatula, lifting from the bottom and folding over.', + 'Stop mixing after the egg white disappears. Divide mixture between 2 prepared ramekins. Place ramekins on prepared baking sheet.', + 'Bake in preheated oven until scuffles are puffed and have risen above the top of the rims, 12 to 15 minutes.', + ], + isGlutenFree: true, + isVegan: false, + isVegetarian: true, + isLactoseFree: false, + ), + Meal( + id: 'm10', + categories: [ + 'c2', + 'c5', + 'c10', + ], + title: 'Asparagus Salad with Cherry Tomatoes', + affordability: Affordability.luxurious, + complexity: Complexity.simple, + imageUrl: + 'https://cdn.pixabay.com/photo/2018/04/09/18/26/asparagus-3304997_1280.jpg', + duration: 30, + ingredients: [ + 'White and Green Asparagus', + '30g Pine Nuts', + '300g Cherry Tomatoes', + 'Salad', + 'Salt, Pepper and Olive Oil' + ], + steps: [ + 'Wash, peel and cut the asparagus', + 'Cook in salted water', + 'Salt and pepper the asparagus', + 'Roast the pine nuts', + 'Halve the tomatoes', + 'Mix with asparagus, salad and dressing', + 'Serve with Baguette' + ], + isGlutenFree: true, + isVegan: true, + isVegetarian: true, + isLactoseFree: true, ), ]; \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index cd04aa5..46c632f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,52 @@ import 'package:flutter/material.dart'; -import 'package:meals_app/categories_screen.dart'; -import 'package:meals_app/category_meals_screen.dart'; +import 'package:meals_app/dummy_data.dart'; +import 'package:meals_app/screens/categories_screen.dart'; +import 'package:meals_app/screens/category_meals_screen.dart'; +import 'package:meals_app/screens/filters_screen.dart'; +import 'package:meals_app/screens/meal_detail_screen.dart'; +import 'package:meals_app/screens/tabs_screen.dart'; + +import 'models/meal.dart'; void main() => runApp(MyApp()); -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + Map _filters = { + 'gluten': false, + 'lactose': false, + 'vegan': false, + 'vegetarian': false, + }; + List _availableMeals = DUMMY_MEALS; + + void _setFilters(Map filterData){ + setState(() { + _filters = filterData; + + _availableMeals = DUMMY_MEALS.where((meal) { + if(_filters['gluten']! && !meal.isGlutenFree){ + return false; + } + if(_filters['lactose']! && !meal.isLactoseFree){ + return false; + } + if(_filters['vegan']! && !meal.isVegan){ + return false; + } + if(_filters['vegetarian']! && !meal.isVegetarian){ + return false; + } + return true; + }).toList(); + }); + } @override Widget build(BuildContext context) { return MaterialApp( @@ -24,8 +64,13 @@ class MyApp extends StatelessWidget { color: Color.fromRGBO(20, 51, 51, 1), ))), routes: { - '/': (ctx) => const CategoriesScreen(), - CategoryMealsScreen.routeName: (ctx) => CategoryMealsScreen(), + '/': (ctx) => const TabScreen(), + CategoryMealsScreen.routeName: (ctx) => CategoryMealsScreen(_availableMeals), + MealDetailScreen.routeName: (ctx) => MealDetailScreen(), + FilterScreen.routeName: (ctx) => FilterScreen(saveFilters: _setFilters, currentFilters: _filters,) + }, + onUnknownRoute: (settings) { + return MaterialPageRoute(builder: (ctx) => CategoriesScreen()); }, ); } diff --git a/lib/models/category.dart b/lib/models/category.dart index 08e7100..66c09b5 100644 --- a/lib/models/category.dart +++ b/lib/models/category.dart @@ -7,5 +7,5 @@ class Category{ final String title; final Color color; - const Category(this.id, this.title, this.color); + const Category({required this.id, required this.title, required this.color}); } \ No newline at end of file diff --git a/lib/models/meal.dart b/lib/models/meal.dart new file mode 100644 index 0000000..3fbb9a8 --- /dev/null +++ b/lib/models/meal.dart @@ -0,0 +1,29 @@ +enum Complexity{ + simple, + challenging, + hard +} + +enum Affordability { + affordable, + pricey, + luxurious +} +class Meal{ + final String id; + final List categories; + final String title; + final String imageUrl; + final List ingredients; + final List steps; + final int duration; + final Complexity complexity; + final Affordability affordability; + final bool isGlutenFree; + final bool isLactoseFree; + final bool isVegan; + final bool isVegetarian; + + const Meal({required this.id,required this.categories,required this.title, required this.imageUrl, required this.ingredients, required this.steps, required this.duration, required this.complexity, required this.affordability, + required this.isGlutenFree, required this.isLactoseFree, required this.isVegetarian, required this.isVegan}); +} \ No newline at end of file diff --git a/lib/categories_screen.dart b/lib/screens/categories_screen.dart similarity index 52% rename from lib/categories_screen.dart rename to lib/screens/categories_screen.dart index 4eb624a..06307ee 100644 --- a/lib/categories_screen.dart +++ b/lib/screens/categories_screen.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:meals_app/category_item.dart'; +import 'package:meals_app/widgets/category_item.dart'; import 'package:meals_app/dummy_data.dart'; class CategoriesScreen extends StatelessWidget { const CategoriesScreen({super.key}); - // Constants for grid configuration static const double _maxCrossAxisExtent = 200; static const double _childAspectRatio = 3 / 2; static const double _crossAxisSpacing = 20; @@ -14,32 +13,22 @@ class CategoriesScreen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - elevation: 5, - title: const Text('DeliMeal'), - backgroundColor: theme.primaryColor, - ), - body: Container( - color: theme.canvasColor, - child: GridView( - padding: const EdgeInsets.all(15), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + return Container( + color: theme.canvasColor, + child: GridView( + padding: const EdgeInsets.all(15), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: _maxCrossAxisExtent, childAspectRatio: _childAspectRatio, crossAxisSpacing: _crossAxisSpacing, - mainAxisSpacing: _mainAxisSpacing, - ), - children: DUMMY_CATEGORIES - .map( - (catData) => CategoryItem( + mainAxisSpacing: _mainAxisSpacing), + children: DUMMY_CATEGORIES + .map((catData) => CategoryItem( title: catData.title, color: catData.color, id: catData.id, - ), - ) - .toList(), - ), + )) + .toList(), ), ); } diff --git a/lib/screens/category_meals_screen.dart b/lib/screens/category_meals_screen.dart new file mode 100644 index 0000000..253439a --- /dev/null +++ b/lib/screens/category_meals_screen.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/dummy_data.dart'; +import 'package:meals_app/widgets/meal_item.dart'; + +import '../models/meal.dart'; + +class CategoryMealsScreen extends StatefulWidget { + static const routeName = '/category-meals'; + + final List availableMeals; + + CategoryMealsScreen(this.availableMeals, {super.key}); + + @override + State createState() => _CategoryMealsScreenState(); +} + +class _CategoryMealsScreenState extends State { + String? categoryTitle; + List? displayMeals; + bool _loadedInitData = false; + + @override + void didChangeDependencies() { + if (!_loadedInitData) { + final routeArgs = ModalRoute.of(context)?.settings.arguments as Map; + categoryTitle = routeArgs['title']; + final categoryId = routeArgs['id']; + displayMeals = widget.availableMeals.where((meal) { + return meal.categories.contains(categoryId); + }).toList(); + _loadedInitData = true; + } + super.didChangeDependencies(); + } + + + void _removeMeal(String mealId) { + setState(() { + displayMeals?.removeWhere((meal) => meal.id == mealId); + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.primaryColor, + title: Text(categoryTitle!), + ), + body: ListView.builder( + itemBuilder: (ctx, index) { + return MealItem( + title: displayMeals![index].title, + imageUrl: displayMeals![index].imageUrl, + duration: displayMeals![index].duration, + complexity: displayMeals![index].complexity, + affordability: displayMeals![index].affordability, + id: displayMeals![index].id, + removeItem: _removeMeal, + ); + }, + itemCount: displayMeals?.length, + )); + } +} diff --git a/lib/screens/favorites_screen.dart b/lib/screens/favorites_screen.dart new file mode 100644 index 0000000..f98d3c9 --- /dev/null +++ b/lib/screens/favorites_screen.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class FavoritesScreen extends StatelessWidget { + const FavoritesScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/screens/filters_screen.dart b/lib/screens/filters_screen.dart new file mode 100644 index 0000000..b5a4920 --- /dev/null +++ b/lib/screens/filters_screen.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/widgets/main_drawer.dart'; + +class FilterScreen extends StatefulWidget { + static const routeName = "filter"; + final Function saveFilters; + final Map currentFilters; + + const FilterScreen({super.key, required this.saveFilters, required this.currentFilters}); + + @override + State createState() => _FilterScreenState(); +} + +class _FilterScreenState extends State { + bool _glutenFree = false; + bool _vegetarian = false; + bool _vegan = false; + bool _lactoseFree = false; + + @override + void initState() { + _glutenFree = widget.currentFilters['gluten']!; + _lactoseFree = widget.currentFilters['lactose']!; + _vegetarian = widget.currentFilters['vegetarian']!; + _vegan = widget.currentFilters['vegan']!; + super.initState(); + } + + Widget _buildSwitchListTitle(String title, String description, + bool currentValue, Function(bool) updateValue) { + return SwitchListTile( + value: currentValue, + onChanged: updateValue, + title: Text(title), + subtitle: Text(description), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.primaryColor, + title: Text("Filters"), + actions: [ + IconButton(onPressed: () { + final selectedFilters = { + 'gluten': _glutenFree, + 'lactose': _lactoseFree, + 'vegan': _vegan, + 'vegetarian': _vegetarian, + }; + widget.saveFilters(selectedFilters); + }, icon: const Icon(Icons.save)) + ], + ), + drawer: MainDrawer(), + body: Container( + color: theme.canvasColor, + child: Column( + children: [ + Container( + padding: EdgeInsets.all(20), + child: Text( + 'Adjust your meal selection.', + style: theme.textTheme.titleLarge, + ), + ), + Expanded( + child: ListView( + children: [ + _buildSwitchListTitle( + 'Gluten-free', + 'Only include gluten-free meals', _glutenFree, ( + newValue) { + setState(() { + _glutenFree = newValue; + }); + }), + _buildSwitchListTitle( + 'Lactose-free', + 'Only include lactose-free meals', _lactoseFree, ( + newValue) { + setState(() { + _lactoseFree = newValue; + }); + }), + _buildSwitchListTitle( + 'Vegetarian', + 'Only include vegetarian meals', _vegetarian, ( + newValue) { + setState(() { + _vegetarian = newValue; + }); + }), + _buildSwitchListTitle( + 'Vegan', + 'Only include vegan meals', _vegan, (newValue) { + setState(() { + _vegan = newValue; + }); + }), + ], + )) + ], + ), + ), + ); + } +} diff --git a/lib/screens/meal_detail_screen.dart b/lib/screens/meal_detail_screen.dart new file mode 100644 index 0000000..faf69f9 --- /dev/null +++ b/lib/screens/meal_detail_screen.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/dummy_data.dart'; + +class MealDetailScreen extends StatelessWidget { + static const routeName = '/meal-detail'; + + const MealDetailScreen({super.key}); + + Widget buildSectionTitle(BuildContext context, String text) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10), + child: Text( + text, + style: Theme.of(context).textTheme.titleLarge, + ), + ); + } + + Widget buildContainer(Widget child) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(10), + padding: const EdgeInsets.all(10), + height: 150, + width: 300, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final mealId = ModalRoute.of(context)?.settings.arguments as String; + final selectedMeal = DUMMY_MEALS.firstWhere((meal) => meal.id == mealId); + return Scaffold( + appBar: AppBar( + title: Text(selectedMeal.title), + backgroundColor: theme.primaryColor, + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + height: 300, + width: double.infinity, + child: Image.network( + selectedMeal.imageUrl, + fit: BoxFit.cover, + )), + buildSectionTitle(context, "Ingredients"), + buildContainer( + ListView.builder( + itemBuilder: (ctx, index) => Card( + color: theme.primaryColor, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + child: Text(selectedMeal.ingredients[index]), + ), + ), + itemCount: selectedMeal.ingredients.length, + ), + ), + buildSectionTitle(context, 'Steps'), + buildContainer(ListView.builder( + itemBuilder: (ctx, index) => Column( + children: [ + ListTile( + leading: CircleAvatar( + child: Text('# ${index + 1}'), + ), + title: Text(selectedMeal.steps[index]), + ), + const Divider() + ], + ), + itemCount: selectedMeal.steps.length, + )) + ], + ), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.delete), + onPressed: () { + Navigator.of(context).pop(mealId); + }, + ), + ); + } +} diff --git a/lib/screens/tabs_screen.dart b/lib/screens/tabs_screen.dart new file mode 100644 index 0000000..d207736 --- /dev/null +++ b/lib/screens/tabs_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/screens/categories_screen.dart'; +import 'package:meals_app/screens/favorites_screen.dart'; +import 'package:meals_app/widgets/main_drawer.dart'; + +class TabScreen extends StatefulWidget { + const TabScreen({super.key}); + + @override + State createState() => _TabScreenState(); +} + +class _TabScreenState extends State { + final List _pages = [ + CategoriesScreen(), + FavoritesScreen(), + ]; + + int _selectedPageIndex = 0; + + void _selectPage(int index) { + setState(() { + _selectedPageIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.primaryColor, + title: Text(_selectedPageIndex == 0 ? 'Categories' : 'Favorites'), + ), + drawer: MainDrawer(), + body: _pages[_selectedPageIndex], + bottomNavigationBar: BottomNavigationBar( + onTap: _selectPage, + backgroundColor: theme.primaryColor, + unselectedItemColor: Colors.white, + selectedItemColor: Colors.black, + currentIndex: _selectedPageIndex, + type: BottomNavigationBarType.shifting, + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.category), + label: 'Categories', + backgroundColor: theme.primaryColor, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.star), + label: 'Favorites', + backgroundColor: theme.primaryColor, + ), + ], + ), + ); + } +} diff --git a/lib/category_item.dart b/lib/widgets/category_item.dart similarity index 81% rename from lib/category_item.dart rename to lib/widgets/category_item.dart index def26a7..d5d9832 100644 --- a/lib/category_item.dart +++ b/lib/widgets/category_item.dart @@ -1,15 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:meals_app/category_meals_screen.dart'; - -class CategoryArguments { - final String id; - final String title; - - CategoryArguments({ - required this.id, - required this.title - }); -} +import 'package:meals_app/screens/category_meals_screen.dart'; class CategoryItem extends StatelessWidget { final String id; @@ -26,10 +16,14 @@ class CategoryItem extends StatelessWidget { void selectCategory(BuildContext ctx) { Navigator.of(ctx).pushNamed( CategoryMealsScreen.routeName, - arguments: CategoryArguments(id: id, title: title), + arguments: { + 'id': id, + 'title': title, + }, ); } + @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/lib/widgets/main_drawer.dart b/lib/widgets/main_drawer.dart new file mode 100644 index 0000000..eb32520 --- /dev/null +++ b/lib/widgets/main_drawer.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/screens/filters_screen.dart'; + +class MainDrawer extends StatelessWidget { + const MainDrawer({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + Widget buildListTile(String title, IconData iconData, Function tapHandler) { + return ListTile( + leading: Icon(iconData), // Create an Icon widget from the IconData + title: Text( + title, + style: const TextStyle( + fontFamily: 'RobotoCondensed', + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + onTap: () => tapHandler(), + ); + } + + return Drawer( + child: Column( + children: [ + Container( + height: 120, + color: theme.canvasColor, + width: double.infinity, + padding: const EdgeInsets.all(20), + alignment: Alignment.centerLeft, + child: Text( + 'Cooking up', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 30, + color: theme.primaryColor, + ), + ), + ), + const SizedBox( + height: 20, + ), + buildListTile('Meals', Icons.restaurant, () { + Navigator.of(context).pushReplacementNamed("/"); + }), + buildListTile('Filters', Icons.settings, (){ + Navigator.of(context).pushReplacementNamed(FilterScreen.routeName); + }) + ], + ), + ); + } +} diff --git a/lib/widgets/meal_item.dart b/lib/widgets/meal_item.dart new file mode 100644 index 0000000..fe6f556 --- /dev/null +++ b/lib/widgets/meal_item.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:meals_app/models/meal.dart'; +import 'package:meals_app/screens/meal_detail_screen.dart'; + +class MealItem extends StatelessWidget { + final String id; + final String title; + final String imageUrl; + final int duration; + final Complexity complexity; + final Affordability affordability; + final Function removeItem; + + const MealItem({super.key, + required this.id, + required this.title, + required this.imageUrl, + required this.duration, + required this.complexity, + required this.affordability, + required this.removeItem + }); + + String get complexityText { + switch (complexity) { + case Complexity.simple: + return 'Simple'; + case Complexity.hard: + return 'Hard'; + case Complexity.challenging: + return 'Challenging'; + } + } + + String get affordabilityText { + switch (affordability) { + case Affordability.affordable: + return 'Affordable'; + case Affordability.pricey: + return 'Pricey'; + case Affordability.luxurious: + return 'Expensive'; + } + } + + void selectMeal(BuildContext context) { + Navigator.of(context) + .pushNamed(MealDetailScreen.routeName, arguments: id).then((value) { + if(value != null){ + removeItem(value); + } + }); + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => selectMeal(context), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 4, + margin: const EdgeInsets.all(10), + child: Column( + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15)), + child: Image.network( + imageUrl, + height: 250, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Positioned( + bottom: 20, + right: 10, + child: Container( + width: 300, + color: Colors.black54, + padding: + const EdgeInsets.symmetric(vertical: 5, horizontal: 20), + child: Text( + title, + style: const TextStyle( + fontSize: 26, + color: Colors.white, + ), + softWrap: true, + overflow: TextOverflow.fade, + ), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + children: [ + const Icon(Icons.schedule), + const SizedBox( + width: 6, + ), + Text('$duration min') + ], + ), + Row( + children: [ + const Icon(Icons.work), + const SizedBox( + width: 6, + ), + Text(complexityText) + ], + ), + Row( + children: [ + const Icon(Icons.attach_money), + const SizedBox( + width: 6, + ), + Text(affordabilityText) + ], + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index cfab2c1..2fd71d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,14 +78,14 @@ flutter: - family: Raleway fonts: - asset: assets/fonts/Raleway-Regular.ttf - - asset: fonts/Raleway-Bold.ttf + - asset: assets/fonts/Raleway-Bold.ttf weight: 700 - asset: assets/fonts/Raleway-Black.ttf weight: 900 - family: RobotoCondensed fonts: - asset: assets/fonts/RobotoCondensed-Regular.ttf - - asset: fonts/RobotoCondensed-Italic.ttf + - asset: assets/fonts/RobotoCondensed-Italic.ttf weight: 700 - asset: assets/fonts/RobotoCondensed-Light.ttf weight: 300