From f80b115caeff4f39c2677caa31b00a327b4f7dcc Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Thu, 26 Mar 2026 22:29:06 +0100 Subject: [PATCH] Reorganise the views and prepare summary --- VIEWS_AND_SCENARIOS.md | 357 +++++++++++++++++++ index.html | 9 +- js/app.js | 30 +- js/data/catalog.js | 70 ++++ js/services/pantryShopping.js | 14 + js/services/planIngredients.js | 8 + js/services/planStore.js | 7 + js/views/Filter.js | 161 +++++++-- js/views/MealPlanner.js | 279 +++++++++++++-- js/views/RecipeDetail.js | 604 ++++++++++++++++++++------------- js/views/RecipeList.js | 280 +++++++-------- js/views/Shopping.js | 59 +++- 12 files changed, 1394 insertions(+), 484 deletions(-) create mode 100644 VIEWS_AND_SCENARIOS.md diff --git a/VIEWS_AND_SCENARIOS.md b/VIEWS_AND_SCENARIOS.md new file mode 100644 index 0000000..c1d0c5a --- /dev/null +++ b/VIEWS_AND_SCENARIOS.md @@ -0,0 +1,357 @@ +# Widoki i scenariusze — Aplikacja Kuchenna + +> **Cel dokumentu:** Opis wszystkich widoków aplikacji z obecnym stanem, scenariusze użytkownika i znane problemy. Odniesienie dla dalszego rozwoju. + +> **Kontekst projektu:** To jest **prototyp / mockup** — celem jest wypracowanie UX, logiki widoków i przepływów użytkownika. Finalna aplikacja będzie pisana w innym języku z backendem. Wartość tego prototypu to przede wszystkim: struktura widoków, scenariusze, model danych i decyzje UX — nie kod sam w sobie. + +--- + +## 1. Profil użytkownika + +| Cecha | Opis | +|-------|------| +| Liczba osób | 1 (gotuje dla siebie) | +| Planowanie | Elastyczne, 1–7 dni do przodu | +| Posiłki | 3 główne (śniadanie, obiad, kolacja) + dodatkowe przy treningu | +| Powtarzalność | Duża (zwłaszcza śniadania) — kopiowanie dnia kluczowe | +| Styl gotowania | Hybrydowy: trochę meal-prep, trochę na bieżąco | +| Zakupy | Mieszane: duże zakupy + uzupełnienia w tygodniu | +| Cel dietetyczny | Utrzymanie wagi, śledzenie per posiłek | +| Pomijanie posiłków | Jawne "pomijam" (jedzenie na mieście) | +| Przepisy | Na razie katalog wbudowany (9 przepisów), bez edytora | + +### Kluczowe pain-pointy (zgłoszone) + +1. Brak możliwości pominięcia slotu w planerze +2. Brak "Dodaj do planera" z widoku przepisu +3. Ilości na liście zakupów nie do edycji +4. Brak kalorii per slot w widoku dnia +5. Za dużo kliknięć w kluczowych flow + +--- + +## 2. Przegląd widoków + +### 2.1 Przepisy (`RecipeList`) + +**Lokalizacja:** `js/views/RecipeList.js` + +Siatka 2-kolumnowa kart przepisów generowana dynamicznie z `RECIPES`. Każda karta zawiera miniaturę (placeholder), tytuł, czas przygotowania, kalorie. Kliknięcie otwiera detal. + +**Elementy:** +- Wyszukiwarka (real-time, po tytule i tagach) +- Przycisk filtrów (overlay) +- Siatka kart + +**Stan:** Kompletny. + +--- + +### 2.2 Filtry (`Filter`) + +**Lokalizacja:** `js/views/Filter.js` + +Overlay z chipami pór posiłku, tagami dietetycznymi i suwakiem czasu. Dynamicznie generowane z `MEAL_SLOTS` i tagów z `RECIPES`. + +**Elementy:** +- Chipy: pory posiłku (z `MEAL_SLOTS`) +- Chipy: tagi dietetyczne (zbierane z `RECIPES`) +- Suwak: maksymalny czas przygotowania +- Przycisk "Wyczyść" + "Pokaż X wyników" + +**Stan:** Kompletny. + +--- + +### 2.3 Szczegóły przepisu (`RecipeDetail`) + +**Lokalizacja:** `js/views/RecipeDetail.js` + +Full-screen overlay z detalami przepisu. Trzy zakładki: Składniki, Kroki, Wartości. + +**Elementy:** +- Hero (placeholder) + strzałka powrotu + **"Do planera"** (nowe) +- Tytuł, tagi, czas, kcal +- Selektor porcji (±) z przeliczaniem +- Zakładka Składniki: lista z checkboxami + badge stanu spiżarni + "Dodaj do listy zakupów" +- Zakładka Kroki: numerowane kroki +- Zakładka Wartości: kcal/białko/tłuszcze/węglowodany × porcje +- **Bottom sheet "Dodaj do planera"**: wybór dnia (7 dni) + slotu → zapis do planStore + +**Stan:** Kompletny. + +--- + +### 2.4 Planer posiłków (`MealPlanner`) + +**Lokalizacja:** `js/views/MealPlanner.js` + +Kalendarz (tydzień/miesiąc) + plan dnia z slotami posiłków. + +**Elementy kalendarza:** +- Widok tygodnia ↔ miesiąca (swipe góra/dół) +- Nawigacja: ←/→ + "Dziś" +- Kropki pod dniem = zaplanowane posiłki + +**Elementy planu dnia:** +- Nagłówek dnia + **"Kopiuj dzień"** (nowe) +- Karta podsumowania kalorycznego (kcal + makro, rozwijalne szczegóły) +- "Składniki na ten dzień" (badge z liczbą braków) +- Sloty posiłków (5 slotów z `MEAL_SLOTS`): + - **Kcal per slot** w nagłówku (nowe) + - Karty przepisów z porcjami, kcal, czasem, przyciskiem usuwania + - Kliknięcie nazwy przepisu → RecipeDetail + - "Dodaj przepis" / "Dodaj kolejny" + - **"Pomijam"** przy pustym slocie (nowe) → slot przygaszony z "Cofnij" + +**Bottom sheety:** +1. **Picker przepisów**: wyszukiwarka + "Ostatnio używane" + lista filtrowana do `allowedSlots` +2. **Składniki i spiżarnia**: porównanie potrzeb vs zapasy + prognoza tygodnia + "Dodaj braki" +3. **Kopiuj plan dnia**: lista 13 dni (3 wstecz, 10 do przodu) → kopiuje cały dzień + +**Stan:** Kompletny. + +--- + +### 2.5 Spiżarnia (`Pantry`) + +**Lokalizacja:** `js/views/Pantry.js` + +Przeglądanie i edycja stanów magazynowych składników. + +**Elementy:** +- Wyszukiwarka +- Chipy filtrów kategorii +- Toggle "Tylko na stanie" +- Siatka chipów składników (kolorowana wg stanu) +- Bottom sheet edycji: ±, input, wartości odżywcze, "Dodaj na listę zakupów" + +**Stan:** Kompletny. + +--- + +### 2.6 Zakupy (`Shopping`) + +**Lokalizacja:** `js/views/Shopping.js` + +Zarządzanie listami zakupów. + +**Elementy:** +- Selektor aktywnej listy +- Przycisk "Nowa lista" +- **Pasek akcji listy kuchennej** (widoczny gdy są zaznaczone pozycje): + - "Kupione → spiżarnia" (z **potwierdzeniem** — nowe) + - "Wyczyść kupione" +- Lista kuchenna: pogrupowana po kategorii, checkbox, **edycja ilości** (klik → prompt, nowe) +- Lista freeform: tekst + notatka, checkbox + +**Stan:** Kompletny. + +--- + +## 3. Przepływy między widokami + +``` +Przepisy ──[klik kartę]──→ Szczegóły przepisu + ├──[Dodaj do listy zakupów]──→ Zakupy (lista kuchenna) + └──[Do planera]──→ Planer (wybór dnia + slotu) + +Planer ──[klik przepis w slocie]──→ Szczegóły przepisu + ──[Składniki na ten dzień]──→ Sheet: porównanie z spiżarnią + ──[Dodaj braki do listy]──→ Zakupy (lista kuchenna) + ──[Kopiuj dzień]──→ Sheet: wybór dnia docelowego + +Spiżarnia ──[Dodaj na listę zakupów]──→ Zakupy + +Zakupy ──[Kupione → spiżarnia]──→ Spiżarnia (stany zaktualizowane) +``` + +--- + +## 4. Scenariusze użytkownika + +### Scenariusz 1: Przeglądanie przepisów + +**Cel:** Użytkownik otwiera apkę, chce zobaczyć co jest dostępne. + +1. Otwiera aplikację → widzi zakładkę **Przepisy** z siatką 9 kart +2. Przewija listę, czyta opisy i kalorie na kartach +3. Klika kartę "Makaron z pomidorami i bazylią" +4. Widzi detal: składniki, kroki, wartości odżywcze +5. Zmienia liczbę porcji z 1 na 3 → składniki i kcal się przeliczają +6. Przełącza zakładkę na "Kroki" → widzi kroki +7. Przełącza na "Wartości" → widzi makroskładniki ×3 +8. Wraca strzałką ← do listy + +**Znane problemy:** +- Brak zdjęć (szare placeholdery) — OK dla prototypu +- Po powrocie z detalu filtr/szukajka powinny się utrzymać (utrzymują się) + +--- + +### Scenariusz 2: Szukanie przepisu na kolację + +**Cel:** Użytkownik szuka czegoś konkretnego. + +1. Wpisuje "łosoś" → lista filtruje się do 1 karty +2. Kasuje tekst → wracają wszystkie +3. Klika ikonę filtrów +4. Zaznacza "Kolacja" → lista się zawęża +5. Dodatkowo zaznacza tag "Wysokobiałkowe" +6. Ustawia suwak na max 25 min +7. Klika "Pokaż X wyników" → wraca do przefiltrowanej listy +8. Wybiera przepis i otwiera detal + +**Znane problemy:** +- Brak wizualnego wskaźnika aktywnych filtrów na ikonce (badge/kropka) +- Po zamknięciu filtrów ← (zamiast "Pokaż") — zachowanie może być nieintuicyjne + +--- + +### Scenariusz 3: Planowanie posiłków na tydzień + +**Cel:** Użytkownik układa menu na kilka dni. + +1. Przechodzi na zakładkę **Planer** +2. Widzi dzisiejszy dzień z demo-danymi +3. Klika na przepis w slocie → otwiera się detal +4. Wraca ← do planera +5. Klika następny dzień w kalendarzu +6. Widzi puste sloty, klika "Dodaj przepis" przy Śniadaniu +7. Picker: wpisuje fragment nazwy, widzi "Ostatnio używane" +8. Wybiera przepis → pojawia się w slocie z **kcal w nagłówku** +9. Przy obiedzie klika **"Pomijam"** (je na mieście) → slot przygaszony +10. Klika "Składniki na ten dzień" → widzi braki vs spiżarnia +11. Klika "Dodaj braki na dziś do listy" → toast +12. Następnego dnia: **"Kopiuj dzień"** → wybiera dzień docelowy → gotowe + +**Znane problemy:** +- Po dodaniu braków brak informacji "już dodano" — ryzyko duplikacji +- "Kopiuj dzień" kopiuje też status "Pominięto" + +--- + +### Scenariusz 4: Zarządzanie spiżarnią + +**Cel:** Użytkownik sprawdza co ma w domu. + +1. Przechodzi na zakładkę **Spiżarnia** +2. Widzi chipy składników pogrupowane po kategorii +3. Włącza "Tylko na stanie" → widzi co ma +4. Klika "Płatki owsiane" → bottom sheet +5. Ustawia 500g +6. Zamyka → chip zmienił się na zielony z "500 g" +7. Chce dodać mleko na listę → klika "Dodaj na listę" w sheecie + +**Znane problemy:** +- Po zamknięciu bottom sheet scroll wraca na górę (re-render) +- Brak możliwości ręcznego wpisania ilości szybko (ale jest input type=number) + +--- + +### Scenariusz 5: Zakupy w sklepie + +**Cel:** Użytkownik jest w sklepie, odznacza kupione. + +1. Otwiera zakładkę **Zakupy** +2. Widzi listę kuchenną pogrupowaną po kategorii +3. Bierze mleko z półki → klika checkbox → przekreślenie +4. Widzi `sourceNote`: "Braki z planu dnia" +5. Ilość się nie zgadza — klika na **ilość** → prompt → poprawia +6. Kupuje dalsze pozycje + +**Znane problemy:** +- Kupione mieszają się z niekupionymi w grupie (brak separacji) +- Brak podsumowania: "Do kupienia: 5, kupione: 3" +- Na telefonie w sklepie — elementy mogłyby być większe + +--- + +### Scenariusz 6: Po zakupach — przeniesienie do spiżarni + +**Cel:** Użytkownik wrócił ze sklepu, aktualizuje spiżarnię. + +1. Otwiera **Zakupy** → widzi zaznaczone pozycje +2. Pojawia się pasek: **"Kupione → spiżarnia"** i **"Wyczyść kupione"** +3. Klika "Kupione → spiżarnia" → **potwierdzenie z podglądem** pozycji +4. Potwierdza → toast "Przeniesiono X pozycji" +5. Kupione znikają z listy +6. Przechodzi do **Spiżarni** → stany zaktualizowane +7. Wraca do **Planera** → braki zmniejszone + +**Znane problemy:** +- Brak undo (cofnięcia przeniesienia) + +--- + +### Scenariusz 7: Gotowanie z przepisu + +**Cel:** Użytkownik gotuje, sprawdza przepis krok po kroku. + +1. Otwiera **Planer** → dzisiejszy dzień +2. Klika przepis w slocie Kolacja +3. Otwiera się detal → przełącza na "Kroki" +4. Czyta krok po kroku +5. Sprawdza ilość składnika → przełącza na "Składniki" +6. Wraca na "Kroki" + +**Znane problemy:** +- Brak trybu "krok po kroku" (full-screen, jeden krok, swipe next) +- Ekran wygasa po chwili (brak wake lock) +- Nie ma checkboxa przy krokach + +--- + +### Scenariusz 8: "Co mogę ugotować?" + +**Cel:** Użytkownik ma coś w spiżarni, chce wiedzieć co da się z tego zrobić. + +1. Otwiera **Spiżarnię** → widzi co ma +2. Chciałby kliknąć "Co mogę ugotować?" → **takiej funkcji jeszcze nie ma** +3. Musi ręcznie sprawdzać przepisy + +**Propozycja:** Filtr "Mam składniki" w widoku Przepisy. + +--- + +## 5. Tabela znanych problemów i luk + +| # | Problem | Scenariusz | Status | +|---|---------|-----------|--------| +| 1 | Brak wskaźnika aktywnych filtrów na ikonce | 2 | TODO | +| 2 | Podwójne dodanie braków do listy zakupów | 3 | TODO | +| 3 | Kupione mieszają się z niekupionymi (brak separacji) | 5 | TODO | +| 4 | Brak podsumowania na liście zakupów (ile kupione/do kupienia) | 5 | TODO | +| 5 | Brak undo przy "Kupione → spiżarnia" | 6 | TODO | +| 6 | Brak trybu "krok po kroku" przy gotowaniu | 7 | PROPOZYCJA | +| 7 | Brak wake lock (ekran wygasa) | 7 | PROPOZYCJA | +| 8 | Brak filtra "Mam składniki" | 8 | PROPOZYCJA | +| 9 | Scroll spiżarni resetuje się po edycji | 4 | TODO | +| 10 | Zamknięcie filtrów ← vs "Pokaż" — niespójne zachowanie? | 2 | DO WERYFIKACJI | + +--- + +## 6. Zrealizowane w ostatniej iteracji + +| # | Funkcja | Widok | +|---|---------|-------| +| P1 | "Dodaj do planera" z widoku przepisu (wybór dnia + slotu) | RecipeDetail | +| P2 | Kopiowanie planu dnia na inny dzień | MealPlanner | +| P3 | "Pomijam" w slocie planera | MealPlanner + planStore | +| P4 | Kalorie per slot w nagłówku slotu | MealPlanner | +| P5 | Edycja ilości na liście zakupów (klik → prompt) | Shopping | +| P6 | Potwierdzenie "Kupione → spiżarnia" z podglądem | Shopping | +| P7 | Lepszy picker przepisów (wyszukiwarka + ostatnio używane) | MealPlanner | + +--- + +## 7. Stos technologiczny + +| Warstwa | Technologia | +|---------|-------------| +| Frontend | Plain HTML + ES modules, imperatywna manipulacja DOM | +| Style | Tailwind CSS (CDN), Font Awesome 6 (CDN) | +| Dane | Statyczny katalog `js/data/catalog.js`, localStorage | +| PWA | manifest.webmanifest + sw.js | +| Deploy | Docker + nginx:alpine, Gitea CI/CD | +| Język UI | Polski | diff --git a/index.html b/index.html index 52c5a01..bb41cf7 100644 --- a/index.html +++ b/index.html @@ -24,9 +24,12 @@ } /* Ingredient Active States */ - .ingredient-active .check-box { background-color: #111827; border-color: #111827; } - .ingredient-active .check-icon { display: block; } - .ingredient-active .ingredient-text { text-decoration: line-through; color: #9ca3af; } + .ingredient-active .check-box, + .ingredient-active .rd-check-box { background-color: #111827; border-color: #111827; } + .ingredient-active .check-icon, + .ingredient-active .rd-check-icon { display: block; } + .ingredient-active .ingredient-text, + .ingredient-active .rd-ing-text { text-decoration: line-through; color: #9ca3af; } /* Utilities */ .no-scrollbar::-webkit-scrollbar { display: none; } diff --git a/js/app.js b/js/app.js index 824b38a..b99edb2 100644 --- a/js/app.js +++ b/js/app.js @@ -1,4 +1,4 @@ -import { getRecipeListHTML } from './views/RecipeList.js'; +import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js'; import { getFilterHTML, setupFilter } from './views/Filter.js'; import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetail.js'; import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js'; @@ -95,36 +95,10 @@ document.addEventListener('DOMContentLoaded', () => { `; setupTabs(); + setupRecipeList(); setupMealPlanner(); setupPantry(); setupShopping(); setupFilter(); setupRecipeDetail(); }); - -// --- GLOBAL NAVIGATION METHODS --- -window.openRecipeDetail = () => { - const view = document.getElementById('recipe-detail-view'); - // Swap Tailwind classes to slide IN - view.classList.remove('translate-x-full', 'opacity-0', 'pointer-events-none'); - view.classList.add('translate-x-0', 'opacity-100', 'pointer-events-auto'); -}; - -window.closeRecipeDetail = () => { - const view = document.getElementById('recipe-detail-view'); - // Swap Tailwind classes to slide OUT - view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto'); - view.classList.add('translate-x-full', 'opacity-0', 'pointer-events-none'); -}; - -window.openFilters = () => { - const fv = document.getElementById('filter-view'); - fv.classList.remove('hidden'); - fv.classList.add('flex'); -}; - -window.closeFilters = () => { - const fv = document.getElementById('filter-view'); - fv.classList.add('hidden'); - fv.classList.remove('flex'); -}; diff --git a/js/data/catalog.js b/js/data/catalog.js index 8162b04..96dea00 100644 --- a/js/data/catalog.js +++ b/js/data/catalog.js @@ -208,113 +208,177 @@ export const RECIPES = { placki: { id: 'placki', title: 'Puszyste placki', + description: 'Klasyczne placki na śniadanie — puszyste i złociste.', minutes: 15, thumbLabel: 'Placki', allowedSlots: ['sniadanie', 'drugie_sniadanie'], + tags: ['wegetariańskie', 'słodkie'], nutritionPerServing: { kcal: 320, protein: 12, fat: 8, carbs: 48 }, ingredients: [ { ingredientId: 'maka_pszenna', amount: 200, unit: 'g' }, { ingredientId: 'mleko', amount: 250, unit: 'ml' }, { ingredientId: 'jajko', amount: 2, unit: 'szt.' }, ], + steps: [ + 'Mąkę przesiej do miski, dodaj szczyptę soli.', + 'Wbij jajka, wlej mleko i wymieszaj trzepaczką na gładkie ciasto.', + 'Rozgrzej patelnię z odrobiną masła na średnim ogniu.', + 'Nakładaj ciasto łyżką wazową i smaż placki po ok. 2 min z każdej strony.', + ], }, salatka: { id: 'salatka', title: 'Sałatka z kurczakiem', + description: 'Zielone warzywa z grillowanym kurczakiem.', minutes: 20, thumbLabel: 'Sałatka', allowedSlots: ['obiad'], + tags: ['wysokobiałkowe', 'niskokaloryczne'], nutritionPerServing: { kcal: 250, protein: 35, fat: 9, carbs: 12 }, ingredients: [ { ingredientId: 'piers_kurczaka', amount: 150, unit: 'g' }, { ingredientId: 'mix_salat', amount: 100, unit: 'g' }, { ingredientId: 'pomidor', amount: 1, unit: 'szt.' }, ], + steps: [ + 'Pierś z kurczaka przypraw solą i pieprzem, griluj na patelni ok. 5 min z każdej strony.', + 'Pokrój kurczaka w paski i odłóż do ostygnięcia.', + 'Wymieszaj mix sałat z pokrojonym pomidorem.', + 'Ułóż kurczaka na sałatce, polej ulubionym dressingiem.', + ], }, makaron: { id: 'makaron', title: 'Makaron z pomidorami i bazylią', + description: 'Aromatyczny sos pomidorowy z czosnkiem i świeżą bazylią.', minutes: 30, thumbLabel: 'Makaron', allowedSlots: ['obiad', 'kolacja'], + tags: ['wegetariańskie', 'wegańskie'], nutritionPerServing: { kcal: 450, protein: 14, fat: 12, carbs: 72 }, ingredients: [ { ingredientId: 'makaron_suchy', amount: 120, unit: 'g' }, { ingredientId: 'pomidory_krojone', amount: 400, unit: 'g' }, { ingredientId: 'bazylia_swieza', amount: 10, unit: 'g' }, ], + steps: [ + 'Ugotuj makaron al dente wg instrukcji na opakowaniu.', + 'Na patelni rozgrzej oliwę, dodaj pomidory krojone i gotuj 10 min.', + 'Dopraw solą, pieprzem i szczyptą cukru.', + 'Wymieszaj makaron z sosem, udekoruj świeżą bazylią.', + ], }, koktajl: { id: 'koktajl', title: 'Koktajl owocowy', + description: 'Mix jagód i jogurtu — szybka przekąska lub drugie śniadanie.', minutes: 5, thumbLabel: 'Koktajl', allowedSlots: ['przekaska', 'drugie_sniadanie'], + tags: ['wegetariańskie', 'szybkie'], nutritionPerServing: { kcal: 180, protein: 8, fat: 3, carbs: 32 }, ingredients: [ { ingredientId: 'jogurt_naturalny', amount: 200, unit: 'g' }, { ingredientId: 'mieszanka_jagod', amount: 150, unit: 'g' }, { ingredientId: 'miod', amount: 15, unit: 'g' }, ], + steps: [ + 'Wrzuć jogurt, jagody i miód do blendera.', + 'Zmiksuj na gładką masę (~30 sekund).', + 'Przelej do szklanki. Gotowe!', + ], }, tost_awokado: { id: 'tost_awokado', title: 'Tost z awokado', + description: 'Chleb na zakwasie z rozgniecionym awokado i cytryną.', minutes: 10, thumbLabel: 'Tost', allowedSlots: ['sniadanie', 'drugie_sniadanie'], + tags: ['wegetariańskie', 'wegańskie', 'szybkie'], nutritionPerServing: { kcal: 220, protein: 6, fat: 14, carbs: 20 }, ingredients: [ { ingredientId: 'chleb_zakwas', amount: 2, unit: 'kromki' }, { ingredientId: 'awokado', amount: 1, unit: 'szt.' }, { ingredientId: 'cytryna', amount: 0.5, unit: 'szt.' }, ], + steps: [ + 'Opiecz kromki chleba w tosterze lub na suchej patelni.', + 'Przekrój awokado, wyjmij pestkę i wyłóż miąższ do miseczki.', + 'Rozgnieć widelcem, dodaj sok z cytryny, sól i pieprz.', + 'Nałóż masę na tosty. Podawaj od razu.', + ], }, losos: { id: 'losos', title: 'Grillowany łosoś', + description: 'Świeży łosoś z masłem cytrynowym i koperkiem.', minutes: 25, thumbLabel: 'Łosoś', allowedSlots: ['kolacja', 'obiad'], + tags: ['wysokobiałkowe'], nutritionPerServing: { kcal: 380, protein: 38, fat: 22, carbs: 4 }, ingredients: [ { ingredientId: 'losos_filet', amount: 180, unit: 'g' }, { ingredientId: 'cytryna', amount: 0.5, unit: 'szt.' }, { ingredientId: 'koper_swiezy', amount: 5, unit: 'g' }, ], + steps: [ + 'Filety oprósz solą, pieprzem i skrop sokiem z cytryny.', + 'Rozgrzej patelnię grillową na dość mocnym ogniu.', + 'Smaż łososia 4–5 min z każdej strony (skórą do dołu na start).', + 'Podawaj z posiekanym koperkiem i plasterkiem cytryny.', + ], }, tacos: { id: 'tacos', title: 'Tacos z wołowiną', + description: 'Pikantna mielona wołowina ze świeżą salsą w tortillach.', minutes: 20, thumbLabel: 'Tacos', allowedSlots: ['kolacja', 'obiad'], + tags: [], nutritionPerServing: { kcal: 410, protein: 28, fat: 18, carbs: 38 }, ingredients: [ { ingredientId: 'mieso_wol_mielone', amount: 200, unit: 'g' }, { ingredientId: 'tortilla_kukurydziana', amount: 4, unit: 'szt.' }, { ingredientId: 'salsa_pomidorowa', amount: 100, unit: 'g' }, ], + steps: [ + 'Na rozgrzanej patelni podsmaż mielone, rozbijając widelcem, aż się zarumieni.', + 'Dopraw kuminem, papryką, solą i pieprzem.', + 'Podgrzej tortille na suchej patelni po 15 sek. z każdej strony.', + 'Nałóż mięso na tortillę, polej salsą i zawiń.', + ], }, owsianka: { id: 'owsianka', title: 'Miska owsianki', + description: 'Ciepła owsianka z miodem — szybki i sycący posiłek.', minutes: 10, thumbLabel: 'Owsianka', allowedSlots: ['sniadanie', 'drugie_sniadanie'], + tags: ['wegetariańskie', 'szybkie'], nutritionPerServing: { kcal: 210, protein: 8, fat: 6, carbs: 34 }, ingredients: [ { ingredientId: 'platki_owsiane', amount: 60, unit: 'g' }, { ingredientId: 'mleko', amount: 200, unit: 'ml' }, { ingredientId: 'miod', amount: 20, unit: 'g' }, ], + steps: [ + 'W garnuszku zagotuj mleko.', + 'Wsyp płatki owsiane, zmniejsz ogień i gotuj 3–4 min, mieszając.', + 'Przełóż do miski, polej miodem. Opcjonalnie dodaj owoce lub orzechy.', + ], }, serek_owoc: { id: 'serek_owoc', title: 'Serek wiejski z orzechami i owocami', + description: 'Lekki, pożywny posiłek: serek z orzechami, truskawkami i borówkami.', minutes: 5, thumbLabel: 'Serek', allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'], + tags: ['wegetariańskie', 'wysokobiałkowe', 'szybkie'], nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 }, ingredients: [ { ingredientId: 'serek_wiejski', amount: 200, unit: 'g' }, @@ -323,6 +387,12 @@ export const RECIPES = { { ingredientId: 'truskawki', amount: 100, unit: 'g' }, { ingredientId: 'borowki_amerykanskie', amount: 80, unit: 'g' }, ], + steps: [ + 'Przełóż serek wiejski do miseczki.', + 'Dodaj miód i delikatnie wymieszaj.', + 'Orzechy posiekaj na mniejsze kawałki i posyp nimi serek z miodem.', + 'Umyj owoce (ew. pokrój na połówki) i ułóż na wierzchu. Gotowe!', + ], }, }; diff --git a/js/services/pantryShopping.js b/js/services/pantryShopping.js index 52eab32..792100d 100644 --- a/js/services/pantryShopping.js +++ b/js/services/pantryShopping.js @@ -296,6 +296,20 @@ export function toggleItemInList(listId, itemId) { return s; } +export function updateKitchenItemAmount(listId, itemId, newAmount) { + const s = loadShoppingState(); + const list = s.lists.find((l) => l.id === listId && l.type === 'kitchen'); + if (!list) return s; + const it = /** @type {KitchenShoppingItem[]} */ (list.items).find((i) => i.id === itemId); + if (!it) return s; + it.amount = Math.max(0, Math.round(newAmount * 100) / 100); + if (it.amount <= 0) { + list.items = list.items.filter((i) => i.id !== itemId); + } + saveShoppingState(s); + return s; +} + export function removeItemFromList(listId, itemId) { const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId); diff --git a/js/services/planIngredients.js b/js/services/planIngredients.js index c5fcc95..dada11e 100644 --- a/js/services/planIngredients.js +++ b/js/services/planIngredients.js @@ -5,7 +5,9 @@ import { getDayPlan } from './planStore.js'; export function dayHasAnyMeal(plans, d) { const p = getDayPlan(plans, d); + const skipped = p._skipped || {}; return MEAL_SLOTS.some((s) => { + if (skipped[s.id]) return false; const arr = p[s.id]; return Array.isArray(arr) && arr.length > 0; }); @@ -17,7 +19,9 @@ export function sumDayNutrition(dayPlan) { let fat = 0; let carbs = 0; let mealCount = 0; + const skipped = dayPlan._skipped || {}; MEAL_SLOTS.forEach((slot) => { + if (skipped[slot.id]) return; const entries = dayPlan[slot.id]; if (!Array.isArray(entries)) return; entries.forEach((entry) => { @@ -56,7 +60,9 @@ function resolveLine(ing, scaledAmount) { export function flattenDayIngredientLines(dayPlan) { /** @type {ReturnType[]} */ const out = []; + const skipped = dayPlan._skipped || {}; MEAL_SLOTS.forEach((slot) => { + if (skipped[slot.id]) return; const entries = dayPlan[slot.id]; if (!Array.isArray(entries)) return; entries.forEach((entry) => { @@ -112,7 +118,9 @@ export function aggregateWeekIngredientNeed(plans, weekStart) { export function aggregateDayIngredientsBySlot(dayPlan) { /** @type {{ mealLabel: string, recipes: { recipeTitle: string, items: { ingredientId: string, name: string, amount: number, unit: string, category: string }[] }[] }[]} */ const blocks = []; + const skipped = dayPlan._skipped || {}; MEAL_SLOTS.forEach((slot) => { + if (skipped[slot.id]) return; const entries = dayPlan[slot.id]; if (!Array.isArray(entries) || entries.length === 0) return; const recipes = []; diff --git a/js/services/planStore.js b/js/services/planStore.js index 4169323..4f17ddb 100644 --- a/js/services/planStore.js +++ b/js/services/planStore.js @@ -47,6 +47,13 @@ export function normalizeDayPlan(day) { const arr = normalizeSlotValue(day[s.id]); if (arr.length > 0) out[s.id] = arr; }); + if (day._skipped && typeof day._skipped === 'object') { + const sk = {}; + for (const k of Object.keys(day._skipped)) { + if (day._skipped[k] === true) sk[k] = true; + } + if (Object.keys(sk).length > 0) out._skipped = sk; + } return out; } diff --git a/js/views/Filter.js b/js/views/Filter.js index 285692b..057ba14 100644 --- a/js/views/Filter.js +++ b/js/views/Filter.js @@ -1,41 +1,48 @@ +import { RECIPES } from '../data/catalog.js'; +import { MEAL_SLOTS } from '../planner/mealSlots.js'; +import { applyFilters, getFilterState, getFilteredCount, refreshRecipeList } from './RecipeList.js'; + +function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function collectAllTags() { + const tags = new Set(); + for (const r of Object.values(RECIPES)) { + for (const t of (r.tags || [])) tags.add(t); + } + return [...tags].sort((a, b) => a.localeCompare(b, 'pl')); +} + export function getFilterHTML() { return `