diff --git a/README.md b/README.md index 9cab5647f..36fc19cc8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,6 @@ -Interaction Programing - Lab assignment - HTML +Interaction Programming - Lab assignment - HTML ================================================= -This project contains the startup code for HTML version of the Interaction Programing course lab assignment. For more details on how to complete the assignment follow the instructions on the [course website](https://www.kth.se/social/course/DH2642). +This project contains the solution for the Interaction Programming course lab assignment. For more details on the assignment check the [course website](https://www.kth.se/social/course/DH2642). -What's in the project ------ - -* [index.html](https://github.com/kth-csc-iprog/dinnerplanner-html/blob/master/index.html) - the only HTML page you will have in this project (though while testing you can create more to make it easier). You will need to implement the skeleton of the layout there and then through code (JavaScript) create the rest -* [js/model/dinnerModel.js](https://github.com/kth-csc-iprog/dinnerplanner-html/blob/master/js/model/dinnerModel.js) - is JavaScript file that contains the model code. The file contains the model (dish, ingredient and general dinner model) as well as methods and comments that you need to fully implement to support the dinner functionalities (guests, selected dishes, etc.) -* [js/view/](https://github.com/kth-csc-iprog/dinnerplanner-html/tree/master/js/view) - here you can find a JavaScript code of an example view. The view sets up some initial components and their values. -* [js/app.js](https://github.com/kth-csc-iprog/dinnerplanner-html/blob/master/js/app.js) - this is the overall code of the application. It is responsible for initial setup of the app (when the page loads for the first time). -* [images/](https://github.com/kth-csc-iprog/dinnerplanner-html/tree/master/images) - folder contains some pictures you can use for your dishes +You can see the demo [here](https://andrewszucs.github.io/dinnerplanner-html/). diff --git a/index.html b/index.html index 79a6b773b..dcb3ae7f6 100644 --- a/index.html +++ b/index.html @@ -6,33 +6,195 @@ Dinner planner - - - + + -
-

Dinner planner

-
-
- Number of guests: -
-
- - -
-
-
- - - - - - - - - - - + +
+ + + +
+ + + +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +
+ +
+ +
+ +
+
+

My Dinner

+
+ SEK 77.20 +
+ +
+ +
+
+ + +
+ +
+
+
Dish name
+
Cost
+
+ +
+ +
+ +
+
+ SEK 77.20 +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +

Find a dish

+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/js/app.js b/js/app.js index 296b0bae2..43f5f2781 100644 --- a/js/app.js +++ b/js/app.js @@ -1,15 +1,102 @@ -$(function() { - //We instantiate our model - var model = new DinnerModel(); - - // And create the instance of ExampleView - var exampleView = new ExampleView($("#exampleView")); - - /** - * IMPORTANT: app.js is the only place where you are allowed to - * use the $('someSelector') to search for elements in the whole HTML. - * In other places you should limit the search only to the children - * of the specific view you're working with (see exampleView.js). - */ - -}); \ No newline at end of file +(function () { + + // Model + const model = createDinnerModel() + + // Views + Controllers + const mainView = createMainView( + document.getElementById('mainView'), + model + ) + + const sidebarView = createSidebarView( + document.getElementById('sidebarView'), + model + ) + + const dishSearchView = createDishSearchView( + document.getElementById('dishSearchView'), + model + ) + + const dishDetailsView = createDishDetailsView( + document.getElementById('dishDetailsView'), + model + ) + + const statusBarView = createStatusBarView( + document.getElementById('statusBar'), + model + ) + + const priceSummaryView = createPriceSummaryView( + document.getElementById('priceSummary'), + model + ) + + const recipeSummaryView = createRecipeSummaryView( + document.getElementById('recipeSummaryView'), + model + ) + + // General State Controller + const generalStateController = { + transitionFromDishDetailsToDishSearch: () => { + dishDetailsView.hide() + dishSearchView.show() + }, + transitionFromDishSearchToDishDetails: (event) => { + const clickedElement = event.target + const clickedDishId = clickedElement.getAttribute('key') + if(clickedDishId !== null && clickedDishId !== undefined) { + dishSearchView.hide() + dishDetailsView.render(Number(clickedDishId)) + dishDetailsView.show() + } + }, + transitionFromMainViewToDishSearch: () => { + mainView.hide() + sidebarView.show() + dishSearchView.show() + }, + transitionFromPriceSummaryToRecipeSummary: () => { + priceSummaryView.hide() + recipeSummaryView.show() + }, + transitionFromDishSearchToPriceSummary: () => { + sidebarView.hide() + dishSearchView.hide() + dishDetailsView.hide() + + statusBarView.show() + priceSummaryView.show() + recipeSummaryView.hide() + }, + transitionFromPriceSummaryToDishSearch: () => { + statusBarView.hide() + priceSummaryView.hide() + recipeSummaryView.hide() + + sidebarView.show() + dishSearchView.show() + dishDetailsView.hide() + } + } + + const mainViewController = createMainViewController(mainView, model, generalStateController) + const sidebarController = createSidebarController(sidebarView, model, generalStateController) + const statusBarController = createStatusBarController(statusBarView, model, generalStateController) + const priceSummaryController = createPriceSummaryController(priceSummaryView, model, generalStateController) + const dishDetailsController = createDishDetailsController(dishDetailsView, model, generalStateController) + const dishSearchController = createDishSearchController(dishSearchView, model, generalStateController) + + // Initial state + mainView.show() + sidebarView.hide() + dishSearchView.hide() + dishDetailsView.hide() + statusBarView.hide() + priceSummaryView.hide() + recipeSummaryView.hide() + +})() diff --git a/js/controllers/DishDetailsController.js b/js/controllers/DishDetailsController.js new file mode 100644 index 000000000..4180636a4 --- /dev/null +++ b/js/controllers/DishDetailsController.js @@ -0,0 +1,9 @@ +const createDishDetailsController = (view, model, generalStateController) => { + view.addToMenuButton.onclick = () => { + const dishId = view.getDishId() + model.addDishToMenu(dishId) + } + + view.backFromDishDetailsButton.onclick = + generalStateController.transitionFromDishDetailsToDishSearch +} \ No newline at end of file diff --git a/js/controllers/DishSearchController.js b/js/controllers/DishSearchController.js new file mode 100644 index 000000000..0824df35b --- /dev/null +++ b/js/controllers/DishSearchController.js @@ -0,0 +1,6 @@ +const createDishSearchController = (view, model, generalStateController) => { + view.dishSearchButton.onclick = view.render + + view.dishSearchResultsElement.onclick = + generalStateController.transitionFromDishSearchToDishDetails +} \ No newline at end of file diff --git a/js/controllers/MainViewController.js b/js/controllers/MainViewController.js new file mode 100644 index 000000000..0d230d82b --- /dev/null +++ b/js/controllers/MainViewController.js @@ -0,0 +1,4 @@ +const createMainViewController = (view, model, generalStateController) => { + + view.startButton.onclick = generalStateController.transitionFromMainViewToDishSearch +} \ No newline at end of file diff --git a/js/controllers/PriceSummaryController.js b/js/controllers/PriceSummaryController.js new file mode 100644 index 000000000..ca0a60baa --- /dev/null +++ b/js/controllers/PriceSummaryController.js @@ -0,0 +1,4 @@ +const createPriceSummaryController = (view, model, generalStateController) => { + + view.printButton.onclick = generalStateController.transitionFromPriceSummaryToRecipeSummary +} \ No newline at end of file diff --git a/js/controllers/SidebarController.js b/js/controllers/SidebarController.js new file mode 100644 index 000000000..c52c7b419 --- /dev/null +++ b/js/controllers/SidebarController.js @@ -0,0 +1,10 @@ +const createSidebarController = (view, model, generalStateController) => { + + view.guestNumber.oninput = (event) => { + const newGuestNumber = event.target.value + model.setNumberOfGuests(newGuestNumber) + } + + view.confirmMenuButton.onclick = generalStateController.transitionFromDishSearchToPriceSummary + +} diff --git a/js/controllers/StatusBarController.js b/js/controllers/StatusBarController.js new file mode 100644 index 000000000..9b3c0f272 --- /dev/null +++ b/js/controllers/StatusBarController.js @@ -0,0 +1,6 @@ +const createStatusBarController = (view, model, generalStateController) => { + + view.backFromMenuSummaryButton.onclick = + generalStateController.transitionFromPriceSummaryToDishSearch + +} diff --git a/js/lib/apiConfig.js b/js/lib/apiConfig.js new file mode 100644 index 000000000..a974c9bef --- /dev/null +++ b/js/lib/apiConfig.js @@ -0,0 +1,3 @@ +const apiConfig = { + apiKey: 'Qu9grxVNWpmshA4Kl9pTwyiJxVGUp1lKzrZjsnghQMkFkfA4LB' +} diff --git a/js/lib/toggleSidebar.js b/js/lib/toggleSidebar.js new file mode 100644 index 000000000..d12108032 --- /dev/null +++ b/js/lib/toggleSidebar.js @@ -0,0 +1,19 @@ +const toggleSidebar = () => { + const collapseContent = document.getElementById('sideBarContent') + const collapsedMenuTotalPrice = document.getElementById('collapsedMenuTotalPrice') + + const collapseContentDisplay = window.getComputedStyle(collapseContent).display + + if(collapseContentDisplay === 'none') { + collapseContent.classList.remove('hideOnMobile') + collapseContent.classList.add('showOnMobile') + collapsedMenuTotalPrice.classList.remove('showOnMobile') + collapsedMenuTotalPrice.classList.add('hideOnMobile') + } + else { + collapseContent.classList.remove('showOnMobile') + collapseContent.classList.add('hideOnMobile') + collapsedMenuTotalPrice.classList.remove('hideOnMobile') + collapsedMenuTotalPrice.classList.add('showOnMobile') + } +} \ No newline at end of file diff --git a/js/model/dinnerModel.js b/js/model/dinnerModel.js index 47476a61b..695dfd14b 100644 --- a/js/model/dinnerModel.js +++ b/js/model/dinnerModel.js @@ -1,330 +1,153 @@ -//DinnerModel Object constructor -var DinnerModel = function() { - - //TODO Lab 1 implement the data structure that will hold number of guest +//DinnerModel Object factory function +const createDinnerModel = () => { + + // the data structure that will hold number of guest // and selected dishes for the dinner menu + const state = { + numberOfGuests: 1, + menu: [], + observers: [] + } + window.getState = () => state - this.setNumberOfGuests = function(num) { - //TODO Lab 1 - } - - this.getNumberOfGuests = function() { - //TODO Lab 1 + const addObserver = (observer) => { + state.observers.push(observer) } - //Returns the dish that is on the menu for selected type - this.getSelectedDish = function(type) { - //TODO Lab 1 + const removeObserver = (observer) => { + state.observers = observers.filter(subscribedObserver => subscribedObserver !== observer) } - //Returns all the dishes on the menu. - this.getFullMenu = function() { - //TODO Lab 1 + const _notifyObservers = (details) => { + state.observers.map(observer => observer(details)) } - //Returns all ingredients for all the dishes on the menu. - this.getAllIngredients = function() { - //TODO Lab 1 + const setNumberOfGuests = num => { + state.numberOfGuests = num + _notifyObservers({ numberOfGuests: state.numberOfGuests }) } - //Returns the total price of the menu (all the ingredients multiplied by number of guests). - this.getTotalMenuPrice = function() { - //TODO Lab 1 + const getNumberOfGuests = () => state.numberOfGuests + + // Returns the dish that is on the menu for the selected type + const getSelectedDish = type => state.menu.find(dish => dish.dishType === type) + + // Returns all the dishes on the menu. + const getFullMenu = () => state.menu + + // Returns all ingredients for all the dishes on the menu (represented by dish) + const getAllIngredients = () => state.menu.reduce((acc, curr) => acc.concat(curr.ingredients), []) + + const getAllIngredientsForDish = dish => { + return dish.extendedIngredients.map(({ name, amount: quantity, unit }) => ({ + name, + unit, + quantity: quantity * state.numberOfGuests + })) } - //Adds the passed dish to the menu. If the dish of that type already exists on the menu - //it is removed from the menu and the new one added. - this.addDishToMenu = function(id) { - //TODO Lab 1 + // Return the full price of a given dish (represented by dish) + const getPriceForDish = dish => dish.pricePerServing + + // Returns the total price of the menu (all the ingredients multiplied by number of guests). + const getTotalMenuPrice = () => state.menu.reduce((acc, dish) => { + acc += state.numberOfGuests * getPriceForDish(dish) + return acc; + }, 0) + + // Adds the passed dish to the menu. If the dish of that type already exists on the menu + // it is removed from the menu and the new one added. + const addDishToMenu = id => { + getDish(id).then(selectedDish => { + const restOfTheMenu = state.menu.filter(dish => dish.dishType !== selectedDish.dishType) + restOfTheMenu.push(selectedDish) + state.menu = restOfTheMenu + _notifyObservers({ menu: state.menu }) + }) } - //Removes dish from menu - this.removeDishFromMenu = function(id) { - //TODO Lab 1 + // Removes dish from menu + const removeDishFromMenu = id => { + const newMenu = state.menu.filter(dish => dish.id !== id) + state.menu = newMenu + _notifyObservers({ menu: state.menu }) } - //function that returns all dishes of specific type (i.e. "starter", "main dish" or "dessert") - //you can use the filter argument to filter out the dish by name or ingredient (use for search) - //if you don't pass any filter all the dishes will be returned - this.getAllDishes = function (type,filter) { - return dishes.filter(function(dish) { - var found = true; - if(filter){ - found = false; - dish.ingredients.forEach(function(ingredient) { - if(ingredient.name.indexOf(filter)!=-1) { - found = true; - } - }); - if(dish.name.indexOf(filter) != -1) - { - found = true; + // function that returns all dishes of specific type (i.e. "starter", "main dish" or "dessert") + // you can use the filter argument to filter out the dish by name or ingredient (use for search) + // if you don't pass any filter all the dishes will be returned + const getAllDishes = (type, filter) => { + let options = { + headers: { + 'X-Mashape-Key': apiConfig.apiKey } + }; + + let queryParams = [`instructionsRequired=true`]; + + if (type !== undefined && type !== 'all') { + queryParams.push(`type=${encodeURIComponent(type)}`); + } + + if (filter !== undefined && filter !== "") { + queryParams.push(`query=${encodeURIComponent(filter)}`); } - return dish.type == type && found; - }); + + return fetch(`https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/search?${queryParams.join('&')}`, options) + .then(res => res.json()) + .then(({ results }) => { + return results + }) } //function that returns a dish of specific ID - this.getDish = function (id) { - for(key in dishes){ - if(dishes[key].id == id) { - return dishes[key]; + const getDish = id => { + let options = { + headers: { + 'X-Mashape-Key': apiConfig.apiKey } - } - } + }; + let queryParams = [`includeNutrition=true`]; - // the dishes variable contains an array of all the - // dishes in the database. each dish has id, name, type, - // image (name of the image file), description and - // array of ingredients. Each ingredient has name, - // quantity (a number), price (a number) and unit (string - // defining the unit i.e. "g", "slices", "ml". Unit - // can sometimes be empty like in the example of eggs where - // you just say "5 eggs" and not "5 pieces of eggs" or anything else. - var dishes = [{ - 'id':1, - 'name':'French toast', - 'type':'starter', - 'image':'toast.jpg', - 'description':"In a large mixing bowl, beat the eggs. Add the milk, brown sugar and nutmeg; stir well to combine. Soak bread slices in the egg mixture until saturated. Heat a lightly oiled griddle or frying pan over medium high heat. Brown slices on both sides, sprinkle with cinnamon and serve hot.", - 'ingredients':[{ - 'name':'eggs', - 'quantity':0.5, - 'unit':'', - 'price':10 - },{ - 'name':'milk', - 'quantity':30, - 'unit':'ml', - 'price':6 - },{ - 'name':'brown sugar', - 'quantity':7, - 'unit':'g', - 'price':1 - },{ - 'name':'ground nutmeg', - 'quantity':0.5, - 'unit':'g', - 'price':12 - },{ - 'name':'white bread', - 'quantity':2, - 'unit':'slices', - 'price':2 - }] - },{ - 'id':2, - 'name':'Sourdough Starter', - 'type':'starter', - 'image':'sourdough.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'active dry yeast', - 'quantity':0.5, - 'unit':'g', - 'price':4 - },{ - 'name':'warm water', - 'quantity':30, - 'unit':'ml', - 'price':0 - },{ - 'name':'all-purpose flour', - 'quantity':15, - 'unit':'g', - 'price':2 - }] - },{ - 'id':3, - 'name':'Baked Brie with Peaches', - 'type':'starter', - 'image':'bakedbrie.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'round Brie cheese', - 'quantity':10, - 'unit':'g', - 'price':8 - },{ - 'name':'raspberry preserves', - 'quantity':15, - 'unit':'g', - 'price':10 - },{ - 'name':'peaches', - 'quantity':1, - 'unit':'', - 'price':4 - }] - },{ - 'id':100, - 'name':'Meat balls', - 'type':'main dish', - 'image':'meatballs.jpg', - 'description':"Preheat an oven to 400 degrees F (200 degrees C). Place the beef into a mixing bowl, and season with salt, onion, garlic salt, Italian seasoning, oregano, red pepper flakes, hot pepper sauce, and Worcestershire sauce; mix well. Add the milk, Parmesan cheese, and bread crumbs. Mix until evenly blended, then form into 1 1/2-inch meatballs, and place onto a baking sheet. Bake in the preheated oven until no longer pink in the center, 20 to 25 minutes.", - 'ingredients':[{ - 'name':'extra lean ground beef', - 'quantity':115, - 'unit':'g', - 'price':20 - },{ - 'name':'sea salt', - 'quantity':0.7, - 'unit':'g', - 'price':3 - },{ - 'name':'small onion, diced', - 'quantity':0.25, - 'unit':'', - 'price':2 - },{ - 'name':'garlic salt', - 'quantity':0.7, - 'unit':'g', - 'price':2 - },{ - 'name':'Italian seasoning', - 'quantity':0.6, - 'unit':'g', - 'price':3 - },{ - 'name':'dried oregano', - 'quantity':0.3, - 'unit':'g', - 'price':3 - },{ - 'name':'crushed red pepper flakes', - 'quantity':0.6, - 'unit':'g', - 'price':3 - },{ - 'name':'Worcestershire sauce', - 'quantity':6, - 'unit':'ml', - 'price':7 - },{ - 'name':'milk', - 'quantity':20, - 'unit':'ml', - 'price':4 - },{ - 'name':'grated Parmesan cheese', - 'quantity':5, - 'unit':'g', - 'price':8 - },{ - 'name':'seasoned bread crumbs', - 'quantity':15, - 'unit':'g', - 'price':4 - }] - },{ - 'id':101, - 'name':'MD 2', - 'type':'main dish', - 'image':'bakedbrie.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ingredient 1', - 'quantity':1, - 'unit':'pieces', - 'price':8 - },{ - 'name':'ingredient 2', - 'quantity':15, - 'unit':'g', - 'price':7 - },{ - 'name':'ingredient 3', - 'quantity':10, - 'unit':'ml', - 'price':4 - }] - },{ - 'id':102, - 'name':'MD 3', - 'type':'main dish', - 'image':'meatballs.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ingredient 1', - 'quantity':2, - 'unit':'pieces', - 'price':8 - },{ - 'name':'ingredient 2', - 'quantity':10, - 'unit':'g', - 'price':7 - },{ - 'name':'ingredient 3', - 'quantity':5, - 'unit':'ml', - 'price':4 - }] - },{ - 'id':103, - 'name':'MD 4', - 'type':'main dish', - 'image':'meatballs.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ingredient 1', - 'quantity':1, - 'unit':'pieces', - 'price':4 - },{ - 'name':'ingredient 2', - 'quantity':12, - 'unit':'g', - 'price':7 - },{ - 'name':'ingredient 3', - 'quantity':6, - 'unit':'ml', - 'price':4 - }] - },{ - 'id':200, - 'name':'Chocolat Ice cream', - 'type':'dessert', - 'image':'icecream.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ice cream', - 'quantity':100, - 'unit':'ml', - 'price':6 - }] - },{ - 'id':201, - 'name':'Vanilla Ice cream', - 'type':'dessert', - 'image':'icecream.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ice cream', - 'quantity':100, - 'unit':'ml', - 'price':6 - }] - },{ - 'id':202, - 'name':'Strawberry', - 'type':'dessert', - 'image':'icecream.jpg', - 'description':"Here is how you make it... Lore ipsum...", - 'ingredients':[{ - 'name':'ice cream', - 'quantity':100, - 'unit':'ml', - 'price':6 - }] - } - ]; + let recognizedDishTypes = [ + "appetizer", + "mainCourse", + "sideDish", + "dessert", + "salad", + "bread", + "breakfast", + "soup", + "beverage", + "sauce", + "drink" + ]; + + return fetch(`https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/${id}/information?${queryParams.join('&')}`, options) + .then(res => res.json()) + .then(result => { + result.dishType = result.dishTypes.find(dishType => recognizedDishTypes.includes(dishType)) || 'main course'; + return result + }) + }; + + return ({ + addObserver, + removeObserver, + setNumberOfGuests, + getNumberOfGuests, + getSelectedDish, + getFullMenu, + getAllIngredients, + getAllIngredientsForDish, + getPriceForDish, + getTotalMenuPrice, + addDishToMenu, + removeDishFromMenu, + getAllDishes, + getDish + }) } diff --git a/js/view/DishDetailsView.js b/js/view/DishDetailsView.js new file mode 100644 index 000000000..1204b5d61 --- /dev/null +++ b/js/view/DishDetailsView.js @@ -0,0 +1,221 @@ +/* + * Dish Details View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createDishDetailsView = (container, model) => { + + const state = { + display: false, + id: undefined + } + + const getDishId = () => { + return state.id + } + + const _createBackButton = (id) => { + const backButton = document.createElement('button') + backButton.setAttribute('id', id) + backButton.classList.add('backButton') + backButton.classList.add('primaryButton') + backButton.innerHTML = 'Back to Search' + return backButton + } + + const _createAddButton = (id) => { + const addButton = document.createElement('button') + addButton.setAttribute('id', id) + addButton.classList.add('primaryButton') + addButton.innerHTML = 'Add to menu' + return addButton + } + + const backFromDishDetailsButton = _createBackButton('backFromDishDetailsButton') + const addToMenuButton = _createAddButton('addToMenuButton') + + const _createIngredientsList = (ingredients, totalPrice, numberOfGuests) => { + const ingredientsListElement = document.createElement('ingredientsList') + ingredientsListElement.setAttribute('id', 'ingredientsList') + + const ingredientsTitleElement = document.createElement('div') + ingredientsTitleElement.setAttribute('id', 'ingredientsTitle') + const titleH3Element = document.createElement('h3') + titleH3Element.innerHTML = `Ingredients for ${numberOfGuests} people` + ingredientsTitleElement.appendChild(titleH3Element) + + const ingredientsTableElement = document.createElement('div') + ingredientsTableElement.setAttribute('id', 'ingredientsTable') + + const createIngredientRow = ({ name, quantity, unit }) => { + + const ingredientRowElement = document.createElement('div') + ingredientRowElement.classList.add('ingredientsTableRow') + + const quantityCell = document.createElement('div') + quantityCell.classList.add('ingredientQuantity') + quantityCell.innerHTML = `${quantity} ${unit}` + ingredientRowElement.appendChild(quantityCell) + + const nameCell = document.createElement('div') + nameCell.classList.add('ingredientName') + nameCell.innerHTML = name + ingredientRowElement.appendChild(nameCell) + + return ingredientRowElement + } + ingredients.map(ingredient => { + ingredientsTableElement.appendChild(createIngredientRow(ingredient)) + }) + + const ingredientsFooterElement = document.createElement('div') + ingredientsFooterElement.setAttribute('id', 'ingredientsFooter') + + const addToMenuContainer = document.createElement('div') + addToMenuContainer.setAttribute('id', 'addToMenu') + addToMenuContainer.classList.add('verticallyCentered') + addToMenuContainer.appendChild(addToMenuButton) + ingredientsFooterElement.appendChild(addToMenuContainer) + + const totalCurrencyElement = document.createElement('div') + totalCurrencyElement.setAttribute('id', 'ingredientsTotalPriceCurrency') + totalCurrencyElement.innerHTML = 'SEK' + ingredientsFooterElement.appendChild(totalCurrencyElement) + + const totalPriceElement = document.createElement('div') + totalPriceElement.setAttribute('id', 'ingredientsTotalPrice') + totalPriceElement.innerHTML = `${totalPrice}.00` + ingredientsFooterElement.appendChild(totalPriceElement) + + + ingredientsListElement.appendChild(ingredientsTitleElement) + ingredientsListElement.appendChild(ingredientsTableElement) + ingredientsListElement.appendChild(ingredientsFooterElement) + return ingredientsListElement + } + + const render = id => { + + _remove() + + state.id = id + + // loading state + const loadingAlert = document.querySelector('#alert-notice') + loadingAlert.style.display = "block" + loadingAlert.innerHTML = "Loading dish details ..." + + model.getDish(id).then(dish => { + loadingAlert.style.display = "none" + loadingAlert.innerHTML = "" + + const ingredientsList = model.getAllIngredientsForDish(dish) + const numberOfGuests = model.getNumberOfGuests() + const totalPriceValue = model.getPriceForDish(dish) + + const createDishDescription = ({ title, image, sourceUrl }) => { + const dishDescriptionElement = document.createElement('div') + dishDescriptionElement.setAttribute('id', 'dishDescription') + + const dishTitleElement = document.createElement('div') + dishTitleElement.setAttribute('id', 'dishTitle') + const titleH2Element = document.createElement('h2') + titleH2Element.innerHTML = `${title}` + dishTitleElement.appendChild(titleH2Element) + + const dishImageElement = document.createElement('div') + dishImageElement.setAttribute('id', 'dishImage') + const dishImage = document.createElement('img') + dishImage.src = image + dishImageElement.appendChild(dishImage) + + const dishDescriptionTextElement = document.createElement('div') + dishDescriptionTextElement.setAttribute('id', 'dishDescriptionText') + const dishDescription = document.createElement('a') + dishDescription.href = sourceUrl + dishDescription.target = "_blank" + dishDescription.innerHTML = "See description on original website" + dishDescriptionTextElement.appendChild(dishDescription) + + dishDescriptionElement.appendChild(dishTitleElement) + dishDescriptionElement.appendChild(dishImageElement) + dishDescriptionElement.appendChild(dishDescriptionTextElement) + dishDescriptionElement.appendChild(backFromDishDetailsButton) + + return dishDescriptionElement + + } + + container.appendChild(createDishDescription(dish)) + container.appendChild(_createIngredientsList(ingredientsList, totalPriceValue, numberOfGuests)) + + const dishPreparationElement = document.createElement('div') + dishPreparationElement.setAttribute('id', 'dishPreparation') + + const titleH3Element = document.createElement('h3') + titleH3Element.innerHTML = 'Preparation' + dishPreparationElement.appendChild(titleH3Element) + + const dishPreparationTextElement = document.createElement('div') + dishPreparationTextElement.setAttribute('id', 'dishPreparationText') + const dishPreparationText = document.createElement('p') + dishPreparationText.innerHTML = dish.instructions + dishPreparationTextElement.appendChild(dishPreparationText) + + dishPreparationElement.appendChild(dishPreparationTextElement) + + container.appendChild(dishPreparationElement) + }).catch(error => { + loadingAlert.innerHTML = `There was a problem with loading the dishes. ${error}`; + }); + } + + const _remove = () => { + Array.from(container.childNodes).map(child => child.remove()) + } + + const _createUpdate = (model) => () => { + if(state.display === true) { + model.getDish(state.id).then(dish => { + container.querySelector('#ingredientsList').remove() + const ingredientsList = model.getAllIngredientsForDish(dish) + const totalPriceValue = model.getPriceForDish(dish) + const numberOfGuests = model.getNumberOfGuests() + + container.insertBefore( + _createIngredientsList(ingredientsList, totalPriceValue, numberOfGuests), + container.children[2] + ) + }); + } + } + const update = _createUpdate(model) + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + model.addObserver(update) + + return ({ + getDishId, + backFromDishDetailsButton, + addToMenuButton, + render, + update, + show, + hide + }) +} diff --git a/js/view/DishSearchView.js b/js/view/DishSearchView.js new file mode 100644 index 000000000..062ee9157 --- /dev/null +++ b/js/view/DishSearchView.js @@ -0,0 +1,101 @@ +/* + * Dish Search View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createDishSearchView = (container, model) => { + + const state = { + display: false, + } + + const dishSearchButton = container.querySelector('#dishSearchButton') + const dishSearchResultsElement = container.querySelector('#dishSearchResults') + + const render = () => { + + _remove() + + const filter = container.querySelector('#dishSearchField').value + const type = container.querySelector('#dishTypeSelectField').value + + const createDishResultElement = ({ image, title: name, id }) => { + const dishResultElement = document.createElement('div') + dishResultElement.classList.add('dishResult') + dishResultElement.setAttribute('key', id) + + const dishResultImageContainerElement = document.createElement('div') + dishResultImageContainerElement.classList.add('dishResultImageContainer') + dishResultImageContainerElement.setAttribute('key', id) + + const dishResultImageElement = document.createElement('img') + dishResultImageElement.classList.add('dishResultImage') + dishResultImageElement.src = `https://spoonacular.com/recipeImages/${image}` + dishResultImageElement.setAttribute('key', id) + + const dishResultNameElement = document.createElement('div') + dishResultNameElement.innerHTML = name + dishResultNameElement.setAttribute('key', id) + + dishResultImageContainerElement.appendChild(dishResultImageElement) + dishResultElement.appendChild(dishResultImageContainerElement) + dishResultElement.appendChild(dishResultNameElement) + + return dishResultElement + } + + + const loadingAlert = document.querySelector('#alert-notice') + loadingAlert.style.display = "block" + loadingAlert.innerHTML = "Loading search results ..." + + model.getAllDishes(type, filter).then(dishes => { + loadingAlert.style.display = "none" + loadingAlert.innerHTML = "" + + if (dishes.length === 0) { + dishSearchResultsElement.innerHTML = `Sorry, no search results were found for '${filter}'.`; + return; + } + + console.log(dishes) + dishes.map(dish => { + dishSearchResultsElement.append(createDishResultElement(dish)) + }) + }) + .catch(error => { + loadingAlert.innerHTML = `There was a problem with loading the dishes. ${error}`; + }); + } + + const _remove = () => { + Array.from(dishSearchResultsElement.childNodes).map(child => child.remove()) + } + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + render() + + return ({ + dishSearchButton, + dishSearchResultsElement, + render, + show, + hide + }) + +} diff --git a/js/view/MainView.js b/js/view/MainView.js new file mode 100644 index 000000000..20fae41fd --- /dev/null +++ b/js/view/MainView.js @@ -0,0 +1,35 @@ +/* + * Main View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createMainView = (container, model) => { + + const state = { + display: false, + } + + const startButton = container.querySelector('#startButton') + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + return ({ + startButton, + show, + hide + }) +} \ No newline at end of file diff --git a/js/view/PriceSummaryView.js b/js/view/PriceSummaryView.js new file mode 100644 index 000000000..e61e658f2 --- /dev/null +++ b/js/view/PriceSummaryView.js @@ -0,0 +1,166 @@ +/* + * Price Summary View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createPriceSummaryView = (container, model) => { + + const state = { + display: false + } + + const printButton = container.querySelector('#printButton') + + const _appetizerContainer = container.querySelector('#appetizer') + const _mainCourseContainer = container.querySelector('#mainCourse') + const _sideDishContainer = container.querySelector('#sideDish') + const _dessertContainer = container.querySelector('#dessert') + const _saladContainer = container.querySelector('#salad') + const _breadContainer = container.querySelector('#bread') + const _breakfastContainer = container.querySelector('#breakfast') + const _soupContainer = container.querySelector('#soup') + const _beverageContainer = container.querySelector('#beverage') + const _sauceContainer = container.querySelector('#sauce') + const _drinkContainer = container.querySelector('#drink') + const _totalPrice = container.querySelector('#totalPriceValue') + + const _createUpdate = model => changeDetails => { + _remove() + + const menu = { + 'appetizer': model.getSelectedDish('appetizer'), + 'mainCourse': model.getSelectedDish('main course'), + 'sideDish': model.getSelectedDish('side dish'), + 'dessert': model.getSelectedDish('dessert'), + 'salad': model.getSelectedDish('salad'), + 'bread': model.getSelectedDish('bread'), + 'breakfast': model.getSelectedDish('breakfast'), + 'soup': model.getSelectedDish('soup'), + 'beverage': model.getSelectedDish('beverage'), + 'sauce': model.getSelectedDish('sauce'), + 'drink': model.getSelectedDish('drink') + } + + Object.keys(menu).map(type => { + const dish = menu[type] + if(dish !== undefined) { + dish.price = model.getPriceForDish(dish) + } + }) + + const totalPriceValue = model.getTotalMenuPrice() + + const createDishElement = ({image, title, pricePerServing}) => { + const dishElement = document.createElement('div') + dishElement.classList.add('dishItemContainer') + + const dishImageContainerElement = document.createElement('div') + dishImageContainerElement.classList.add('verticallyCentered') + dishImageContainerElement.classList.add('dishImage') + const dishImageElement = document.createElement('img') + dishImageElement.src = image + dishImageContainerElement.appendChild(dishImageElement) + + const dishNameElement = document.createElement('p') + dishNameElement.classList.add('dishName') + dishNameElement.innerHTML = title + + const dishPriceElement = document.createElement('p') + dishPriceElement.classList.add('dishPrice') + dishPriceElement.innerHTML = `${pricePerServing} SEK` + + dishElement.appendChild(dishImageContainerElement) + dishElement.appendChild(dishNameElement) + dishElement.appendChild(dishPriceElement) + + return dishElement + } + + Object.keys(menu).map(type => { + const dish = menu[type] + if(dish !== undefined) { + switch (type) { + case 'appetizer': + _appetizerContainer.append(createDishElement(dish)) + break; + case 'mainCourse': + _mainCourseContainer.append(createDishElement(dish)) + break; + case 'sideDish': + _sideDishContainer.append(createDishElement(dish)) + break; + case 'dessert': + _dessertContainer.append(createDishElement(dish)) + break; + case 'salad': + _saladContainer.append(createDishElement(dish)) + break; + case 'bread': + _breadContainer.append(createDishElement(dish)) + break; + case 'breakfast': + _breakfastContainer.append(createDishElement(dish)) + break; + case 'soup': + _soupContainer.append(createDishElement(dish)) + break; + case 'beverage': + _beverageContainer.append(createDishElement(dish)) + break; + case 'sauce': + _sauceContainer.append(createDishElement(dish)) + break; + case 'drink': + _drinkContainer.append(createDishElement(dish)) + break; + } + } + }) + + _totalPrice.innerHTML = `${totalPriceValue}` + } + const update = _createUpdate(model) + + const _remove = () => { + const childNodesToRemove = Array.from([]) + .concat(Array.from(_appetizerContainer.childNodes)) + .concat(Array.from(_mainCourseContainer.childNodes)) + .concat(Array.from(_sideDishContainer.childNodes)) + .concat(Array.from(_dessertContainer.childNodes)) + .concat(Array.from(_saladContainer.childNodes)) + .concat(Array.from(_breadContainer.childNodes)) + .concat(Array.from(_breakfastContainer.childNodes)) + .concat(Array.from(_soupContainer.childNodes)) + .concat(Array.from(_beverageContainer.childNodes)) + .concat(Array.from(_sauceContainer.childNodes)) + .concat(Array.from(_drinkContainer.childNodes)) + childNodesToRemove.map(child => child.remove()) + _totalPrice.innerHTML = "0.00" + } + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + model.addObserver(update) + update() + + return ({ + printButton, + update, + show, + hide + }) +} diff --git a/js/view/RecipeSummaryView.js b/js/view/RecipeSummaryView.js new file mode 100644 index 000000000..cf9dc6f72 --- /dev/null +++ b/js/view/RecipeSummaryView.js @@ -0,0 +1,116 @@ +/* + * Recipe Summary View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createRecipeSummaryView = (container, model) => { + + const state = { + display: false + } + + const _createUpdate = (model) => (changeDetails) => { + _remove() + + const menu = { + 'appetizer': model.getSelectedDish('appetizer'), + 'mainCourse': model.getSelectedDish('main course'), + 'sideDish': model.getSelectedDish('side dish'), + 'dessert': model.getSelectedDish('dessert'), + 'salad': model.getSelectedDish('salad'), + 'bread': model.getSelectedDish('bread'), + 'breakfast': model.getSelectedDish('breakfast'), + 'soup': model.getSelectedDish('soup'), + 'beverage': model.getSelectedDish('beverage'), + 'sauce': model.getSelectedDish('sauce'), + 'drink': model.getSelectedDish('drink') + } + + const createRecipeElement = ({title, image, description, sourceUrl, instructions}) => { + + const recipeElement = document.createElement('div') + recipeElement.classList.add('recipe') + + const recipeImageContainerElement = document.createElement('div') + recipeImageContainerElement.classList.add('recipeImage') + + const recipeImageElement = document.createElement('img') + recipeImageElement.src = image + recipeImageContainerElement.appendChild(recipeImageElement) + + const recipeTextboxElement = document.createElement('div') + recipeTextboxElement.classList.add('recipeTextbox') + + const recipeNameElement = document.createElement('h2') + recipeNameElement.classList.add('recipeName') + recipeNameElement.innerHTML = `${title}` + + const recipeDescriptionElement = document.createElement('p') + recipeDescriptionElement.classList.add('recipeDescriptionText') + const recipeDescription = document.createElement('a') + recipeDescription.href = sourceUrl + recipeDescription.target = "_blank" + recipeDescription.innerHTML = "See description on original website" + recipeDescriptionElement.appendChild(recipeDescription) + + recipeTextboxElement.appendChild(recipeNameElement) + recipeTextboxElement.appendChild(recipeDescriptionElement) + + const preparationTextboxElement = document.createElement('div') + preparationTextboxElement.classList.add('recipeTextbox') + + const preparationTitleElement = document.createElement('h3') + preparationTitleElement.innerHTML = 'Preparation' + + const preparationDescriptionElement = document.createElement('p') + preparationDescriptionElement.classList.add('recipePreparationText') + preparationDescriptionElement.innerHTML = instructions + + preparationTextboxElement.appendChild(preparationTitleElement) + preparationTextboxElement.appendChild(preparationDescriptionElement) + + recipeElement.appendChild(recipeImageContainerElement) + recipeElement.appendChild(recipeTextboxElement) + recipeElement.appendChild(preparationTextboxElement) + + return recipeElement + } + + Object.keys(menu).map(type => { + const dish = menu[type] + if(dish !== undefined) { + container.append(createRecipeElement(dish)) + } + }) + } + const update = _createUpdate(model) + + const _remove = () => { + Array.from(container.childNodes).map(child => child.remove()) + } + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + model.addObserver(update) + update() + + return ({ + update, + show, + hide + }) +} diff --git a/js/view/SidebarView.js b/js/view/SidebarView.js new file mode 100644 index 000000000..47b9fa515 --- /dev/null +++ b/js/view/SidebarView.js @@ -0,0 +1,95 @@ +/* + * Sidebar View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createSidebarView = (container, model) => { + + const state = { + display: false, + numberOfGuests: undefined, + menu: undefined, + totalMenuPrice: undefined + } + + const guestNumber = container.querySelector('#guestNumber') + const confirmMenuButton = container.querySelector('#confirmMenuButton') + const _menuTableContent = container.querySelector('#tableContent') + const _totalPrice = container.querySelector('#totalPriceValue') + const _hiddenTotalPrice = container.querySelector('#hiddenTotalPriceValue') + + const _createUpdate = (model) => (changeDetails) => { + state.numberOfGuests = model.getNumberOfGuests() + state.menu = model.getFullMenu().map(item => { + item.price = model.getPriceForDish(item.id) + return item + }) + state.totalMenuPrice = model.getTotalMenuPrice() + + _remove() + _render() + } + const update = _createUpdate(model) + + const _render = () => { + guestNumber.value = state.numberOfGuests + + const createNewRow = (name, cost) => { + + const tableRow = document.createElement('div') + tableRow.classList.add('tableRow') + + const rowName = document.createElement('div') + rowName.classList.add('rowName') + rowName.innerHTML = `${name}` + + const rowCost = document.createElement('div') + rowCost.classList.add('rowCost') + rowCost.innerHTML = `${cost}:-` + + tableRow.appendChild(rowName) + tableRow.appendChild(rowCost) + + return tableRow + } + + state.menu.map(item => + _menuTableContent.appendChild(createNewRow(item.title, item.pricePerServing)) + ) + + _totalPrice.innerHTML = `${state.totalMenuPrice}` + _hiddenTotalPrice.innerHTML = `${state.totalMenuPrice}` + + } + + const _remove = () => { + Array.from(_menuTableContent.childNodes).map(child => child.remove()) + } + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + model.addObserver(update) + update() + + return ({ + guestNumber, + confirmMenuButton, + update, + show, + hide + }) +} diff --git a/js/view/StatusBarView.js b/js/view/StatusBarView.js new file mode 100644 index 000000000..4d20c8270 --- /dev/null +++ b/js/view/StatusBarView.js @@ -0,0 +1,46 @@ +/* + * Status Bar View Object factory function + + * @param {HTML DOM element} container - references the HTML parent element that contains the view. + * @param {Object} model - the reference to the Dinner Model +*/ + +const createStatusBarView = (container, model) => { + + const state = { + display: false + } + + const backFromMenuSummaryButton = container.querySelector('#backFromMenuSummaryButton') + + const numberOfGuests = container.querySelector('#numberOfGuests') + + const _createUpdate = (model) => (changeDetails) => { + numberOfGuests.innerHTML = model.getNumberOfGuests() + } + const update = _createUpdate(model) + + const show = () => { + if(state.display !== true) { + state.display = true + } + container.style = '' + } + + const hide = () => { + if(state.display !== false) { + state.display = false + } + container.style.display = 'none' + } + + model.addObserver(update) + update() + + return ({ + backFromMenuSummaryButton, + update, + show, + hide + }) +} \ No newline at end of file diff --git a/js/view/exampleView.js b/js/view/exampleView.js deleted file mode 100644 index 508225ff7..000000000 --- a/js/view/exampleView.js +++ /dev/null @@ -1,58 +0,0 @@ -/** ExampleView Object constructor - * - * This object represents the code for one specific view (in this case the Example view). - * - * It is responsible for: - * - constructing the view (e.g. if you need to create some HTML elements procedurally) - * - populating the view with the data - * - updating the view when the data changes - * - * You should create a view Object like this for every view in your UI. - * - * @param {jQuery object} container - references the HTML parent element that contains the view. - * @param {Object} model - the reference to the Dinner Model - */ -var ExampleView = function (container, model) { - - /** - * We use the @method find() on @var {jQuery object} container to look for various elements - * inside the view in orther to use them later on. For instance: - * - * @var {jQuery object} numberOfGuests is a reference to the element that - * represents the placeholder for where we want to show the number of guests. It's - * a reference to HTML element (wrapped in jQuery object for added benefit of jQuery methods) - * and we can use it to modify , for example to populate it with dynamic data (for now - * only 'Hello world', but you should change this by end of Lab 1). - * - * We use variables when we want to make the reference private (only available within) the - * ExampleView. - * - * IMPORTANT: Never use $('someSelector') directly in the views. Always use container.find - * or some other way of searching only among the containers child elements. In this way you - * make your view code modular and ensure it dosn't break if by mistake somebody else - * in some other view gives the same ID to another element. - * - */ - var numberOfGuests = container.find("#numberOfGuests"); - - /** - * When we want references to some view elements to be available from outside of view, we - * define them as this.someName. We don't need this in Lab 1 yet, but in Lab 2 it - * will be important for assigning listeners to these buttons, because the listeners - * should not be assigned in the view, but rather in controller. - * - * We can then, in some other code, use exampleView.plusButton to reference the - * this button and do something with it (see Lab 2). - * - */ - this.plusButton = container.find("#plusGuest"); - this.minusButton = container.find("#minusGuest"); - - /** - * Here we use @var {jQuery object} numberOfGuests that is a reference to - * in our view to dynamically set it's value to "Hello World". - */ - numberOfGuests.html("Hello World"); - -} - diff --git a/styles/main.css b/styles/main.css new file mode 100644 index 000000000..32fb53161 --- /dev/null +++ b/styles/main.css @@ -0,0 +1,552 @@ +body { + margin: 0; +} + +h4 { + margin: 0; +} + +p { + margin: 0; +} + +#alert-notice { + display: none; + background: skyblue; + padding: 15px; + text-align: center; +} + +.showOnMobile { + display: none; +} + +.hideOnMobile { + display: inline; +} + +.left { + text-align: left; +} + +.right { + text-align: right; +} + +.primaryButton { + color: rgba(0, 0, 0, 1); + background-color: orange; + border: 0.1em solid black; + box-shadow: 0.1em 0.1em 0.1em black; + font-size: 1em; + padding: 0.5em; + + margin: 0.5rem 0rem; +} + +.secondaryButton { + color: rgba(0, 0, 0, 1); + background-color: orange; + border: 0.1em solid black; + box-shadow: 0.1em 0.1em 0.1em black; + font-size: 0.7em; + padding: 0.5em; +} + +.verticallyCentered { + display: flex; + justify-content: center; + align-items: center; +} + +.container { + height: 100vh; + display: grid; + grid-template-areas: + "header" + "content"; + grid-template-rows: 6rem auto; +} + +#header { + grid-area: header; + text-align: center; + background-color: rgba(173, 173, 173, 1); +} + +#content { + grid-area: content; + display: grid; +} + +#mainView { + margin: 0 8rem; + text-align: center; + align-self: center; + justify-self: center; +} + +#welcomeText { + margin-bottom: 1rem; +} + +#appView { + display: grid; + grid-template-columns: 12rem auto; + grid-template-areas: + "sidebarView sectionView"; +} + +/* Sidebar View */ +#sidebarView { + grid-area: sidebarView; + + display: grid; + grid-template-rows: 2rem 1fr; + grid-template-areas: + "titleBar" + "collapseContent"; + + background-color: pink; + text-align: left; +} + +#titleBar { + grid-area: titleBar; + display: flex; + justify-content: flex-end; +} + +.collapseContent { + grid-area: collapseContent; + display: grid; + grid-template-rows: 2rem fit-content(100%) 3rem; + grid-template-areas: + "guestSelector" + "dishTable" + "confirmButtonContainer"; +} + +#titleBarTitle { + align-self: center; + margin-right: auto; +} + +#collapsedMenuTotalPrice { + align-self: center; + justify-self: center; + margin-right: 1rem; +} + +#guestSelector { + grid-area: guestSelector; +} + +#dishTable { + grid-area: dishTable; + display: grid; + grid-template-areas: + "tableHeader" + "tableContent" + "tableFooter" +} + +#tableHeader { + grid-area: tableHeader; + display: grid; + grid-template-areas: + "tableName tableCost"; + + background-color: gainsboro; + font-weight: bold; +} + +#tableName { + grid-area: tableName; + text-align: left; +} + +#tableCost { + grid-area: tableCost; + text-align: right; +} + +#tableContent { + grid-area: tableContent; + display: grid; + grid-template-rows: auto; +} + +.tableRow { + display: grid; + grid-template-areas: + "rowName rowCost"; +} + +.rowName { + grid-area: rowName; + text-align: left; +} + +.rowCost { + grid-area: rowCost; + text-align: right; +} + +#tableFooter { + grid-area: tableFooter; + display: flex; + align-items: baseline; + justify-content: flex-end; +} + +#confirmButtonContainer { + grid-area: confirmButtonContainer; + text-align: center; +} + +#sectionView { + grid-area: sectionView; +} + +/* Dish Search View */ + +#dishSearchView { + display: grid; + grid-template-areas: + "dishSearchTitle" + "dishSearchForm" + "dishSearchResults"; +} + +#dishSearchTitle { + grid-area: dishSearchTitle; + margin: 1rem 1rem; +} + +#dishSearchForm { + grid-area: dishSearchForm; + margin: 0 1rem; + align-self: center; + justify-self: center; +} + +#dishSearchField { + grid-area: dishSearchField; + width: 8rem; +} + +#dishTypeSelectField { + grid-area: dishTypeSelectField; +} + +#dishSearchButton { + grid-area: dishSearchButton; +} + +#dishSearchResults { + grid-area: dishSearchResults; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + margin: 2rem 2rem; + + align-items: center; + justify-items: center; + text-align: center; +} + +/* Dish Details View */ +#dishDetailsView { + display: grid; + grid-template-areas: + "dishDescription ingredientsList" + "dishPreparation dishPreparation"; + + text-align: left; +} + +#dishDescription { + grid-area: dishDescription; +} + +#ingredientsList { + grid-area: ingredientsList; + display: grid; + grid-template-rows: 4rem fit-content(100%) 4rem; + grid-template-areas: + "ingredientsTitle" + "ingredientsTable" + "ingredientsFooter"; + + background-color: burlywood; + text-align: left; +} + +#ingredientsTitle { + grid-area: ingredientsTitle; +} + +#ingredientsTable { + grid-area: ingredientsTable; + display: grid; + grid-template-rows: auto; +} + +.ingredientsTableRow { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-areas: + "ingredientQuantity ingredientName ingredientCurrency ingredientPrice"; + padding: 0.5rem 0; +} + +.ingredientQuantity { + grid-area: ingredientQuantity; +} + +.ingredientName { + grid-area: ingredientName; +} + +.ingredientCurrency { + grid-area: ingredientCurrency; + text-align: right; +} + +.ingredientPrice { + grid-area: ingredientPrice; + text-align: right; +} + +#ingredientsFooter { + grid-area: ingredientsFooter; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-areas: + "addToMenu addToMenu ingredientsTotalPriceCurrency ingredientsTotalPrice" +} + +#addToMenu { + grid-area: addToMenu; + grid-column: span 2; +} + +#ingredientsTotalPriceCurrency { + grid-area: ingredientsTotalPriceCurrency; + display: flex; + align-items: center; + justify-content: flex-end; +} + +#ingredientsTotalPrice { + grid-area: ingredientsTotalPrice; + display: flex; + align-items: center; + justify-content: flex-end; +} + +#dishPreparation { + grid-area: dishPreparation; +} + +/* Menu Summary View */ +#menuSummaryView { + display: grid; + grid-template-rows: 4rem auto; + grid-template-areas: + "statusBar" + "summaryView" +} + +#statusBar { + grid-area: statusBar; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + border-bottom: 0.2rem solid black; +} + +#summaryView { + grid-area: summaryView; +} + +/* Price Summary View */ +#priceSummary { + display: grid; + grid-template-areas: + "dishListContainer" + "printButtonContainer"; +} + +#dishListContainer { + grid-area: dishListContainer; + display: grid; + grid-gap: 3rem; + grid-template-areas: + "appetizer mainCourse dessert"; + + align-items: center; + justify-content: center; + text-align: center; + margin: 5rem 0rem; +} + + +#appetizer { + grid-area: starter; +} + +#mainCourse { + grid-area: mainCourse; +} + +#dessert { + grid-area: dessert; +} + +#totalPrice { + grid-area: totalPrice; + border-left: 0.2rem solid black; + padding: 0rem 1rem; +} + +.dishItemContainer { + display: grid; + grid-template-rows: 140px repeat(2, auto); + grid-template-areas: + "dishImage" + "dishName" + "dishPrice"; +} + +.dishImage { + grid-area: dishImage; +} + +.dishImage img { + max-width: 200px; +} + +.dishName { + grid-area: dishName; +} + +.dishPrice { + grid-area: dishPrice; + margin-left: auto; +} + +.dishResultImage { + max-width: 50px; +} + +#printButtonContainer { + grid-area: printButtonContainer; + border-top: 0.2rem solid black; + + text-align: center; + padding: 1rem 0rem; +} + +/* Recipe Summary View */ +#recipeSummaryView { + margin-top: 3rem; + grid-area: recipes; + display: grid; + grid-template-rows: auto; + text-align: left; +} + +#recipeSummaryView img { + max-width: 200px; +} + +.recipe { + display: grid; + grid-gap: 2rem; + grid-template-columns: 140px 1fr 1fr; +} + +.recipeImage { + align-self: baseline; + justify-self: center; + margin: 1.5rem 0; +} + +.recipeTextbox { + display: grid; + grid-template-rows: 4rem auto; +} + +@media (min-width: 701px) { + #collapsedMenuTotalPrice { + display: none; + } +} + +@media (max-width: 700px) { + #mainView { + margin: 0 6rem; + } + + .showOnMobile { + display: inline; + } + + .hideOnMobile { + display: none; + } + + #appView { + grid-template-areas: + "sidebarView" + "sectionView"; + grid-template-columns: none; + } + + #dishDetailsView { + grid-template-areas: + "dishDescription" + "ingredientsList" + "dishPreparation"; + } + + #dishListContainer { + grid-template-areas: + "starter" + "mainCourse" + "dessert" + "totalPrice"; + margin: 1rem 0rem; + } + + #totalPrice { + display: inline; + border-left: none; + border-top: 0.2rem solid black; + padding: 1rem 0rem; + } + + #recipeSummaryView { + grid-gap: 2rem; + } + + .recipe { + grid-gap: 0; + grid-template-columns: none; + grid-template-rows: repeat(3, auto); + } +} + +@media (max-width: 520px) { + #mainView { + margin: 0 4rem; + } +} + +@media (max-width: 320px) { + #mainView { + margin: 0 2rem; + } +}