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 @@ + + + + + + Recipe Library + + + + +
+ aprikos +

Recipe Library

+ +
+

Search recipes

+ +
+
+ +
+
+

Filter on kitchen

+ +
+ +
+

Filter on Diets

+ +
+ +
+

Sort on time

+ +
+ +
+

Sort by popularity

+ +
+
+

Random

+ +
+ +
+ +
+
+
+ + + diff --git a/script.js b/script.js new file mode 100644 index 000000000..e8c3aa60d --- /dev/null +++ b/script.js @@ -0,0 +1,339 @@ +//============================= CONSTANTS & DOM REFS ============================= +const cardsEl = document.getElementById("cards"); +const showAllButton = document.getElementById("btn-show-all"); +const cuisineSelect = document.getElementById("cuisine"); +const dietSelect = document.getElementById("diet"); +const sortTimeSelect = document.getElementById("sortTime"); +const sortPopSelect = document.getElementById("sortPop"); +const searchInput = document.getElementById("search"); + +const renderStars = (num) => "⭐".repeat(num); + +const CACHE_KEY = "recipesCache"; // localStorage key +const CACHE_TIME = 6 * 60 * 60 * 1000; // 6 hours in ms + +const API_KEY = "023721190a824bc5b967171438a1f9af"; +const RANDOM_URL = `https://api.spoonacular.com/recipes/random?number=10&apiKey=${API_KEY}`; + +//===========================CREATING VARIABLES=====================================// +// Using "let" instead of "const" below because these values will change dynamically +// when the user interacts with the filters, sorting, or search bar. +let selectedCuisine = "all"; +let selectedSort = "asc"; +let selectedDiet = "diet-all"; +let selectedPopular = "most"; +let selectedSortType = "time"; +let searchQuery = ""; + +// Create an empty array that will later be filled with recipe data from the API. +const recipes = []; + +// ======================== FUNCTIONS TO UPDATE FILTER VALUES ======================== // +// These functions are used when the user changes filter options. +// They update the global variables and re-render the recipe list on the page. + +// Updates the selected cuisine type +const setCuisine = (c) => { + selectedCuisine = c; // Save the chosen cuisine to the variable + renderResult(); // Re-render the recipes with the updated filter +}; + +// Updates how the list should be sorted (ascending or descending) +const setSort = (s) => { + selectedSort = s; //sparar valet + renderResult(); +}; + +// Updates which diet filter is active. +const setDiet = (d) => { + selectedDiet = d; + renderResult(); +}; + +// Updates which popularity filter is active. +const setPopular = (p) => { + selectedPopular = p; + renderResult(); +}; + +// ========================= GET THE CURRENT RECIPE LIST (FILTER + SEARCH + SORT) ========================= +// Returns a fresh array of recipes based on the user's current selections: + +const getCurrentList = () => { + // Start with a working list we can modify without touching the original array. + // If "all" is selected, clone the full array; otherwise filter by cuisine. + let list; + if (selectedCuisine === "all") { + list = recipes.slice(); + } else { + list = recipes.filter((r) => r.cuisine === selectedCuisine); + } + + // Apply diet filter if the user selected a specific diet. + if (selectedDiet !== "diet-all") { + list = list.filter((r) => { + const d = (r.diet || "").toLowerCase(); + return d === selectedDiet; + }); + } + + // Apply free-text search over title and ingredients (case-insensitive). + const query = searchQuery.trim().toLowerCase(); + if (query) { + list = list.filter((r) => { + const inTitle = (r.title || "").toLowerCase().includes(query); + const inIngredients = (r.ingredients || []).some((i) => + (i || "").toLowerCase().includes(query) + ); + return inTitle || inIngredients; + }); + } + + // Sort the list based on the selected sort type and order. + if (selectedSortType === "time") { + // Sort by time (ascending = fastest first, descending = slowest first) + if (selectedSort === "asc") { + list.sort((a, b) => a.time - b.time); + } else { + list.sort((a, b) => b.time - a.time); + } + } else if (selectedSortType === "popular") { + // Sort by popularity (most/least) + if (selectedPopular === "most") { + list.sort((a, b) => b.popularity - a.popularity); + } else { + list.sort((a, b) => a.popularity - b.popularity); + } + } + // Return the final filtered/sorted array to be rendered. + return list; +}; + +//================== FUNCTION TO DISPLAY RECIPES ON THE PAGE ================== +const renderResult = () => { + // Get the current list of recipes based on filters and sorting + const list = getCurrentList(); + + // If the list is empty, show a message and stop the function + if (list.length === 0) { + cardsEl.innerHTML = `

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 (
  • ) + const ingHtml = r.ingredients.map((i) => `
  • ${i}
  • `).join(""); //Här görs igridienslistan om från array till html + + // Build the HTML structure for a single recipe card + html += ` +
    + ${r.title} +

    ${r.title}

    +
    +

    Cuisine: ${cuisineText}

    +

    Diet: ${dietText}

    +

    Popularity:${popularText}

    +

    Time: ${timeText}

    +
    +

    Ingredients

    + +
    + `; + }); + + // Insert all built HTML into the page + cardsEl.innerHTML = html; +}; + +//========================= RENDER A SINGLE RECIPE CARD ========================= +const renderSingleResult = (r) => { + // Make cuisine start with uppercase (e.g., "italian" -> "Italian") + const cuisineText = r.cuisine[0].toUpperCase() + r.cuisine.slice(1); + + // Build small display strings + const timeText = r.time + " minutes"; //konstanta variabler skapas.. En visningstext för minuter + const dietText = r.diet || "-"; // om diet finns skrivs den ut annars "-" + const popularText = renderStars(r.popularity); + + // Turn the ingredients array into
  • items + const ingHtml = r.ingredients.map((i) => `
  • ${i}
  • `).join(""); //Här görs igridienslistan om från array till html + + // Return one complete recipe card HTML + return ` +
    + ${r.title} +

    ${r.title}

    +
    +

    Cuisine: ${cuisineText}

    +

    Diet: ${dietText}

    +

    Popularity:${popularText}

    +

    Time: ${timeText}

    +
    +

    Ingredients

    + +
    + `; +}; + +//===================== MAP API RECIPE TO INTERNAL SHAPE ======================= +const mapApiRecipe = (r) => ({ + // Title with fallback + title: r.title || "Untitled", + cuisine: (r.cuisines?.[0] || "unknown").toLowerCase(), + diet: (r.diets?.[0] || "none").toLowerCase(), + popularity: Math.max( + 1, + Math.min(5, Math.round((r.aggregateLikes || 0) / 100)) + ), + time: r.readyInMinutes || 0, + img: r.image || "https://via.placeholder.com/600x400?text=No+image", + ingredients: (r.extendedIngredients || []).map( + (i) => i.original || i.name || "" + ), +}); + +//=========================== RANDOM BUTTON HANDLER ============================ +const randomButton = document.getElementById("btn-random"); + +randomButton.addEventListener("click", () => { + // Get the currently filtered/sorted list + const list = getCurrentList(); + if (!list.length) { + cardsEl.innerHTML = "

    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; + } +}