Add more example recipes
All checks were successful
Build and Deploy / build-and-push (push) Successful in 27s

This commit is contained in:
2026-03-27 22:52:14 +01:00
parent 58ff081bc8
commit 7944ad2dbf
13 changed files with 458 additions and 603 deletions

View File

@@ -1,6 +1,6 @@
# 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.
> **Cel dokumentu:** Opis widoków, przepływów i scenariuszy użytkownika. 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.
@@ -11,14 +11,15 @@
| Cecha | Opis |
|-------|------|
| Liczba osób | 1 (gotuje dla siebie) |
| Planowanie | Elastyczne, 17 dni do przodu |
| Planowanie | Mieszane — część z wyprzedzeniem (np. niedziela na tydzień), część ad hoc na bieżąco |
| Posiłki | 5 slotów (śniadanie, drugie śniadanie, obiad, przekąska, kolacja) |
| 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 itp.) |
| Przepisy | Katalog wbudowany (9 przepisów, 24 składniki), bez edytora |
| Powtarzalność | Duża (zwłaszcza śniadania), ale chce stopniowo urozmaicać |
| Styl gotowania | Hybrydowy: głównie na bieżąco, ale chce zacząć gotować na zapas (np. zupy na kilka dni) |
| Jedzenie poza domem | 23× w tygodniu (restauracja, kantyna, zamówienie) — stąd jawne "Pomijam" |
| Zakupy | Duże zakupy raz w tygodniu + drobne uzupełnienia |
| Cel dietetyczny | Świadomość makro — chce widzieć wartości, ale nie liczy co do grama |
| Bóle | Brak inspiracji ("co ugotować?"), marnowanie jedzenia, brak czasu w tygodniu, chaotyczne zakupy |
| Przepisy | Katalog wbudowany (6 przepisów, 34 składniki), bez edytora |
---
@@ -45,7 +46,8 @@ js/
views/
RecipeList.js ← lista przepisów
Filter.js ← overlay filtrów
RecipeDetail.js ← detal przepisu (slide-in overlay)
RecipeDetailV2.js ← detal przepisu (aktywna wersja)
RecipeDetail.js ← detal przepisu (oryginał, nieużywany — 3-zakładkowy)
MealPlanner.js ← planer posiłków + kalendarz
Pantry.js ← spiżarnia
Shopping.js ← listy zakupów
@@ -68,128 +70,42 @@ js/
### 3.1 Przepisy (`RecipeList`)
**Lokalizacja:** `js/views/RecipeList.js`
Siatka 2-kolumnowa kart przepisów z wyszukiwarką i filtrami. Kliknięcie karty otwiera detal. Filtry (overlay `Filter.js`): pory posiłku, tagi dietetyczne, suwak czasu.
Siatka 2-kolumnowa kart przepisów generowana dynamicznie z `RECIPES`. Każda karta zawiera miniaturę (placeholder), tytuł, opis, czas przygotowania, kalorie, chipy slotów. Kliknięcie otwiera detal.
### 3.2 Szczegóły przepisu (`RecipeDetailV2`)
**Elementy:**
- Wyszukiwarka (real-time, po tytule i tagach)
- Przycisk filtrów (otwiera overlay)
- Siatka kart
- Empty state gdy brak wyników
Slide-in overlay z detalami przepisu. Dwie zakładki: **Składniki** i **Kroki**.
**Stan:** Kompletny.
**Zakładka Składniki:**
- Podsumowanie wartości odżywczych na górze (4-kolumnowa siatka: kalorie, białko, węgle, tłuszcze) — przeliczane dynamicznie z uwzględnieniem wybranych zamienników
- Składniki jako karty z wartościami odżywczymi per składnik (makro + kcal po prawej, obok gramów)
- **Wymienne składniki** — kliknięcie ikony shuffle rozwija listę opcji (oryginał + alternatywy) z radio-przyciskami; wybranie zamiennika: karta zmienia się na wybraną opcję (amber obramowanie), podsumowanie kaloryczne przelicza się na żywo
- Selektor porcji (±, zakres 112) przelicza składniki i wartości
---
**Bottom sheet "Zaplanuj":**
1. Kalendarz (tydzień/miesiąc, nawigacja ←/→/Dziś)
2. Pora posiłku — chipy filtrowane do `allowedSlots` przepisu
3. Wymienne składniki — **wstępnie ustawione z wyborów na liście składników**, z możliwością dalszej zmiany
4. Przycisk "Dodaj" → zapis do `planStore` (z opcjonalnym obiektem `substitutions`)
### 3.2 Filtry (`Filter`)
**Lokalizacja:** `js/views/Filter.js`
Full-screen overlay (z-50) z chipami pór posiłku, tagami dietetycznymi i suwakiem czasu. Filtr zamknięty przyciskiem ← odrzuca niezapisane zmiany; "Pokaż X wyników" aplikuje i zamyka.
**Elementy:**
- Chipy: pory posiłku (z `MEAL_SLOTS`)
- Chipy: tagi dietetyczne (zbierane dynamicznie z `RECIPES`)
- Suwak: maksymalny czas przygotowania (5120 min)
- Przycisk "Wyczyść" + "Pokaż X wyników"
**Stan:** Kompletny.
---
### 3.3 Szczegóły przepisu (`RecipeDetail`)
**Lokalizacja:** `js/views/RecipeDetail.js`
Slide-in overlay z detalami przepisu. Trzy zakładki: Składniki, Kroki, Wartości.
**Elementy:**
- Hero (placeholder) + strzałka powrotu + przycisk "Zaplanuj"
- Tytuł, tagi (sloty + tagi przepisu), czas, kcal
- Selektor porcji (± , zakres 112) z przeliczaniem składników i wartości
- Zakładka Składniki: lista read-only (bez checkboxów, bez badge'ów spiżarni). Składniki z wymiennymi wariantami mają ikonę shuffle — kliknięcie rozwija karty z alternatywami i ich wartościami odżywczymi (informacyjnie, bez możliwości wyboru)
- Zakładka Kroki: numerowane kroki
- Zakładka Wartości: kcal/białko/tłuszcze/węglowodany × porcje
- Bottom sheet "Zaplanuj":
1. Kalendarz (tydzień/miesiąc, nawigacja ←/→/Dziś, toggle rozwinięcia) — styl ujednolicony z `MealPlanner`
2. Pora posiłku — chipy filtrowane do `allowedSlots` przepisu
3. Wymienne składniki (opcjonalne, widoczne tylko gdy przepis ma `alternatives`) — kompaktowe karty per składnik wyświetlające aktualny wybór z wartościami odżywczymi; kliknięcie rozwija listę opcji z radio-przyciskami; po wyborze karta się zwija; zmieniony składnik ma amber tło
4. Przycisk "Dodaj" → zapis do `planStore` (z opcjonalnym obiektem `substitutions`)
> **Uwaga:** Istnieje starsza wersja (`RecipeDetail.js`) z 3 zakładkami (Składniki, Kroki, Wartości) i read-only alternatywami. Aktualnie nieużywana — import w `app.js` wskazuje na `RecipeDetailV2.js`.
**Model danych — wymienne składniki:**
- W `RECIPES`, składnik może mieć pole `alternatives: ['id1', 'id2', ...]` — tablica ID alternatywnych składników
- Przy dodawaniu do planera, wybrane zamienniki zapisywane jako `substitutions: { originalId: chosenAltId }` w `planStore`
- W `RECIPES`, składnik może mieć pole `alternatives: ['id1', 'id2', ...]`
- Wybrane zamienniki zapisywane jako `substitutions: { originalId: chosenAltId }` w `planStore`
- Przykład: serek wiejski ma 3 wymienne składniki — orzechy (5 opcji), truskawki (banany), borówki (jagody)
**Stan:** Kompletny.
### 3.3 Planer posiłków (`MealPlanner`)
---
Kalendarz (tydzień/miesiąc) + plan dnia z 5 slotami posiłków. Karty przepisów z porcjami (±), kcal, usuwaniem. "Pomijam" przy pustym slocie. Podsumowanie kaloryczne dnia. "Składniki na ten dzień" z porównaniem do spiżarni i prognozą tygodniową. Kopiowanie planu dnia. Picker przepisów do dodawania. Demo-dane przy pustym localStorage.
### 3.4 Planer posiłków (`MealPlanner`)
### 3.4 Spiżarnia (`Pantry`)
**Lokalizacja:** `js/views/MealPlanner.js`
Chipy składników pogrupowane po kategorii z kolorami wg stanu. Wyszukiwarka, filtry kategorii, toggle "Tylko na stanie". Bottom sheet edycji z ± i inputem, wartościami odżywczymi, "Dodaj na listę zakupów".
Kalendarz (tydzień/miesiąc) + plan dnia z 5 slotami posiłków.
### 3.5 Zakupy (`Shopping`)
**Elementy kalendarza:**
- Widok tygodnia ↔ miesiąca (swipe góra/dół na `#calendar-swipe-zone`)
- Nawigacja: ←/→ + "Dziś"
- Kropki pod dniem = zaplanowane posiłki
**Elementy planu dnia:**
- Nagłówek dnia + przycisk "Kopiuj dzień"
- Karta podsumowania kalorycznego (kcal + makro, rozwijalne szczegóły)
- "Składniki na ten dzień" (badge z liczbą braków vs "OK")
- Sloty posiłków (5 slotów z `MEAL_SLOTS`):
- Kcal per slot w nagłówku
- Karty przepisów z porcjami (±), kcal, czasem, przyciskiem usuwania
- Kliknięcie nazwy przepisu → `RecipeDetail`
- "Dodaj przepis" / "Dodaj kolejny"
- "Pomijam" przy pustym slocie → slot przygaszony z "Cofnij"
**Bottom sheety:**
1. **Picker przepisów**: wyszukiwarka + sekcja "Ostatnio używane" + lista filtrowana do `allowedSlots`
2. **Składniki i spiżarnia**: porównanie potrzeb vs zapasy + prognoza tygodniowa (`computeFullForecast`) + "Dodaj braki" (dzień / tydzień)
3. **Kopiuj plan dnia**: lista 13 dni (3 wstecz, 10 do przodu) → kopiuje cały dzień (w tym statusy "Pominięto")
**Demo:** `seedDemoIfEmpty` wypełnia dzisiejszy dzień danymi demo gdy localStorage jest pusty.
**Stan:** Kompletny.
---
### 3.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 (multi-select)
- Toggle "Tylko na stanie"
- Siatka chipów składników pogrupowana po kategorii (kolor wg stanu)
- Bottom sheet edycji (`#pv2-edit-sheet`): ± z krokiem (`pantryQtyStep`), input numeryczny, opcjonalne wartości odżywcze (per 100g), "Dodaj na listę zakupów" (z `purchasePack`)
**Stan:** Kompletny.
---
### 3.6 Zakupy (`Shopping`)
**Lokalizacja:** `js/views/Shopping.js`
Zarządzanie listami zakupów — jedna stała lista kuchenna + dowolna liczba list freeform.
**Elementy:**
- Selektor aktywnej listy (dropdown)
- Przycisk "Nowa lista" (freeform) + usuwanie list (nie dotyczy kuchennej)
- **Lista kuchenna** (`KITCHEN_LIST_ID`): pogrupowana po kategorii, checkbox, edycja ilości (klik → prompt), usuwanie pozycji
- **Pasek akcji** (widoczny gdy są zaznaczone pozycje): "Kupione → spiżarnia" (z potwierdzeniem i podglądem) + "Wyczyść kupione"
- **Lista freeform**: pozycje tekstowe z opcjonalną notatką, checkbox
**Stan:** Kompletny.
Lista kuchenna (pogrupowana po kategorii, checkboxy, edycja ilości) + listy freeform. Pasek akcji dla zaznaczonych: "Kupione → spiżarnia" (z podglądem) i "Wyczyść kupione".
---
@@ -197,7 +113,8 @@ Zarządzanie listami zakupów — jedna stała lista kuchenna + dowolna liczba l
```
Przepisy ──[klik kartę]──→ Szczegóły przepisu
──[Zaplanuj]──→ Bottom sheet (kalendarz + pora + opcjonalnie wymiana składników) → Planer
──[zamiana składnika]──→ podsumowanie przelicza się na żywo
└──[Zaplanuj]──→ Bottom sheet (kalendarz + pora + zamienniki z detalu) → Planer
Planer ──[klik przepis w slocie]──→ Szczegóły przepisu
──[Składniki na ten dzień]──→ Sheet: porównanie z spiżarnią + prognoza
@@ -214,162 +131,63 @@ Zakupy ──[Kupione → spiżarnia]──→ Spiżarnia (stany zaktualizowane)
## 5. Scenariusze użytkownika
### Scenariusz 1: Przeglądanie przepisów
### Niedzielne planowanie tygodnia
**Cel:** Użytkownik otwiera apkę, chce zobaczyć co jest dostępne.
Wieczorem w niedzielę siada z apką i układa plan na najbliższe 45 dni. Kopiuje sprawdzone dni, dodaje nowe przepisy tam gdzie chce urozmaicenie. Przy kilku posiłkach z góry wie, że będzie jeść poza domem — oznacza je jako pominięte. Sprawdza podsumowanie składników na zaplanowane dni, generuje braki na listę zakupów. Rano idzie do sklepu z gotową listą.
1. Otwiera aplikację → widzi zakładkę **Przepisy** z siatką 9 kart
2. Przewija listę, czyta opisy i kalorie na kartach
3. Klika kartę "Serek wiejski z orzechami i owocami"
4. Widzi detal: składniki, kroki, wartości odżywcze
5. Przy orzechach, truskawkach i borówkach widzi ikonę shuffle — klika ją przy orzechach
6. Rozwija się lista alternatyw (laskowe, nerkowca, migdały, pekan) z wartościami odżywczymi — informacyjnie
7. Zmienia liczbę porcji z 1 na 2 → składniki i kcal się przeliczają
8. Przełącza zakładkę na "Kroki" → widzi numerowane kroki
9. Przełącza na "Wartości" → widzi makroskładniki ×2
10. Wraca strzałką ← do listy
**Co mu to daje:** Nie musi codziennie myśleć "co ugotować". Kupuje celowo — mniej marnowania.
**Uwagi:**
- Brak zdjęć (szare placeholdery) — OK dla prototypu
- Po powrocie z detalu filtr/szukajka się utrzymują
### Poranek — rutynowe śniadanie z wariacją
---
Od tygodni je to samo śniadanie. Tym razem otwiera detal przepisu i klika shuffle przy jednym ze składników. Porównuje wartości odżywcze zamienników, wybiera coś innego. Podsumowanie kaloryczne przelicza się od razu — widzi, że różnica jest niewielka. Dodaje do planu z zamiennikiem.
### Scenariusz 2: Szukanie przepisu na kolację
**Co mu to daje:** Urozmaicenie bez wysiłku. Widzi wpływ zamiany na makro zanim się zdecyduje.
**Cel:** Użytkownik szuka czegoś konkretnego.
### Środa wieczór — "nie chce mi się gotować"
1. Wpisuje "łosoś" → lista filtruje się do 1 karty
2. Kasuje tekst → wracają wszystkie
3. Klika ikonę filtrów → otwiera się overlay
4. Zaznacza "Kolacja" → aktualizuje się licznik wyników
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
Wraca zmęczony z pracy. Otwiera apkę — miał zaplanowany obiad, ale nie ma energii. Oznacza posiłek jako pominięty (zamówi albo zje na mieście). Albo: przegląda przepisy filtrując po czasie do 15 min, szybko coś wybiera i dodaje na dziś.
---
**Co mu to daje:** Brak poczucia winy — pominięcie jest jawne, plan się dostosowuje. Albo szybka alternatywa bez przeglądania całego katalogu.
### Scenariusz 3: Planowanie posiłków na tydzień
### Gotowanie na zapas
**Cel:** Użytkownik układa menu na kilka dni.
W weekend wybiera przepis na obiad i ustawia 3 porcje. Dodaje ten sam przepis na poniedziałek, wtorek i środę. Gotuje raz — ma obiady na trzy dni odhaczone. Składniki na liście zakupów są policzone na pełną ilość.
1. Przechodzi na zakładkę **Planer**
2. Widzi dzisiejszy dzień (demo-dane lub wcześniej zaplanowane)
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 + prognoza
11. Klika "Dodaj braki na dziś do listy" → toast potwierdzenia
12. Następnego dnia: "Kopiuj dzień" → wybiera dzień docelowy → gotowe
**Co mu to daje:** Oszczędność czasu w tygodniu. Jedno gotowanie zamiast trzech.
---
### W sklepie z listą
### Scenariusz 4: Zarządzanie spiżarnią
Stoi w sklepie, otwiera listę zakupów. Produkty pogrupowane po kategoriach — idzie przez alejki i odznacza kolejne pozycje. Wraca do domu, klika "Kupione → spiżarnia" — stany magazynowe aktualizują się jednym ruchem.
**Cel:** Użytkownik sprawdza co ma w domu.
**Co mu to daje:** Nie zapomina co kupić. Spiżarnia jest aktualna bez ręcznego wpisywania.
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 (przyciskami ± lub inputem)
6. Zamyka → chip zmienił się na zielony z "500 g"
7. Chce dodać mleko na listę → klika "Dodaj na listę" w sheecie
### Sprawdzenie przed zakupami
---
Przed wyjściem do sklepu sprawdza spiżarnię — co jeszcze ma. Przechodzi do planera, ogląda najbliższe dni i klika "Dodaj braki do listy". Lista zakupów zawiera tylko to, czego naprawdę potrzebuje.
### 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` z informacją skąd pozycja pochodzi
5. Ilość się nie zgadza — klika na ilość → prompt → poprawia
6. Kupuje dalsze pozycje
---
### 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
---
### 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"
---
### Scenariusz 8: Dodanie przepisu do planera z widoku detalu
**Cel:** Użytkownik znalazł przepis i chce go zaplanować.
1. Przegląda **Przepisy** → otwiera detal "Serek wiejski z orzechami i owocami"
2. Klika "Zaplanuj" (górny przycisk)
3. Otwiera się bottom sheet z kalendarzem (domyślnie widok tygodnia, można rozwinąć do miesiąca)
4. Wybiera dzień w kalendarzu
5. Wybiera porę posiłku (np. "Śniadanie")
6. Widzi sekcję "Wymienne składniki" — 3 kompaktowe karty (orzechy, truskawki, borówki) z aktualnym wyborem
7. Klika kartę "Orzechy włoskie" → rozwija się lista opcji (włoskie, laskowe, nerkowca, migdały, pekan) z wartościami odżywczymi i radio-przyciskami
8. Wybiera "Migdały" → karta się zwija, wybrany składnik zmienia się na "Migdały" z amber tłem
9. Klika "Dodaj" → toast potwierdzenia, sheet się zamyka, przepis dodany do planera z informacją o zamianie (substitutions)
---
### Scenariusz 9: "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.
**Co mu to daje:** Nie kupuje podwójnie. Nie marnuje jedzenia, które zalega w szafce.
---
## 6. Znane problemy i propozycje ulepszeń
### Do poprawy (TODO)
### Do poprawy
| # | Problem | Dotyczy scenariusza |
|---|---------|---------------------|
| 1 | Brak wskaźnika aktywnych filtrów na ikonce (badge/kropka) | 2 |
| 2 | Po dodaniu braków do listy zakupów brak ochrony przed duplikacją (brak info "już dodano") | 3 |
| 3 | Kupione pozycje mieszają się z niekupionymi w tej samej grupie (brak separacji) | 5 |
| 4 | Brak podsumowania na liście zakupów ("Do kupienia: X, kupione: Y") | 5 |
| 5 | Brak undo przy "Kupione → spiżarnia" | 6 |
| 6 | Scroll spiżarni resetuje się po edycji (re-render) | 4 |
| 7 | "Kopiuj dzień" kopiuje też status "Pominięto" — może nie zawsze pożądane | 3 |
| # | Problem |
|---|---------|
| 1 | Brak wskaźnika aktywnych filtrów na ikonce (badge/kropka) |
| 2 | Po dodaniu braków do listy zakupów brak ochrony przed duplikacją |
| 3 | Kupione pozycje mieszają się z niekupionymi w tej samej grupie |
| 4 | Brak podsumowania na liście zakupów ("Do kupienia: X, kupione: Y") |
| 5 | Brak undo przy "Kupione → spiżarnia" |
| 6 | Scroll spiżarni resetuje się po edycji (re-render) |
| 7 | "Kopiuj dzień" kopiuje też status "Pominięto" — może nie zawsze pożądane |
### Propozycje nowych funkcji
| # | Funkcja | Opis |
|---|---------|------|
| P1 | Tryb "krok po kroku" przy gotowaniu | Full-screen, jeden krok, swipe next + checkbox |
| P1 | Tryb "krok po kroku" przy gotowaniu | Full-screen, jeden krok, swipe next + checkbox |
| P2 | Wake lock | Zapobiega wygaszeniu ekranu podczas gotowania |
| P3 | Filtr "Mam składniki" | W widoku Przepisy — pokaż co da się ugotować z aktualnej spiżarni |
| P4 | Większe elementy na liście zakupów | Ułatwienie obsługi w sklepie na telefonie |

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

@@ -47,6 +47,6 @@
navigator.serviceWorker.register('./sw.js', { scope: './' }).catch(() => {});
}
</script>
<script type="module" src="js/app.js"></script>
<script type="module" src="js/app.js?v=2"></script>
</body>
</html>

View File

@@ -1,9 +1,9 @@
import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js';
import { getFilterHTML, setupFilter } from './views/Filter.js';
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js';
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js';
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js';
import { getShoppingHTML, refreshShopping, setupShopping } from './views/Shopping.js';
import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js?v=2';
import { getFilterHTML, setupFilter } from './views/Filter.js?v=2';
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2';
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=2';
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2';
import { getShoppingHTML, refreshShopping, setupShopping } from './views/Shopping.js?v=2';
function getAppToastHTML() {
return `

View File

@@ -24,21 +24,16 @@ export const CATEGORY_LABELS = {
/** @type {Record<string, IngredientDef>} */
export const INGREDIENTS = {
maka_pszenna: {
id: 'maka_pszenna',
name: 'Mąka pszenna',
category: 'suche',
pantryUnit: 'g',
nutritionPer100g: { kcal: 364, protein: 10, fat: 1, carbs: 76 },
},
mleko: {
id: 'mleko',
name: 'Mleko',
category: 'nabial',
pantryUnit: 'ml',
purchasePack: { amount: 1000, label: 'butelka 1 l' },
nutritionPer100g: { kcal: 42, protein: 3.4, fat: 1, carbs: 5 },
/* ── Pieczywo ─────────────────────────────────────── */
bulka_grahamka: {
id: 'bulka_grahamka',
name: 'Bułka grahamka',
category: 'pieczywo',
pantryUnit: 'szt',
purchasePack: { amount: 1, label: '1 bułka ~70 g' },
nutritionPer100g: { kcal: 260, protein: 9, fat: 3, carbs: 48 },
},
/* ── Nabiał ───────────────────────────────────────── */
jajko: {
id: 'jajko',
name: 'Jajka',
@@ -46,131 +41,21 @@ export const INGREDIENTS = {
pantryUnit: 'szt',
nutritionPer100g: { kcal: 143, protein: 13, fat: 9.5, carbs: 1.1 },
},
piers_kurczaka: {
id: 'piers_kurczaka',
name: 'Pierś z kurczaka',
category: 'mieso_ryby',
pantryUnit: 'g',
nutritionPer100g: { kcal: 165, protein: 31, fat: 3.6, carbs: 0 },
},
mix_salat: {
id: 'mix_salat',
name: 'Mix sałat',
category: 'warzywa',
pantryUnit: 'g',
nutritionPer100g: { kcal: 20, protein: 1.5, fat: 0.3, carbs: 3 },
},
pomidor: {
id: 'pomidor',
name: 'Pomidor',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 18, protein: 0.9, fat: 0.2, carbs: 3.9 },
},
makaron_suchy: {
id: 'makaron_suchy',
name: 'Makaron',
category: 'suche',
pantryUnit: 'g',
nutritionPer100g: { kcal: 371, protein: 13, fat: 1.5, carbs: 74 },
},
pomidory_krojone: {
id: 'pomidory_krojone',
name: 'Pomidory krojone (puszka)',
category: 'warzywa',
pantryUnit: 'g',
nutritionPer100g: { kcal: 20, protein: 1, fat: 0.2, carbs: 4 },
},
bazylia_swieza: {
id: 'bazylia_swieza',
name: 'Bazylia świeża',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 23, protein: 3.2, fat: 0.6, carbs: 2.7 },
},
jogurt_naturalny: {
id: 'jogurt_naturalny',
name: 'Jogurt naturalny',
mozzarella: {
id: 'mozzarella',
name: 'Mozzarella',
category: 'nabial',
pantryUnit: 'g',
nutritionPer100g: { kcal: 61, protein: 3.5, fat: 3.3, carbs: 4.7 },
purchasePack: { amount: 125, label: 'kulka 125 g' },
nutritionPer100g: { kcal: 280, protein: 22, fat: 20, carbs: 2 },
},
mieszanka_jagod: {
id: 'mieszanka_jagod',
name: 'Mieszanka jagód',
category: 'owoce',
ricotta: {
id: 'ricotta',
name: 'Ricotta',
category: 'nabial',
pantryUnit: 'g',
nutritionPer100g: { kcal: 50, protein: 0.7, fat: 0.3, carbs: 12 },
},
miod: {
id: 'miod',
name: 'Miód',
category: 'inne',
pantryUnit: 'g',
nutritionPer100g: { kcal: 304, protein: 0.3, fat: 0, carbs: 82 },
},
chleb_zakwas: {
id: 'chleb_zakwas',
name: 'Chleb na zakwasie',
category: 'pieczywo',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 250, protein: 9, fat: 1.5, carbs: 49 },
},
awokado: {
id: 'awokado',
name: 'Awokado',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 160, protein: 2, fat: 15, carbs: 9 },
},
cytryna: {
id: 'cytryna',
name: 'Cytryna',
category: 'owoce',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 29, protein: 1.1, fat: 0.3, carbs: 9 },
},
losos_filet: {
id: 'losos_filet',
name: 'Filet z łososia',
category: 'mieso_ryby',
pantryUnit: 'g',
nutritionPer100g: { kcal: 208, protein: 20, fat: 13, carbs: 0 },
},
koper_swiezy: {
id: 'koper_swiezy',
name: 'Koper',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 43, protein: 3.5, fat: 1.1, carbs: 7 },
},
mieso_wol_mielone: {
id: 'mieso_wol_mielone',
name: 'Mięso mielone wołowe',
category: 'mieso_ryby',
pantryUnit: 'g',
nutritionPer100g: { kcal: 250, protein: 26, fat: 15, carbs: 0 },
},
tortilla_kukurydziana: {
id: 'tortilla_kukurydziana',
name: 'Tortille kukurydziane',
category: 'pieczywo',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 218, protein: 5.7, fat: 2.9, carbs: 44 },
},
salsa_pomidorowa: {
id: 'salsa_pomidorowa',
name: 'Salsa pomidorowa',
category: 'warzywa',
pantryUnit: 'g',
nutritionPer100g: { kcal: 36, protein: 1.5, fat: 0.2, carbs: 8 },
},
platki_owsiane: {
id: 'platki_owsiane',
name: 'Płatki owsiane',
category: 'suche',
pantryUnit: 'g',
nutritionPer100g: { kcal: 389, protein: 17, fat: 7, carbs: 66 },
purchasePack: { amount: 250, label: 'opakowanie 250 g' },
nutritionPer100g: { kcal: 174, protein: 11, fat: 13, carbs: 3 },
},
serek_wiejski: {
id: 'serek_wiejski',
@@ -180,6 +65,127 @@ export const INGREDIENTS = {
purchasePack: { amount: 200, label: 'opakowanie 200 g' },
nutritionPer100g: { kcal: 97, protein: 11, fat: 5, carbs: 3 },
},
serek_smietankowy: {
id: 'serek_smietankowy',
name: 'Serek śmietankowy',
category: 'nabial',
pantryUnit: 'g',
purchasePack: { amount: 150, label: 'opakowanie 150 g' },
nutritionPer100g: { kcal: 230, protein: 6, fat: 21, carbs: 4 },
},
/* ── Mięso i ryby ─────────────────────────────────── */
szynka_parmenska: {
id: 'szynka_parmenska',
name: 'Szynka parmeńska',
category: 'mieso_ryby',
pantryUnit: 'g',
purchasePack: { amount: 100, label: 'opakowanie 100 g' },
nutritionPer100g: { kcal: 250, protein: 28, fat: 15, carbs: 0 },
},
szynka_z_kurczaka: {
id: 'szynka_z_kurczaka',
name: 'Szynka z kurczaka',
category: 'mieso_ryby',
pantryUnit: 'g',
purchasePack: { amount: 100, label: 'opakowanie 100 g' },
nutritionPer100g: { kcal: 105, protein: 19.5, fat: 2, carbs: 1.5 },
},
losos_wedzony: {
id: 'losos_wedzony',
name: 'Łosoś wędzony',
category: 'mieso_ryby',
pantryUnit: 'g',
purchasePack: { amount: 100, label: 'opakowanie 100 g' },
nutritionPer100g: { kcal: 150, protein: 20, fat: 7, carbs: 0 },
},
/* ── Warzywa ──────────────────────────────────────── */
pomidor: {
id: 'pomidor',
name: 'Pomidor',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 18, protein: 0.9, fat: 0.2, carbs: 3.9 },
},
pomidorki_koktajlowe: {
id: 'pomidorki_koktajlowe',
name: 'Pomidorki koktajlowe',
category: 'warzywa',
pantryUnit: 'g',
purchasePack: { amount: 250, label: 'opakowanie 250 g' },
nutritionPer100g: { kcal: 18, protein: 0.9, fat: 0.2, carbs: 3.9 },
},
papryka_czerwona: {
id: 'papryka_czerwona',
name: 'Papryka czerwona',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 31, protein: 1, fat: 0.3, carbs: 6 },
},
ogorek: {
id: 'ogorek',
name: 'Ogórek',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 15, protein: 0.7, fat: 0.1, carbs: 3 },
},
czosnek: {
id: 'czosnek',
name: 'Czosnek',
category: 'warzywa',
pantryUnit: 'szt',
nutritionPer100g: { kcal: 149, protein: 6.4, fat: 0.5, carbs: 33 },
},
kielki_rzodkiewki: {
id: 'kielki_rzodkiewki',
name: 'Kiełki rzodkiewki',
category: 'warzywa',
pantryUnit: 'g',
nutritionPer100g: { kcal: 28, protein: 3, fat: 0.6, carbs: 3.5 },
},
/* ── Owoce ────────────────────────────────────────── */
truskawki: {
id: 'truskawki',
name: 'Truskawki',
category: 'owoce',
pantryUnit: 'g',
nutritionPer100g: { kcal: 32, protein: 0.7, fat: 0.3, carbs: 8 },
},
borowki_amerykanskie: {
id: 'borowki_amerykanskie',
name: 'Borówki amerykańskie',
category: 'owoce',
pantryUnit: 'g',
nutritionPer100g: { kcal: 57, protein: 0.7, fat: 0.3, carbs: 14 },
},
banany: {
id: 'banany',
name: 'Banany',
category: 'owoce',
pantryUnit: 'g',
nutritionPer100g: { kcal: 89, protein: 1.1, fat: 0.3, carbs: 23 },
},
jagody: {
id: 'jagody',
name: 'Jagody',
category: 'owoce',
pantryUnit: 'g',
nutritionPer100g: { kcal: 44, protein: 0.7, fat: 0.4, carbs: 10 },
},
/* ── Suche i kasze ────────────────────────────────── */
makaron_suchy: {
id: 'makaron_suchy',
name: 'Makaron',
category: 'suche',
pantryUnit: 'g',
nutritionPer100g: { kcal: 371, protein: 13, fat: 1.5, carbs: 74 },
},
nasiona_slonecznika: {
id: 'nasiona_slonecznika',
name: 'Nasiona słonecznika',
category: 'suche',
pantryUnit: 'g',
nutritionPer100g: { kcal: 584, protein: 21, fat: 51, carbs: 20 },
},
orzechy_wloskie: {
id: 'orzechy_wloskie',
name: 'Orzechy włoskie',
@@ -215,202 +221,192 @@ export const INGREDIENTS = {
pantryUnit: 'g',
nutritionPer100g: { kcal: 691, protein: 9, fat: 72, carbs: 14 },
},
truskawki: {
id: 'truskawki',
name: 'Truskawki',
category: 'owoce',
/* ── Przyprawy i zioła ────────────────────────────── */
bazylia_swieza: {
id: 'bazylia_swieza',
name: 'Bazylia świeża',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 32, protein: 0.7, fat: 0.3, carbs: 8 },
nutritionPer100g: { kcal: 23, protein: 3.2, fat: 0.6, carbs: 2.7 },
},
borowki_amerykanskie: {
id: 'borowki_amerykanskie',
name: 'Borówki amerykańskie',
category: 'owoce',
koper_swiezy: {
id: 'koper_swiezy',
name: 'Koper',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 57, protein: 0.7, fat: 0.3, carbs: 14 },
nutritionPer100g: { kcal: 43, protein: 3.5, fat: 1.1, carbs: 7 },
},
banany: {
id: 'banany',
name: 'Banany',
category: 'owoce',
szczypiorek: {
id: 'szczypiorek',
name: 'Szczypiorek',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 89, protein: 1.1, fat: 0.3, carbs: 23 },
nutritionPer100g: { kcal: 30, protein: 3.3, fat: 0.7, carbs: 1.8 },
},
jagody: {
id: 'jagody',
name: 'Jagody',
category: 'owoce',
tymianek: {
id: 'tymianek',
name: 'Tymianek suszony',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 44, protein: 0.7, fat: 0.4, carbs: 10 },
nutritionPer100g: { kcal: 276, protein: 9, fat: 7, carbs: 45 },
},
chrzan: {
id: 'chrzan',
name: 'Chrzan tarty',
category: 'przyprawy',
pantryUnit: 'g',
nutritionPer100g: { kcal: 44, protein: 1, fat: 0.5, carbs: 8 },
},
/* ── Inne ─────────────────────────────────────────── */
miod: {
id: 'miod',
name: 'Miód',
category: 'inne',
pantryUnit: 'g',
nutritionPer100g: { kcal: 304, protein: 0.3, fat: 0, carbs: 82 },
},
oliwa: {
id: 'oliwa',
name: 'Oliwa z oliwek',
category: 'inne',
pantryUnit: 'ml',
nutritionPer100g: { kcal: 884, protein: 0, fat: 100, carbs: 0 },
},
hummus: {
id: 'hummus',
name: 'Hummus',
category: 'inne',
pantryUnit: 'g',
purchasePack: { amount: 200, label: 'opakowanie 200 g' },
nutritionPer100g: { kcal: 166, protein: 8, fat: 10, carbs: 14 },
},
};
/** Porcja bazowa = 1; składniki przez ingredientId */
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.',
kanapka_parmenska: {
id: 'kanapka_parmenska',
title: 'Kanapka z szynką parmeńską i mozzarellą',
description: 'Bułka grahamka z szynką parmeńską, mozzarellą i pomidorkami — włoskie smaki na szybko.',
minutes: 5,
thumbLabel: 'Koktajl',
allowedSlots: ['przekaska', 'drugie_sniadanie'],
tags: ['wegetariańskie', 'szybkie'],
nutritionPerServing: { kcal: 180, protein: 8, fat: 3, carbs: 32 },
thumbLabel: 'Parmeńska',
image: 'images/recipes/kanapka_parmenska.jpg',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
tags: ['szybkie'],
nutritionPerServing: { kcal: 606, protein: 47, fat: 29, carbs: 39 },
ingredients: [
{ ingredientId: 'jogurt_naturalny', amount: 200, unit: 'g' },
{ ingredientId: 'mieszanka_jagod', amount: 150, unit: 'g' },
{ ingredientId: 'miod', amount: 15, unit: 'g' },
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
{ ingredientId: 'szynka_parmenska', amount: 95, unit: 'g' },
{ ingredientId: 'mozzarella', amount: 60, unit: 'g' },
{ ingredientId: 'pomidorki_koktajlowe', amount: 100, unit: 'g' },
],
steps: [
'Wrzuć jogurt, jagody i miód do blendera.',
'Zmiksuj na gładką masę (~30 sekund).',
'Przelej do szklanki. Gotowe!',
'Bułkę grahamkę przekrój na pół.',
'Na bułce ułóż plastry szynki parmeńskiej, na nią pokrojoną mozzarellę.',
'Podawaj z pomidorkami koktajlowymi.',
],
},
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 45 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.',
makaron_ricotta: {
id: 'makaron_ricotta',
title: 'Makaron z ricottą i pomidorami',
description: 'Makaron z sosem z pieczonych pomidorków koktajlowych, ricottą i słonecznikiem.',
minutes: 20,
thumbLabel: 'Tacos',
allowedSlots: ['kolacja', 'obiad'],
tags: [],
nutritionPerServing: { kcal: 410, protein: 28, fat: 18, carbs: 38 },
thumbLabel: 'Ricotta',
image: 'images/recipes/makaron_ricotta.jpg',
allowedSlots: ['obiad', 'kolacja'],
tags: ['wegetariańskie'],
nutritionPerServing: { kcal: 608, protein: 24, fat: 24, carbs: 75 },
ingredients: [
{ ingredientId: 'mieso_wol_mielone', amount: 200, unit: 'g' },
{ ingredientId: 'tortilla_kukurydziana', amount: 4, unit: 'szt.' },
{ ingredientId: 'salsa_pomidorowa', amount: 100, unit: 'g' },
{ ingredientId: 'makaron_suchy', amount: 80, unit: 'g' },
{ ingredientId: 'pomidorki_koktajlowe', amount: 200, unit: 'g' },
{ ingredientId: 'czosnek', amount: 6, unit: 'g' },
{ ingredientId: 'tymianek', amount: 1, unit: 'g' },
{ ingredientId: 'oliwa', amount: 5, unit: 'ml' },
{ ingredientId: 'ricotta', amount: 75, unit: 'g' },
{ ingredientId: 'bazylia_swieza', amount: 3, unit: 'g' },
{ ingredientId: 'nasiona_slonecznika', amount: 15, 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ń.',
'Makaron ugotuj wg przepisu na opakowaniu. Po ugotowaniu pozostaw 12 łyżki wody.',
'Na patelni rozgrzej oliwę, dodaj przeciśnięty czosnek, przekrojone na pół pomidorki i tymianek — podsmażaj 23 minuty.',
'Na patelnię z sosem dodaj ugotowany makaron. Wymieszaj, przypraw solą i pieprzem.',
'Ricottę wymieszaj z 12 łyżkami wody z gotowania makaronu. Nałóż na makaron.',
'Posyp bazylią świeżą i nasionami słonecznika.',
],
},
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 },
jajecznica: {
id: 'jajecznica',
title: 'Jajecznica z pieczywem',
description: 'Klasyczna jajecznica z 4 jajek z bułką grahamką i szczypiorkiem.',
minutes: 5,
thumbLabel: 'Jajecznica',
image: 'images/recipes/jajecznica.png',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
tags: ['szybkie', 'wysokobiałkowe'],
nutritionPerServing: { kcal: 548, protein: 36, fat: 28, carbs: 36 },
ingredients: [
{ ingredientId: 'platki_owsiane', amount: 60, unit: 'g' },
{ ingredientId: 'mleko', amount: 200, unit: 'ml' },
{ ingredientId: 'miod', amount: 20, unit: 'g' },
{ ingredientId: 'jajko', amount: 4, unit: 'szt.' },
{ ingredientId: 'oliwa', amount: 5, unit: 'ml' },
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
{ ingredientId: 'szczypiorek', amount: 5, unit: 'g' },
],
steps: [
'W garnuszku zagotuj mleko.',
'Wsyp płatki owsiane, zmniejsz ogień i gotuj 34 min, mieszając.',
'Przełóż do miski, polej miodem. Opcjonalnie dodaj owoce lub orzechy.',
'Na patelni rozgrzej oliwę na małej mocy palnika.',
'Wbij jajka bezpośrednio na patelnię. Smaż bez przykrycia, delikatnie mieszając łopatką, aż żółtka i białka stopniowo się połączą.',
'Dopraw solą i pieprzem. Kontynuuj smażenie, aż jajka się zetną, ale pozostaną lekko kremowe — ok. 34 minuty.',
'Posyp posiekanym szczypiorkiem i podawaj z bułką grahamką.',
],
},
kanapka_hummus: {
id: 'kanapka_hummus',
title: 'Kanapka z hummusem, wędliną i warzywami',
description: 'Bułka grahamka z hummusem, szynką z kurczaka i świeżymi warzywami.',
minutes: 5,
thumbLabel: 'Hummus',
image: 'images/recipes/kanapka_hummus.png',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
tags: ['szybkie', 'wysokobiałkowe'],
nutritionPerServing: { kcal: 609, protein: 46, fat: 19, carbs: 66 },
ingredients: [
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
{ ingredientId: 'szynka_z_kurczaka', amount: 130, unit: 'g' },
{ ingredientId: 'pomidor', amount: 80, unit: 'g' },
{ ingredientId: 'papryka_czerwona', amount: 85, unit: 'g' },
{ ingredientId: 'ogorek', amount: 75, unit: 'g' },
{ ingredientId: 'szczypiorek', amount: 20, unit: 'g' },
{ ingredientId: 'hummus', amount: 140, unit: 'g' },
],
steps: [
'Pomidora i ogórka pokrój w plastry. Paprykę pokrój w paski. Szczypiorek posiekaj.',
'Bułkę grahamkę przekrój i posmaruj hummusem.',
'Na bułce ułóż szynkę z kurczaka, pomidora, paprykę i ogórka.',
'Posyp szczypiorkiem i podawaj.',
],
},
kanapka_losos: {
id: 'kanapka_losos',
title: 'Kanapka z wędzonym łososiem',
description: 'Bułka grahamka z łososiem wędzonym, pastą chrzanowo-serową i kiełkami.',
minutes: 5,
thumbLabel: 'Łosoś',
image: 'images/recipes/kanapka_losos.jpg',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
tags: ['szybkie'],
nutritionPerServing: { kcal: 443, protein: 30, fat: 18, carbs: 39 },
ingredients: [
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
{ ingredientId: 'losos_wedzony', amount: 100, unit: 'g' },
{ ingredientId: 'serek_smietankowy', amount: 40, unit: 'g' },
{ ingredientId: 'chrzan', amount: 10, unit: 'g' },
{ ingredientId: 'koper_swiezy', amount: 5, unit: 'g' },
{ ingredientId: 'kielki_rzodkiewki', amount: 5, unit: 'g' },
{ ingredientId: 'ogorek', amount: 75, unit: 'g' },
],
steps: [
'Bułkę grahamkę przekrój i podsmaż na patelni na średnim ogniu przez 23 minuty.',
'Chrzan dokładnie wymieszaj z serkiem śmietankowym.',
'Koperek drobno posiekaj. Ogórka pokrój na mniejsze kawałki.',
'Na bułce rozsmaruj pastę chrzanowo-serową. Ułóż łososia, koperek, ogórka i kiełki.',
],
},
serek_owoc: {
@@ -419,6 +415,7 @@ export const RECIPES = {
description: 'Lekki, pożywny posiłek: serek z orzechami, truskawkami i borówkami.',
minutes: 5,
thumbLabel: 'Serek',
image: 'images/recipes/serek_owoc.jpg',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'],
tags: ['wegetariańskie', 'wysokobiałkowe', 'szybkie'],
nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 },

View File

@@ -523,8 +523,10 @@ function renderDayContent(state) {
<div class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" data-recipe-id="${escapeHtml(recipe.id)}">
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="min-w-0">
<p class="text-[13px] font-bold text-gray-900 truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>
@@ -629,8 +631,10 @@ function getRecentRecipeIds(plans, limit = 5) {
function recipeCardHtml(r) {
return `
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
${r.image
? `<img src="${escapeHtml(r.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>`}
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="text-[13px] font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
@@ -872,9 +876,9 @@ function seedDemoIfEmpty(plans) {
return {
...plans,
[todayKey]: {
sniadanie: [{ id: newPlanEntryId(), recipeId: 'owsianka', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'salatka', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'makaron', servings: 1 }],
sniadanie: [{ id: newPlanEntryId(), recipeId: 'jajecznica', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'makaron_ricotta', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'kanapka_losos', servings: 1 }],
},
};
}

View File

@@ -67,8 +67,9 @@ export function getRecipeDetailHTML() {
</div>
</div>
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
<span id="rd-hero-label" class="text-white font-medium text-[15px]"></span>
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] relative overflow-hidden">
<img id="rd-hero-img" src="" alt="" class="w-full h-full object-cover hidden">
<span id="rd-hero-label" class="absolute inset-0 flex items-center justify-center text-white font-medium text-[15px]"></span>
</div>
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden">
@@ -128,7 +129,18 @@ function populateDetail(recipeId) {
currentSubstitutions = {};
expandedAlternatives.clear();
document.getElementById('rd-hero-label').textContent = `Zdjęcie: ${recipe.title}`;
const heroImg = document.getElementById('rd-hero-img');
const heroLabel = document.getElementById('rd-hero-label');
if (recipe.image) {
heroImg.src = recipe.image;
heroImg.alt = recipe.title;
heroImg.classList.remove('hidden');
heroLabel.textContent = '';
} else {
heroImg.classList.add('hidden');
heroImg.src = '';
heroLabel.textContent = `Zdjęcie: ${recipe.title}`;
}
document.getElementById('rd-title').textContent = recipe.title;
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
updateKcalDisplay();
@@ -190,10 +202,11 @@ function renderNutritionLine(nutrition) {
/* ── ingredients tab with inline nutrition + summary ───── */
function computeIngredientNutritionTotals(recipe) {
function computeEffectiveNutritionTotals(recipe) {
let kcal = 0, protein = 0, fat = 0, carbs = 0;
for (const ing of recipe.ingredients) {
const n = nutritionForAmount(ing.ingredientId, ing.amount * currentServings);
const effectiveId = getEffectiveIngredientId(ing.ingredientId);
const n = nutritionForAmount(effectiveId, ing.amount * currentServings);
if (n) {
kcal += n.kcal;
protein += n.protein;
@@ -210,14 +223,8 @@ function computeIngredientNutritionTotals(recipe) {
}
function renderNutritionSummary(recipe) {
const n = recipe.nutritionPerServing;
const s = currentServings;
const total = {
kcal: Math.round(n.kcal * s * 10) / 10,
protein: Math.round(n.protein * s * 10) / 10,
fat: Math.round(n.fat * s * 10) / 10,
carbs: Math.round(n.carbs * s * 10) / 10,
};
const total = computeEffectiveNutritionTotals(recipe);
return `
<div class="mb-4 pt-1 pb-3 border-b border-gray-100">
@@ -255,7 +262,7 @@ function renderIngredientCard(name, amount, unit, nutrition, extra) {
: '';
return `
<div class="${extra?.cls || 'bg-white'} rounded-xl p-2.5 border ${extra?.border || 'border-gray-200'} flex items-center gap-2.5">
<div class="${extra?.cls || 'bg-white'} rounded-xl p-2.5 border ${extra?.border || 'border-gray-200'} flex items-center gap-2.5" ${extra?.dataAttrs || ''}>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
${extra?.prefix || ''}
@@ -277,35 +284,52 @@ function renderIngredients(recipe) {
if (!container) return;
const rows = recipe.ingredients.map((ing) => {
const def = INGREDIENTS[ing.ingredientId];
const name = def?.name || ing.ingredientId;
const scaledAmount = ing.amount * currentServings;
const nutrition = nutritionForAmount(ing.ingredientId, scaledAmount);
const origId = ing.ingredientId;
const hasAlts = ing.alternatives && ing.alternatives.length > 0;
const isExpanded = expandedAlternatives.has(ing.ingredientId);
const effectiveId = hasAlts ? getEffectiveIngredientId(origId) : origId;
const effectiveDef = INGREDIENTS[effectiveId];
const effectiveName = effectiveDef?.name || effectiveId;
const scaledAmount = ing.amount * currentServings;
const nutrition = nutritionForAmount(effectiveId, scaledAmount);
const isSwapped = effectiveId !== origId;
const isExpanded = expandedAlternatives.has(origId);
const toggleBtn = hasAlts
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 rounded-full ${isExpanded ? 'bg-amber-50 text-amber-500' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'} flex items-center justify-center transition-colors" data-original-id="${escapeHtml(ing.ingredientId)}"><i class="fas fa-shuffle text-[8px]"></i></button>`
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 rounded-full ${isExpanded ? 'bg-amber-50 text-amber-500' : isSwapped ? 'bg-amber-50 text-amber-500' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'} flex items-center justify-center transition-colors" data-original-id="${escapeHtml(origId)}"><i class="fas fa-shuffle text-[8px]"></i></button>`
: '';
const cardHtml = renderIngredientCard(name, scaledAmount, ing.unit, nutrition, { suffix: toggleBtn });
const cardBorder = isSwapped ? 'border-amber-200' : 'border-gray-200';
const cardCls = isSwapped ? 'bg-amber-50/30' : 'bg-white';
const cardHtml = renderIngredientCard(effectiveName, scaledAmount, ing.unit, nutrition, {
suffix: toggleBtn,
border: cardBorder,
cls: cardCls,
});
let altListHtml = '';
if (hasAlts) {
const altCards = ing.alternatives.map((altId) => {
const altDef = INGREDIENTS[altId];
const altName = altDef?.name || altId;
if (hasAlts && isExpanded) {
const allOptions = [origId, ...ing.alternatives];
const optionCards = allOptions.map((altId) => {
const def = INGREDIENTS[altId];
const altName = def?.name || altId;
const isSelected = effectiveId === altId;
const isOriginal = altId === origId;
const altNutrition = nutritionForAmount(altId, scaledAmount);
const radioDot = `<div class="w-3.5 h-3.5 rounded-full border-2 shrink-0 ${isSelected ? 'border-gray-900' : 'border-gray-300'} flex items-center justify-center">${isSelected ? '<div class="w-1.5 h-1.5 rounded-full bg-gray-900"></div>' : ''}</div>`;
const defaultTag = isOriginal ? `<span class="text-[9px] px-1.5 py-0.5 rounded ${isSelected ? 'bg-gray-200 text-gray-600' : 'bg-gray-100 text-gray-400'} font-medium ml-1">Domyślny</span>` : '';
return renderIngredientCard(altName, scaledAmount, ing.unit, altNutrition, {
cls: 'bg-gray-50',
border: 'border-gray-100',
cls: isSelected ? 'bg-gray-50' : 'bg-white hover:bg-gray-50 cursor-pointer',
border: isSelected ? 'border-gray-900 ring-1 ring-gray-900' : 'border-gray-100',
prefix: radioDot,
badge: defaultTag,
dataAttrs: `data-original-id="${escapeHtml(origId)}" data-alt-id="${escapeHtml(altId)}"`,
});
});
altListHtml = `
<div class="rd-alt-list ${isExpanded ? '' : 'hidden'} mt-1.5 space-y-1.5" data-original-id="${escapeHtml(ing.ingredientId)}">
<p class="text-[10px] text-gray-400 font-medium mb-1 pl-1">Można zamienić na:</p>
${altCards.join('')}
<div class="mt-1.5 space-y-1.5 rd-alt-options" data-original-id="${escapeHtml(origId)}">
${optionCards.join('')}
</div>`;
}
@@ -324,12 +348,23 @@ function renderIngredients(recipe) {
} else {
expandedAlternatives.add(origId);
}
const list = container.querySelector(`.rd-alt-list[data-original-id="${origId}"]`);
if (list) list.classList.toggle('hidden');
btn.classList.toggle('bg-gray-100');
btn.classList.toggle('text-gray-400');
btn.classList.toggle('bg-amber-50');
btn.classList.toggle('text-amber-500');
renderIngredients(recipe);
});
});
container.querySelectorAll('.rd-alt-options').forEach((group) => {
group.querySelectorAll('[data-alt-id]').forEach((card) => {
card.addEventListener('click', () => {
const originalId = card.dataset.originalId;
const altId = card.dataset.altId;
if (altId === originalId) {
delete currentSubstitutions[originalId];
} else {
currentSubstitutions[originalId] = altId;
}
expandedAlternatives.delete(originalId);
renderIngredients(recipe);
});
});
});
}
@@ -625,7 +660,6 @@ export function setupRecipeDetail() {
if (!recipe) return;
document.getElementById('rd-planner-recipe-name').textContent = recipe.title;
currentSubstitutions = {};
expandedVariantGroups.clear();
const today = startOfDay(new Date());

View File

@@ -55,8 +55,10 @@ function renderRecipeCard(recipe) {
const labels = slotLabelsFor(recipe);
return `
<div onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
<span class="text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
: `<span class="absolute inset-0 flex items-center justify-center text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="p-3 flex flex-col flex-1">
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">${escapeHtml(recipe.title)}</h3>