-
Notifications
You must be signed in to change notification settings - Fork 60
My Recipe Library---https://saras-js-project-recipe-library.netlify.app/ #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9a2a8c9
ccbecf6
f1e7758
c4e46fc
0f97052
3d0a287
67b03f2
feab0e4
d63d173
b5044e4
46d91b7
f76f9c6
53cdb34
86096b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| # js-project-recipe-library | ||
| https://saras-js-project-recipe-library.netlify.app/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Recipe Library</title> | ||
| <link rel="stylesheet" href="style.css" /> | ||
| </head> | ||
|
|
||
| <body> | ||
| <main> | ||
| <h1>Recipe Library</h1> | ||
|
|
||
| <!-- ====== FILTERS / SORT / LUCKY CONTROLS ====== --> | ||
| <div class="controls"> | ||
|
|
||
| <!-- FILTER SECTION --> | ||
| <section class="filters" id="cuisineFilter"> | ||
| <h2 class="section-title">Filter on kitchen</h2> | ||
| <div class="filter-buttons"> | ||
| <label class="filter-option"> | ||
| <input checked type="radio" name="cuisine" value="all" /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart to use radio-buttons! 💡 |
||
| All | ||
| </label> | ||
| <label class="filter-option"> | ||
| <input type="radio" name="cuisine" value="asian" /> | ||
| Asian | ||
| </label> | ||
| <label class="filter-option"> | ||
| <input type="radio" name="cuisine" value="italian" /> | ||
| Italian | ||
| </label> | ||
| <label class="filter-option"> | ||
| <input type="radio" name="cuisine" value="mexican" /> | ||
| Mexican | ||
| </label> | ||
| <label class="filter-option"> | ||
| <input type="radio" name="cuisine" value="middle eastern" /> | ||
| Middle Eastern | ||
| </label> | ||
| <label class="filter-option"> | ||
| <input type="radio" name="cuisine" value="nordic" /> | ||
| Nordic | ||
| </label> | ||
| </div> | ||
| </section> | ||
|
|
||
| <!-- SORT SECTION --> | ||
| <section class="sort" id="sortOnTime"> | ||
| <h2 class="section-title">Sort on time</h2> | ||
| <div class="sort-buttons"> | ||
| <label class="sort-button"> | ||
| <input checked type="radio" name="order" value="desc" /> | ||
| Descending | ||
| </label> | ||
| <label class="sort-button"> | ||
| <input type="radio" name="order" value="asc" /> | ||
| Ascending | ||
| </label> | ||
| </div> | ||
| </section> | ||
|
|
||
| <!-- FEELING LUCKY SECTION --> | ||
| <section class="lucky"> | ||
| <h2 class="section-title">Feeling lucky?</h2> | ||
| <div class="lucky-button"> | ||
| <button id="random-button" class="random-button">Surprise me!</button> | ||
| </div> | ||
| </section> | ||
| </div> | ||
|
|
||
| <!-- ====== RECIPE CARDS GRID ====== --> | ||
| <section | ||
| class="cards" | ||
| id="cardsContainer" | ||
| aria-label="Recipe cards grid"> | ||
| </section> | ||
| </main> | ||
|
|
||
| <script src="script.js"></script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| const cardsContainer = document.getElementById('cardsContainer') | ||
| const cuisineFilter = document.getElementById('cuisineFilter') | ||
| const sortSection = document.getElementById('sortOnTime') | ||
| const randomButton = document.getElementById('random-button') | ||
|
|
||
| const API_KEY = 'c0e5e1580c434758805e8f05530e9b26' | ||
| const API_KEY2 = 'd4c7b9ec0f33432ea2aa56c816a17159' | ||
|
|
||
| const CACHE_KEY = 'recipes' | ||
| const CACHE_DURATION = 24 * 60 * 60 * 1000 | ||
|
|
||
| let recipesArray = [] | ||
|
|
||
| const params = new URLSearchParams({ | ||
| number: 30, | ||
| cuisine: 'Asian,Italian,Mexican,Middle Eastern', | ||
| diet: 'vegan,vegetarian,gluten free', | ||
| type: 'breakfast,main course,dessert', | ||
| sort: 'popularity', | ||
| addRecipeInformation: true, | ||
| instructionsRequired: true, | ||
| fillIngredients: true, | ||
| apiKey: API_KEY | ||
| }) | ||
|
Comment on lines
+14
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice that you have used URLSearchParams! Super useful and clever |
||
|
|
||
| const URL = `https://api.spoonacular.com/recipes/complexSearch?${params.toString()}` | ||
|
|
||
| const showCardsContainer = (recipesArray) => { | ||
| cardsContainer.innerHTML = '' | ||
|
|
||
| if (!recipesArray || recipesArray.length === 0) { | ||
| cardsContainer.innerHTML = ` | ||
| <div class='empty-message'> | ||
| <h2>Oops 😅</h2> | ||
| <p>We couldn't find any recipes for your filter...<br> | ||
| Try changing it or click <strong>Surprise me!</strong> 🎉</p> | ||
| </div> | ||
|
Comment on lines
+35
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice empty state msg and nudge to try the random button :) |
||
| ` | ||
| return | ||
| } | ||
|
|
||
| recipesArray.forEach((recipe) => { | ||
| cardsContainer.innerHTML += ` | ||
| <div class='card'> | ||
| <img src='${recipe.image}' alt='${recipe.title}' /> | ||
| <div class='card-content'> | ||
| <h3>${recipe.title}</h3> | ||
| <div class='divider'></div> | ||
| <p><strong>Cuisine:</strong> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of is intuitive since it might be read out loud of accessibility reasons |
||
| <span class='cuisine-value'> | ||
| ${recipe?.cuisines?.length ? recipe.cuisines.join(', ') : 'N/A'} | ||
| </span> | ||
|
Comment on lines
+51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good fallback |
||
| </p> | ||
| <p><strong>Time:</strong> | ||
| <span class='time-value'>${recipe.readyInMinutes} min</span> | ||
| </p> | ||
| <div class='divider'></div> | ||
| <p class='ingredients-header'>Ingredients:</p> | ||
| <ul> | ||
| ${recipe?.extendedIngredients | ||
| .map(({ name }) => `<li>${name}</li>`) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looked nice to have the list with dots |
||
| .join('')} | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| ` | ||
| }) | ||
| } | ||
|
|
||
| const fetchData = async () => { | ||
| const cachedData = localStorage.getItem(CACHE_KEY) | ||
|
|
||
| if (cachedData) { | ||
| try { | ||
| const { recipes, timestamp } = JSON.parse(cachedData) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does the timestamp do? |
||
| const isExpired = Date.now() - timestamp > CACHE_DURATION | ||
| recipesArray = recipes | ||
|
Comment on lines
+73
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smart, nice use of a cache "timer" too |
||
|
|
||
| if (!isExpired) { | ||
| showCardsContainer(recipes) | ||
| return | ||
| } | ||
| } catch (error) { | ||
| console.error('Error parsing cached data:', error) | ||
| localStorage.removeItem(CACHE_KEY) | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| const res = await fetch(URL) | ||
|
|
||
| if (res.status === 402) { | ||
| cardsContainer.innerHTML = ` | ||
| <div class='empty-message'> | ||
| <h2>Daily API limit reached 🚫</h2> | ||
| <p>Please try again later.</p> | ||
| </div>`; | ||
|
Comment on lines
+92
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⭐ |
||
| return; | ||
| } | ||
|
|
||
| if (!res.ok) { | ||
| cardsContainer.innerHTML = ` | ||
| <div class='empty-message'> | ||
| <h2>Oops 😅</h2> | ||
| <p>No recipes found...</p> | ||
| </div> | ||
| ` | ||
| throw new Error(`HTTP error! Status: ${res.status}`) | ||
| } | ||
|
|
||
| const data = await res.json() | ||
| const recipes = data.results | ||
| recipesArray = recipes | ||
|
|
||
| localStorage.setItem( | ||
| CACHE_KEY, | ||
| JSON.stringify({ | ||
| recipes, | ||
| timestamp: Date.now() | ||
| }) | ||
| ) | ||
|
|
||
| showCardsContainer(recipes) | ||
| } catch (error) { | ||
| console.error('Error occurred:', error) | ||
| } | ||
| } | ||
|
|
||
| fetchData() | ||
|
|
||
| const filterOnCuisine = () => { | ||
| const selected = cuisineFilter.querySelector('input[name="cuisine"]:checked') | ||
| const selectedCuisine = selected ? selected.value : 'all' | ||
|
|
||
| const filteredRecipes = | ||
| selectedCuisine === 'all' | ||
| ? recipesArray | ||
| : recipesArray.filter((recipe) => | ||
| recipe.cuisines | ||
| .map((cuisine) => cuisine.toLowerCase()) | ||
| .includes(selectedCuisine.toLowerCase()) | ||
|
Comment on lines
+140
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart to use toLowerCase if anything has a capital letter somewhere, to make sure nothings missed |
||
| ) | ||
|
|
||
| showCardsContainer(filteredRecipes) | ||
| } | ||
|
|
||
| const sortOnTime = () => { | ||
| const selected = sortSection.querySelector('input[name="order"]:checked') | ||
| const order = selected ? selected.value : 'desc' | ||
|
|
||
| const sorted = [...recipesArray].sort((a, b) => { | ||
| return order === 'asc' | ||
| ? a.readyInMinutes - b.readyInMinutes | ||
| : b.readyInMinutes - a.readyInMinutes | ||
| }) | ||
|
|
||
| showCardsContainer(sorted) | ||
| } | ||
|
Comment on lines
+147
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice job! If you want to keep the sorting even when you change the cuisine filters you could maybe save the sorting rule in a global variable. Just something to take with you going forward. |
||
|
|
||
| const getRandomRecipe = () => { | ||
| const randomIndex = Math.floor(Math.random() * recipesArray.length) | ||
| const randomRecipe = [recipesArray[randomIndex]] | ||
|
|
||
| showCardsContainer(randomRecipe) | ||
| } | ||
|
|
||
| cuisineFilter.addEventListener('change', filterOnCuisine) | ||
| sortSection.addEventListener('change', sortOnTime) | ||
| randomButton.addEventListener('click', getRandomRecipe) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really neat way to organize your code!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you:)