Skip to content

Conversation

@qabalany
Copy link

Recipe Library - Complete

https://recipelibrarya.netlify.app
screenshot

✅ Requirements Met

  • Responsive HTML/CSS design (Week 5)
  • Dynamic JavaScript functionality (Week 6)
  • API integration with localStorage caching (Week 7)

✅ Stretch Goals Completed

  • Search functionality with debouncing
  • Favorites system with localStorage persistence
  • Pagination (15 recipes per page)
  • Multiple filters (diet, cuisine, sorting)
  • Loading states with UX enhancements
  • Smart fallback system (API → cache → backup data)

Ready for review! 🎯

Copy link

@Nicolinabl Nicolinabl left a comment

Choose a reason for hiding this comment

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

Great work!
I think you have done a very thorough job with your web site. I like that you had a bit of creative freedom with the design and layout. I especially like the use of emojis here and there for clarity and that you have a clear all button, good idea.

I see that you have put a lot of effort into your javascript file, i dont understand all of the code but i understand what it is supposed to do thanks to all of your structuring and explaining with comments.

- **Live App:** https://recipelibrarya.netlify.app
- **Screenshot:**

![Recipe Library Screenshot](/screenshot/screenshot.png)

Choose a reason for hiding this comment

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

Nice idea to add a screen shot of the finished result in readme file

<body>
<!-- Main Content -->
<main class="main-content">
<div class="container">

Choose a reason for hiding this comment

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

Suggestion: class name "container" could be more specific, so it is easier to see what it contains without having to read through all the code

<h1 class="section-title">Recipe Library</h1>

<!-- Search Bar -->
<div class="search-container">

Choose a reason for hiding this comment

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

Nice and clear class name

type="text"
id="search-input"
class="search-input"
placeholder="🔍 Search recipes by title or ingredients..."

Choose a reason for hiding this comment

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

I like that you also put the emoji in the placeholder and not only text

<div class="dropdown-filters-row">
<!-- Diet Filter -->
<div class="filter-group">
<label class="filter-title" for="diet-filter">Dietary Preferences</label>

Choose a reason for hiding this comment

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

Great that you have used labels, for accessibility

}
};

// Global recipe storage

Choose a reason for hiding this comment

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

Great idea to clarify parts that are in global scope

Comment on lines +40 to +45
const clearFiltersBtn = document.getElementById('clear-filters-btn');
const randomRecipeBtn = document.getElementById('random-recipe-btn');
const favoritesBtn = document.getElementById('favorites-btn');
const recipeContainer = document.getElementById('recipe-container');
const messageDisplay = document.getElementById('message-display');
const searchInput = document.getElementById('search-input');

Choose a reason for hiding this comment

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

Nice that you change the names in js from kebab-case to camelCase

Comment on lines +149 to +156
function extractDiets(spoonacularRecipe) {
const diets = [];
if (spoonacularRecipe.vegetarian) diets.push('vegetarian');
if (spoonacularRecipe.vegan) diets.push('vegan');
if (spoonacularRecipe.glutenFree) diets.push('gluten-free');
if (spoonacularRecipe.dairyFree) diets.push('dairy-free');
return diets;
}

Choose a reason for hiding this comment

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

Nice and clean function!

}

/**
* Extract ingredient list from recipe

Choose a reason for hiding this comment

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

I like that you write a comment explaining what every function does

* Clear all filter selections and reset the display
* Like clearAllFiltersQuiet but also shows all recipes again
*/
function clearAllFilters() {

Choose a reason for hiding this comment

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

Really like that you implemented a button to clear all chosen filters. I think that is good for user experience

@JennieDalgren JennieDalgren self-assigned this Oct 26, 2025
Copy link

@JennieDalgren JennieDalgren left a comment

Choose a reason for hiding this comment

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

Great job with your project!
Your code is well structured and well documented. I really like that you put some effort into the Readme.
Great job explaining your code in the comments, some times I feel it might be slioghtly too much comments. You are using good naming convetions in your functions and variables so it might be explanatory enough some times to skip the comments, or keep them shorter. It is totally fine, also for your own sake to keep a lot of explanation in your code. However, when you're working on real production code I would suggest to trim it down a bit.

You have implemented all requreiements and stretch goals and you site is responsive. I like that you have added a little counter to tell how many favorites I have or how many vegetarian recipes there is for a specific filter. Also YAY for implementing pagination.

It seems like you had a lot of fun building this project and you really pushed yourself! Keep up the good work 🚀

Comment on lines +208 to +222
function mapSpoonacularToLocal(spoonacularRecipe) {
return {
id: spoonacularRecipe.id,
title: spoonacularRecipe.title,
image: spoonacularRecipe.image,
readyInMinutes: spoonacularRecipe.readyInMinutes || 30,
servings: spoonacularRecipe.servings || 4,
sourceUrl: spoonacularRecipe.sourceUrl || spoonacularRecipe.spoonacularSourceUrl,
diets: extractDiets(spoonacularRecipe),
cuisine: extractCuisine(spoonacularRecipe),
ingredients: extractIngredients(spoonacularRecipe),
pricePerServing: calculatePrice(spoonacularRecipe),
popularity: calculatePopularity(spoonacularRecipe)
};
}

Choose a reason for hiding this comment

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

clever to create a function to organise your data as you want it

Comment on lines +55 to +70
function getMergedRecipes() {
const cachedRecipes = getCachedRecipes() || [];
const mappedBackup = backupData.recipes.map(mapSpoonacularToLocal);

// Merge both sources, removing duplicates by ID
const combinedRecipes = [...cachedRecipes];
const existingIds = new Set(cachedRecipes.map(r => r.id));

mappedBackup.forEach(recipe => {
if (!existingIds.has(recipe.id)) {
combinedRecipes.push(recipe);
}
});

return combinedRecipes;
}

Choose a reason for hiding this comment

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

I get what you're doing here, and nice job figuring that out. However, is it necessary?

Comment on lines +293 to +302
function toggleFavorite(recipeId) {
const index = favoriteRecipes.indexOf(recipeId);

if (index > -1) {
favoriteRecipes.splice(index, 1);
showMessage('❤️ Removed from favorites', 'success');
} else {
favoriteRecipes.push(recipeId);
showMessage('💖 Added to favorites', 'success');
}

Choose a reason for hiding this comment

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

nice!

Comment on lines +228 to +240
function setCachedRecipes(recipesToCache) {
try {
const cacheData = {
recipes: recipesToCache,
timestamp: Date.now(),
expires: Date.now() + (24 * 60 * 60 * 1000)
};
localStorage.setItem('spoonacular_recipes', JSON.stringify(cacheData));
console.log(`✅ Cached ${recipesToCache.length} recipes`);
} catch (error) {
console.warn('⚠️ Failed to cache recipes:', error);
}
}

Choose a reason for hiding this comment

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

great job with implementing local storage and also set a time limit for the cache

Comment on lines +518 to +532
function displayPage(recipes, pageNum = 1) {
if (recipes.length === 0) {
displayEmptyState();
return;
}

const totalPages = Math.ceil(recipes.length / recipesPerPage);
currentPage = Math.max(1, Math.min(pageNum, totalPages));

const startIndex = (currentPage - 1) * recipesPerPage;
const endIndex = startIndex + recipesPerPage;
const pageRecipes = recipes.slice(startIndex, endIndex);

const recipeCards = pageRecipes.map(r => createRecipeCardHTML(r)).join('');

Choose a reason for hiding this comment

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

Yay! First project I review that implemented pagination. Well done ⭐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants