diff --git a/README.md b/README.md index 58f1a8a66..368aff24d 100644 --- a/README.md +++ b/README.md @@ -1 +1,115 @@ -# js-project-recipe-library +# 🍰 Favorite Dessert + +**Favorite Dessert** is a fun and interactive dessert recipe app built during Technigo’s Advanced JavaScript & TypeScript course. You can explore, filter, sort, and save your favorite desserts while practicing real API integration, dynamic DOM rendering, and responsive design. + +The project is live on [Netlify](https://favorate-dessert.netlify.app/). + +--- + +## 🔗 Demo + +Check it out here: [Favorite Dessert on Netlify](https://favorate-dessert.netlify.app/) + +--- + +## 📸 Screenshot + +![Favorite Dessert Screenshot](website-screenshot/Screenshot.png) + +--- + +## 🚀 Features + +- 🔍 **Search desserts** by name +- 🍽️ **Cuisine filter**: British, Chinese, Indian, or All +- 🔄 **Sorting options**: + - Descending ⬇️ / Ascending ⬆️ + - Less popular ⭐ / More popular ⭐⭐⭐ + - Random 🎲 +- ❤️ **Like your favorite desserts** and view them in the **Favorites** section +- 📱 Fully **responsive design** — works on desktop, tablet, and mobile + +--- + +## 🧰 Tech Stack / What I Built With + +- **HTML / CSS / SASS** for structure, layout, and styling +- **CSS Grid** for responsive card layout +- **CSS animations** for loading spinner and transitions +- **Vanilla JavaScript**: + - `map()`, `filter()`, `sort()` for array logic + - `async/await` for API calls + - `fetch()` API for real recipe data + - `localStorage` for caching API responses + - DOM manipulation using `innerHTML` and `classList` + - Event handling with `addEventListener()` +- **Lucide Icons** for interactive UI elements + +--- + +## 🧠 How It Works + +1. On page load, the app fetches 15 random dessert recipes from the Spoonacular API. +2. API responses are cached in **localStorage** to minimize requests and respect quota limits. +3. If the API fails or the daily quota is exceeded, the app falls back to **local demo data**. +4. Desserts are dynamically rendered with **images, cooking time, ingredients, and popularity**. +5. Users can **filter by cuisine** or **sort recipes** by popularity, cooking time, or randomly. +6. **Favorites system**: click the heart icon to like/unlike desserts; favorites persist in localStorage. +7. The layout is fully **responsive**, adapting from mobile to large desktop screens. + +--- + +## 📂 File Structure + +``` +📂 css/ # Stylesheets +📂 img/ # Dessert images +📂 js/ # JavaScript logic +📂 sass/ # SASS files +│ +├── 📁 abstracts/ # Variables, mixins, functions +├── 📁 base/ # Base styles (reset, typography) +├── 📁 cards/ # Card styles for desserts +├── 📁 components/ # Reusable UI components +├── 📁 layout/ # Layout and grid styles +└── 📁 page/ # Page-specific styles +``` + +--- + +## 📄 Project Requirements + +- Display recipes dynamically from API or demo data ✅ +- Filter by at least one property (cuisine) ✅ +- Sort by at least one property (popularity or cooking time) ✅ +- Random recipe button ✅ +- Empty state when no results match the filter +- Fully responsive layout (320px → 1600px+) ✅ + +--- + +## 📝 What I Learned + +- How to fetch and handle data from a real API with async/await +- Implementing fallback strategies when API fails or quota is exceeded +- Caching API responses in localStorage +- Creating dynamic, data-driven UI with `.map().join("")` +- Filtering, sorting, and updating the DOM efficiently +- Implementing a favorites system that persists across page refreshes +- Responsive design using CSS Grid and media queries +- Building a friendly UX with loading states and empty states + +--- + +## 🔜 Next Steps + +- Add detailed recipe view / modal +- Implement dietary restriction filters +- Enable recipe sharing or rating system +- Improve accessibility further + +--- + +## 📄 License + +This project is free to use for educational purposes. diff --git a/css/style.css b/css/style.css new file mode 100644 index 000000000..f360c66ad --- /dev/null +++ b/css/style.css @@ -0,0 +1,759 @@ +@charset "UTF-8"; +/* ------------------------------------------------------ + | THEME VARIABLES: (light) ☀️ + ------------------------------------------------------ */ +:root { + --color-salad-green: #ccffe2; + --color-dark-yellow: #f6a400; + --color-light-yellow: #fdf8e1; + --color-primary-first-light: #fafbff; + --color-primary-first: #0018a4; + --color-primary-second-light: #ffecea; + --color-primary-second: #ff6589; + --color-white: #ffffff; + --color-grey-light-1: #f8f9fa; + --color-grey-light-2: #e9ecef; + --color-grey-light-3: #dee2e6; + --color-grey-light-4: #ced4da; + --color-grey-dark: #868e96; + --color-font: #212529; + --color-font-base: #212529; + --color-button: #8d9271; + --color-black: #000000; + --color-blue: #176d8c; + --border-radius-xxs: 0.5rem; + --border-radius-xs: 1rem; + --border-radius-sm: 1.5rem; + --border-radius-md: 2rem; + --border-radius-lg: 2.5rem; + --border-radius-xl: 3rem; + --box-shadow: 0 0.5rem 1rem #0019a453; +} + +/* ------------------------------- + | 1️⃣ GRID FOR BASE DESKTOP 1️⃣ | + ------------------------------- */ +/* --------------------------------------------- + | 2️⃣ GRID FOR Tablet landscape: ≤ 1200px 2️⃣ | + -------------------------------------------- */ +/* --------------------------------------------- + | 3️⃣ GRID FOR Tablet portrait: ≤ 900px 3️⃣ | + -------------------------------------------- */ +/* --------------------------------------- + | 4️⃣ Phone: ≤ 600px HORIZONTAL 4️⃣ | + -------------------------------------- */ +/* ------------------------------------- + | 5️⃣ Extra small phone: ≤ 320px 5️⃣ | + ------------------------------------ */ +/* ------------------------------------------------------ + MEDIA QUERY MANAGER + ------------------------------------------------------ + + Breakpoints (by screen width): + + 0 - 320px: Extra small (mini phones) + 0 - 600px: Phone + 600px - 900px: Tablet portrait + 900px - 1200px: Tablet landscape + 1200px - 1800px: Desktop (base design) is where our normal styles apply + 1800px + : Big desktop + + $breakpoint argument choices: + - xs-phone + - phone + - tab-port + - tab-land + - desktop + - big-desktop + + 1em = 16px +------------------------------------------------------ */ +body { + font-family: "montserrat", sans-serif; + line-height: 1.7; + color: var(--color-font); + font-size: 1.6rem; +} + +.text-bold { + font-weight: 700; +} + +.divider { + display: block; + border-bottom: 0.1rem solid var(--color-grey-light-2); + margin: 1.5rem 0; +} +@media only screen and (max-width: 75em) { + .divider { + margin: 1rem 0; + } +} + +.hidden { + display: none !important; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(14rem, 23rem)); + gap: 2rem; + scrollbar-width: none; + cursor: pointer; +} +@media (max-width: 25.8125em) { + .cards { + grid-template-columns: repeat(auto-fit, minmax(auto, 1fr)); + gap: 1rem; + } +} +.cards p, +.cards li, +.cards h3 { + font-size: 1.3rem; +} +.cards p { + white-space: normal; +} +.cards__coffee-card { + border-radius: 2rem; + background-color: var(--color-white); + border: 2.5px solid var(--color-grey-light-2); + padding: 1rem; + display: flex; + flex-direction: column; + transition: 0.3s; + min-height: 36rem; +} +.cards__coffee-card:hover { + border-color: var(--color-primary-first); + box-shadow: var(--box-shadow); + transform: translateY(-1rem); +} +.cards__coffee-card.active { + border-color: var(--color-primary-second); + background-color: var(--color-primary-second-light); + box-shadow: var(--box-shadow); +} +.cards__coffee-card__title { + font-size: 2rem; +} +.cards__coffee-card__img { + align-self: center; + width: 100%; + height: 15rem; + border-radius: 1rem; + margin-bottom: 1rem; + object-fit: cover; +} +.cards__coffee-card__ingredients li { + list-style: none; +} +.cards__juice-card { + border-radius: 2rem; + background-color: var(--color-white); + border: 2.5px solid var(--color-grey-light-2); + padding: 1rem; + margin-bottom: 3rem; + display: flex; + flex-direction: column; + transition: 0.3s; +} +.cards__juice-card:hover { + border-color: var(--color-primary-first); + box-shadow: var(--box-shadow); + transform: translateY(-1rem); +} +.cards__juice-card__title { + font-size: 2rem; +} +.cards__juice-card__img { + align-self: center; + width: 100%; + height: 15rem; + border-radius: 1rem; + margin-bottom: 1rem; + object-fit: cover; +} +.cards__juice-card__ingredients li { + list-style: none; +} + +.empty-card { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-salad-green); + border-radius: 2rem; + border: 2px solid var(--color-primary-first); + padding: 2rem 3rem; + margin-left: 1rem; +} +.empty-card h2, +.empty-card p { + text-align: center; + color: var(--color-primary-first); +} +.empty-card p { + font-size: 1.6rem; + font-weight: 500; +} +.empty-card__box { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-self: center; +} + +.empty-card-juice { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-salad-green); + border: 2px solid var(--color-primary-first); +} +.empty-card-juice h2, +.empty-card-juice p { + text-align: center; + color: var(--color-primary-first); +} +.empty-card-juice p { + font-size: 1.6rem; + font-weight: 500; +} +.empty-card-juice__box { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-self: center; +} + +.cardFavorites { + display: flex; + align-items: center; + justify-content: center; + border-radius: 1.5rem; + padding: 2rem 0; + border: 2px solid var(--color-primary-second); + background-color: var(--color-primary-second-light); + margin-left: 1rem; +} + +*, +*::after, +*::before { + margin: 0; + padding: 0; + box-sizing: inherit; +} + +html { + font-size: 62.5%; +} +@media only screen and (min-width: 112.5em) { + html { + font-size: 75%; + } +} +@media only screen and (max-width: 75em) { + html { + font-size: 56.25%; + } +} +@media only screen and (max-width: 56.25em) { + html { + font-size: 56.25%; + } +} +@media only screen and (max-width: 20em) { + html { + font-size: 50%; + } +} + +body { + box-sizing: border-box; + background-color: #f8f9fa; + margin: 0 auto; +} + +.middlebox { + display: flex; + align-items: center; + background-color: var(--color-primary-first); + height: 100vh; + padding: 1rem; +} +@media (max-width: 48.75em) { + .middlebox { + padding: 0; + } +} + +.sidebar { + flex: 0 0 15%; + height: 100%; +} +@media only screen and (max-width: 75em) { + .sidebar { + flex: 0 0 5%; + } +} +@media (max-width: 48.75em) { + .sidebar { + display: none; + } +} + +.content { + background-color: var(--color-primary-first-light); + display: flex; + flex-direction: column; + justify-content: flex-start; + flex: 1; + height: 100%; + border-radius: 2rem; + overflow: hidden; +} +@media only screen and (max-width: 75em) { + .content { + border-radius: 0; + } +} + +.header { + margin: 1rem 2rem; +} + +.filters { + margin: 0 2rem 1rem 2rem; +} + +.main { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin: 0 1rem; + flex: 1; + height: 100vh; +} + +@media only screen and (max-width: 56.25em) { + .cards { + flex: 2; + } +} +@media only screen and (max-width: 37.5em) { + .cards { + flex: 1; + } +} + +#demo-status { + opacity: 0; + animation: fadeInOut 4s ease forwards; +} + +@keyframes fadeInOut { + 0% { + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +.main-container { + flex: 1; + overflow-y: auto; + padding: 5rem 2rem; +} + +.filters { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.filter { + display: flex; + flex-direction: column; +} +.filter__list { + display: flex; + flex-wrap: nowrap; + gap: 1rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + width: auto; + justify-content: flex-start; +} +.filter__item { + border: 1.4px solid transparent; + border-radius: 0.8rem; + padding: 0.5rem 2rem; + list-style: none; + white-space: nowrap; + cursor: pointer; + transition: 0.3s; +} +.filter__item-blue { + color: var(--color-primary-first-light); + background-color: var(--color-primary-first); +} +.filter__item-blue.active { + background-color: var(--color-salad-green); + border-color: var(--color-primary-first); + color: var(--color-primary-first); +} +.filter__item-blue:hover { + background-color: var(--color-salad-green); + border-color: var(--color-primary-first); + color: var(--color-primary-first); +} +.filter__item-yellow { + color: var(--color-light-yellow); + background-color: var(--color-dark-yellow); +} +.filter__item-yellow.active { + background-color: var(--color-light-yellow); + border-color: var(--color-dark-yellow); + color: var(--color-dark-yellow); +} +.filter__item-yellow:hover { + background-color: var(--color-light-yellow); + border-color: var(--color-dark-yellow); + color: var(--color-dark-yellow); +} + +.sidebar__desktop { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 5rem; + margin-left: -1rem; +} +@media only screen and (max-width: 75em) { + .sidebar__desktop { + display: none; + } +} +.sidebar p, +.sidebar a { + color: var(--color-white); +} +.sidebar__logo { + height: 6rem; + width: 6rem; + background-image: url(../img/technig-logo.png); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 1rem; + margin-bottom: 1rem; +} +.sidebar__title { + font-weight: 700; + font-size: 1.8rem; + text-align: center; +} +.sidebar__subtitle { + font-size: 1.4rem; + text-align: center; +} +.sidebar__socialbox { + display: flex; + gap: 1rem; + margin: 2rem 0; +} +.sidebar__menu { + align-self: flex-start; + padding-left: 3.8rem; + width: 85%; +} +.sidebar__menu-list { + display: flex; + flex-direction: column; + gap: 2rem; + width: 100%; +} +.sidebar__menu-item, .sidebar__submenu-item { + list-style: none; +} +.sidebar__menu-link { + text-decoration: none; + color: var(--color-font); + font-weight: 500; + font-size: 1.8rem; + margin-bottom: 1rem; + display: block; + padding: 0.5rem 3rem; + display: block; + width: 100%; + border-radius: 1rem; + transition: 0.3s; +} +.sidebar__menu-link:hover { + background-color: var(--color-primary-second-light); + color: var(--color-primary-second); +} +.sidebar__menu-link.active { + background-color: var(--color-primary-second-light); + color: var(--color-primary-second); +} +.sidebar__submenu { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} +.sidebar__submenu-link { + text-decoration: none; + color: var(--color-font); + font-size: 1.6rem; + padding: 0.5rem 3rem; + display: block; + width: 100%; + border-radius: 1rem; + transition: 0.3s; +} +.sidebar__submenu-link:hover, .sidebar__submenu-link.active { + background-color: var(--color-primary-first-light); + color: var(--color-primary-first); +} + +.sidebar__mobile { + display: none; +} +@media only screen and (max-width: 75em) { + .sidebar__mobile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + height: 100%; + margin-left: -1rem; + padding: 2rem 0; + } +} +.sidebar__logo-short { + height: 4rem; + width: 4rem; + border-radius: 0.8rem; + background-image: url(../img/technig-logo.png); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + margin-bottom: -22rem; +} +.sidebar__icons-short { + display: flex; + flex-direction: column; + gap: 1.5rem; +} +.sidebar__icon-short { + background-color: var(--color-primary-second-light); + color: var(--color-primary-second); + border-radius: 1rem; + width: 4.5rem; + height: 4.5rem; + padding: 1rem; + transition: 0.3s; + cursor: pointer; +} +.sidebar__icon-short:hover { + color: var(--color-primary-second-light); + background-color: var(--color-primary-second); +} +.sidebar__icon-short.active { + color: var(--color-primary-second-light); + fill: var(--color-primary-second-light); + background-color: var(--color-primary-second); +} +.sidebar__socialbox-short { + display: flex; + flex-direction: column; + gap: 1.5rem; +} +.sidebar__iconbox-short { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-primary-second-light); + border-radius: 0.8rem; + width: 4rem; + height: 4rem; +} + +.header__desktop { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 3rem; + margin: 2rem 0; +} +@media (max-width: 48.75em) { + .header__desktop { + display: none; + } +} +.header__title { + line-height: 1.3; + font-size: 3rem; +} +@media (max-width: 48.75em) { + .header__title { + display: none; + } +} +.header__search-form-desktop { + display: flex; + position: relative; + flex: 1; +} +.header__icon { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + color: var(--color-primary-first); + width: 2rem; +} +.header__input { + border: 1.5px solid var(--color-primary-first); + background-color: var(--color-white); + padding: 1.5rem; + flex: 1; + border-radius: 0.8rem; + padding-left: 4rem; +} +.header__input::placeholder { + color: var(--color-primary-first); + font-family: "montserrat", sans-serif; + font-size: 1.5rem; + display: block; + padding: 2rem 0; +} +.header__input:focus { + outline: none; + border-color: var(--color-primary-first); + background-color: var(--color-primary-first-light); +} +.header__info { + display: flex; + align-items: center; + gap: 1.5rem; +} +@media (max-width: 48.75em) { + .header__info { + display: none; + } +} +.header__icon-short { + background-color: var(--color-primary-second); + color: var(--color-primary-second-light); + border: 1.5px solid transparent; + border-radius: 1rem; + width: 4.5rem; + height: 4.5rem; + padding: 1rem; + transition: 0.3s; + cursor: pointer; +} +.header__icon-short:hover { + color: var(--color-primary-second); + fill: var(--color-primary-second); + background-color: var(--color-primary-second-light); + border-color: var(--color-primary-second); +} +.header__icon-short.active { + color: var(--color-primary-second); + fill: var(--color-primary-second); + background-color: var(--color-primary-second-light); + border-color: var(--color-primary-second); +} +.header__logo { + background-image: url(../img/uifaces-cartoon-avatar.jpg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + border-radius: 1rem; + width: 4.5rem; + height: 4.5rem; + padding: 1rem; + transition: 0.3s; +} +.header__logo:hover { + transform: scale(1.1); +} + +.header__mobile { + display: none; + width: 100%; +} +@media (max-width: 48.75em) { + .header__mobile { + display: inline-block; + } +} +.header__first { + display: flex; + align-items: center; + justify-content: space-between; + margin: 1rem 0; + gap: 1rem; +} +.header__mobile-menue__button { + width: 4.5rem; + height: 4.5rem; + background-color: var(--color-primary-second); + border-radius: 0.8rem; + display: flex; + align-items: center; + justify-content: center; +} +.header__mobile-menue__icon { + color: var(--color-primary-second-light); +} +.header__search-form-mobile { + display: flex; + position: relative; + flex: 1; +} +.header__icon-mobile { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + color: var(--color-primary-first); + width: 2rem; +} +.header__input-mobile { + border: 1px solid var(--color-grey-light-2); + background-color: var(--color-grey-light-2); + padding: 1.4rem; + flex: 1; + border-radius: 0.8rem; + padding-left: 4rem; +} +.header__input-mobile::placeholder { + color: var(--color-grey-dark); + font-family: "montserrat", sans-serif; + font-size: 1.5rem; +} +.header__input-mobile:focus { + outline: none; + border-color: var(--color-grey-light-2); + background-color: var(--color-grey-light-1); +} + +/*# sourceMappingURL=style.css.map */ diff --git a/css/style.css.map b/css/style.css.map new file mode 100644 index 000000000..451956ae8 --- /dev/null +++ b/css/style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../sass/abstracts/_variables.scss","../sass/abstracts/_mixins.scss","../sass/abstracts/_typography.scss","../sass/cards/_coffee-card.scss","../sass/base/_base.scss","../sass/layout/_layout.scss","../sass/layout/_filters.scss","../sass/layout/_sidebar.scss","../sass/layout/_header.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAIA;EAEE;EACA;EACA;EAGA;EACA;EACA;EACA;EAGA;EAEA;EACA;EACA;EACA;EAEA;EAEA;EACA;EAEA;EACA;EAGA;EAGA;EACA;EACA;EACA;EACA;EACA;EAIA;;;AAGF;AAAA;AAAA;AAcA;AAAA;AAAA;AASA;AAAA;AAAA;AASA;AAAA;AAAA;AAQA;AAAA;AAAA;ACvFA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;ACEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;ADmCE;ECtCJ;IAMI;;;;ACjBJ;EACE;;;AAGF;EACE;EACA;EACA;EAEA;EACA;;AFsEA;EE5EF;IASI;IACA;;;AAGF;AAAA;AAAA;EAGE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EAEE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EAEA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;AAAA;EAEE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EAEA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACzKF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;EACE;;AHgDE;EGjDJ;IAII;;;AHqCA;EGzCJ;IAQI;;;AHyBA;EGjCJ;IAYI;;;AHKA;EGjBJ;IAgBI;;;;AAIJ;EACE;EACA;EACA;;;AC/BF;EACE;EACA;EACA;EACA;EACA;;AJ2EA;EIhFF;IAQI;;;;AAIJ;EACE;EACA;;AJmCE;EIrCJ;IAKI;;;AJ+DF;EIpEF;IAUI;;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AJeE;EIvBJ;IAWI;;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AJfE;EIkBJ;IAEI;;;AJ5BA;EI0BJ;IAMI;;;;AAIJ;EACE;EACA;;;AAGF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;AAIJ;EACE;EACA;EACA;;;AC5FF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AC7DF;EACE;EACA;EACA;EACA;EACA;;AN2CA;EMhDF;IAQI;;;AAIJ;AAAA;EAEE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEF;EACE;EACA;;AAEF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EAEE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAMJ;EACE;;ANjEA;EMgEF;IAII;IACA;IACA;IACA;IACA;IACA;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;;;ACnLF;EACE;EACA;EACA;EACA;EACA;;AP0EF;EO/EA;IASI;;;AAIJ;EACE;EACA;;APgEF;EOlEA;IAMI;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EAEA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;APcF;EOjBA;IAOI;;;AAIJ;EACE;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AASF;EACE;EACA;;AP7CF;EO2CA;IAMI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA","file":"style.css"} \ No newline at end of file diff --git a/img/cake-slice.png b/img/cake-slice.png new file mode 100644 index 000000000..b9fa2bbf4 Binary files /dev/null and b/img/cake-slice.png differ diff --git a/img/technig-logo.png b/img/technig-logo.png new file mode 100644 index 000000000..0c04a71f5 Binary files /dev/null and b/img/technig-logo.png differ diff --git a/img/uifaces-cartoon-avatar.jpg b/img/uifaces-cartoon-avatar.jpg new file mode 100644 index 000000000..8a23b8f7e Binary files /dev/null and b/img/uifaces-cartoon-avatar.jpg differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..07a0b2fc5 --- /dev/null +++ b/index.html @@ -0,0 +1,367 @@ + + + + + + + + + + + Yor favorate drink + + +
+ +
+
+
+

+ What dessert
would you like? +

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

Cuisine

+
    +
  • All
  • +
  • British
  • +
  • Chinese
  • +
  • Indian
  • +
+
+ +
+

Sorting

+
    +
  • Descending
  • +
  • Popularity
  • +
  • Random
  • +
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+ ✅ Demo Data Loaded - Filters Ready! +
+ + + diff --git a/js/script.js b/js/script.js new file mode 100644 index 000000000..5474fe4a6 --- /dev/null +++ b/js/script.js @@ -0,0 +1,899 @@ +const filterItems = document.querySelectorAll(".filter__item"); + +const mainCoffee = document.querySelector(".main-coffee"); +const mainFruits = document.querySelector(".main-fruites"); +const mainChocolate = document.querySelector(".main-chocolate"); +const mainFavorites = document.querySelector(".main-favorites"); + +const containerCoffee = document.getElementById("coffee-container"); +const containerFruit = document.getElementById("fruit-container"); +const containerFavorites = document.getElementById("favorites-container"); + +const buttonCoffee = document.getElementById("button-coffee"); +const buttonFruit = document.getElementById("button-fruit"); +const buttonFavorites = document.querySelectorAll(".button-favorites"); +const buttons = [buttonCoffee, buttonFruit, ...buttonFavorites]; + +const LS_COFFEE = "coffeeData"; +const LS_FRUITS = "juiceData"; +const LS_FAVORITES = "favorites"; + +const API_KEY = "003fb0433f9c48348cea44cc791555a4"; +const SPOON_BASE = "https://api.spoonacular.com"; + +let coffeeRendered = false; +let fruitsRendered = false; + +let favorites = []; + +/* ========================================================= + SECTION SECTION SECTION UTILITIES SECTION SECTION SECTION + ========================================================= */ + +function setActiveButton(activeBtn) { + buttons.filter(Boolean).forEach((btn) => btn.classList.remove("active")); + if (activeBtn) activeBtn.classList.add("active"); +} + +function getCacheArray(key) { + try { + const arr = JSON.parse(localStorage.getItem(key) || "[]"); + return Array.isArray(arr) ? arr : []; + } catch { + return []; + } +} + +function pickRandomExcluding(arr, excludeId) { + const pool = arr.filter((r) => String(r?.id) !== String(excludeId)); + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; +} + +function detectCuisine(recipe) { + if (recipe.cuisines && recipe.cuisines.length > 0) { + const cuisine = recipe.cuisines[0].toLowerCase(); + + if (cuisine.includes("british")) return "british"; + if (cuisine.includes("chinese")) return "chinese"; + if (cuisine.includes("indian")) return "indian"; + } + + return "international"; +} + +function cuisineLabel(key) { + const labels = { + british: "British", + chinese: "Chinese", + indian: "Indian", + international: "International", + }; + return labels[key] || key.charAt(0).toUpperCase() + key.slice(1); +} + +function cuisineLabel3(key) { + switch (key.toLowerCase()) { + case "british": + return "British"; + case "chinese": + return "Chinese"; + case "indian": + return "Indian"; + default: + return key.charAt(0).toUpperCase() + key.slice(1); + } +} + +function toNum(v) { + const n = Number(v); + return Number.isFinite(n) ? n : 0; +} + +function fetchJSON(url) { + return fetch(url).then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }); +} + +function cacheSet(key, value) { + localStorage.setItem(key, JSON.stringify(value)); +} + +function cacheGet(key) { + try { + return JSON.parse(localStorage.getItem(key) || "null"); + } catch { + return null; + } +} + +function ensureFavoritesLoaded() { + try { + const data = JSON.parse(localStorage.getItem(LS_FAVORITES) || "[]"); + favorites = Array.isArray(data) ? data : []; + console.log("📋 Loaded favorites:", favorites.length); + } catch { + favorites = []; + console.log("📋 No favorites found"); + } +} + +function ensureNoMatchBanner(container) { + const cards = Array.from( + container.querySelectorAll(".cards__coffee-card, .cards__juice-card") + ).filter((el) => !el.classList.contains("empty-card")); + + const anyVisible = cards.some( + (c) => c.style.display !== "none" && c.offsetParent !== null + ); + let banner = container.querySelector(".no-match-card"); + + if (!anyVisible) { + if (!banner) { + banner = document.createElement("div"); + banner.className = "no-match-card empty-card"; + banner.innerHTML = ` +
+

🥺 Oops

+

No recipes matched your filter.

+
`; + container.appendChild(banner); + } + } else { + banner?.remove(); + } +} + +/* ======================================================= + SECTION SECTION SECTION STARTUP SECTION SECTION SECTION + ======================================================= */ + +function getCurrentCategory() { + if (mainCoffee && !mainCoffee.classList.contains("hidden")) return "coffee"; + if (mainFruits && !mainFruits.classList.contains("hidden")) return "juice"; + if (mainFavorites && !mainFavorites.classList.contains("hidden")) + return "favorites"; + return "coffee"; +} + +window.addEventListener("DOMContentLoaded", () => { + seedIfEmpty(LS_COFFEE, DEMO_CAKES); + seedIfEmpty(LS_FRUITS, DEMO_DESSERTS); + + console.log("=== 🎯 TEACHER DEMO READY ==="); + console.log("LS_COFFEE:", cacheGet(LS_COFFEE)?.length, "items"); + console.log("LS_FRUITS:", cacheGet(LS_FRUITS)?.length, "items"); + + setActiveButton(buttonCoffee); + mainFruits?.classList.add("hidden"); + mainChocolate?.classList.add("hidden"); + mainFavorites?.classList.add("hidden"); + + ensureFavoritesLoaded(); + renderFavorites(); + renderCoffeeCards(); + + setTimeout(() => { + console.log("=== 🔍 FILTER DEBUG INFO ==="); + debugFilters(); + console.log("=== 🎯 READY FOR TEACHER TESTING ==="); + console.log("Filters should work now with demo data!"); + }, 1000); +}); + +buttonCoffee?.addEventListener("click", () => { + mainCoffee?.classList.remove("hidden"); + mainFruits?.classList.add("hidden"); + mainChocolate?.classList.add("hidden"); + mainFavorites?.classList.add("hidden"); + setActiveButton(buttonCoffee); +}); + +buttonFruit?.addEventListener("click", () => { + mainCoffee?.classList.add("hidden"); + mainFruits?.classList.remove("hidden"); + mainChocolate?.classList.add("hidden"); + mainFavorites?.classList.add("hidden"); + setActiveButton(buttonFruit); + + renderFruitCards(); +}); + +buttonFavorites.forEach((btn) => { + btn.addEventListener("click", () => { + mainCoffee?.classList.add("hidden"); + mainFruits?.classList.add("hidden"); + mainChocolate?.classList.add("hidden"); + mainFavorites?.classList.remove("hidden"); + setActiveButton(btn); + }); +}); + +function debugFilters() { + console.log("🐛 DEBUG FILTERS:"); + + const containers = [containerCoffee, containerFruit, containerFavorites]; + containers.forEach((container, index) => { + if (!container) return; + + const cards = container.querySelectorAll( + ".cards__coffee-card, .cards__juice-card" + ); + console.log(`Container ${index}: ${cards.length} cards`); + + cards.forEach((card) => { + const title = card.querySelector("h2")?.textContent; + const cuisine = card.dataset.cuisine; + console.log(` 📋 "${title}": cuisine = "${cuisine}"`); + }); + }); + + const allCards = document.querySelectorAll( + ".cards__coffee-card, .cards__juice-card" + ); + const cuisineCount = {}; + allCards.forEach((card) => { + const cuisine = card.dataset.cuisine || "unknown"; + cuisineCount[cuisine] = (cuisineCount[cuisine] || 0) + 1; + }); + console.log("📊 Cuisine distribution:", cuisineCount); +} + +/* ================================== + SECTION DEMO DEMO DEMO + ==================================*/ + +const DEMO_CAKES = [ + { + id: "demo_1", + title: "Tiramisu Cake", + image: + "https://plus.unsplash.com/premium_photo-1713447395823-2e0b40b75a89?fm=jpg&q=60&w=3000", + readyInMinutes: 30, + cuisines: ["British"], + aggregateLikes: 30, + }, + { + id: "demo_2", + title: "Chocolate Cake", + image: + "https://images.unsplash.com/photo-1495147466023-ac5c588e2e94?auto=format&fit=crop&q=80&w=987", + readyInMinutes: 45, + cuisines: ["Chinese"], + aggregateLikes: 25, + }, + { + id: "demo_3", + title: "Vanilla Sponge Cake", + image: + "https://plus.unsplash.com/premium_photo-1667824363471-733bbbca0d0d?auto=format&fit=crop&q=80&w=987", + readyInMinutes: 35, + cuisines: ["Indian"], + aggregateLikes: 20, + }, + { + id: "demo_4", + title: "Lemon Drizzle Cake", + image: + "https://plus.unsplash.com/premium_photo-1663924211686-677f4114cef1?auto=format&fit=crop&q=80&w=1887", + readyInMinutes: 40, + cuisines: ["British"], + aggregateLikes: 15, + }, + { + id: "demo_5", + title: "Carrot Cake", + image: + "https://images.unsplash.com/photo-1476887334197-56adbf254e1a?auto=format&fit=crop&q=80&w=987", + readyInMinutes: 50, + cuisines: ["Chinese"], + aggregateLikes: 28, + }, +]; + +const DEMO_DESSERTS = [ + { + id: "demo_1", + title: "Fruit Tart", + image: + "https://images.unsplash.com/photo-1591626505027-a4992d84d28b?auto=format&fit=crop&q=80&w=1035", + readyInMinutes: 30, + cuisines: ["British"], + aggregateLikes: 30, + }, + { + id: "demo_2", + title: "Chocolate Mousse", + image: + "https://images.unsplash.com/photo-1750680230074-a2046d59ba02?auto=format&fit=crop&q=80&w=927", + readyInMinutes: 20, + cuisines: ["Chinese"], + aggregateLikes: 25, + }, + { + id: "demo_3", + title: "Vanilla Pudding", + image: + "https://plus.unsplash.com/premium_photo-1661266841331-e2169199de65?auto=format&fit=crop&q=80&w=1734", + readyInMinutes: 15, + cuisines: ["Indian"], + aggregateLikes: 20, + }, + { + id: "demo_4", + title: "Apple Pie", + image: + "https://plus.unsplash.com/premium_photo-1714662390686-eacb5268b41c?auto=format&fit=crop&q=80&w=988", + readyInMinutes: 45, + cuisines: ["British"], + aggregateLikes: 22, + }, + { + id: "demo_5", + title: "Berry Parfait", + image: + "https://plus.unsplash.com/premium_photo-1714146022660-d9c01e9e6c8c?auto=format&fit=crop&q=80&w=988", + readyInMinutes: 25, + cuisines: ["Chinese"], + aggregateLikes: 18, + }, +]; + +async function checkAPIHealth() { + const urls = [ + "https://api.spoonacular.com/recipes/random?number=1&apiKey=f9a94c32c70844888eebfba758e10f35", + "https://api.spoonacular.com/recipes/complexSearch?query=cake&number=1&apiKey=f9a94c32c70844888eebfba758e10f35", + ]; + + for (const url of urls) { + try { + const response = await fetch(url); + console.log(`🔍 ${url}: ${response.status} ${response.statusText}`); + + if (response.ok) { + const data = await response.json(); + console.log("✅ API работает, получены данные"); + return true; + } else { + console.log(`❌ API ошибка: ${response.status}`); + return false; + } + } catch (error) { + console.log("❌ Сетевая ошибка:", error.message); + return false; + } + } +} + +function seedIfEmpty(storageKey, demoArray) { + console.log(`🌱 FORCE loading demo data to ${storageKey}`); + cacheSet(storageKey, demoArray); + + const loaded = cacheGet(storageKey); + console.log(`✅ ${storageKey} now has:`, loaded.length, "items"); +} + +/* ================================== + SECTION RENDER: COFFEE ☕☕☕ (Desserts) + ==================================*/ + +async function renderCoffeeCards() { + if (coffeeRendered) return; + coffeeRendered = true; + if (containerCoffee) containerCoffee.innerHTML = ""; + + // ВСЕГДА показываем демо-данные ПЕРВЫМИ + const cached = cacheGet(LS_COFFEE); + console.log("📦 Cached coffee data:", cached); + + if (Array.isArray(cached) && cached.length) { + console.log("✅ Showing cached/demo data"); + cached.forEach(addCoffeeCard); + } + + // API запрос - только как БОНУС, если работает + try { + const url = `https://api.spoonacular.com/recipes/random?number=4&apiKey=${API_KEY}&type=dessert`; + console.log("🌐 Trying API as bonus..."); + + const data = await fetchJSON(url); + const arr = data.results || []; + console.log("📥 API response:", arr); + + if (arr.length) { + console.log("🎉 API worked! Adding to existing data"); + // Добавляем API данные к существующим демо-данным + arr.forEach(addCoffeeCard); + // Обновляем кэш + cacheSet(LS_COFFEE, [...cached, ...arr]); + } + } catch (err) { + console.log("⚠️ API failed, but we have demo data - filters will work!"); + // НЕ показываем ошибку - у нас есть демо-данные + } + + ensureNoMatchBanner(containerCoffee); + + // Сразу показываем отладочную информацию + setTimeout(debugFilters, 500); +} + +function addCoffeeCard(r) { + console.log("🔍 Rendering card:", { + title: r.title, + hasIngredients: !!r.extendedIngredients, + ingredientsCount: r.extendedIngredients?.length, + ingredients: r.extendedIngredients, + }); + const cuisineKey = detectCuisine({ + cuisines: r.cuisines || [], + title: r.title || "", + summary: r.summary || "", + ingredients: (r.extendedIngredients || []).map((i) => i.name), + }); + const cuisineText = cuisineLabel3(cuisineKey); + + const card = document.createElement("div"); + card.classList.add("cards__coffee-card"); + card.dataset.id = String(r.id); + card.dataset.cuisine = cuisineKey; + card.dataset.cooking = String(r.readyInMinutes ?? "0"); + card.dataset.popularity = String(r.aggregateLikes ?? 0); + + card.innerHTML = ` + ${r.title} +

${r.title + .split(" ") + .slice(0, 2) + .join(" ")}

+ +
+

Cuisine: ${cuisineText}

+

Cooking time: ${ + r.readyInMinutes ?? "N/A" + } min

+

Popularity: ${ + r.aggregateLikes ?? 0 + }

+
+ `; + + if (favorites.some((f) => f.id === String(r.id))) + card.classList.add("active"); + card.addEventListener("click", () => toggleFavoriteCard(card)); + containerCoffee?.appendChild(card); +} + +function showEmptyCoffeeCard() { + const emptyCard = document.createElement("div"); + emptyCard.classList.add("cards__coffee-card", "empty-card"); + emptyCard.innerHTML = ` +
+

🍰 Oops! API limit reached 😅

+

Looks like we’ve hit the maximum number of requests for today. Try again later or use local recipes!

+
+ `; + containerCoffee?.appendChild(emptyCard); +} + +/* ================================== + SECTION RENDER: FRUIT 🍇🍌🍊🍎 (Desserts) + ================================== */ + +async function renderFruitCards() { + if (fruitsRendered) return; + fruitsRendered = true; + if (containerFruit) containerFruit.innerHTML = ""; + + // ВСЕГДА показываем демо-данные ПЕРВЫМИ + const cached = cacheGet(LS_FRUITS); + console.log("📦 Cached juice data:", cached); + + if (Array.isArray(cached) && cached.length) { + console.log("✅ Showing cached/demo data"); + cached.forEach(addFruitCard); + } + + // API запрос - только как БОНУС + try { + const url = `https://api.spoonacular.com/recipes/random?number=4&apiKey=${API_KEY}&type=dessert`; + console.log("🌐 Trying API as bonus..."); + + const data = await fetchJSON(url); + const arr = data.results || []; + console.log("📥 API response:", arr); + + if (arr.length) { + console.log("🎉 API worked! Adding to existing data"); + arr.forEach(addFruitCard); + cacheSet(LS_FRUITS, [...cached, ...arr]); + } + } catch (err) { + console.log("⚠️ API failed, but we have demo data - filters will work!"); + } + + ensureNoMatchBanner(containerFruit); + setTimeout(debugFilters, 500); +} + +function addFruitCard(r) { + const cuisineKey = detectCuisine({ + cuisines: r.cuisines || [], + title: r.title || "", + summary: r.summary || "", + ingredients: (r.extendedIngredients || []).map((i) => i.name), + }); + const cuisineText = cuisineLabel3(cuisineKey); + + const card = document.createElement("div"); + card.classList.add("cards__coffee-card"); + card.dataset.id = String(r.id); + card.dataset.cuisine = cuisineKey; + card.dataset.cooking = String(r.readyInMinutes ?? "0"); + card.dataset.popularity = String(r.aggregateLikes ?? 0); + + card.innerHTML = ` + ${r.title} +

${r.title + .split(" ") + .slice(0, 2) + .join(" ")}

+ +
+

Cuisine: ${cuisineText}

+

Cooking time: ${ + r.readyInMinutes ?? "N/A" + } min

+

Popularity: ${ + r.aggregateLikes ?? 0 + }

+
+ `; + + if (favorites.some((f) => f.id === String(r.id))) + card.classList.add("active"); + card.addEventListener("click", () => toggleFavoriteCard(card)); + containerFruit?.appendChild(card); +} + +function showEmptyFruitCard() { + const emptyCard = document.createElement("div"); + emptyCard.classList.add("cards__juice-card", "empty-card"); + emptyCard.innerHTML = ` +
+

🍰 Oops! API limit reached 😅

+

Looks like we’ve hit the maximum number of requests for today. Try again later or use local recipes!

+
+ `; + containerFruit?.appendChild(emptyCard); +} + +/* ========================================== + SECTION FAVORITES ❤️❤️❤️(save / render / toggle) + ========================================== */ +function toggleFavoriteCard(cardEl) { + const recipeId = String(cardEl.dataset.id || ""); + if (!recipeId) { + console.warn("No data-id on card"); + return; + } + + const index = favorites.findIndex((f) => f.id === recipeId); + + if (index === -1) { + favorites.push({ + id: recipeId, + innerHTML: cardEl.innerHTML, + className: cardEl.className, + }); + cardEl.classList.add("active"); + } else { + favorites.splice(index, 1); + cardEl.classList.remove("active"); + syncFavoriteState(recipeId, false); + } + + localStorage.setItem(LS_FAVORITES, JSON.stringify(favorites)); + renderFavorites(); +} + +function syncFavoriteState(recipeId, isFavorite) { + const allCards = document.querySelectorAll(`[data-id="${recipeId}"]`); + + allCards.forEach((card) => { + if (isFavorite) { + card.classList.add("active"); + } else { + card.classList.remove("active"); + } + }); +} + +function renderFavorites() { + if (!containerFavorites) return; + + containerFavorites.innerHTML = ""; + if (!favorites.length) { + containerFavorites.innerHTML = ` +
No favorites yet 💔
+ `; + return; + } + + favorites.forEach((f) => { + const favCard = document.createElement("div"); + favCard.className = f.className; + favCard.innerHTML = f.innerHTML; + favCard.dataset.id = String(f.id); + favCard.classList.add("active"); + favCard.addEventListener("click", () => toggleFavoriteCard(favCard)); + containerFavorites.appendChild(favCard); + }); +} + +function syncAllFavoriteStates() { + const allCards = document.querySelectorAll( + ".cards__coffee-card, .cards__juice-card" + ); + allCards.forEach((card) => { + card.classList.remove("active"); + }); + + favorites.forEach((fav) => { + const cards = document.querySelectorAll(`[data-id="${fav.id}"]`); + cards.forEach((card) => { + card.classList.add("active"); + }); + }); +} + +function debugFavoriteStates() { + console.log("❤️ DEBUG FAVORITE STATES:"); + console.log("Favorites array:", favorites); + + const allCards = document.querySelectorAll( + ".cards__coffee-card, .cards__juice-card" + ); + console.log(`Total cards: ${allCards.length}`); + + const activeCards = document.querySelectorAll( + ".cards__coffee-card.active, .cards__juice-card.active" + ); + console.log(`Active cards: ${activeCards.length}`); + + activeCards.forEach((card) => { + console.log( + `Active card: ${card.querySelector("h2")?.textContent} (ID: ${ + card.dataset.id + })` + ); + }); +} + +/* ========================================== + SECTION WINDOWS WINDOWS WINDOWS WINDOWS + ========================================== */ + +window.addEventListener("DOMContentLoaded", () => { + seedIfEmpty(LS_COFFEE, DEMO_CAKES); + seedIfEmpty(LS_JUICE, DEMO_DESSERTS); + + console.log("LS_COFFEE after seed:", cacheGet(LS_COFFEE)); + console.log("LS_JUICE after seed:", cacheGet(LS_JUICE)); + + setActiveButton(buttonCoffee); + mainFruites?.classList.add("hidden"); + mainChocolate?.classList.add("hidden"); + mainFavorites?.classList.add("hidden"); + + ensureFavoritesLoaded(); + renderFavorites(); + renderCoffeeCards(); + + setTimeout(syncAllFavoriteStates, 500); +}); + +/* ==================================================================== + SECTION FILTERS / SORTING (single .filters block) BUTTON BUTTON BUTTON + ==================================================================== */ +let speedDescending = true; +let popularityDescending = true; + +filterItems.forEach((item) => { + item.addEventListener("click", async () => { + const type = item.dataset.type; // 'cuisine', 'sort', etc. + const value = item.dataset.value; + + // Visual "active" state inside the same