Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# js-project-recipe-library
https://pebblesrecipefinder.netlify.app
73 changes: 73 additions & 0 deletions index.html
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>

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! 👍

<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>
220 changes: 220 additions & 0 deletions script.js
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");

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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);
Loading