diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..aef844305
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.port": 5501
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 58f1a8a66..2b9a0cb8f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
# js-project-recipe-library
+
+netlify link: https://carolinas-recipe-library.netlify.app/
diff --git a/img/French-toast.jpg b/img/French-toast.jpg
new file mode 100644
index 000000000..60879608e
Binary files /dev/null and b/img/French-toast.jpg differ
diff --git a/img/beef-stew.jpg b/img/beef-stew.jpg
new file mode 100644
index 000000000..62f667c70
Binary files /dev/null and b/img/beef-stew.jpg differ
diff --git a/img/lentil-soup.jpg b/img/lentil-soup.jpg
new file mode 100644
index 000000000..86c2e0e49
Binary files /dev/null and b/img/lentil-soup.jpg differ
diff --git a/img/pasta-pesto.jpg b/img/pasta-pesto.jpg
new file mode 100644
index 000000000..7e0bd362c
Binary files /dev/null and b/img/pasta-pesto.jpg differ
diff --git a/index.html b/index.html
new file mode 100644
index 000000000..21b47f36d
--- /dev/null
+++ b/index.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+ Carolinas recipe library
+
+
+
+ Recipe library
+
+
+
+
Meals
+
+
+
+
+
+
+
+
+
+
+
Cooking time
+
+
+
+
+
+
+
+
Random recipe
+
+
+
+
+
Search for recipes:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/script.js b/script.js
new file mode 100644
index 000000000..46bfa4813
--- /dev/null
+++ b/script.js
@@ -0,0 +1,215 @@
+// ====== DOM SELECTORS ======
+const btnAll = document.getElementById("btnAll")
+const btnBreakfast = document.getElementById("btnBreakfast")
+const btnLunch = document.getElementById("btnLunch")
+const btnFika = document.getElementById("btnFika")
+const btnDinner = document.getElementById("btnDinner")
+const btnAscending = document.getElementById("btnAscending")
+const btnDescending = document.getElementById("btnDescending")
+const mealBtns = document.querySelectorAll(".meal-btn")
+const timeBtns = document.querySelectorAll(".time-btn")
+const randomBtn = document.getElementById("btnRandom")
+const recipeContainer = document.getElementById("recipeContainer")
+const noResultsContainer = document.getElementById("noResultsContainer")
+const loading = document.getElementById("loading")
+const searchInput = document.getElementById("searchBar")
+const searchBtn = document.getElementById("searchBtn")
+
+// ====== API SETTINGS ======
+const URL = `https://api.spoonacular.com/recipes/random?number=15&apiKey=4d3e2b2a43464de48c4a0aac3abf2c52`
+
+// ====== GLOBAL STATE ======
+let recipeInfo = []
+let activeFilters = {
+ mealType: "All",
+ sortOrder: null
+}
+
+// ====== DISPLAY FUNCTIONS ======
+const showRecipes = (recipesToShow) => {
+ recipeContainer.innerHTML = ``
+
+ recipesToShow.forEach(recipe => {
+ recipeContainer.innerHTML += `
+
+

+
${recipe.title}
+
+
+ Meal:
+ ${recipe.dishTypes?.join(", ") || "N/A"}
+ Cooking time:
+ ${recipe.readyInMinutes} min
+
+
+
Ingredients
+
${recipe.extendedIngredients
+ ? recipe.extendedIngredients.map(ing => `${ing.name}`).join("
")
+ : "No ingredients listed"
+ }
+
`
+ })
+}
+
+// ====== FILTERING & SORTING ======
+const filterAndSorting = () => {
+ let filtered = [...recipeInfo]
+
+ // Filter by meal type
+ if (activeFilters.mealType && activeFilters.mealType !== "All") {
+ filtered = filtered.filter(recipe => recipe.dishTypes?.includes(activeFilters.mealType))
+ }
+
+ // Sort by time (ascending or descending)
+ if (activeFilters.sortOrder === "asc") {
+ filtered.sort((a, b) => a.readyInMinutes - b.readyInMinutes)
+ } else if (activeFilters.sortOrder === "desc") {
+ filtered.sort((a, b) => b.readyInMinutes - a.readyInMinutes)
+ }
+
+ // Display filtered results or show a message if none found
+ if (filtered.length > 0) {
+ noResultsContainer.innerHTML = ``
+ showRecipes(filtered)
+ } else {
+ recipeContainer.innerHTML = ``
+ noResultsContainer.innerHTML = `
+
+
No recipes found for "${activeFilters.mealType}"
+
Try another filter
+
`
+ }
+}
+
+// ====== SEARCH ======
+const searchRecipes = () => {
+ const query = searchInput.value.trim().toLowerCase()
+ if (!query) {
+ filterAndSorting()
+ return
+ }
+
+ let filtered = [...recipeInfo]
+
+ if (activeFilters.mealType && activeFilters.mealType !== "All") {
+ filtered = filtered.filter(recipe =>
+ recipe.dishTypes?.includes(activeFilters.mealType)
+ )
+ }
+
+ filtered = filtered.filter(recipe => {
+ const titleMatch = recipe.title.toLowerCase().includes(query)
+ const ingredientsMatch = recipe.extendedIngredients?.some(ing =>
+ ing.name.toLowerCase().includes(query)
+ )
+ return titleMatch || ingredientsMatch
+ })
+
+ if (filtered.length > 0) {
+ noResultsContainer.innerHTML = ``
+ showRecipes(filtered)
+ } else {
+ recipeContainer.innerHTML = ``
+ noResultsContainer.innerHTML = `
+
+
No recipes found matching "${query}"
+
Try another search term or combination of filters.
+
`
+ }
+}
+
+// ====== RANDOM RECIPE FEATURE ======
+const getRandomRecipe = () => {
+ const randomIndex = Math.floor(Math.random() * recipeInfo.length)
+ const randomRecipe = recipeInfo[randomIndex]
+ showRecipes([randomRecipe])
+}
+
+// ====== DATA FETCHING (with caching + error handling) ======
+const fetchData = async (URL) => {
+ loading.style.display = "block"
+ recipeContainer.innerHTML = ``
+ noResultsContainer.innerHTML = ``
+
+ try {
+ const cachedData = localStorage.getItem("cachedRecipes")
+ const cacheTime = localStorage.getItem("cacheTime")
+
+ const now = Date.now()
+ const sixHours = 6 * 60 * 60 * 1000 // cache duration
+
+ // Use cache if still valid
+ if (cachedData && cacheTime && now - cacheTime < sixHours) {
+ recipeInfo = JSON.parse(cachedData)
+ loading.style.display = "none"
+ filterAndSorting()
+ } else {
+ // Otherwise, fetch new data
+ const response = await fetch(URL)
+
+ // Handle API quota errors
+ if (!response.ok) {
+ if (response.status === 402 || response.status === 429) {
+ loading.style.display = "none"
+ recipeContainer.innerHTML = ``
+ noResultsContainer.innerHTML = `
+
+
Daily API quota reached
+
Spoonacular limits the number of requests per day.
+ Please try again later or use cached results if available.
+
`
+ return
+ } else {
+ throw new Error(`HTTP error ${response ? response.status : "unknown"}`)
+ }
+ }
+
+ const data = await response.json()
+ recipeInfo = data.recipes
+
+ // Save to cache
+ localStorage.setItem("cachedRecipes", JSON.stringify(recipeInfo))
+ localStorage.setItem("cacheTime", Date.now())
+
+ loading.style.display = "none"
+ filterAndSorting()
+ }
+ } catch (err) {
+ loading.style.display = "none"
+ noResultsContainer.innerHTML = `
+
+
Something went wrong
+
${err.message || "Unable to load recipes right now. Please try again later."}
+
`
+ }
+}
+
+// ====== EVENT LISTENERS ======
+btnAll.addEventListener("click", () => { activeFilters.mealType = "All"; filterAndSorting() })
+btnBreakfast.addEventListener("click", () => { activeFilters.mealType = "breakfast"; filterAndSorting() })
+btnLunch.addEventListener("click", () => { activeFilters.mealType = "lunch"; filterAndSorting() })
+btnFika.addEventListener("click", () => { activeFilters.mealType = "fika"; filterAndSorting() })
+btnDinner.addEventListener("click", () => { activeFilters.mealType = "dinner"; filterAndSorting() })
+btnAscending.addEventListener("click", () => { activeFilters.sortOrder = "asc"; filterAndSorting() })
+btnDescending.addEventListener("click", () => { activeFilters.sortOrder = "desc"; filterAndSorting() })
+randomBtn.addEventListener("click", getRandomRecipe)
+searchBtn.addEventListener("click", searchRecipes)
+searchInput.addEventListener("keypress", (e) => { if (e.key === "Enter") searchRecipes() })
+
+// ====== INITIAL FETCH ======
+fetchData(URL)
+
+// ====== BUTTON STATE TOGGLE ======
+mealBtns.forEach(clickedButton => {
+ clickedButton.addEventListener("click", () => {
+ mealBtns.forEach(currentButton => currentButton.classList.remove("active"))
+ clickedButton.classList.add("active")
+ })
+})
+
+timeBtns.forEach(clickedButton => {
+ clickedButton.addEventListener("click", () => {
+ timeBtns.forEach(currentButton => currentButton.classList.remove("active"))
+ clickedButton.classList.add("active")
+ })
+})
\ No newline at end of file
diff --git a/style.css b/style.css
new file mode 100644
index 000000000..2229e12cb
--- /dev/null
+++ b/style.css
@@ -0,0 +1,203 @@
+body {
+ font-family: "futura";
+ background-color: #FAFBFF;
+ margin: 16px 8px;
+}
+
+h1 {
+ color: #0018A4;
+ font-size: 64px;
+ font-weight: 700;
+}
+
+h2 {
+ font-size: 22px;
+ font-weight: 700;
+ color: black;
+}
+
+
+.filter-row {
+ display: flex;
+ flex-direction: row;
+ gap: 25px;
+ flex-wrap: wrap;
+}
+
+.meal-buttons-container,
+.time-buttons-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 18px;
+}
+
+button {
+ font-family: "futura";
+ font-size: 18px;
+ font-weight: 500;
+ border: 2px solid transparent;
+ transition: border 0.3s ease;
+}
+
+.meal-btn {
+ background: #CCFFE2;
+ border-radius: 50px;
+ padding: 8px 16px;
+}
+
+.time-btn {
+ background: #FFECEA;
+ border-radius: 50px;
+ padding: 8px 16px;
+}
+
+
+.time-btn:hover {
+ background: #FF6589;
+ color: white;
+ border: 2px solid #0018A4;
+}
+
+.meal-btn:hover {
+ border: 2px solid #0018A4;
+}
+
+.random-btn:hover {
+ border: 2px solid #0018A4;
+}
+
+.meal-btn.active {
+ background: #0018A4;
+ color: white;
+}
+
+.time-btn.active {
+ background: #FF6589;
+ color: white;
+ border: 2px solid transparent;
+}
+
+.random-btn {
+ background: #ffe065;
+ border-radius: 50px;
+ padding: 8px 16px;
+}
+
+.random-btn.active {
+ background: #b2ff65;
+}
+
+.search-bar-container {
+ display: flex;
+ align-items: center;
+ border: 1px solid #0018A4;
+ border-radius: 6px;
+ margin-left: 5px;
+}
+
+.search-bar {
+ width: 200px;
+ height: 38px;
+ border-radius: 6px 0 0 6px;
+ padding: 8px;
+ font-size: 16px;
+ box-sizing: border-box;
+ border: none;
+}
+
+.search-button {
+ height: 38px;
+ box-sizing: border-box;
+ background: #ccd3fa;
+ border-radius: 0 6px 6px 0;
+ width: 45px;
+ transition: .3s ease;
+}
+
+.search-button:hover {
+ background: #8594f4;
+}
+
+.loading p {
+ font-size: 20px;
+ margin-top: 50px;
+ text-align: center;
+}
+
+.recipe-card-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 25px;
+}
+
+.recipe-card {
+ display: flex;
+ flex-direction: column;
+ border-radius: 16px;
+ width: 300px;
+ height: fit-content;
+ background: white;
+ border: 1px solid #E9E9E9;
+ margin-top: 30px;
+ padding: 16px 16px 24px 16px;
+ gap: 16px;
+ box-sizing: border-box;
+ box-shadow: 0 0 5px 5px #0019a405;
+}
+
+.recipe-card:hover {
+ border: 2px solid #0018A4;
+}
+
+img {
+ border-radius: 12px;
+ height: 200px;
+ object-fit: cover;
+}
+
+.recipe-card h3 {
+ font-size: 22px;
+ font-weight: 700;
+ margin: 0;
+}
+
+hr {
+ border: 1px solid #E9E9E9;
+ height: 0;
+ width: 268px;
+ margin: 0;
+}
+
+.MealLabel {
+ font-size: 18px;
+ font-weight: 700;
+ margin: 0;
+}
+
+.value {
+ font-size: 16px;
+ font-weight: 500;
+ margin: 0;
+}
+
+h4 {
+ font-size: 18px;
+ font-weight: 700;
+ margin: 0;
+}
+
+p {
+ font-size: 16px;
+ font-weight: 500;
+ margin: 0;
+}
+
+.no-results-container {
+ font-size: 16px;
+ max-width: 700px;
+ margin: 20px 0px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
\ No newline at end of file