-
Notifications
You must be signed in to change notification settings - Fork 60
Pull 1 #56
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?
Pull 1 #56
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 |
|---|---|---|
| @@ -1 +1 @@ | ||
| # js-project-recipe-library | ||
| https://pebblesrecipefinder.netlify.app |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <link rel="stylesheet" href="styles.css" /> | ||
| <script src="script.js" defer></script> | ||
| <title>Recipe Finder</title> | ||
| </head> | ||
| <body> | ||
| <h1>Recipe Finder</h1> | ||
|
|
||
| <div id="search-form"> | ||
| <input type="text" id="ingredient-input" placeholder="Enter an ingredient" /> | ||
|
|
||
| <!-- Meal Type --> | ||
| <select id="meal-type"> | ||
| <option value="">Any meal type</option> | ||
| <option value="breakfast">Breakfast</option> | ||
| <option value="main course">Main Course</option> | ||
| <option value="dessert">Dessert</option> | ||
| <option value="salad">Salad</option> | ||
| <option value="snack">Snack</option> | ||
| <option value="soup">Soup</option> | ||
| <option value="beverage">Beverage</option> | ||
| </select> | ||
|
|
||
| <!-- Country / Cuisine --> | ||
| <select id="country-select"> | ||
| <option value="">Any cuisine</option> | ||
| <option value="african">African</option> | ||
| <option value="american">American</option> | ||
| <option value="british">British</option> | ||
| <option value="chinese">Chinese</option> | ||
| <option value="french">French</option> | ||
| <option value="greek">Greek</option> | ||
| <option value="indian">Indian</option> | ||
| <option value="italian">Italian</option> | ||
| <option value="japanese">Japanese</option> | ||
| <option value="mexican">Mexican</option> | ||
| <option value="middle eastern">Middle Eastern</option> | ||
| <option value="spanish">Spanish</option> | ||
| <option value="swedish">Swedish</option> | ||
| <option value="thai">Thai</option> | ||
| </select> | ||
|
|
||
| <!-- Diet --> | ||
| <select id="diet-select" title="Diet filter"> | ||
| <option value="">Any diet</option> | ||
| <option value="vegetarian">Vegetarian</option> | ||
| <option value="vegan">Vegan</option> | ||
| <option value="gluten free">Gluten Free</option> | ||
| <option value="dairy free">Dairy Free</option> | ||
| </select> | ||
|
|
||
| <!-- Sort --> | ||
| <select id="sort-select" title="Sort results"> | ||
| <option value="">Default sort</option> | ||
| <option value="popularity">Popularity</option> | ||
| </select> | ||
|
|
||
| <button id="search-button">Search</button> | ||
| <button id="random-button">Random</button> | ||
| </div> | ||
|
|
||
| <div id="results"></div> | ||
|
|
||
| <!-- Recipe details modal --> | ||
| <div id="recipe-details" style="display:none"> | ||
| <div id="recipe-content"></div> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| // Replace this with your valid Spoonacular API key | ||
| const apiKey = "cfc30adf3b0e422b9a0d562cf3181fa1"; | ||
|
|
||
| // DOM refs | ||
| const formEl = document.getElementById("search-form"); | ||
| const inputEl = document.getElementById("ingredient-input"); | ||
|
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. Not something to comment really but what does E1 or is it El but why u have that? |
||
| const mealTypeEl = document.getElementById("meal-type"); | ||
| const countryEl = document.getElementById("country-select"); | ||
| const dietEl = document.getElementById("diet-select"); | ||
| const sortEl = document.getElementById("sort-select"); | ||
| const resultsEl = document.getElementById("results"); | ||
| const detailsEl = document.getElementById("recipe-details"); | ||
| const detailsContentEl = document.getElementById("recipe-content"); | ||
| const searchBtn = document.getElementById("search-button"); | ||
| const randomBtn = document.getElementById("random-button"); | ||
|
|
||
| // Map diet selection to Spoonacular params | ||
| function buildDietParams(selected) { | ||
| const params = new URLSearchParams(); | ||
| if (!selected) return params; | ||
|
|
||
| if (selected === "dairy free") { | ||
| params.set("intolerances", "dairy"); | ||
| } else { | ||
| params.set("diet", selected); // Spoonacular accepts "gluten free" here too | ||
| } | ||
| return params; | ||
| } | ||
|
|
||
| function card(recipe) { | ||
| const div = document.createElement("div"); | ||
| div.className = "recipe-item"; | ||
|
|
||
| const img = document.createElement("img"); | ||
| img.src = recipe.image; | ||
| img.alt = recipe.title; | ||
|
|
||
| const title = document.createElement("h3"); | ||
| title.textContent = recipe.title; | ||
|
|
||
| const link = document.createElement("a"); | ||
| link.href = "#"; | ||
| link.textContent = "View Recipe"; | ||
| link.addEventListener("click", async (e) => { | ||
| e.preventDefault(); | ||
| await showRecipeDetails(recipe.id); | ||
| }); | ||
|
|
||
| div.append(img, title, link); | ||
| return div; | ||
| } | ||
|
|
||
| function setLoading(loading, message = "Searching…") { | ||
| if (loading) { | ||
| resultsEl.innerHTML = `<p>${message}</p>`; | ||
| searchBtn.disabled = true; | ||
| randomBtn.disabled = true; | ||
| searchBtn.style.opacity = 0.7; | ||
| randomBtn.style.opacity = 0.7; | ||
| } else { | ||
| searchBtn.disabled = false; | ||
| randomBtn.disabled = false; | ||
| searchBtn.style.opacity = ""; | ||
| randomBtn.style.opacity = ""; | ||
| } | ||
|
Comment on lines
+53
to
+65
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! |
||
| } | ||
|
|
||
| async function searchRecipes() { | ||
| const ingredient = (inputEl?.value || "").trim(); | ||
| if (!ingredient) { | ||
| resultsEl.innerHTML = "<p>Please enter an ingredient.</p>"; | ||
| return; | ||
| } | ||
|
|
||
| const type = mealTypeEl.value; | ||
| const cuisine = countryEl.value; | ||
| const diet = dietEl.value; | ||
| const sort = sortEl.value; | ||
|
|
||
| try { | ||
| setLoading(true, "Searching…"); | ||
|
|
||
| const qs = new URLSearchParams({ | ||
| query: ingredient, | ||
| number: "9", | ||
| addRecipeInformation: "true", | ||
| apiKey | ||
| }); | ||
|
|
||
| if (type) qs.set("type", type); | ||
| if (cuisine) qs.set("cuisine", cuisine); | ||
| if (sort) qs.set("sort", sort); | ||
|
|
||
| const dietParams = buildDietParams(diet); | ||
| dietParams.forEach((v, k) => qs.set(k, v)); | ||
|
|
||
| const url = `https://api.spoonacular.com/recipes/complexSearch?${qs.toString()}`; | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| const msg = response.status === 402 || response.status === 401 | ||
| ? "API key error or quota reached. Please check your Spoonacular API key." | ||
| : `Request failed (HTTP ${response.status}).`; | ||
| throw new Error(msg); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| const items = data.results || []; | ||
|
|
||
| resultsEl.innerHTML = items.length | ||
| ? "" | ||
| : "<p>No recipes found. Try another ingredient or adjust filters.</p>"; | ||
|
|
||
| items.forEach((r) => resultsEl.appendChild(card(r))); | ||
| } catch (err) { | ||
| console.error(err); | ||
| resultsEl.innerHTML = `<p>${err.message || "Something went wrong. Please try again."}</p>`; | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
|
|
||
| async function randomRecipe() { | ||
| const type = mealTypeEl.value; | ||
| const cuisine = countryEl.value; | ||
| const diet = dietEl.value; | ||
|
|
||
| try { | ||
| setLoading(true, "Picking a random recipe…"); | ||
|
|
||
| const tags = []; | ||
| if (type) tags.push(type); | ||
| if (cuisine) tags.push(cuisine); | ||
| if (diet) tags.push(diet); // Spoonacular accepts "dairy free" tag too | ||
|
|
||
| const qs = new URLSearchParams({ number: "1", apiKey }); | ||
| if (tags.length) qs.set("tags", tags.join(",")); | ||
|
Comment on lines
+135
to
+136
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 use of URLSearchParams! we haven't talked about this concept yet but it is a very lcean way of structuring the code. also to set the query string with the tags like that. ⭐ |
||
|
|
||
| const url = `https://api.spoonacular.com/recipes/random?${qs.toString()}`; | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| const msg = response.status === 402 || response.status === 401 | ||
| ? "API key error or quota reached. Please check your Spoonacular API key." | ||
| : `Request failed (HTTP ${response.status}).`; | ||
| throw new Error(msg); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| const recipes = data.recipes || []; | ||
|
|
||
| resultsEl.innerHTML = recipes.length | ||
| ? "" | ||
| : "<p>No random recipe found. Try different filters.</p>"; | ||
|
|
||
|
Comment on lines
+150
to
+153
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. ⭐ |
||
| if (recipes.length) resultsEl.appendChild(card(recipes[0])); | ||
| } catch (err) { | ||
| console.error(err); | ||
| resultsEl.innerHTML = `<p>${err.message || "Couldn’t fetch a random recipe."}</p>`; | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
|
|
||
| async function showRecipeDetails(recipeId) { | ||
| try { | ||
| detailsContentEl.innerHTML = "<p>Loading…</p>"; | ||
| detailsEl.style.display = "flex"; | ||
| detailsEl.setAttribute("aria-hidden", "false"); | ||
|
|
||
| const response = await fetch( | ||
| `https://api.spoonacular.com/recipes/${recipeId}/information?apiKey=${apiKey}` | ||
| ); | ||
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | ||
|
|
||
| const recipe = await response.json(); | ||
|
|
||
| const ingredients = Array.isArray(recipe.extendedIngredients) | ||
| ? recipe.extendedIngredients.map((i) => i.original).join(", ") | ||
| : "Not available"; | ||
|
|
||
| detailsContentEl.innerHTML = ` | ||
| <button id="close-card-btn" aria-label="Close details" onclick="closeDetails()">✖ Close</button> | ||
| <h2>${recipe.title}</h2> | ||
| <img src="${recipe.image}" alt="${recipe.title}"> | ||
| <p><strong>Cuisine:</strong> ${recipe.cuisines?.join(", ") || "N/A"}</p> | ||
| <p><strong>Meal Type:</strong> ${recipe.dishTypes?.join(", ") || "N/A"}</p> | ||
| <p><strong>Ingredients:</strong> ${ingredients}</p> | ||
| <p><strong>Instructions:</strong> ${recipe.instructions || "No instructions provided."}</p> | ||
| `; | ||
| } catch (err) { | ||
| console.error(err); | ||
| detailsContentEl.innerHTML = "<p>Couldn't load recipe details.</p>"; | ||
| } | ||
| } | ||
|
|
||
| function closeDetails() { | ||
| detailsEl.style.display = "none"; | ||
| detailsEl.setAttribute("aria-hidden", "true"); | ||
| } | ||
|
|
||
| // Close modal when clicking outside content (mobile-friendly) | ||
| detailsEl.addEventListener("click", (e) => { | ||
| if (e.target === detailsEl) closeDetails(); | ||
| }); | ||
|
|
||
| // Close with Escape key | ||
| document.addEventListener("keydown", (e) => { | ||
| if (e.key === "Escape") closeDetails(); | ||
| }); | ||
|
|
||
| /* --- Event bindings --- */ | ||
|
|
||
| // Submit form triggers search (so Enter works) | ||
| formEl.addEventListener("submit", (e) => { | ||
| e.preventDefault(); | ||
| searchRecipes(); | ||
| }); | ||
|
|
||
| // Buttons | ||
| searchBtn.addEventListener("click", searchRecipes); | ||
| randomBtn.addEventListener("click", randomRecipe); | ||
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.
I've heard that you should have this in the in the end of the code just above .
I don´t know why but so u know! 👍