diff --git a/code/matt/javascript/minicapstone/css/style.css b/code/matt/javascript/minicapstone/css/style.css new file mode 100644 index 00000000..59791f95 --- /dev/null +++ b/code/matt/javascript/minicapstone/css/style.css @@ -0,0 +1,161 @@ +@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap'); +body { + font-family: 'Nunito', sans-serif; +} +#app { + height: 100vh; +} +h1, h2, h3, h4, h5 { + margin-top: 5px; + margin-bottom: 5px; +} +main { + display: flex; +} +#ing-select { + height: 100vh; + background-color: #A1EF8B; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} +#ing-search { + flex-direction: column; + align-items: center; +} +#ing-search-results { + background-color: white; + position: absolute; + overflow-y: auto; + overflow-x: hidden; + max-height: 20vh; + border-radius: 10px; + padding: 10px; + box-sizing: border-box; +} +.active #ing-search-results, .active #my-ingredients { + width: 40vw; +} +.inactive #ing-search-results, .inactive #my-ingredients { + width: 18vw; +} +#ing-select a { + color: #7F80B8; +} +#ing-select a:hover { + color: #62BFED; +} +#my-ingredients { + background-color: rgba(255,255,255,.2); + border-radius: 10px; + padding: 10px; + box-sizing: border-box; + margin-top: 20px; + margin-bottom: 20px; +} +#recipe-select { + height: 100vh; + background-color: #62BFED; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-flow: wrap; + align-items: center; + justify-content: center; +} +#recipe-view { + height: 100vh; + background-color: #7F80B8; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +@media screen and ( max-height: 1000px ) { + #recipe-view.has-recipe { + justify-content: flex-start; + } +} +#recipe-view div { + max-width: 700px; + display: flex; + flex-direction: column; + align-items: center; +} +div.active { + width: 60vw +} +div.inactive { + width: 20vw; +} +.transition, .transition * { + transition: .25s; +} +#drink-image { + max-width: 100%; +} +.active h1 { + font-size: xx-large; +} +.inactive h1 { + font-size: small; +} +#recipe-view p, #recipe-view li { + text-align: center; +} +.active p, .active li { + font-size: medium; +} +.inactive p, .inactive li { + font-size: small; +} +ul { + list-style-type: none; + margin: 0; + padding: 0; +} +#recipe-select ul { + margin: 10px; + width: 25vw; + text-align: center; +} +.open-button { + position: absolute; + bottom: 0px; +} +.num-missing { + background-color: rgba(255,255,255,.2); + border-radius: 10px; + padding: 10px; +} +.num-missing a { + color: #7F80B8; +} +.num-missing a:hover { + color: #A1EF8B; +} +input { + text-align: center; + font-size: large; + height: 40px; + border-radius: 10px; + border: 0px; + box-sizing: border-box; + margin-top: 20px; +} +input:focus { + border: 0px; + outline: none; +} +.active input { + width: 40vw; +} +.inactive input { + width: 18vw; +} \ No newline at end of file diff --git a/code/matt/javascript/minicapstone/index.html b/code/matt/javascript/minicapstone/index.html new file mode 100644 index 00000000..1f6ea037 --- /dev/null +++ b/code/matt/javascript/minicapstone/index.html @@ -0,0 +1,106 @@ + + + + + + + Drink Decider 2000 + + + + + + + + +
+
+
+ + + + +

+ Drink Decider 2000 +

+ + +
+

my ingredients:

+

select ingredients to continue

+
    +
  • + {{ ingredient }} +
  • +
+
+ reset my ingredients + + +
+
+ + + + +

+ add ingredients to continue +

+ +
+
+ +
+
+ + +
+
+ + + + +

+ select a recipe to continue +

+ +
+

{{ currentRecipe.name }}

+ currentrecipe.name +
    +
  • + {{ measure }} {{ ingredient }} +
  • +
+

+ Serve in a {{ currentRecipe.glass }} +

+

+ {{ currentRecipe.instructions }} +

+
+ + +
+
+ + + \ No newline at end of file diff --git a/code/matt/javascript/minicapstone/js/script.js b/code/matt/javascript/minicapstone/js/script.js new file mode 100644 index 00000000..62498590 --- /dev/null +++ b/code/matt/javascript/minicapstone/js/script.js @@ -0,0 +1,235 @@ +// generate app +Vue.createApp({ + data () { + return { + // main API endpoint and modifiers + baseURL: `https://www.thecocktaildb.com/api/json/v2/${API_KEY}/`, + listURL: 'list.php', + lookupURL: 'lookup.php', + searchURL: 'search.php', + filterURL: 'filter.php', + // object to hold full ingredient list + ingredientsObj: {}, + // user ingredient search input + ingredientSearchInput: '', + // current search results + filteredIngredients: [], + // user's selected ingredients + myIngredients: [], + // recipes containing at least one of user's ingredients + myRecipes: {}, + // array of arrays of recipes indexed according to number of missing ingredients + possibleRecipes: [], + currentRecipe: null, + // array holding active states for each panel + isActive: [true,false,false], + } + }, + created () { + this.loadFromLocal() + this.getIngredients() + // console.log(this.ingredientsObj) + }, + methods: { + // handles activating/deactivating panels by flipping their boolean + makeActive (element) { + this.isActive.forEach((item, index) => { + if (index == element) { + this.isActive[index] = true + } else { + this.isActive[index] = false + } + }) + }, + // gets full list of ingredients from API + getIngredients () { + axios({ + url: this.baseURL + this.listURL, + method: 'get', + headers: { + Accept: 'application/json', + }, + params: { + i: 'list', + } + }).then(res => { + // iterate through response and add each ingredient to array as an object + res.data.drinks.forEach(item => { + // loosely correct capitalization errors on API + let itemName = item.strIngredient1[0].toUpperCase() + item.strIngredient1.substring(1) + // add name key with string value and recipes key with empty array to each ingredient + this.ingredientsObj[itemName] = { + name: itemName, + recipes: {}, + } + }) + // console.log(this.ingredientsObj) + }) + }, + // searches full list of ingredients against user input + ingredientSearch () { + // reset search results + this.filteredIngredients = [] + // iterate through ingredients Object + Object.keys(this.ingredientsObj).forEach(item => { + // add to filtered/results array if input is not blank AND the current string is found in ingredients Object keys + if (!(this.ingredientSearchInput == '') && (item.toLowerCase().includes(this.ingredientSearchInput.toLowerCase()))) { + this.filteredIngredients.push(item) + this.filteredIngredients.sort() + } + }) + }, + // adds a given ingredient to an array storing the user's ingredients + addToMyIngredients (ingredient) { + // only run if the ingredient isn't already added + if (!(this.myIngredients.includes(ingredient))) { + // case-insensitive search + const found = this.filteredIngredients.find(element => { + return element.toLowerCase() === ingredient.toLowerCase() + }) + // return ingredient with matching capitalization if case-insensitive search was successful + if (!(this.filteredIngredients.includes(ingredient)) && (found !== undefined)) { + this.filteredIngredients.forEach(element => { + if (element.toLowerCase() == ingredient.toLowerCase()) { + ingredient = element + } + }) + } + // only run if current input is a valid ingredient + if (this.filteredIngredients.includes(ingredient)) { + // add to array and sort + this.myIngredients.push(ingredient) + this.myIngredients.sort() + // get list of recipes for this ingredient + this.ingredientRecipes(ingredient) + // save to local storage + this.saveToLocal() + // clear search after adding + this.filteredIngredients = [] + this.ingredientSearchInput = null + } + } + }, + // gets all cocktails that a given ingredient is used in + ingredientRecipes (ingredient) { + axios({ + url: this.baseURL + this.filterURL, + method: 'get', + headers: { + Accept: 'application/json', + }, + params: { + i: ingredient, + } + }).then(res => { + // iterate through response and add recipes to ingredient object as objects + if (!(res.data.drinks == "None Found")) { + res.data.drinks.forEach(item => { + // format each object as drinkNameString: drinkID + this.ingredientsObj[ingredient].recipes[item.strDrink] = item.idDrink + // request full recipe for each drink returned + this.recipeDetails(item.idDrink) + }) + } + // console.log(this.ingredientsObj) + }) + }, + // gets details of recipes + recipeDetails (recipeID) { + axios({ + url: this.baseURL + this.lookupURL, + method: 'get', + headers: { + Accept: 'application/json', + }, + params: { + i: recipeID,//change to int id + } + }).then(res => { + // assign drink response to variable for easier access + let recipeResponse = res.data.drinks[0] + // construct recipe object + this.myRecipes[recipeResponse.strDrink] = { + name: recipeResponse.strDrink, + id: recipeID, + glass: recipeResponse.strGlass, + instructions: recipeResponse.strInstructions, + image: recipeResponse.strDrinkThumb, + ingredients: {} + } + // get arrays of keys for ingredients and measures + let recipeIngredients = Object.keys(recipeResponse).filter(eachString => {return eachString.includes('Ingredient')}) + let recipeMeasures = Object.keys(recipeResponse).filter(eachString => {return eachString.includes('Measure')}) + // iterate over non-null ingredients and add them and their measures to object + recipeIngredients.forEach((item, index) => { + if (recipeResponse[item]) { + this.myRecipes[recipeResponse.strDrink].ingredients[recipeResponse[item]] = recipeResponse[recipeMeasures[index]] + } + }) + this.saveToLocal() + this.missingIngredientCount() + // console.log(this.myRecipes) + }) + }, + // finds number of missing ingredients per recipe + missingIngredientCount () { + // reset array + this.possibleRecipes = [] + // iterate through each recipe + Object.keys(this.myRecipes).forEach(thisRecipe => { + // reset counter for missing ingredients + let numMissing = 0 + // iterate through each ingredient in the recipe + Object.keys(this.myRecipes[thisRecipe].ingredients).forEach(thisIngredient => { + // increment counter if a missing ingredient is found + if (!(this.myIngredients.includes(thisIngredient))) { + numMissing++ + } + }) + // add an empty array at the index matching the number of missing ingredients if an array is not already present + if (!(this.possibleRecipes[numMissing])) { + this.possibleRecipes[numMissing] = [] + } + // add recipe to possibleRecipes array at the index position matching the number of missing ingredients + this.possibleRecipes[numMissing].push(thisRecipe) + this.possibleRecipes[numMissing].sort() + }) + // save to local storage + this.saveToLocal() + // console.log(this.possibleRecipes) + }, + // assigns a specific recipe to currentRecipe for display + showCurrentRecipe (recipeName) { + this.currentRecipe = this.myRecipes[recipeName] + }, + // saves myIngredients and myRecipes arrays to local storage + saveToLocal () { + const parsedMyIngredients = JSON.stringify(this.myIngredients) + localStorage.setItem('myIngredients', parsedMyIngredients) + const parsedMyRecipes = JSON.stringify(this.myRecipes) + localStorage.setItem('myRecipes', parsedMyRecipes) + const parsedPossibleRecipes = JSON.stringify(this.possibleRecipes) + localStorage.setItem('possibleRecipes', parsedPossibleRecipes) + }, + // loads myIngredients and myRecipes arrays from local storage + loadFromLocal () { + if (localStorage.getItem('myIngredients')) { + this.myIngredients = JSON.parse(localStorage.getItem('myIngredients')) + } + if (localStorage.getItem('myRecipes')) { + this.myRecipes = JSON.parse(localStorage.getItem('myRecipes')) + } + if (localStorage.getItem('possibleRecipes')) { + this.possibleRecipes = JSON.parse(localStorage.getItem('possibleRecipes')) + } + }, + // resets local storage and clears associated variables + clearLocal () { + this.myIngredients = [] + this.myRecipes = {} + this.possibleRecipes = [] + this.currentRecipe = null + localStorage.clear() + }, + } +}).mount('#app') \ No newline at end of file