diff --git a/README.md b/README.md index 58f1a8a66..a2e21f0df 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # js-project-recipe-library +https://saras-js-project-recipe-library.netlify.app/ diff --git a/index.html b/index.html new file mode 100644 index 000000000..cf9ff33b2 --- /dev/null +++ b/index.html @@ -0,0 +1,82 @@ + + + + + + Recipe Library + + + + +
+

Recipe Library

+ + +
+ + +
+

Filter on kitchen

+
+ + + + + + +
+
+ + +
+

Sort on time

+
+ + +
+
+ + +
+

Feeling lucky?

+
+ +
+
+
+ + +
+
+
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 000000000..474eb3544 --- /dev/null +++ b/script.js @@ -0,0 +1,170 @@ +const cardsContainer = document.getElementById('cardsContainer') +const cuisineFilter = document.getElementById('cuisineFilter') +const sortSection = document.getElementById('sortOnTime') +const randomButton = document.getElementById('random-button') + +const API_KEY = 'c0e5e1580c434758805e8f05530e9b26' +const API_KEY2 = 'd4c7b9ec0f33432ea2aa56c816a17159' + +const CACHE_KEY = 'recipes' +const CACHE_DURATION = 24 * 60 * 60 * 1000 + +let recipesArray = [] + +const params = new URLSearchParams({ + number: 30, + cuisine: 'Asian,Italian,Mexican,Middle Eastern', + diet: 'vegan,vegetarian,gluten free', + type: 'breakfast,main course,dessert', + sort: 'popularity', + addRecipeInformation: true, + instructionsRequired: true, + fillIngredients: true, + apiKey: API_KEY +}) + +const URL = `https://api.spoonacular.com/recipes/complexSearch?${params.toString()}` + +const showCardsContainer = (recipesArray) => { + cardsContainer.innerHTML = '' + + if (!recipesArray || recipesArray.length === 0) { + cardsContainer.innerHTML = ` +
+

Oops 😅

+

We couldn't find any recipes for your filter...
+ Try changing it or click Surprise me! 🎉

+
+ ` + return + } + + recipesArray.forEach((recipe) => { + cardsContainer.innerHTML += ` +
+ ${recipe.title} +
+

${recipe.title}

+
+

Cuisine: + + ${recipe?.cuisines?.length ? recipe.cuisines.join(', ') : 'N/A'} + +

+

Time: + ${recipe.readyInMinutes} min +

+
+

Ingredients:

+ +
+
+ ` + }) +} + +const fetchData = async () => { + const cachedData = localStorage.getItem(CACHE_KEY) + + if (cachedData) { + try { + const { recipes, timestamp } = JSON.parse(cachedData) + const isExpired = Date.now() - timestamp > CACHE_DURATION + recipesArray = recipes + + if (!isExpired) { + showCardsContainer(recipes) + return + } + } catch (error) { + console.error('Error parsing cached data:', error) + localStorage.removeItem(CACHE_KEY) + } + } + + try { + const res = await fetch(URL) + + if (res.status === 402) { + cardsContainer.innerHTML = ` +
+

Daily API limit reached 🚫

+

Please try again later.

+
`; + return; + } + + if (!res.ok) { + cardsContainer.innerHTML = ` +
+

Oops 😅

+

No recipes found...

+
+ ` + throw new Error(`HTTP error! Status: ${res.status}`) + } + + const data = await res.json() + const recipes = data.results + recipesArray = recipes + + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ + recipes, + timestamp: Date.now() + }) + ) + + showCardsContainer(recipes) + } catch (error) { + console.error('Error occurred:', error) + } +} + +fetchData() + +const filterOnCuisine = () => { + const selected = cuisineFilter.querySelector('input[name="cuisine"]:checked') + const selectedCuisine = selected ? selected.value : 'all' + + const filteredRecipes = + selectedCuisine === 'all' + ? recipesArray + : recipesArray.filter((recipe) => + recipe.cuisines + .map((cuisine) => cuisine.toLowerCase()) + .includes(selectedCuisine.toLowerCase()) + ) + + showCardsContainer(filteredRecipes) +} + +const sortOnTime = () => { + const selected = sortSection.querySelector('input[name="order"]:checked') + const order = selected ? selected.value : 'desc' + + const sorted = [...recipesArray].sort((a, b) => { + return order === 'asc' + ? a.readyInMinutes - b.readyInMinutes + : b.readyInMinutes - a.readyInMinutes + }) + + showCardsContainer(sorted) +} + +const getRandomRecipe = () => { + const randomIndex = Math.floor(Math.random() * recipesArray.length) + const randomRecipe = [recipesArray[randomIndex]] + + showCardsContainer(randomRecipe) +} + +cuisineFilter.addEventListener('change', filterOnCuisine) +sortSection.addEventListener('change', sortOnTime) +randomButton.addEventListener('click', getRandomRecipe) + diff --git a/style.css b/style.css new file mode 100644 index 000000000..45d70d88a --- /dev/null +++ b/style.css @@ -0,0 +1,380 @@ +/* ========== GLOBAL STYLES ========== */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --page-bg: #fafafa; + --blue-main: #0018a4; + --blue-outline: #0044cc; + --mint-green: #c0f8d1; + --pink-bg: #fddddf; + --text-blue: #0033aa; +} + +body { + background: var(--page-bg); + color: #222; + font-family: "Futura", "Trebuchet MS", Arial, sans-serif; + padding: 0 64px; +} + +/* ========== HEADINGS ========== */ +h1 { + font-family: "Futura", "Trebuchet MS", Arial, sans-serif; + font-size: 64px; + font-weight: 700; + color: var(--blue-main); + line-height: 100%; + text-align: left; + margin-top: 64px; + margin-bottom: 32px; +} + +h2 { + font-family: "Futura", sans-serif; + font-size: 22px; + font-weight: 700; + line-height: 100%; + color: #000; +} + +/* ========== CONTROLS WRAPPER ========== */ +.controls { + display: flex; + justify-content: flex-start; + align-items: flex-start; + gap: 88px; + flex-wrap: wrap; + margin-bottom: 24px; +} + +/* ========== FILTERS / SORT / LUCKY ========== */ +.filters, +.sort, +.lucky { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + text-align: left; + min-width: 220px; +} + +.filters h2, +.sort h2, +.lucky h2 { + margin-bottom: 8px; +} + +.filters label, +.sort label, +.lucky button { + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* ========== BUTTON STYLES (FILTER & SORT) ========== */ +.filter-option, +.sort-button { + display: inline-block; + padding: 8px 20px; + border: 2px solid transparent; + border-radius: 999px; + font-size: 16px; + font-weight: 500; + color: var(--text-blue); + cursor: pointer; + transition: all 0.2s ease-in-out; + margin-right: 4px; +} + +/* ===== FILTER BUTTONS ===== */ +.filter-option { + background-color: var(--mint-green); +} + +.filter-option:hover, +.filter-option:has(input[type="radio"]:checked) { + border-color: var(--blue-outline); +} + +.filter-option:has(input[type="radio"]:checked) { + background-color: var(--blue-main); + color: #fff; +} + +/* ===== SORT BUTTONS ===== */ +.sort-button { + background-color: var(--pink-bg); +} + +.sort-button:hover, +.sort-button:has(input[type="radio"]:checked) { + background-color: #ff6589; + color: #fff; +} + +/* ===== HIDE RADIO INPUTS ===== */ +input[type="radio"] { + display: none; +} + +/* ========== FEELING LUCKY BUTTON ========== */ +.lucky { + align-items: flex-start; +} + +.random-button { + padding: 8px 20px; + border: 2px solid transparent; + border-radius: 999px; + font-family: "Futura", sans-serif; + font-size: 16px; + font-weight: 500; + background-color: rgb(250, 136, 210); + color: var(--text-blue); + cursor: pointer; + transition: all 0.2s ease-in-out; +} + +.random-button:hover { + background-color: rebeccapurple; + border-color: var(--blue-outline); + color: #fff; +} + +/* ========== CARD GRID ========== */ +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 24px; + margin-top: 32px; +} + +.card { + width: 300px; + border-radius: 16px; + border: 1px solid #e9e9e9; + padding: 16px 16px 24px 16px; + background: #fff; + overflow: hidden; + transition: all 0.2s ease-in-out; + cursor: pointer; +} + +.card:hover { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + border-color: var(--blue-main); +} + +.card img { + width: 100%; + height: auto; + border-radius: 12px; +} + +.card-content { + padding: 16px 16px 24px 16px; +} + +.card-content h3 { + font-family: "Futura", sans-serif; + font-weight: 700; + font-size: 22px; + color: #000; +} + +.card-content p { + font-size: 18px; + font-weight: 600; +} + +.card-content .ingredients-header { + font-size: 18px; + font-weight: 700; + margin: 12px 0 8px 0; +} + +.card-content ul { + list-style: disc; + padding-left: 20px; + font-size: 16px; + color: #000; +} + +.cuisine-value, +.time-value { + font-size: 16px; + font-weight: 500; + margin-left: 4px; +} + +/* ========== EMPTY MESSAGE ========== */ +.empty-message { + text-align: center; + background-color: #fff8e1; + border: 2px dashed #ff9800; + border-radius: 12px; + padding: 2rem; + margin-top: 2rem; + font-family: "Futura", sans-serif; + color: #444; + animation: pop-in 0.5s ease; +} + +/* ========== ANIMATIONS ========== */ +@keyframes pop-in { + 0% { + transform: scale(0.8); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +/* ========== RESPONSIVE DESIGN ========== */ +@media (max-width: 1024px) { + .controls { + flex-wrap: wrap; + gap: 48px; + justify-content: flex-start; + margin-left: 48px; + } + + .filters, + .sort, + .lucky { + min-width: 260px; + } + + h1 { + font-size: 56px; + margin-left: 48px; + } + + .cards { + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 20px; + } +} + +@media (max-width: 900px) { + .filters { + flex-wrap: wrap; + gap: 12px; + } + + .filter-option { + margin-bottom: 8px; + } +} + +@media (max-width: 768px) { + .controls { + flex-wrap: wrap; + flex-direction: row; + gap: 32px; + margin-left: 32px; + } + + .filters, + .sort, + .lucky { + flex: 1 1 100%; + min-width: 100%; + } + + h1 { + font-size: 48px; + margin-left: 32px; + } + + h2 { + font-size: 20px; + } + + .cards { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + } +} + +@media (max-width: 680px) { + .controls { + flex-direction: column; + align-items: flex-start; + gap: 28px; + } + + .filters, + .sort, + .lucky { + width: 100%; + } + + .filter-option, + .sort-button, + .random-button { + flex-wrap: wrap; + margin-bottom: 8px; + } +} + +@media (max-width: 480px) { + .controls { + flex-direction: column; + align-items: flex-start; + gap: 24px; + } + + .filters, + .sort, + .lucky { + width: 100%; + align-items: flex-start; + } + + h1 { + font-size: 42px; + margin-top: 48px; + } + + h2 { + font-size: 18px; + } + + .cards { + grid-template-columns: 1fr; + gap: 14px; + } +} + +@media (min-width: 1600px) { + h1 { + font-size: 72px; + } + + .cards { + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + gap: 28px; + } + + .card { + width: 320px; + } + + .card-content h3 { + font-size: 24px; + } + + .card-content p, + .card-content ul { + font-size: 17px; + } +} \ No newline at end of file