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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# js-project-recipe-library
Basic structure HTML, Style in CSS, Add functionalities in js.

https://mysecondprojectlibrary.netlify.app/

Week 1
Start to build a Recipe Library
HTML structure:
Input fields for filters and sorting options
A placeholder recipe card
Writing JavaScript functions to handle user selections
Sorting options

Week 2
Arrays, object and loops to show recipes
using selections, be able to display all of the recioes, feature a button that selects a random recipe, hav an empty state, responsive from 32npx to 1600px

Week 3
Fetch real recipe data from Spoonacular's API
Display dynamic recipe cards based on the API data
Adapt filtering & sorting to match the API response format
Show a useful message to the user in case the daily quota has been reached

The goal was to practice working with arrays, objects, and functions in JavaScript and to fetch and display real data from an external API
Styled by using CSS variables for what I call Technigo colors (blue, pink, and aqua) to make it look as much as the Figma demo.
Comment on lines +2 to +25

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gillar din planering, har inte använt readme filen så mycket, ska nog börja göra det. för denna planering gör det väldigt tydligt!

92 changes: 92 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Recipe Library</title>
<link rel="stylesheet" href="styles.css" />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
</head>

<body>
<div class="wrapper">
<h1 class="page-title">Recipe Library</h1>

<!-- Controls -->
<section class="controls">

<!-- Kitchen filters -->

<div class="control-group">
<h2 class="control-title">Kitchen</h2>
<div class="pill-group">
<button class="pill active" data-kitchen="all">All</button>
<button class="pill" data-kitchen="Italian">Italian</button>
<button class="pill" data-kitchen="Asian">Asian</button>
<button class="pill" data-kitchen="Middle Eastern">Middle Eastern</button>
<button class="pill" data-kitchen="American">American</button>
</div>
</div>

<!-- Diet filters -->
<div class="control-group">
<h2 class="control-title">Diets</h2>
<div class="pill-group">
<button class="pill active" data-diet="all">All</button>
<button class="pill" data-diet="vegan">Vegan</button>
<button class="pill" data-diet="vegetarian">Vegetarian</button>
<button class="pill" data-diet="gluten free">Gluten-free</button>
<button class="pill" data-diet="dairy free">Dairy-free</button>
</div>
</div>


<!-- Cooking time filters -->
<div class="control-group">
<h2 class="control-title">Cooking time</h2>
<div class="pill-group">
<button class="pill active" data-time="all">All</button>
<button class="pill" data-time="under15">Under 15 min</button>
<button class="pill" data-time="15to30">15–30 min</button>
<button class="pill" data-time="30to60">30–60 min</button>
<button class="pill" data-time="over60">Over 60 min</button>
</div>
</div>

<!-- Ingredient filters -->
<div class="control-group">
<h2 class="control-title">Amount of ingredients</h2>
<div class="pill-group">
<button class="pill active" data-ingredients="all">All</button>
<button class="pill" data-ingredients="under5">Under 5</button>
<button class="pill" data-ingredients="6to10">6–10</button>
<button class="pill" data-ingredients="11to15">11–15</button>
<button class="pill" data-ingredients="over16">Over 16</button>
</div>
</div>

<!-- Sorting options + Random button -->
<div class="control-group sorting-row">
<h2 class="control-title">Sorting options</h2>

<div class="pill-group sort-row">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undrar över att det finns två classes, finns det en anledning till det?


<button class="pill" data-sort="popularity">By popularity</button>
<button class="pill" data-sort="ingredients">By ingredients</button>
<button id="random-btn" class="pill pink-btn">Random Recipe</button>
</div>
</div>



<!-- Message output -->
<p id="msg" class="note"></p>

<!-- Recipe cards -->
<main id="recipes" class="recipes"></main>
</div>

<script src="script.js"></script>
</body>
</html>

161 changes: 161 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// --- CONNECTING JS WITH HTML ---
const recipesEl = document.getElementById("recipes");
const msgEl = document.getElementById("msg");
const pills = document.querySelectorAll(".pill");
const randomBtn = document.getElementById("random-btn");

// --- API SETUP ---
const API_KEY = "2065aff4499d4fe29bdfbad342732432";
const BASE_URL = "https://api.spoonacular.com/recipes/complexSearch";

// --- STATE ---
let recipes = [];
let filters = {
kitchen: "all",
diet: "all",
time: "all",
ingredients: "all",
sort: "popularity",
};

// --- FETCH DATA FUNCTION ---
async function fetchData() {
recipesEl.innerHTML = `<p>Loading recipes...</p>`;
msgEl.textContent = "Fetching data...";

// Build query
let url = `${BASE_URL}?number=12&addRecipeInformation=true&apiKey=${API_KEY}`;

if (filters.kitchen !== "all") url += `&cuisine=${filters.kitchen}`;
if (filters.diet !== "all") url += `&diet=${filters.diet}`;

try {
const res = await fetch(url);

if (res.status === 402) {
recipesEl.innerHTML = `<p>Daily API quota exceeded. Please try again tomorrow.</p>`;
msgEl.textContent = "API limit reached.";
throw new Error("Quota reached");
}

const data = await res.json();
console.log("Fetched data:", data);

if (!data.results || data.results.length === 0) {
recipesEl.innerHTML = `<p>No recipes found for your selected filters.</p>`;
msgEl.textContent = "No results found.";
return;
}

// Normalize data
recipes = data.results.map(recipe => ({
id: recipe.id,
title: recipe.title || "Unknown title",
image: recipe.image || "https://via.placeholder.com/300x200?text=No+Image",
kitchen:
recipe.cuisines && recipe.cuisines.length > 0
? recipe.cuisines[0]
: "Various",
diet:
recipe.diets && recipe.diets.length > 0
? recipe.diets[0]
: "General",
time: recipe.readyInMinutes || 0,
ingredients: recipe.extendedIngredients
? recipe.extendedIngredients.length
: 0,
popularity: recipe.aggregateLikes || 0,
}));

showRecipes(recipes);
msgEl.textContent = `Fetched ${recipes.length} recipes (${filters.kitchen}, ${filters.diet})`;
} catch (err) {
console.error("Fetch error:", err);
recipesEl.innerHTML = `<p>Something went wrong. Please try again later.</p>`;
msgEl.textContent = "Failed to load recipes.";
}
}

// --- FILTER FUNCTION ---
const filterRecipes = list =>
list.filter(r => {
// Time filters
if (filters.time === "under15" && !(r.time < 15)) return false;
if (filters.time === "15to30" && !(r.time >= 15 && r.time <= 30)) return false;
if (filters.time === "30to60" && !(r.time >= 30 && r.time <= 60)) return false;
if (filters.time === "over60" && !(r.time > 60)) return false;

// Ingredient filters
if (filters.ingredients === "under5" && !(r.ingredients < 5)) return false;
if (filters.ingredients === "6to10" && !(r.ingredients >= 6 && r.ingredients <= 10)) return false;
if (filters.ingredients === "11to15" && !(r.ingredients >= 11 && r.ingredients <= 15)) return false;
if (filters.ingredients === "over16" && !(r.ingredients > 16)) return false;

return true;
});

// --- SORT FUNCTION ---
const sortRecipes = list => {
const key = filters.sort;
const sorted = [...list];
sorted.sort((a, b) => b[key] - a[key]); // highest first
return sorted;
};

// --- SHOW RECIPES ---
const showRecipes = list => {
const filtered = filterRecipes(list);
const sorted = sortRecipes(filtered);

recipesEl.innerHTML = sorted.length
? sorted
.map(
r => `
<div class="card">
<img src="${r.image}" alt="${r.title}" class="card-img">
<div class="card-body">
<h3>${r.title}</h3>
<p><strong>Cuisine:</strong> ${r.kitchen}</p>
<p><strong>Diet:</strong> ${r.diet}</p>
<p><strong>Time:</strong> ${r.time} min</p>
<p><strong>Ingredients:</strong> ${r.ingredients}</p>
<p><strong>Popularity:</strong> ${r.popularity}</p>
</div>
</div>`
)
.join("")
: `<p>No recipes match your filters.</p>`;

msgEl.textContent = `Results: ${sorted.length} | Kitchen: ${filters.kitchen} | Diet: ${filters.diet} | Sort: ${filters.sort}`;
};

// --- EVENT LISTENERS ---
pills.forEach(btn => {
btn.addEventListener("click", () => {
const group = btn.parentElement.querySelectorAll(".pill");
group.forEach(b => b.classList.remove("active"));
btn.classList.add("active");

if (btn.dataset.kitchen) filters.kitchen = btn.dataset.kitchen;
if (btn.dataset.diet) filters.diet = btn.dataset.diet;
if (btn.dataset.time) filters.time = btn.dataset.time;
if (btn.dataset.ingredients) filters.ingredients = btn.dataset.ingredients;
if (btn.dataset.sort) filters.sort = btn.dataset.sort;

if (btn.dataset.kitchen || btn.dataset.diet) {
fetchData(); // refetch from API
} else {
showRecipes(recipes); // filter locally
}
});
});

// --- RANDOM RECIPE BUTTON ---
randomBtn.addEventListener("click", () => {
const random = recipes[Math.floor(Math.random() * recipes.length)];
showRecipes([random]);
});

// --- INITIAL FETCH ---
fetchData();

Loading