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
+
+
+
+---
+
+## 🚀 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+ .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
+ .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
+ item.parentElement
+ ?.querySelectorAll(".filter__item")
+ .forEach((li) => li.classList.remove("active"));
+ item.classList.add("active");
+
+ // Work against the currently visible category
+ const category = getCurrentCategory();
+ let container;
+ if (category === "coffee") container = containerCoffee;
+ else if (category === "fruits") container = containerFruit;
+ else return;
+
+ // Sorting: speed -> by data-cooking
+ if (type === "sort" && value === "speed") {
+ speedDescending = !speedDescending;
+ item.textContent = speedDescending ? "Descending" : "Ascending";
+ sortCards(container, "cooking", speedDescending);
+ return;
+ }
+
+ // Sorting: popularity -> by data-popularity
+ if (type === "sort" && value === "popular") {
+ popularityDescending = !popularityDescending;
+ item.textContent = popularityDescending ? "More popular" : "Less popular";
+ sortCards(container, "popularity", popularityDescending);
+ return;
+ }
+
+ // Sorting: random -> fetch new random card for current category
+ if (type === "sort" && value === "random") {
+ await fetchRandomCard(category);
+ return;
+ }
+
+ // Filtering: cuisine (pipe-separated) or generic
+ filterCards(container, type, value);
+ });
+});
+
+function filterCards(container, type, value) {
+ const cards = container.querySelectorAll(
+ ".cards__coffee-card, .cards__juice-card"
+ );
+
+ console.log(`🔍 Filtering by ${type} = ${value}`);
+
+ if (type === "cuisine") {
+ const wanted = (value || "all").toLowerCase();
+
+ cards.forEach((card) => {
+ const cardCuisine = (card.dataset.cuisine || "").toLowerCase();
+
+ if (wanted === "all") {
+ card.style.display = "";
+ } else {
+ card.style.display = cardCuisine === wanted ? "" : "none";
+ }
+ });
+
+ ensureNoMatchBanner(container);
+ return;
+ }
+
+ cards.forEach((card) => {
+ card.style.display =
+ value === "all" || card.dataset[type] == value ? "" : "none";
+ });
+}
+
+function sortCards(container, field, descending) {
+ const cards = Array.from(
+ container.querySelectorAll(".cards__coffee-card, .cards__juice-card")
+ );
+ cards.sort((a, b) => {
+ const aVal = toNum(a.dataset[field]);
+ const bVal = toNum(b.dataset[field]);
+ return descending ? bVal - aVal : aVal - bVal;
+ });
+ cards.forEach((card) => container.appendChild(card));
+}
+
+/* ==================================================================
+ SECTION RANDOM PICK (real random + fallbacks) BUTTON BUTTON BUTTON
+ ================================================================== */
+
+function pickRandomFromStorage(storageKey) {
+ try {
+ const arr = JSON.parse(localStorage.getItem(storageKey) || "[]");
+ if (!Array.isArray(arr) || !arr.length) return null;
+ return arr[Math.floor(Math.random() * arr.length)];
+ } catch {
+ return null;
+ }
+}
+
+function showEmptyCard(container, title, message) {
+ const emptyCard = document.createElement("div");
+ emptyCard.classList.add("cards__coffee-card", "empty-card");
+ emptyCard.innerHTML = `
+
+
${title}
+
${message}
+
`;
+ container.appendChild(emptyCard);
+}
+
+async function fetchRandomCard(category) {
+ const isCoffee = category === "coffee";
+ const container = isCoffee ? containerCoffee : containerFruit;
+ if (!container) return;
+
+ const visible = Array.from(
+ container.querySelectorAll(".cards__coffee-card, .cards__juice-card")
+ ).filter(
+ (el) => el.offsetParent !== null && !el.classList.contains("empty-card")
+ );
+
+ const currentId =
+ visible.length === 1 ? String(visible[0].dataset.id || "") : "";
+
+ const storageKey = isCoffee ? LS_COFFEE : LS_FRUITS;
+ const cacheArr = getCacheArray(storageKey);
+ const otherFromCache = pickRandomExcluding(cacheArr, currentId);
+
+ if (otherFromCache) {
+ container
+ .querySelectorAll(
+ ".cards__coffee-card, .cards__juice-card, .empty-card, .no-match-card"
+ )
+ .forEach((el) => el.remove());
+ (isCoffee ? addCoffeeCard : addFruitCard)(otherFromCache);
+ const added = container.lastElementChild;
+ if (added) {
+ added.classList.add("pulse");
+ setTimeout(() => added.classList.remove("pulse"), 800);
+ added.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ return;
+ }
+
+ try {
+ const query = isCoffee ? "cake" : "dessert";
+ const url = `${SPOON_BASE}/recipes/complexSearch?query=${encodeURIComponent(
+ query
+ )}&addRecipeInformation=true&number=1&offset=${Math.floor(
+ Math.random() * 50
+ )}&apiKey=${API_KEY}`;
+ const data = await fetchJSON(url);
+ const recipe = data.results?.[0];
+
+ if (recipe && String(recipe.id) !== currentId) {
+ container
+ .querySelectorAll(
+ ".cards__coffee-card, .cards__juice-card, .empty-card, .no-match-card"
+ )
+ .forEach((el) => el.remove());
+ (isCoffee ? addCoffeeCard : addFruitCard)(recipe);
+ const added = container.lastElementChild;
+ if (added) {
+ added.classList.add("pulse");
+ setTimeout(() => added.classList.remove("pulse"), 800);
+ added.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ return;
+ }
+ } catch (e) {
+ const code = String(e?.message || "");
+ if (code.startsWith("HTTP_402") || code.startsWith("HTTP_429")) {
+ container
+ .querySelectorAll(
+ ".cards__coffee-card, .cards__juice-card, .no-match-card"
+ )
+ .forEach((el) => el.remove());
+ showEmptyCard(
+ container,
+ "⏳ Random unavailable",
+ "API limit reached — try again after reset."
+ );
+ return;
+ }
+ }
+
+ if (visible[0]) {
+ visible[0].classList.add("pulse");
+ setTimeout(() => visible[0].classList.remove("pulse"), 800);
+ visible[0].scrollIntoView({ behavior: "smooth", block: "center" });
+ } else {
+ showEmptyCard(container, "😕 Nothing to pick", "Load some recipes first.");
+ }
+}
diff --git a/node_modules/.bin/detect-libc b/node_modules/.bin/detect-libc
new file mode 100644
index 000000000..76becf36f
--- /dev/null
+++ b/node_modules/.bin/detect-libc
@@ -0,0 +1,16 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+ *CYGWIN*|*MINGW*|*MSYS*)
+ if command -v cygpath > /dev/null 2>&1; then
+ basedir=`cygpath -w "$basedir"`
+ fi
+ ;;
+esac
+
+if [ -x "$basedir/node" ]; then
+ exec "$basedir/node" "$basedir/../detect-libc/bin/detect-libc.js" "$@"
+else
+ exec node "$basedir/../detect-libc/bin/detect-libc.js" "$@"
+fi
diff --git a/node_modules/.bin/detect-libc.cmd b/node_modules/.bin/detect-libc.cmd
new file mode 100644
index 000000000..1c5d86d2b
--- /dev/null
+++ b/node_modules/.bin/detect-libc.cmd
@@ -0,0 +1,17 @@
+@ECHO off
+GOTO start
+:find_dp0
+SET dp0=%~dp0
+EXIT /b
+:start
+SETLOCAL
+CALL :find_dp0
+
+IF EXIST "%dp0%\node.exe" (
+ SET "_prog=%dp0%\node.exe"
+) ELSE (
+ SET "_prog=node"
+ SET PATHEXT=%PATHEXT:;.JS;=;%
+)
+
+endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\detect-libc\bin\detect-libc.js" %*
diff --git a/node_modules/.bin/detect-libc.ps1 b/node_modules/.bin/detect-libc.ps1
new file mode 100644
index 000000000..5ebeae109
--- /dev/null
+++ b/node_modules/.bin/detect-libc.ps1
@@ -0,0 +1,28 @@
+#!/usr/bin/env pwsh
+$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
+
+$exe=""
+if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
+ # Fix case when both the Windows and Linux builds of Node
+ # are installed in the same directory
+ $exe=".exe"
+}
+$ret=0
+if (Test-Path "$basedir/node$exe") {
+ # Support pipeline input
+ if ($MyInvocation.ExpectingInput) {
+ $input | & "$basedir/node$exe" "$basedir/../detect-libc/bin/detect-libc.js" $args
+ } else {
+ & "$basedir/node$exe" "$basedir/../detect-libc/bin/detect-libc.js" $args
+ }
+ $ret=$LASTEXITCODE
+} else {
+ # Support pipeline input
+ if ($MyInvocation.ExpectingInput) {
+ $input | & "node$exe" "$basedir/../detect-libc/bin/detect-libc.js" $args
+ } else {
+ & "node$exe" "$basedir/../detect-libc/bin/detect-libc.js" $args
+ }
+ $ret=$LASTEXITCODE
+}
+exit $ret
diff --git a/node_modules/.bin/sass b/node_modules/.bin/sass
new file mode 100644
index 000000000..dde84001a
--- /dev/null
+++ b/node_modules/.bin/sass
@@ -0,0 +1,16 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+ *CYGWIN*|*MINGW*|*MSYS*)
+ if command -v cygpath > /dev/null 2>&1; then
+ basedir=`cygpath -w "$basedir"`
+ fi
+ ;;
+esac
+
+if [ -x "$basedir/node" ]; then
+ exec "$basedir/node" "$basedir/../sass/sass.js" "$@"
+else
+ exec node "$basedir/../sass/sass.js" "$@"
+fi
diff --git a/node_modules/.bin/sass.cmd b/node_modules/.bin/sass.cmd
new file mode 100644
index 000000000..0cf95f64c
--- /dev/null
+++ b/node_modules/.bin/sass.cmd
@@ -0,0 +1,17 @@
+@ECHO off
+GOTO start
+:find_dp0
+SET dp0=%~dp0
+EXIT /b
+:start
+SETLOCAL
+CALL :find_dp0
+
+IF EXIST "%dp0%\node.exe" (
+ SET "_prog=%dp0%\node.exe"
+) ELSE (
+ SET "_prog=node"
+ SET PATHEXT=%PATHEXT:;.JS;=;%
+)
+
+endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sass\sass.js" %*
diff --git a/node_modules/.bin/sass.ps1 b/node_modules/.bin/sass.ps1
new file mode 100644
index 000000000..715ffd5ae
--- /dev/null
+++ b/node_modules/.bin/sass.ps1
@@ -0,0 +1,28 @@
+#!/usr/bin/env pwsh
+$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
+
+$exe=""
+if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
+ # Fix case when both the Windows and Linux builds of Node
+ # are installed in the same directory
+ $exe=".exe"
+}
+$ret=0
+if (Test-Path "$basedir/node$exe") {
+ # Support pipeline input
+ if ($MyInvocation.ExpectingInput) {
+ $input | & "$basedir/node$exe" "$basedir/../sass/sass.js" $args
+ } else {
+ & "$basedir/node$exe" "$basedir/../sass/sass.js" $args
+ }
+ $ret=$LASTEXITCODE
+} else {
+ # Support pipeline input
+ if ($MyInvocation.ExpectingInput) {
+ $input | & "node$exe" "$basedir/../sass/sass.js" $args
+ } else {
+ & "node$exe" "$basedir/../sass/sass.js" $args
+ }
+ $ret=$LASTEXITCODE
+}
+exit $ret
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
new file mode 100644
index 000000000..b38027d35
--- /dev/null
+++ b/node_modules/.package-lock.json
@@ -0,0 +1,263 @@
+{
+ "name": "js-project-recipe-library",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
+ "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.93.2",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
+ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ }
+ }
+}
diff --git a/node_modules/@parcel/watcher-win32-x64/LICENSE b/node_modules/@parcel/watcher-win32-x64/LICENSE
new file mode 100644
index 000000000..7fb9bc953
--- /dev/null
+++ b/node_modules/@parcel/watcher-win32-x64/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present Devon Govett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/@parcel/watcher-win32-x64/README.md b/node_modules/@parcel/watcher-win32-x64/README.md
new file mode 100644
index 000000000..762083109
--- /dev/null
+++ b/node_modules/@parcel/watcher-win32-x64/README.md
@@ -0,0 +1 @@
+This is the win32-x64 build of @parcel/watcher. See https://github.com/parcel-bundler/watcher for details.
\ No newline at end of file
diff --git a/node_modules/@parcel/watcher-win32-x64/package.json b/node_modules/@parcel/watcher-win32-x64/package.json
new file mode 100644
index 000000000..dbbc6d156
--- /dev/null
+++ b/node_modules/@parcel/watcher-win32-x64/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@parcel/watcher-win32-x64",
+ "version": "2.5.1",
+ "main": "watcher.node",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/parcel-bundler/watcher.git"
+ },
+ "description": "A native C++ Node module for querying and subscribing to filesystem events. Used by Parcel 2.",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "files": [
+ "watcher.node"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "os": [
+ "win32"
+ ],
+ "cpu": [
+ "x64"
+ ]
+}
diff --git a/node_modules/@parcel/watcher-win32-x64/watcher.node b/node_modules/@parcel/watcher-win32-x64/watcher.node
new file mode 100644
index 000000000..32648898b
Binary files /dev/null and b/node_modules/@parcel/watcher-win32-x64/watcher.node differ
diff --git a/node_modules/@parcel/watcher/LICENSE b/node_modules/@parcel/watcher/LICENSE
new file mode 100644
index 000000000..7fb9bc953
--- /dev/null
+++ b/node_modules/@parcel/watcher/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present Devon Govett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/@parcel/watcher/README.md b/node_modules/@parcel/watcher/README.md
new file mode 100644
index 000000000..d212b9321
--- /dev/null
+++ b/node_modules/@parcel/watcher/README.md
@@ -0,0 +1,135 @@
+# @parcel/watcher
+
+A native C++ Node module for querying and subscribing to filesystem events. Used by [Parcel 2](https://github.com/parcel-bundler/parcel).
+
+## Features
+
+- **Watch** - subscribe to realtime recursive directory change notifications when files or directories are created, updated, or deleted.
+- **Query** - performantly query for historical change events in a directory, even when your program is not running.
+- **Native** - implemented in C++ for performance and low-level integration with the operating system.
+- **Cross platform** - includes backends for macOS, Linux, Windows, FreeBSD, and Watchman.
+- **Performant** - events are throttled in C++ so the JavaScript thread is not overwhelmed during large filesystem changes (e.g. `git checkout` or `npm install`).
+- **Scalable** - tens of thousands of files can be watched or queried at once with good performance.
+
+## Example
+
+```javascript
+const watcher = require('@parcel/watcher');
+const path = require('path');
+
+// Subscribe to events
+let subscription = await watcher.subscribe(process.cwd(), (err, events) => {
+ console.log(events);
+});
+
+// later on...
+await subscription.unsubscribe();
+
+// Get events since some saved snapshot in the past
+let snapshotPath = path.join(process.cwd(), 'snapshot.txt');
+let events = await watcher.getEventsSince(process.cwd(), snapshotPath);
+
+// Save a snapshot for later
+await watcher.writeSnapshot(process.cwd(), snapshotPath);
+```
+
+## Watching
+
+`@parcel/watcher` supports subscribing to realtime notifications of changes in a directory. It works recursively, so changes in sub-directories will also be emitted.
+
+Events are throttled and coalesced for performance during large changes like `git checkout` or `npm install`, and a single notification will be emitted with all of the events at the end.
+
+Only one notification will be emitted per file. For example, if a file was both created and updated since the last event, you'll get only a `create` event. If a file is both created and deleted, you will not be notifed of that file. Renames cause two events: a `delete` for the old name, and a `create` for the new name.
+
+```javascript
+let subscription = await watcher.subscribe(process.cwd(), (err, events) => {
+ console.log(events);
+});
+```
+
+Events have two properties:
+
+- `type` - the event type: `create`, `update`, or `delete`.
+- `path` - the absolute path to the file or directory.
+
+To unsubscribe from change notifications, call the `unsubscribe` method on the returned subscription object.
+
+```javascript
+await subscription.unsubscribe();
+```
+
+`@parcel/watcher` has the following watcher backends, listed in priority order:
+
+- [FSEvents](https://developer.apple.com/documentation/coreservices/file_system_events) on macOS
+- [Watchman](https://facebook.github.io/watchman/) if installed
+- [inotify](http://man7.org/linux/man-pages/man7/inotify.7.html) on Linux
+- [ReadDirectoryChangesW](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v%3Dvs.85%29.aspx) on Windows
+- [kqueue](https://man.freebsd.org/cgi/man.cgi?kqueue) on FreeBSD, or as an alternative to FSEvents on macOS
+
+You can specify the exact backend you wish to use by passing the `backend` option. If that backend is not available on the current platform, the default backend will be used instead. See below for the list of backend names that can be passed to the options.
+
+## Querying
+
+`@parcel/watcher` also supports querying for historical changes made in a directory, even when your program is not running. This makes it easy to invalidate a cache and re-build only the files that have changed, for example. It can be **significantly** faster than traversing the entire filesystem to determine what files changed, depending on the platform.
+
+In order to query for historical changes, you first need a previous snapshot to compare to. This can be saved to a file with the `writeSnapshot` function, e.g. just before your program exits.
+
+```javascript
+await watcher.writeSnapshot(dirPath, snapshotPath);
+```
+
+When your program starts up, you can query for changes that have occurred since that snapshot using the `getEventsSince` function.
+
+```javascript
+let events = await watcher.getEventsSince(dirPath, snapshotPath);
+```
+
+The events returned are exactly the same as the events that would be passed to the `subscribe` callback (see above).
+
+`@parcel/watcher` has the following watcher backends, listed in priority order:
+
+- [FSEvents](https://developer.apple.com/documentation/coreservices/file_system_events) on macOS
+- [Watchman](https://facebook.github.io/watchman/) if installed
+- [fts](http://man7.org/linux/man-pages/man3/fts.3.html) (brute force) on Linux and FreeBSD
+- [FindFirstFile](https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-findfirstfilea) (brute force) on Windows
+
+The FSEvents (macOS) and Watchman backends are significantly more performant than the brute force backends used by default on Linux and Windows, for example returning results in miliseconds instead of seconds for large directory trees. This is because a background daemon monitoring filesystem changes on those platforms allows us to query cached data rather than traversing the filesystem manually (brute force).
+
+macOS has good performance with FSEvents by default. For the best performance on other platforms, install [Watchman](https://facebook.github.io/watchman/) and it will be used by `@parcel/watcher` automatically.
+
+You can specify the exact backend you wish to use by passing the `backend` option. If that backend is not available on the current platform, the default backend will be used instead. See below for the list of backend names that can be passed to the options.
+
+## Options
+
+All of the APIs in `@parcel/watcher` support the following options, which are passed as an object as the last function argument.
+
+- `ignore` - an array of paths or glob patterns to ignore. uses [`is-glob`](https://github.com/micromatch/is-glob) to distinguish paths from globs. glob patterns are parsed with [`micromatch`](https://github.com/micromatch/micromatch) (see [features](https://github.com/micromatch/micromatch#matching-features)).
+ - paths can be relative or absolute and can either be files or directories. No events will be emitted about these files or directories or their children.
+ - glob patterns match on relative paths from the root that is watched. No events will be emitted for matching paths.
+- `backend` - the name of an explicitly chosen backend to use. Allowed options are `"fs-events"`, `"watchman"`, `"inotify"`, `"kqueue"`, `"windows"`, or `"brute-force"` (only for querying). If the specified backend is not available on the current platform, the default backend will be used instead.
+
+## WASM
+
+The `@parcel/watcher-wasm` package can be used in place of `@parcel/watcher` on unsupported platforms. It relies on the Node `fs` module, so in non-Node environments such as browsers, an `fs` polyfill will be needed.
+
+**Note**: the WASM implementation is significantly less efficient than the native implementations because it must crawl the file system to watch each directory individually. Use the native `@parcel/watcher` package wherever possible.
+
+```js
+import {subscribe} from '@parcel/watcher-wasm';
+
+// Use the module as documented above.
+subscribe(/* ... */);
+```
+
+## Who is using this?
+
+- [Parcel 2](https://parceljs.org/)
+- [VSCode](https://code.visualstudio.com/updates/v1_62#_file-watching-changes)
+- [Tailwind CSS Intellisense](https://github.com/tailwindlabs/tailwindcss-intellisense)
+- [Gatsby Cloud](https://twitter.com/chatsidhartha/status/1435647412828196867)
+- [Nx](https://nx.dev)
+- [Nuxt](https://nuxt.com)
+
+## License
+
+MIT
diff --git a/node_modules/@parcel/watcher/binding.gyp b/node_modules/@parcel/watcher/binding.gyp
new file mode 100644
index 000000000..9b8f6ffd7
--- /dev/null
+++ b/node_modules/@parcel/watcher/binding.gyp
@@ -0,0 +1,93 @@
+{
+ "targets": [
+ {
+ "target_name": "watcher",
+ "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
+ "sources": [ "src/binding.cc", "src/Watcher.cc", "src/Backend.cc", "src/DirTree.cc", "src/Glob.cc", "src/Debounce.cc" ],
+ "include_dirs" : [" unknown;
+ export interface AsyncSubscription {
+ unsubscribe(): Promise;
+ }
+ export interface Event {
+ path: FilePath;
+ type: EventType;
+ }
+ export function getEventsSince(
+ dir: FilePath,
+ snapshot: FilePath,
+ opts?: Options
+ ): Promise;
+ export function subscribe(
+ dir: FilePath,
+ fn: SubscribeCallback,
+ opts?: Options
+ ): Promise;
+ export function unsubscribe(
+ dir: FilePath,
+ fn: SubscribeCallback,
+ opts?: Options
+ ): Promise;
+ export function writeSnapshot(
+ dir: FilePath,
+ snapshot: FilePath,
+ opts?: Options
+ ): Promise;
+}
+
+export = ParcelWatcher;
\ No newline at end of file
diff --git a/node_modules/@parcel/watcher/index.js b/node_modules/@parcel/watcher/index.js
new file mode 100644
index 000000000..8afb2b112
--- /dev/null
+++ b/node_modules/@parcel/watcher/index.js
@@ -0,0 +1,41 @@
+const {createWrapper} = require('./wrapper');
+
+let name = `@parcel/watcher-${process.platform}-${process.arch}`;
+if (process.platform === 'linux') {
+ const { MUSL, family } = require('detect-libc');
+ if (family === MUSL) {
+ name += '-musl';
+ } else {
+ name += '-glibc';
+ }
+}
+
+let binding;
+try {
+ binding = require(name);
+} catch (err) {
+ handleError(err);
+ try {
+ binding = require('./build/Release/watcher.node');
+ } catch (err) {
+ handleError(err);
+ try {
+ binding = require('./build/Debug/watcher.node');
+ } catch (err) {
+ handleError(err);
+ throw new Error(`No prebuild or local build of @parcel/watcher found. Tried ${name}. Please ensure it is installed (don't use --no-optional when installing with npm). Otherwise it is possible we don't support your platform yet. If this is the case, please report an issue to https://github.com/parcel-bundler/watcher.`);
+ }
+ }
+}
+
+function handleError(err) {
+ if (err?.code !== 'MODULE_NOT_FOUND') {
+ throw err;
+ }
+}
+
+const wrapper = createWrapper(binding);
+exports.writeSnapshot = wrapper.writeSnapshot;
+exports.getEventsSince = wrapper.getEventsSince;
+exports.subscribe = wrapper.subscribe;
+exports.unsubscribe = wrapper.unsubscribe;
diff --git a/node_modules/@parcel/watcher/index.js.flow b/node_modules/@parcel/watcher/index.js.flow
new file mode 100644
index 000000000..d75da93d7
--- /dev/null
+++ b/node_modules/@parcel/watcher/index.js.flow
@@ -0,0 +1,48 @@
+// @flow
+declare type FilePath = string;
+declare type GlobPattern = string;
+
+export type BackendType =
+ | 'fs-events'
+ | 'watchman'
+ | 'inotify'
+ | 'windows'
+ | 'brute-force';
+export type EventType = 'create' | 'update' | 'delete';
+export interface Options {
+ ignore?: Array,
+ backend?: BackendType
+}
+export type SubscribeCallback = (
+ err: ?Error,
+ events: Array
+) => mixed;
+export interface AsyncSubscription {
+ unsubscribe(): Promise
+}
+export interface Event {
+ path: FilePath,
+ type: EventType
+}
+declare module.exports: {
+ getEventsSince(
+ dir: FilePath,
+ snapshot: FilePath,
+ opts?: Options
+ ): Promise>,
+ subscribe(
+ dir: FilePath,
+ fn: SubscribeCallback,
+ opts?: Options
+ ): Promise,
+ unsubscribe(
+ dir: FilePath,
+ fn: SubscribeCallback,
+ opts?: Options
+ ): Promise,
+ writeSnapshot(
+ dir: FilePath,
+ snapshot: FilePath,
+ opts?: Options
+ ): Promise
+}
\ No newline at end of file
diff --git a/node_modules/@parcel/watcher/package.json b/node_modules/@parcel/watcher/package.json
new file mode 100644
index 000000000..dc415008f
--- /dev/null
+++ b/node_modules/@parcel/watcher/package.json
@@ -0,0 +1,88 @@
+{
+ "name": "@parcel/watcher",
+ "version": "2.5.1",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/parcel-bundler/watcher.git"
+ },
+ "description": "A native C++ Node module for querying and subscribing to filesystem events. Used by Parcel 2.",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "files": [
+ "index.js",
+ "index.js.flow",
+ "index.d.ts",
+ "wrapper.js",
+ "package.json",
+ "README.md",
+ "LICENSE",
+ "src",
+ "scripts/build-from-source.js",
+ "binding.gyp"
+ ],
+ "scripts": {
+ "prebuild": "prebuildify --napi --strip --tag-libc",
+ "format": "prettier --write \"./**/*.{js,json,md}\"",
+ "build": "node-gyp rebuild",
+ "install": "node scripts/build-from-source.js",
+ "test": "mocha"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged"
+ }
+ },
+ "lint-staged": {
+ "*.{js,json,md}": [
+ "prettier --write",
+ "git add"
+ ]
+ },
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "devDependencies": {
+ "esbuild": "^0.19.8",
+ "fs-extra": "^10.0.0",
+ "husky": "^7.0.2",
+ "lint-staged": "^11.1.2",
+ "mocha": "^9.1.1",
+ "napi-wasm": "^1.1.0",
+ "prebuildify": "^6.0.1",
+ "prettier": "^2.3.2"
+ },
+ "binary": {
+ "napi_versions": [
+ 3
+ ]
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1"
+ }
+}
diff --git a/node_modules/@parcel/watcher/scripts/build-from-source.js b/node_modules/@parcel/watcher/scripts/build-from-source.js
new file mode 100644
index 000000000..4602008ba
--- /dev/null
+++ b/node_modules/@parcel/watcher/scripts/build-from-source.js
@@ -0,0 +1,13 @@
+#!/usr/bin/env node
+
+const {spawn} = require('child_process');
+
+if (process.env.npm_config_build_from_source === 'true') {
+ build();
+}
+
+function build() {
+ spawn('node-gyp', ['rebuild'], { stdio: 'inherit', shell: true }).on('exit', function (code) {
+ process.exit(code);
+ });
+}
diff --git a/node_modules/@parcel/watcher/src/Backend.cc b/node_modules/@parcel/watcher/src/Backend.cc
new file mode 100644
index 000000000..fcf554465
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/Backend.cc
@@ -0,0 +1,182 @@
+#ifdef FS_EVENTS
+#include "macos/FSEventsBackend.hh"
+#endif
+#ifdef WATCHMAN
+#include "watchman/WatchmanBackend.hh"
+#endif
+#ifdef WINDOWS
+#include "windows/WindowsBackend.hh"
+#endif
+#ifdef INOTIFY
+#include "linux/InotifyBackend.hh"
+#endif
+#ifdef KQUEUE
+#include "kqueue/KqueueBackend.hh"
+#endif
+#ifdef __wasm32__
+#include "wasm/WasmBackend.hh"
+#endif
+#include "shared/BruteForceBackend.hh"
+
+#include "Backend.hh"
+#include
+
+static std::unordered_map> sharedBackends;
+
+std::shared_ptr getBackend(std::string backend) {
+ // Use FSEvents on macOS by default.
+ // Use watchman by default if available on other platforms.
+ // Fall back to brute force.
+ #ifdef FS_EVENTS
+ if (backend == "fs-events" || backend == "default") {
+ return std::make_shared();
+ }
+ #endif
+ #ifdef WATCHMAN
+ if ((backend == "watchman" || backend == "default") && WatchmanBackend::checkAvailable()) {
+ return std::make_shared();
+ }
+ #endif
+ #ifdef WINDOWS
+ if (backend == "windows" || backend == "default") {
+ return std::make_shared();
+ }
+ #endif
+ #ifdef INOTIFY
+ if (backend == "inotify" || backend == "default") {
+ return std::make_shared();
+ }
+ #endif
+ #ifdef KQUEUE
+ if (backend == "kqueue" || backend == "default") {
+ return std::make_shared();
+ }
+ #endif
+ #ifdef __wasm32__
+ if (backend == "wasm" || backend == "default") {
+ return std::make_shared();
+ }
+ #endif
+ if (backend == "brute-force" || backend == "default") {
+ return std::make_shared();
+ }
+
+ return nullptr;
+}
+
+std::shared_ptr Backend::getShared(std::string backend) {
+ auto found = sharedBackends.find(backend);
+ if (found != sharedBackends.end()) {
+ return found->second;
+ }
+
+ auto result = getBackend(backend);
+ if (!result) {
+ return getShared("default");
+ }
+
+ result->run();
+ sharedBackends.emplace(backend, result);
+ return result;
+}
+
+void removeShared(Backend *backend) {
+ for (auto it = sharedBackends.begin(); it != sharedBackends.end(); it++) {
+ if (it->second.get() == backend) {
+ sharedBackends.erase(it);
+ break;
+ }
+ }
+
+ // Free up memory.
+ if (sharedBackends.size() == 0) {
+ sharedBackends.rehash(0);
+ }
+}
+
+void Backend::run() {
+ #ifndef __wasm32__
+ mThread = std::thread([this] () {
+ try {
+ start();
+ } catch (std::exception &err) {
+ handleError(err);
+ }
+ });
+
+ if (mThread.joinable()) {
+ mStartedSignal.wait();
+ }
+ #else
+ try {
+ start();
+ } catch (std::exception &err) {
+ handleError(err);
+ }
+ #endif
+}
+
+void Backend::notifyStarted() {
+ mStartedSignal.notify();
+}
+
+void Backend::start() {
+ notifyStarted();
+}
+
+Backend::~Backend() {
+ #ifndef __wasm32__
+ // Wait for thread to stop
+ if (mThread.joinable()) {
+ // If the backend is being destroyed from the thread itself, detach, otherwise join.
+ if (mThread.get_id() == std::this_thread::get_id()) {
+ mThread.detach();
+ } else {
+ mThread.join();
+ }
+ }
+ #endif
+}
+
+void Backend::watch(WatcherRef watcher) {
+ std::unique_lock lock(mMutex);
+ auto res = mSubscriptions.find(watcher);
+ if (res == mSubscriptions.end()) {
+ try {
+ this->subscribe(watcher);
+ mSubscriptions.insert(watcher);
+ } catch (std::exception &err) {
+ unref();
+ throw;
+ }
+ }
+}
+
+void Backend::unwatch(WatcherRef watcher) {
+ std::unique_lock lock(mMutex);
+ size_t deleted = mSubscriptions.erase(watcher);
+ if (deleted > 0) {
+ this->unsubscribe(watcher);
+ unref();
+ }
+}
+
+void Backend::unref() {
+ if (mSubscriptions.size() == 0) {
+ removeShared(this);
+ }
+}
+
+void Backend::handleWatcherError(WatcherError &err) {
+ unwatch(err.mWatcher);
+ err.mWatcher->notifyError(err);
+}
+
+void Backend::handleError(std::exception &err) {
+ std::unique_lock lock(mMutex);
+ for (auto it = mSubscriptions.begin(); it != mSubscriptions.end(); it++) {
+ (*it)->notifyError(err);
+ }
+
+ removeShared(this);
+}
diff --git a/node_modules/@parcel/watcher/src/Backend.hh b/node_modules/@parcel/watcher/src/Backend.hh
new file mode 100644
index 000000000..d673bd1a1
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/Backend.hh
@@ -0,0 +1,37 @@
+#ifndef BACKEND_H
+#define BACKEND_H
+
+#include "Event.hh"
+#include "Watcher.hh"
+#include "Signal.hh"
+#include
+
+class Backend {
+public:
+ virtual ~Backend();
+ void run();
+ void notifyStarted();
+
+ virtual void start();
+ virtual void writeSnapshot(WatcherRef watcher, std::string *snapshotPath) = 0;
+ virtual void getEventsSince(WatcherRef watcher, std::string *snapshotPath) = 0;
+ virtual void subscribe(WatcherRef watcher) = 0;
+ virtual void unsubscribe(WatcherRef watcher) = 0;
+
+ static std::shared_ptr getShared(std::string backend);
+
+ void watch(WatcherRef watcher);
+ void unwatch(WatcherRef watcher);
+ void unref();
+ void handleWatcherError(WatcherError &err);
+
+ std::mutex mMutex;
+ std::thread mThread;
+private:
+ std::unordered_set mSubscriptions;
+ Signal mStartedSignal;
+
+ void handleError(std::exception &err);
+};
+
+#endif
diff --git a/node_modules/@parcel/watcher/src/Debounce.cc b/node_modules/@parcel/watcher/src/Debounce.cc
new file mode 100644
index 000000000..be07e7828
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/Debounce.cc
@@ -0,0 +1,113 @@
+#include "Debounce.hh"
+
+#ifdef __wasm32__
+extern "C" void on_timeout(void *ctx) {
+ Debounce *debounce = (Debounce *)ctx;
+ debounce->notify();
+}
+#endif
+
+std::shared_ptr Debounce::getShared() {
+ static std::weak_ptr sharedInstance;
+ std::shared_ptr shared = sharedInstance.lock();
+ if (!shared) {
+ shared = std::make_shared();
+ sharedInstance = shared;
+ }
+
+ return shared;
+}
+
+Debounce::Debounce() {
+ mRunning = true;
+ #ifndef __wasm32__
+ mThread = std::thread([this] () {
+ loop();
+ });
+ #endif
+}
+
+Debounce::~Debounce() {
+ mRunning = false;
+ #ifndef __wasm32__
+ mWaitSignal.notify();
+ mThread.join();
+ #endif
+}
+
+void Debounce::add(void *key, std::function cb) {
+ std::unique_lock lock(mMutex);
+ mCallbacks.emplace(key, cb);
+}
+
+void Debounce::remove(void *key) {
+ std::unique_lock lock(mMutex);
+ mCallbacks.erase(key);
+}
+
+void Debounce::trigger() {
+ std::unique_lock lock(mMutex);
+ #ifdef __wasm32__
+ notifyIfReady();
+ #else
+ mWaitSignal.notify();
+ #endif
+}
+
+#ifndef __wasm32__
+void Debounce::loop() {
+ while (mRunning) {
+ mWaitSignal.wait();
+ if (!mRunning) {
+ break;
+ }
+
+ notifyIfReady();
+ }
+}
+#endif
+
+void Debounce::notifyIfReady() {
+ if (!mRunning) {
+ return;
+ }
+
+ // If we haven't seen an event in more than the maximum wait time, notify callbacks immediately
+ // to ensure that we don't wait forever. Otherwise, wait for the minimum wait time and batch
+ // subsequent fast changes. This also means the first file change in a batch is notified immediately,
+ // separately from the rest of the batch. This seems like an acceptable tradeoff if the common case
+ // is that only a single file was updated at a time.
+ auto time = std::chrono::steady_clock::now();
+ if ((time - mLastTime) > std::chrono::milliseconds(MAX_WAIT_TIME)) {
+ mLastTime = time;
+ notify();
+ } else {
+ wait();
+ }
+}
+
+void Debounce::wait() {
+ #ifdef __wasm32__
+ clear_timeout(mTimeout);
+ mTimeout = set_timeout(MIN_WAIT_TIME, this);
+ #else
+ auto status = mWaitSignal.waitFor(std::chrono::milliseconds(MIN_WAIT_TIME));
+ if (mRunning && (status == std::cv_status::timeout)) {
+ notify();
+ }
+ #endif
+}
+
+void Debounce::notify() {
+ std::unique_lock lock(mMutex);
+
+ mLastTime = std::chrono::steady_clock::now();
+ for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
+ auto cb = it->second;
+ cb();
+ }
+
+ #ifndef __wasm32__
+ mWaitSignal.reset();
+ #endif
+}
diff --git a/node_modules/@parcel/watcher/src/Debounce.hh b/node_modules/@parcel/watcher/src/Debounce.hh
new file mode 100644
index 000000000..a17fdef7b
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/Debounce.hh
@@ -0,0 +1,49 @@
+#ifndef DEBOUNCE_H
+#define DEBOUNCE_H
+
+#include
+#include
+#include
+#include "Signal.hh"
+
+#define MIN_WAIT_TIME 50
+#define MAX_WAIT_TIME 500
+
+#ifdef __wasm32__
+extern "C" {
+ int set_timeout(int ms, void *ctx);
+ void clear_timeout(int timeout);
+ void on_timeout(void *ctx);
+};
+#endif
+
+class Debounce {
+public:
+ static std::shared_ptr getShared();
+
+ Debounce();
+ ~Debounce();
+
+ void add(void *key, std::function cb);
+ void remove(void *key);
+ void trigger();
+ void notify();
+
+private:
+ bool mRunning;
+ std::mutex mMutex;
+ #ifdef __wasm32__
+ int mTimeout;
+ #else
+ Signal mWaitSignal;
+ std::thread mThread;
+ #endif
+ std::unordered_map> mCallbacks;
+ std::chrono::time_point mLastTime;
+
+ void loop();
+ void notifyIfReady();
+ void wait();
+};
+
+#endif
diff --git a/node_modules/@parcel/watcher/src/DirTree.cc b/node_modules/@parcel/watcher/src/DirTree.cc
new file mode 100644
index 000000000..ac17c15c4
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/DirTree.cc
@@ -0,0 +1,152 @@
+#include "DirTree.hh"
+#include
+
+static std::mutex mDirCacheMutex;
+static std::unordered_map> dirTreeCache;
+
+struct DirTreeDeleter {
+ void operator()(DirTree *tree) {
+ std::lock_guard lock(mDirCacheMutex);
+ dirTreeCache.erase(tree->root);
+ delete tree;
+
+ // Free up memory.
+ if (dirTreeCache.size() == 0) {
+ dirTreeCache.rehash(0);
+ }
+ }
+};
+
+std::shared_ptr DirTree::getCached(std::string root) {
+ std::lock_guard lock(mDirCacheMutex);
+
+ auto found = dirTreeCache.find(root);
+ std::shared_ptr tree;
+
+ // Use cached tree, or create an empty one.
+ if (found != dirTreeCache.end()) {
+ tree = found->second.lock();
+ } else {
+ tree = std::shared_ptr(new DirTree(root), DirTreeDeleter());
+ dirTreeCache.emplace(root, tree);
+ }
+
+ return tree;
+}
+
+DirTree::DirTree(std::string root, FILE *f) : root(root), isComplete(true) {
+ size_t size;
+ if (fscanf(f, "%zu", &size)) {
+ for (size_t i = 0; i < size; i++) {
+ DirEntry entry(f);
+ entries.emplace(entry.path, entry);
+ }
+ }
+}
+
+// Internal find method that has no lock
+DirEntry *DirTree::_find(std::string path) {
+ auto found = entries.find(path);
+ if (found == entries.end()) {
+ return NULL;
+ }
+
+ return &found->second;
+}
+
+DirEntry *DirTree::add(std::string path, uint64_t mtime, bool isDir) {
+ std::lock_guard lock(mMutex);
+
+ DirEntry entry(path, mtime, isDir);
+ auto it = entries.emplace(entry.path, entry);
+ return &it.first->second;
+}
+
+DirEntry *DirTree::find(std::string path) {
+ std::lock_guard lock(mMutex);
+ return _find(path);
+}
+
+DirEntry *DirTree::update(std::string path, uint64_t mtime) {
+ std::lock_guard lock(mMutex);
+
+ DirEntry *found = _find(path);
+ if (found) {
+ found->mtime = mtime;
+ }
+
+ return found;
+}
+
+void DirTree::remove(std::string path) {
+ std::lock_guard lock(mMutex);
+
+ DirEntry *found = _find(path);
+
+ // Remove all sub-entries if this is a directory
+ if (found && found->isDir) {
+ std::string pathStart = path + DIR_SEP;
+ for (auto it = entries.begin(); it != entries.end();) {
+ if (it->first.rfind(pathStart, 0) == 0) {
+ it = entries.erase(it);
+ } else {
+ it++;
+ }
+ }
+ }
+
+ entries.erase(path);
+}
+
+void DirTree::write(FILE *f) {
+ std::lock_guard lock(mMutex);
+
+ fprintf(f, "%zu\n", entries.size());
+ for (auto it = entries.begin(); it != entries.end(); it++) {
+ it->second.write(f);
+ }
+}
+
+void DirTree::getChanges(DirTree *snapshot, EventList &events) {
+ std::lock_guard lock(mMutex);
+ std::lock_guard snapshotLock(snapshot->mMutex);
+
+ for (auto it = entries.begin(); it != entries.end(); it++) {
+ auto found = snapshot->entries.find(it->first);
+ if (found == snapshot->entries.end()) {
+ events.create(it->second.path);
+ } else if (found->second.mtime != it->second.mtime && !found->second.isDir && !it->second.isDir) {
+ events.update(it->second.path);
+ }
+ }
+
+ for (auto it = snapshot->entries.begin(); it != snapshot->entries.end(); it++) {
+ size_t count = entries.count(it->first);
+ if (count == 0) {
+ events.remove(it->second.path);
+ }
+ }
+}
+
+DirEntry::DirEntry(std::string p, uint64_t t, bool d) {
+ path = p;
+ mtime = t;
+ isDir = d;
+ state = NULL;
+}
+
+DirEntry::DirEntry(FILE *f) {
+ size_t size;
+ if (fscanf(f, "%zu", &size)) {
+ path.resize(size);
+ if (fread(&path[0], sizeof(char), size, f)) {
+ int d = 0;
+ fscanf(f, "%" PRIu64 " %d\n", &mtime, &d);
+ isDir = d == 1;
+ }
+ }
+}
+
+void DirEntry::write(FILE *f) const {
+ fprintf(f, "%zu%s%" PRIu64 " %d\n", path.size(), path.c_str(), mtime, isDir);
+}
diff --git a/node_modules/@parcel/watcher/src/DirTree.hh b/node_modules/@parcel/watcher/src/DirTree.hh
new file mode 100644
index 000000000..328f46996
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/DirTree.hh
@@ -0,0 +1,50 @@
+#ifndef DIR_TREE_H
+#define DIR_TREE_H
+
+#include
+#include
+#include
+#include "Event.hh"
+
+#ifdef _WIN32
+#define DIR_SEP "\\"
+#else
+#define DIR_SEP "/"
+#endif
+
+struct DirEntry {
+ std::string path;
+ uint64_t mtime;
+ bool isDir;
+ mutable void *state;
+
+ DirEntry(std::string p, uint64_t t, bool d);
+ DirEntry(FILE *f);
+ void write(FILE *f) const;
+ bool operator==(const DirEntry &other) const {
+ return path == other.path;
+ }
+};
+
+class DirTree {
+public:
+ static std::shared_ptr getCached(std::string root);
+ DirTree(std::string root) : root(root), isComplete(false) {}
+ DirTree(std::string root, FILE *f);
+ DirEntry *add(std::string path, uint64_t mtime, bool isDir);
+ DirEntry *find(std::string path);
+ DirEntry *update(std::string path, uint64_t mtime);
+ void remove(std::string path);
+ void write(FILE *f);
+ void getChanges(DirTree *snapshot, EventList &events);
+
+ std::mutex mMutex;
+ std::string root;
+ bool isComplete;
+ std::unordered_map entries;
+
+private:
+ DirEntry *_find(std::string path);
+};
+
+#endif
diff --git a/node_modules/@parcel/watcher/src/Event.hh b/node_modules/@parcel/watcher/src/Event.hh
new file mode 100644
index 000000000..8d09712e5
--- /dev/null
+++ b/node_modules/@parcel/watcher/src/Event.hh
@@ -0,0 +1,109 @@
+#ifndef EVENT_H
+#define EVENT_H
+
+#include
+#include
+#include "wasm/include.h"
+#include
+#include
+#include