${r.title}
+ +Ingredients
+- ${ingHtml}
diff --git a/backup.js b/backup.js new file mode 100644 index 000000000..45896e150 --- /dev/null +++ b/backup.js @@ -0,0 +1,81 @@ +//==============JUST SAVING OLD ARRAY IN A BACKUP=========> +/*const recipes = [ + { + title: "Cheat’s cheesy Focaccia", + cuisine: "italy", + diet: "vegetarian", + time: 40, + popularity: 1, + img: "./images/pizza.jpg", + ingredients: [ + "500 pack bread mix", + "2tbsp. olive oil, plus a little extra for drizzling", + "25g parmesan (or vegetarian alternative), grated", + "75g dolcelatte cheese (or vegetarian alternative)", + ], + }, + + { + title: "Baked Chicken", + cuisine: "china", + diet: "Gluten free", + popularity: 5, + time: 35, + img: "./images/meat.jpg", + ingredients: [ + " 6 bone-in chicken breast halves, pr 6 chicken thighs and wings skin-on", + "1/2 tsp. coarse salt", + "1/2 tsp. Mrs.Dash seasoning", + "1/4 tsp. freshly grounded black pepper", + ], + }, + { + title: "Deep Fried Fish Bones", + cuisine: "South-East Asia", + diet: "dairy free", + popularity: 2, + time: 10, + img: "./images/chips.jpg", + ingredients: ["8 small whiting fish or smelt", "4 cups vegetable oil"], + }, + { + title: "Sweet and Sour Tofu", + cuisine: "china", + diet: "vegan", + popularity: 3, + time: 25, + img: "./images/tofu.jpg", + ingredients: [ + "2 cloves of garlic (minced)", + "1 onion (diced)", + "2 carrots (sliced)", + "1 green bell pepper (diced)", + "a package of tofu", + ], + }, + { + title: "American pancakes", + cuisine: "american", + diet: "gluten free", + popularity: 2, + time: 20, + img: "./images/pancake.jpg", + ingredients: [ + "2 1/4 dl gluten-free flour mix", + "2 tsp baking powder", + "2 tbsp granulated sugar", + "2 1/2 dl milk", + "30 g butter", + ], + }, +];*/ + +/*//Funktion som körs när användaren klickar på knappen +const getRandomRecipe = () => { + //Slumpar fram ett heltal mellan 0 och antal recept – 1. + const randomIndex = Math.floor(Math.random() * recipes.length); + //Hämtar det slumpade receptet från listan recipes. + const recipe = recipes[randomIndex]; + //Gör om receptet till HTML (med hjälp av renderSingleResult) och visar det i sidan, i elementet cardsEl. + cardsEl.innerHTML = renderSingleResult(recipe); +};*/ diff --git a/images/aprikos.jpg b/images/aprikos.jpg new file mode 100644 index 000000000..265609682 Binary files /dev/null and b/images/aprikos.jpg differ diff --git a/images/chips.jpg b/images/chips.jpg new file mode 100644 index 000000000..f36b64a4c Binary files /dev/null and b/images/chips.jpg differ diff --git a/images/fish.jpg b/images/fish.jpg new file mode 100644 index 000000000..2265ea3f6 Binary files /dev/null and b/images/fish.jpg differ diff --git a/images/food.png b/images/food.png new file mode 100644 index 000000000..dbf862f9a Binary files /dev/null and b/images/food.png differ diff --git a/images/meat.jpg b/images/meat.jpg new file mode 100644 index 000000000..b62be9d36 Binary files /dev/null and b/images/meat.jpg differ diff --git a/images/pancake.jpg b/images/pancake.jpg new file mode 100644 index 000000000..b18dba247 Binary files /dev/null and b/images/pancake.jpg differ diff --git a/images/pizza.jpg b/images/pizza.jpg new file mode 100644 index 000000000..0a05c1819 Binary files /dev/null and b/images/pizza.jpg differ diff --git a/images/tofu.jpg b/images/tofu.jpg new file mode 100644 index 000000000..2a46d77c5 Binary files /dev/null and b/images/tofu.jpg differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..50f2c5b79 --- /dev/null +++ b/index.html @@ -0,0 +1,83 @@ + + +
+ + +
+ No recipes found. Try another filter
`; + return; + } + + // Create an empty string to build the HTML for all recipes + let html = ""; + // Loop through each recipe in the list + list.forEach((r) => { + // Make the first letter of cuisine uppercase (e.g., "italian" → "Italian") + const cuisineText = r.cuisine[0].toUpperCase() + r.cuisine.slice(1); + // Add the cooking time followed by "minutes" + const timeText = r.time + " minutes"; + // Get the diet type (vegan, vegetarian, etc.) + const dietText = r.diet; + // Convert popularity score into stars + const popularText = renderStars(r.popularity); + // Convert the ingredients array into HTML list items (No recipes to randomize. Adjust the filters!
"; + return; + } + + // Pick a random recipe from the list + const randomIndex = Math.floor(Math.random() * list.length); + const recipe = list[randomIndex]; + + // Replace the grid with the single random recipe card + cardsEl.innerHTML = renderSingleResult(recipe); + + // Reveal the "Show all" button so the user can restore the list + showAllButton.style.display = "inline-block"; +}); + +//===================== FETCH RECIPES (WITH SIMPLE CACHE) ====================== +const fetchRecipes = async () => { + cardsEl.innerHTML = "Fetching recipes…
"; + + // 1) Try cache first + const cached = localStorage.getItem(CACHE_KEY); + if (cached) { + const saved = JSON.parse(cached); + const tooOld = Date.now() - saved.timestamp > CACHE_TIME; + + // Use cached recipes and render immediately + if (!tooOld) { + cardsEl.innerHTML = "Showing recipes from cache
"; + recipes.length = 0; + recipes.push(...saved.items); + renderResult(); + return; // stop here if cache is fresh + } + } + + // 2) Fetch via RANDOM endpoint (ensures extendedIngredients are present) + try { + const res = await fetch(RANDOM_URL, { cache: "no-store" }); + if (!res.ok) throw new Error(String(res.status)); + + const json = await res.json(); + const mapped = (json.recipes || []).map(mapApiRecipe); + + // Replace current recipes and render the grid + recipes.length = 0; + recipes.push(...mapped); + renderResult(); + + // 3) Save to localStorage for next time + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ timestamp: Date.now(), items: mapped }) + ); + + //Error messages + } catch (error) { + console.error("Fetch error:", error); + let message; + switch (error.message) { + case "401": + message = "Invalid or missing API key (401).
"; + break; + case "402": + case "429": + message = ` +You have reached your daily API quota or are rate-limited (402/429).
+Try again tomorrow, reduce requests, or use cached data.
+ `; + break; + default: + message = + "Could not fetch recipes right now. Please try again later.
"; + } + cardsEl.innerHTML = message; + } +}; + +//======================= FILTER & SORT EVENT LISTENERS ======================== +// Cuisine filter +document.getElementById("cuisine").addEventListener("change", (e) => { + setCuisine(e.target.value); // uppdaterar state + renderResult(); // rita om listan (eller hämta från API om du vill) +}); + +// Diet filter +document.getElementById("diet").addEventListener("change", (e) => { + setDiet(e.target.value); + renderResult(); +}); + +// Sort by time (asc/desc) +document.getElementById("sortTime").addEventListener("change", (e) => { + selectedSortType = "time"; + setSort(e.target.value); // "asc" eller "desc" +}); + +// Sort by popularity (most/least) +document.getElementById("sortPop").addEventListener("change", (e) => { + selectedSortType = "popular"; + setPopular(e.target.value); // "most" eller "least" +}); +document.getElementById("btn-show-all").addEventListener("click", () => { + renderResult(); +}); +document.getElementById("search").addEventListener("input", (e) => { + searchQuery = e.target.value; // uppdatera state + renderResult(); // rita om listan +}); + +//=========================== SHOW-ALL BUTTON HANDLER ========================== +showAllButton.addEventListener("click", () => { + // Re-render the full filtered/sorted grid + renderResult(); + + // Hide the button again + showAllButton.style.display = "none"; +}); + +//============================== INITIALIZATION ================================ +document.addEventListener("DOMContentLoaded", () => { + cuisineSelect.value = selectedCuisine; // "all" + dietSelect.value = selectedDiet; // "diet-all" + sortTimeSelect.value = selectedSort; // "asc" + sortPopSelect.value = selectedPopular; // "most" + + fetchRecipes(); +}); diff --git a/style.css b/style.css new file mode 100644 index 000000000..bd0f33e61 --- /dev/null +++ b/style.css @@ -0,0 +1,449 @@ +/* =========================== + Base + =========================== */ +* { + box-sizing: border-box; +} +body { + font-family: "Montserrat", Arial, sans-serif; + margin: 0; + padding-bottom: 30px; + padding-top: 240px; +} + +/* =========================== + Headings / Typography + =========================== */ +h1 { + color: #fff; + font-size: 20px; + text-align: center; + font-weight: 700; + text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, + 1px 1px 0 #000; +} +h2 { + font-size: 14px; + line-height: 100%; + letter-spacing: 0%; + text-align: center; + color: black; +} + +.search-area h2 { + color: white; + text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, + 1px 1px 0 #000; +} + +p { + font-size: 12px; + line-height: 100%; + letter-spacing: 0%; + font-weight: 400; +} + +/* =========================== + Top Navigation + =========================== */ +.top-nav { + position: relative; + background: linear-gradient(135deg, #ffcc33, #ff9933, #ff6600); + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + width: 100%; + height: 240px; + left: 0; + top: 0; + position: fixed; + z-index: 1000; + overflow: hidden; + border-radius: 0 0 10px 10px; +} + +.top-nav img.food { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center center; + z-index: 0; + opacity: 0.9; + opacity: 0.85; +} + +.top-nav h1, +.top-nav h2, +.top-nav .search { + position: relative; + z-index: 1; +} + +.top-nav h1 { + margin-top: 80px; +} + +.top-nav h2 { + margin: 0 0 6px 0; +} + +/* =========================== + Filters / Controls + =========================== */ +.search { + border: 2px solid white; + border-radius: 10px; + padding: 6px 10px; + width: 250px; +} + +.kitchen, +.time, +.diet, +.popular, +.random, +.show-all { + width: 250px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); + border-radius: 10px; + border: 2px solid transparent; + box-sizing: border-box; + padding: 10px 40px 10px 15px; + font-size: 13px; + font-family: "Montserrat", sans-serif; + color: #333; + outline: none; + background-repeat: no-repeat; + background-position: right 10px center; +} + +.kitchen, +.time, +.diet, +.popular { + appearance: none; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='gray'%20viewBox='0%200%2016%2016'%3E%3Cpath%20d='M1.5%205.5l6%206%206-6'/%3E%3C/svg%3E"); /* liten pil */ +} + +.kitchen { + background-color: #ccffe2; +} + +.time { + background-color: #ffecea; +} + +.diet { + background-color: lightsalmon; +} +.popular { + background-color: lightskyblue; +} +.random { + background-color: aquamarine; +} + +.show-all { + background-color: plum; +} + +.filters { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 10px; + margin-bottom: 10px; +} + +.filters h2 { + margin-bottom: 10px; +} + +/* =========================== + Layout / Sections + =========================== */ + +section { + display: flex; + flex-direction: row; + gap: 88px; + padding-left: 10px; +} + +.recipes-boxes { + display: grid; + grid-template-columns: 1fr; + justify-items: center; + align-items: start; + gap: 15px; + margin: 0 auto; +} + +/* =========================== + Recipe Card + =========================== */ + +.recipe { + display: flex; + flex-direction: column; + justify-content: flex-start; + border: 2px solid lightgrey; + box-sizing: border-box; + border-radius: 10px; + width: 250px; + align-items: flex-start; + overflow: hidden; + padding: 10px; + box-sizing: border-box; + height: auto; +} + +.image-wrapper { + width: 100%; + margin-bottom: 8px; +} +img { + width: 100%; + height: 200px; + object-fit: cover; + border-radius: 10px; + display: block; +} + +.meta { + width: 100%; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 18px 0; + margin: 0px 0; +} + +.meta p { + margin: 4px 0; +} +/* =========================== + Lists + =========================== */ +ul { + margin: 0px; + padding-left: 0px; + list-style: none; + font-size: 12px; +} + +/* ===== Tablet (>= 768px) ===== */ +@media (min-width: 768px) { + .top-nav { + position: relative; + height: 300px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 16px 24px; + border-radius: 10px; + width: 100%; + max-width: 100%; + overflow: hidden; + margin-bottom: 30px; + } + + .top-nav .search-area { + transform: translateY(-30px); + } + + body { + padding: 0; + } + + .search { + margin-bottom: 10px; + } + + .kitchen, + .diet, + .time, + .popular, + .random, + .show-all { + font-size: 14px; + margin-bottom: 10px; + } + + .recipes-boxes { + grid-template-columns: repeat(2, 1fr); + padding-top: 20px; + } + + .recipe { + width: 350px; + height: auto; + } + + h1 { + padding: 10px; + margin-top: 20px; + margin-bottom: 0; + font-size: 48px; + text-align: center; + } + + h2 { + font-size: 18px; + } + + h4 { + font-size: 16px; + } + + p { + font-size: 14px; + } + + .search-area h2 { + font-size: 22px; + } + + .filters { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + gap: 24px; + padding-left: 0; + margin-bottom: 10px; + text-align: center; + } + + .random { + order: 1; + } + + .show-all { + order: 2; + flex-basis: 70%; + } +} + +/* ===== Desktop (>= 1024px) ===== */ +@media (min-width: 1024px) { + .top-nav { + max-width: 1400px; + margin: 0 auto; + padding: 20px 20px; + margin-bottom: 30px; + height: clamp(300px, 34vw, 460px); + } + + .top-nav .search-area { + transform: translateY(-90px); + } + + .top-nav h1 { + margin-top: 80px; + margin-bottom: 0; + font-size: 64px; + } + + body { + padding: 0 0 0 30px; + } + + .kitchen, + .diet, + .time, + .popular, + .random, + .show-all { + cursor: pointer; + font-size: 16px; + margin-bottom: 30px; + } + + h2 { + font-size: 17px; + } + + h4 { + font-size: 15px; + } + + p { + font-size: 14px; + } + + .recipes-boxes { + grid-template-columns: repeat(4, 1fr); + justify-items: start; + justify-content: center; + margin: 0 auto; + gap: 20px; + } + + .recipe { + width: 250px; + height: auto; + } + + .filters { + flex-direction: row; + align-items: center; + text-align: left; + gap: 40px; + margin-bottom: 20px; + } + + .kitchen:hover { + border-color: rgba(0, 24, 164, 1); + } + + .time:hover { + border-color: rgba(0, 24, 164, 1); + background-color: #ff6589; + color: #fff; + } + + .diet:hover { + border-color: rgb(162, 53, 10); + } + + .popular:hover { + border-color: rgb(70, 203, 50); + } + + .show-all:hover { + border-color: rgba(0, 24, 164, 1); + } + + .random:hover { + border-color: rgb(162, 53, 10); + } + + .recipe:hover { + border: 2px solid rgba(0, 24, 164, 1); + box-shadow: 0 0 30px 0 rgba(0, 24, 164, 0.2); + } + + .diet.selected { + background-color: rgb(162, 53, 10); + color: #fff; + } + + .time.selected { + background-color: rgba(255, 101, 137, 1); + color: #fff; + } + + .popular.selected { + background-color: rgb(38, 104, 37); + color: #fff; + } + + .kitchen.selected { + background-color: rgb(17, 13, 238); + color: #fff; + } +}