-
Notifications
You must be signed in to change notification settings - Fork 60
Recipe Library project by Evip88 #67
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
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 |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| <!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="styles.css" /> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <h1>Recipe Library</h1> | ||
|
|
||
| <section id="controls"> | ||
| <div> | ||
| <h2>Filter by cuisine</h2> | ||
| <div class="controls__container filter"> | ||
| <button | ||
| onclick="setActiveFilter('all')" | ||
| id="filter__all" | ||
| class="btn"> | ||
| All | ||
| </button> | ||
| <button | ||
| onclick="setActiveFilter('italian')" | ||
| id="filter__italian" | ||
| class="btn"> | ||
| Italy | ||
| </button> | ||
| <button | ||
| onclick="setActiveFilter('american')" | ||
| id="filter__american" | ||
| class="btn"> | ||
| USA | ||
| </button> | ||
| <button | ||
| onclick="setActiveFilter('chinese')" | ||
| id="filter__chinese" | ||
| class="btn"> | ||
| China | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div> | ||
| <h2>Sort by time</h2> | ||
| <div class="controls__container sort"> | ||
| <button onclick="setActiveSort('desc')" id="sort__desc" class="btn"> | ||
| Descending | ||
| </button> | ||
| <button onclick="setActiveSort('asc')" id="sort__asc" class="btn"> | ||
| Ascending | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section id="recipe__grid"></section> | ||
| </div> | ||
| <script src="script.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // --- State --- | ||
| let cuisineQueryParam = ""; // e.g., "&cuisine=italian" | ||
| let sortQueryParam = "&sort=time&sortDirection=desc"; // default by time, descending | ||
| let activeFilterBtn = null; | ||
| let activeSortBtn = null; | ||
|
|
||
| const API_KEY = "3f2445631f2d458b92fe40f9832bcc51"; | ||
|
|
||
| // --- Marks the clicked button as active and removes the active class from the previously clicked one. --- | ||
| function markActive(newBtn, prevBtnRefName) { | ||
| if (newBtn === null) return; | ||
| if (window[prevBtnRefName]) { | ||
| window[prevBtnRefName].classList.remove("active"); | ||
| } | ||
| newBtn.classList.add("active"); | ||
| window[prevBtnRefName] = newBtn; | ||
| } | ||
|
|
||
| //Fetches updated recipes and sends them to the UI rendering function (showCase()). | ||
| function updateAndRender() { | ||
| showCase(fetchRecipes()); | ||
| } | ||
|
|
||
| // --- UI Actions Handles what happens when the user clicks a filter button (like “Italian” or “Mexican”).--- | ||
| function setActiveFilter(filter) { | ||
| // Set query param | ||
| if (filter === "all") { | ||
| cuisineQueryParam = ""; | ||
| } else { | ||
| cuisineQueryParam = "&cuisine=" + encodeURIComponent(filter); | ||
| } | ||
| // Mark active button | ||
| const btn = document.getElementById(`filter__${filter}`); | ||
| markActive(btn, "activeFilterBtn"); | ||
|
|
||
| // Refresh results | ||
| updateAndRender(); | ||
| } | ||
|
|
||
| function setActiveSort(direction) { | ||
| // Validate: asc | desc | ||
| const dir = direction === "asc" ? "asc" : "desc"; | ||
| sortQueryParam = `&sort=time&sortDirection=${dir}`; | ||
|
|
||
| const btn = document.getElementById(`sort__${dir}`); | ||
|
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. clever to use the variable to get the correct element from the DOM |
||
| markActive(btn, "activeSortBtn"); | ||
|
|
||
| // Refresh results | ||
| updateAndRender(); | ||
| } | ||
|
|
||
| // --- Data Fetch recipe from spoonacular API, based on filter and sort settings --- | ||
| async function fetchRecipes() { | ||
| const URL = `https://api.spoonacular.com/recipes/complexSearch?fillIngredients=true&addRecipeInformation=true&number=12&apiKey=${API_KEY}${cuisineQueryParam}${sortQueryParam}`; | ||
|
|
||
| try { | ||
| const response = await fetch(URL); | ||
| if (!response.ok) { | ||
| throw new Error(`HTTP ${response.status}`); | ||
| } | ||
| const data = await response.json(); | ||
| // data.results is an array of recipe objects | ||
| return data.results || []; | ||
| } catch (err) { | ||
| console.error(err); | ||
| // Graceful fallback | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| // --- Format time, and minustes --- | ||
| function formatTime(totalMinutes) { | ||
| const h = Math.floor(totalMinutes / 60); | ||
| const m = totalMinutes % 60; | ||
| if (h && m) return `${h}h ${m}m`; | ||
| if (h) return `${h}h`; | ||
| return `${m}m`; | ||
| } | ||
|
Comment on lines
+72
to
+78
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 |
||
|
|
||
| // Display recepie card in HTML Grid/ CSS style on it | ||
| async function showCase(recipesPromise) { | ||
| const recipeGrid = document.getElementById("recipe__grid"); | ||
| recipeGrid.innerHTML = `<div id="loading">Loading…</div>`; | ||
|
|
||
| const recipes = await recipesPromise; | ||
|
|
||
| if (!recipes.length) { | ||
| recipeGrid.innerHTML = `<p>No recipes found. Try a different filter.</p>`; | ||
| return; | ||
| } | ||
|
|
||
| recipeGrid.innerHTML = ""; | ||
|
|
||
| recipes.forEach((recipe) => { | ||
| const recipeCard = document.createElement("div"); | ||
| recipeCard.className = "recipe__card"; | ||
|
|
||
| const cuisines = | ||
| Array.isArray(recipe.cuisines) && recipe.cuisines.length | ||
| ? recipe.cuisines.join(", ") | ||
| : "—"; | ||
|
|
||
| const ingredients = Array.isArray(recipe.extendedIngredients) | ||
| ? recipe.extendedIngredients.map((ing) => ing.original) | ||
| : []; | ||
|
|
||
| const ingredientList = document.createElement("ul"); | ||
| ingredientList.className = "recipe__ingredients"; | ||
| ingredients.forEach((text) => { | ||
| const li = document.createElement("li"); | ||
| li.textContent = text; | ||
| ingredientList.appendChild(li); | ||
| }); | ||
| // html structor | ||
| recipeCard.innerHTML = ` | ||
| <img src="${recipe.image}" alt="${recipe.title}" class="recipe__image" /> | ||
| <h3 class="recipe__title">${recipe.title}</h3> | ||
| <p class="recipe__meta"><b>Cuisine:</b> ${cuisines}</p> | ||
| <p class="recipe__meta"><b>Time:</b> ${formatTime( | ||
| recipe.readyInMinutes || 0 | ||
| )}</p> | ||
| <h4 class="recipe__subtitle">Ingredients:</h4> | ||
| `; | ||
|
|
||
| recipeCard.appendChild(ingredientList); | ||
| recipeGrid.appendChild(recipeCard); | ||
| }); | ||
| } | ||
| // initialization (auto-run- on load) | ||
| // --- Init (after DOM is ready because script is at the end of body) --- | ||
| (function init() { | ||
| // Set defaults visually & load first page | ||
| setActiveFilter("all"); | ||
| setActiveSort("desc"); | ||
| })(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| * { | ||
| margin: 0; | ||
| } | ||
|
|
||
| h1 { | ||
| color: blue; | ||
| font-size: 3.5rem; | ||
| margin-top: 50px; | ||
| margin-bottom: 50px; | ||
| } | ||
|
|
||
| h2 { | ||
| margin: 25px 0 25px; | ||
| } | ||
|
|
||
| #controls { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| max-width: 900px; | ||
| flex-flow: row wrap; | ||
| } | ||
|
|
||
| .controls__container { | ||
| display: flex; | ||
| justify-content: start; | ||
| gap: 30px; | ||
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| .controls__container > button { | ||
| color: #0018a4; | ||
| border: none; | ||
|
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. To avoid the "jumping" of the buttons when you hover over them you could change the |
||
| padding: 15px 32px; | ||
| text-align: center; | ||
| text-decoration: none; | ||
| display: inline-block; | ||
| font-size: 16px; | ||
| cursor: pointer; | ||
| border-radius: 30px; | ||
| } | ||
| .controls__container.filter > button { | ||
| background-color: #ccffe2; | ||
| } | ||
| .controls__container.filter > button.active { | ||
| color: white; | ||
| background-color: #0018a4; | ||
| } | ||
| .controls__container.filter > button:hover { | ||
| border: 2px blue solid; | ||
| } | ||
|
|
||
| .controls__container.sort > button { | ||
| background-color: #ffecea; | ||
| } | ||
| .controls__container.sort > button.active { | ||
| background-color: #ff6589; | ||
| } | ||
| .controls__container.sort > button:hover { | ||
| border: 2px blue solid; | ||
| background-color: #ff6589; | ||
| } | ||
|
|
||
| #recipe__grid { | ||
| display: grid; | ||
| grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | ||
| margin-top: 2rem; | ||
| column-gap: 10px; | ||
| row-gap: 20px; | ||
| } | ||
|
|
||
| .recipe__card { | ||
| display: flex; | ||
| flex-direction: column; | ||
| padding: 1rem; | ||
| /* border: 2px grey solid; */ | ||
| border-radius: 10px; | ||
| width: 300px; | ||
| gap: 10px; | ||
| height: fit-content; | ||
| box-shadow: 1px 2px 3px 2px #dadada; | ||
| } | ||
|
|
||
| .recipe__card > img { | ||
| border-radius: 20px; | ||
| align-self: center; | ||
| max-width: 100%; | ||
| margin: 16px; | ||
| } | ||
|
|
||
| .recipe__card > h3, | ||
| h4 { | ||
| border-bottom: 1px grey solid; | ||
| padding-bottom: inherit; | ||
| } | ||
|
|
||
| .recipe__card > ul { | ||
| list-style: none; | ||
| padding-inline-start: 0px; | ||
| } | ||
|
|
||
| .container { | ||
| /* padding: 20px; */ | ||
| width: 90dvw; | ||
| margin: 0 auto; | ||
| } | ||
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.
nice to use ternary operators here.