Add more example recipes
All checks were successful
Build and Deploy / build-and-push (push) Successful in 27s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 27s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Widoki i scenariusze — Aplikacja Kuchenna
|
# 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.
|
> **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 |
|
| Cecha | Opis |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| Liczba osób | 1 (gotuje dla siebie) |
|
| Liczba osób | 1 (gotuje dla siebie) |
|
||||||
| Planowanie | Elastyczne, 1–7 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) |
|
| Posiłki | 5 slotów (śniadanie, drugie śniadanie, obiad, przekąska, kolacja) |
|
||||||
| Powtarzalność | Duża (zwłaszcza śniadania) — kopiowanie dnia kluczowe |
|
| Powtarzalność | Duża (zwłaszcza śniadania), ale chce stopniowo urozmaicać |
|
||||||
| Styl gotowania | Hybrydowy: trochę meal-prep, trochę na bieżąco |
|
| Styl gotowania | Hybrydowy: głównie na bieżąco, ale chce zacząć gotować na zapas (np. zupy na kilka dni) |
|
||||||
| Zakupy | Mieszane: duże zakupy + uzupełnienia w tygodniu |
|
| Jedzenie poza domem | 2–3× w tygodniu (restauracja, kantyna, zamówienie) — stąd jawne "Pomijam" |
|
||||||
| Cel dietetyczny | Utrzymanie wagi, śledzenie per posiłek |
|
| Zakupy | Duże zakupy raz w tygodniu + drobne uzupełnienia |
|
||||||
| Pomijanie posiłków | Jawne "Pomijam" (jedzenie na mieście itp.) |
|
| Cel dietetyczny | Świadomość makro — chce widzieć wartości, ale nie liczy co do grama |
|
||||||
| Przepisy | Katalog wbudowany (9 przepisów, 24 składniki), bez edytora |
|
| 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/
|
views/
|
||||||
RecipeList.js ← lista przepisów
|
RecipeList.js ← lista przepisów
|
||||||
Filter.js ← overlay filtró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
|
MealPlanner.js ← planer posiłków + kalendarz
|
||||||
Pantry.js ← spiżarnia
|
Pantry.js ← spiżarnia
|
||||||
Shopping.js ← listy zakupów
|
Shopping.js ← listy zakupów
|
||||||
@@ -68,128 +70,42 @@ js/
|
|||||||
|
|
||||||
### 3.1 Przepisy (`RecipeList`)
|
### 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:**
|
Slide-in overlay z detalami przepisu. Dwie zakładki: **Składniki** i **Kroki**.
|
||||||
- Wyszukiwarka (real-time, po tytule i tagach)
|
|
||||||
- Przycisk filtrów (otwiera overlay)
|
|
||||||
- Siatka kart
|
|
||||||
- Empty state gdy brak wyników
|
|
||||||
|
|
||||||
**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 1–12) przelicza składniki i wartości
|
||||||
|
|
||||||
---
|
**Bottom sheet "Zaplanuj":**
|
||||||
|
1. Kalendarz (tydzień/miesiąc, nawigacja ←/→/Dziś)
|
||||||
### 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 (5–120 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 1–12) 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
|
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
|
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`)
|
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:**
|
**Model danych — wymienne składniki:**
|
||||||
- W `RECIPES`, składnik może mieć pole `alternatives: ['id1', 'id2', ...]` — tablica ID alternatywnych składników
|
- W `RECIPES`, składnik może mieć pole `alternatives: ['id1', 'id2', ...]`
|
||||||
- Przy dodawaniu do planera, wybrane zamienniki zapisywane są jako `substitutions: { originalId: chosenAltId }` w `planStore`
|
- 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)
|
- 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:**
|
Lista kuchenna (pogrupowana po kategorii, checkboxy, edycja ilości) + listy freeform. Pasek akcji dla zaznaczonych: "Kupione → spiżarnia" (z podglądem) i "Wyczyść kupione".
|
||||||
- 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -197,7 +113,8 @@ Zarządzanie listami zakupów — jedna stała lista kuchenna + dowolna liczba l
|
|||||||
|
|
||||||
```
|
```
|
||||||
Przepisy ──[klik kartę]──→ Szczegóły przepisu
|
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
|
Planer ──[klik przepis w slocie]──→ Szczegóły przepisu
|
||||||
──[Składniki na ten dzień]──→ Sheet: porównanie z spiżarnią + prognoza
|
──[Składniki na ten dzień]──→ Sheet: porównanie z spiżarnią + prognoza
|
||||||
@@ -214,156 +131,57 @@ Zakupy ──[Kupione → spiżarnia]──→ Spiżarnia (stany zaktualizowane)
|
|||||||
|
|
||||||
## 5. Scenariusze użytkownika
|
## 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 4–5 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
|
**Co mu to daje:** Nie musi codziennie myśleć "co ugotować". Kupuje celowo — mniej marnowania.
|
||||||
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
|
|
||||||
|
|
||||||
**Uwagi:**
|
### Poranek — rutynowe śniadanie z wariacją
|
||||||
- Brak zdjęć (szare placeholdery) — OK dla prototypu
|
|
||||||
- Po powrocie z detalu filtr/szukajka się utrzymują
|
|
||||||
|
|
||||||
---
|
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
|
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ś.
|
||||||
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
|
|
||||||
|
|
||||||
---
|
**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**
|
**Co mu to daje:** Oszczędność czasu w tygodniu. Jedno gotowanie zamiast trzech.
|
||||||
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
|
|
||||||
|
|
||||||
---
|
### 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**
|
### Sprawdzenie przed zakupami
|
||||||
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
|
|
||||||
|
|
||||||
---
|
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
|
**Co mu to daje:** Nie kupuje podwójnie. Nie marnuje jedzenia, które zalega w szafce.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Znane problemy i propozycje ulepszeń
|
## 6. Znane problemy i propozycje ulepszeń
|
||||||
|
|
||||||
### Do poprawy (TODO)
|
### Do poprawy
|
||||||
|
|
||||||
| # | Problem | Dotyczy scenariusza |
|
| # | Problem |
|
||||||
|---|---------|---------------------|
|
|---|---------|
|
||||||
| 1 | Brak wskaźnika aktywnych filtrów na ikonce (badge/kropka) | 2 |
|
| 1 | Brak wskaźnika aktywnych filtrów na ikonce (badge/kropka) |
|
||||||
| 2 | Po dodaniu braków do listy zakupów brak ochrony przed duplikacją (brak info "już dodano") | 3 |
|
| 2 | Po dodaniu braków do listy zakupów brak ochrony przed duplikacją |
|
||||||
| 3 | Kupione pozycje mieszają się z niekupionymi w tej samej grupie (brak separacji) | 5 |
|
| 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 |
|
| 4 | Brak podsumowania na liście zakupów ("Do kupienia: X, kupione: Y") |
|
||||||
| 5 | Brak undo przy "Kupione → spiżarnia" | 6 |
|
| 5 | Brak undo przy "Kupione → spiżarnia" |
|
||||||
| 6 | Scroll spiżarni resetuje się po edycji (re-render) | 4 |
|
| 6 | Scroll spiżarni resetuje się po edycji (re-render) |
|
||||||
| 7 | "Kopiuj dzień" kopiuje też status "Pominięto" — może nie zawsze pożądane | 3 |
|
| 7 | "Kopiuj dzień" kopiuje też status "Pominięto" — może nie zawsze pożądane |
|
||||||
|
|
||||||
### Propozycje nowych funkcji
|
### Propozycje nowych funkcji
|
||||||
|
|
||||||
|
|||||||
BIN
images/recipes/jajecznica.png
Normal file
BIN
images/recipes/jajecznica.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 KiB |
BIN
images/recipes/kanapka_hummus.png
Normal file
BIN
images/recipes/kanapka_hummus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 594 KiB |
BIN
images/recipes/kanapka_losos.jpg
Normal file
BIN
images/recipes/kanapka_losos.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
BIN
images/recipes/kanapka_parmenska.jpg
Normal file
BIN
images/recipes/kanapka_parmenska.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
BIN
images/recipes/makaron_ricotta.jpg
Normal file
BIN
images/recipes/makaron_ricotta.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
images/recipes/serek_owoc.jpg
Normal file
BIN
images/recipes/serek_owoc.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
@@ -47,6 +47,6 @@
|
|||||||
navigator.serviceWorker.register('./sw.js', { scope: './' }).catch(() => {});
|
navigator.serviceWorker.register('./sw.js', { scope: './' }).catch(() => {});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="js/app.js"></script>
|
<script type="module" src="js/app.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
12
js/app.js
12
js/app.js
@@ -1,9 +1,9 @@
|
|||||||
import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js';
|
import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js?v=2';
|
||||||
import { getFilterHTML, setupFilter } from './views/Filter.js';
|
import { getFilterHTML, setupFilter } from './views/Filter.js?v=2';
|
||||||
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js';
|
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2';
|
||||||
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js';
|
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=2';
|
||||||
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js';
|
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2';
|
||||||
import { getShoppingHTML, refreshShopping, setupShopping } from './views/Shopping.js';
|
import { getShoppingHTML, refreshShopping, setupShopping } from './views/Shopping.js?v=2';
|
||||||
|
|
||||||
function getAppToastHTML() {
|
function getAppToastHTML() {
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -24,21 +24,16 @@ export const CATEGORY_LABELS = {
|
|||||||
|
|
||||||
/** @type {Record<string, IngredientDef>} */
|
/** @type {Record<string, IngredientDef>} */
|
||||||
export const INGREDIENTS = {
|
export const INGREDIENTS = {
|
||||||
maka_pszenna: {
|
/* ── Pieczywo ─────────────────────────────────────── */
|
||||||
id: 'maka_pszenna',
|
bulka_grahamka: {
|
||||||
name: 'Mąka pszenna',
|
id: 'bulka_grahamka',
|
||||||
category: 'suche',
|
name: 'Bułka grahamka',
|
||||||
pantryUnit: 'g',
|
category: 'pieczywo',
|
||||||
nutritionPer100g: { kcal: 364, protein: 10, fat: 1, carbs: 76 },
|
pantryUnit: 'szt',
|
||||||
},
|
purchasePack: { amount: 1, label: '1 bułka ~70 g' },
|
||||||
mleko: {
|
nutritionPer100g: { kcal: 260, protein: 9, fat: 3, carbs: 48 },
|
||||||
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 },
|
|
||||||
},
|
},
|
||||||
|
/* ── Nabiał ───────────────────────────────────────── */
|
||||||
jajko: {
|
jajko: {
|
||||||
id: 'jajko',
|
id: 'jajko',
|
||||||
name: 'Jajka',
|
name: 'Jajka',
|
||||||
@@ -46,131 +41,21 @@ export const INGREDIENTS = {
|
|||||||
pantryUnit: 'szt',
|
pantryUnit: 'szt',
|
||||||
nutritionPer100g: { kcal: 143, protein: 13, fat: 9.5, carbs: 1.1 },
|
nutritionPer100g: { kcal: 143, protein: 13, fat: 9.5, carbs: 1.1 },
|
||||||
},
|
},
|
||||||
piers_kurczaka: {
|
mozzarella: {
|
||||||
id: 'piers_kurczaka',
|
id: 'mozzarella',
|
||||||
name: 'Pierś z kurczaka',
|
name: 'Mozzarella',
|
||||||
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',
|
|
||||||
category: 'nabial',
|
category: 'nabial',
|
||||||
pantryUnit: 'g',
|
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: {
|
ricotta: {
|
||||||
id: 'mieszanka_jagod',
|
id: 'ricotta',
|
||||||
name: 'Mieszanka jagód',
|
name: 'Ricotta',
|
||||||
category: 'owoce',
|
category: 'nabial',
|
||||||
pantryUnit: 'g',
|
pantryUnit: 'g',
|
||||||
nutritionPer100g: { kcal: 50, protein: 0.7, fat: 0.3, carbs: 12 },
|
purchasePack: { amount: 250, label: 'opakowanie 250 g' },
|
||||||
},
|
nutritionPer100g: { kcal: 174, protein: 11, fat: 13, carbs: 3 },
|
||||||
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 },
|
|
||||||
},
|
},
|
||||||
serek_wiejski: {
|
serek_wiejski: {
|
||||||
id: 'serek_wiejski',
|
id: 'serek_wiejski',
|
||||||
@@ -180,6 +65,127 @@ export const INGREDIENTS = {
|
|||||||
purchasePack: { amount: 200, label: 'opakowanie 200 g' },
|
purchasePack: { amount: 200, label: 'opakowanie 200 g' },
|
||||||
nutritionPer100g: { kcal: 97, protein: 11, fat: 5, carbs: 3 },
|
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: {
|
orzechy_wloskie: {
|
||||||
id: 'orzechy_wloskie',
|
id: 'orzechy_wloskie',
|
||||||
name: 'Orzechy włoskie',
|
name: 'Orzechy włoskie',
|
||||||
@@ -215,202 +221,192 @@ export const INGREDIENTS = {
|
|||||||
pantryUnit: 'g',
|
pantryUnit: 'g',
|
||||||
nutritionPer100g: { kcal: 691, protein: 9, fat: 72, carbs: 14 },
|
nutritionPer100g: { kcal: 691, protein: 9, fat: 72, carbs: 14 },
|
||||||
},
|
},
|
||||||
truskawki: {
|
/* ── Przyprawy i zioła ────────────────────────────── */
|
||||||
id: 'truskawki',
|
bazylia_swieza: {
|
||||||
name: 'Truskawki',
|
id: 'bazylia_swieza',
|
||||||
category: 'owoce',
|
name: 'Bazylia świeża',
|
||||||
|
category: 'przyprawy',
|
||||||
pantryUnit: 'g',
|
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: {
|
koper_swiezy: {
|
||||||
id: 'borowki_amerykanskie',
|
id: 'koper_swiezy',
|
||||||
name: 'Borówki amerykańskie',
|
name: 'Koper',
|
||||||
category: 'owoce',
|
category: 'przyprawy',
|
||||||
pantryUnit: 'g',
|
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: {
|
szczypiorek: {
|
||||||
id: 'banany',
|
id: 'szczypiorek',
|
||||||
name: 'Banany',
|
name: 'Szczypiorek',
|
||||||
category: 'owoce',
|
category: 'przyprawy',
|
||||||
pantryUnit: 'g',
|
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: {
|
tymianek: {
|
||||||
id: 'jagody',
|
id: 'tymianek',
|
||||||
name: 'Jagody',
|
name: 'Tymianek suszony',
|
||||||
category: 'owoce',
|
category: 'przyprawy',
|
||||||
pantryUnit: 'g',
|
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 */
|
/** Porcja bazowa = 1; składniki przez ingredientId */
|
||||||
export const RECIPES = {
|
export const RECIPES = {
|
||||||
placki: {
|
kanapka_parmenska: {
|
||||||
id: 'placki',
|
id: 'kanapka_parmenska',
|
||||||
title: 'Puszyste placki',
|
title: 'Kanapka z szynką parmeńską i mozzarellą',
|
||||||
description: 'Klasyczne placki na śniadanie — puszyste i złociste.',
|
description: 'Bułka grahamka z szynką parmeńską, mozzarellą i pomidorkami — włoskie smaki na szybko.',
|
||||||
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,
|
minutes: 5,
|
||||||
thumbLabel: 'Koktajl',
|
thumbLabel: 'Parmeńska',
|
||||||
allowedSlots: ['przekaska', 'drugie_sniadanie'],
|
image: 'images/recipes/kanapka_parmenska.jpg',
|
||||||
tags: ['wegetariańskie', 'szybkie'],
|
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
|
||||||
nutritionPerServing: { kcal: 180, protein: 8, fat: 3, carbs: 32 },
|
tags: ['szybkie'],
|
||||||
|
nutritionPerServing: { kcal: 606, protein: 47, fat: 29, carbs: 39 },
|
||||||
ingredients: [
|
ingredients: [
|
||||||
{ ingredientId: 'jogurt_naturalny', amount: 200, unit: 'g' },
|
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
|
||||||
{ ingredientId: 'mieszanka_jagod', amount: 150, unit: 'g' },
|
{ ingredientId: 'szynka_parmenska', amount: 95, unit: 'g' },
|
||||||
{ ingredientId: 'miod', amount: 15, unit: 'g' },
|
{ ingredientId: 'mozzarella', amount: 60, unit: 'g' },
|
||||||
|
{ ingredientId: 'pomidorki_koktajlowe', amount: 100, unit: 'g' },
|
||||||
],
|
],
|
||||||
steps: [
|
steps: [
|
||||||
'Wrzuć jogurt, jagody i miód do blendera.',
|
'Bułkę grahamkę przekrój na pół.',
|
||||||
'Zmiksuj na gładką masę (~30 sekund).',
|
'Na bułce ułóż plastry szynki parmeńskiej, na nią pokrojoną mozzarellę.',
|
||||||
'Przelej do szklanki. Gotowe!',
|
'Podawaj z pomidorkami koktajlowymi.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
tost_awokado: {
|
makaron_ricotta: {
|
||||||
id: 'tost_awokado',
|
id: 'makaron_ricotta',
|
||||||
title: 'Tost z awokado',
|
title: 'Makaron z ricottą i pomidorami',
|
||||||
description: 'Chleb na zakwasie z rozgniecionym awokado i cytryną.',
|
description: 'Makaron z sosem z pieczonych pomidorków koktajlowych, ricottą i słonecznikiem.',
|
||||||
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,
|
minutes: 20,
|
||||||
thumbLabel: 'Tacos',
|
thumbLabel: 'Ricotta',
|
||||||
allowedSlots: ['kolacja', 'obiad'],
|
image: 'images/recipes/makaron_ricotta.jpg',
|
||||||
tags: [],
|
allowedSlots: ['obiad', 'kolacja'],
|
||||||
nutritionPerServing: { kcal: 410, protein: 28, fat: 18, carbs: 38 },
|
tags: ['wegetariańskie'],
|
||||||
|
nutritionPerServing: { kcal: 608, protein: 24, fat: 24, carbs: 75 },
|
||||||
ingredients: [
|
ingredients: [
|
||||||
{ ingredientId: 'mieso_wol_mielone', amount: 200, unit: 'g' },
|
{ ingredientId: 'makaron_suchy', amount: 80, unit: 'g' },
|
||||||
{ ingredientId: 'tortilla_kukurydziana', amount: 4, unit: 'szt.' },
|
{ ingredientId: 'pomidorki_koktajlowe', amount: 200, unit: 'g' },
|
||||||
{ ingredientId: 'salsa_pomidorowa', amount: 100, 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: [
|
steps: [
|
||||||
'Na rozgrzanej patelni podsmaż mielone, rozbijając widelcem, aż się zarumieni.',
|
'Makaron ugotuj wg przepisu na opakowaniu. Po ugotowaniu pozostaw 1–2 łyżki wody.',
|
||||||
'Dopraw kuminem, papryką, solą i pieprzem.',
|
'Na patelni rozgrzej oliwę, dodaj przeciśnięty czosnek, przekrojone na pół pomidorki i tymianek — podsmażaj 2–3 minuty.',
|
||||||
'Podgrzej tortille na suchej patelni po 15 sek. z każdej strony.',
|
'Na patelnię z sosem dodaj ugotowany makaron. Wymieszaj, przypraw solą i pieprzem.',
|
||||||
'Nałóż mięso na tortillę, polej salsą i zawiń.',
|
'Ricottę wymieszaj z 1–2 łyżkami wody z gotowania makaronu. Nałóż na makaron.',
|
||||||
|
'Posyp bazylią świeżą i nasionami słonecznika.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
owsianka: {
|
jajecznica: {
|
||||||
id: 'owsianka',
|
id: 'jajecznica',
|
||||||
title: 'Miska owsianki',
|
title: 'Jajecznica z pieczywem',
|
||||||
description: 'Ciepła owsianka z miodem — szybki i sycący posiłek.',
|
description: 'Klasyczna jajecznica z 4 jajek z bułką grahamką i szczypiorkiem.',
|
||||||
minutes: 10,
|
minutes: 5,
|
||||||
thumbLabel: 'Owsianka',
|
thumbLabel: 'Jajecznica',
|
||||||
allowedSlots: ['sniadanie', 'drugie_sniadanie'],
|
image: 'images/recipes/jajecznica.png',
|
||||||
tags: ['wegetariańskie', 'szybkie'],
|
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'kolacja'],
|
||||||
nutritionPerServing: { kcal: 210, protein: 8, fat: 6, carbs: 34 },
|
tags: ['szybkie', 'wysokobiałkowe'],
|
||||||
|
nutritionPerServing: { kcal: 548, protein: 36, fat: 28, carbs: 36 },
|
||||||
ingredients: [
|
ingredients: [
|
||||||
{ ingredientId: 'platki_owsiane', amount: 60, unit: 'g' },
|
{ ingredientId: 'jajko', amount: 4, unit: 'szt.' },
|
||||||
{ ingredientId: 'mleko', amount: 200, unit: 'ml' },
|
{ ingredientId: 'oliwa', amount: 5, unit: 'ml' },
|
||||||
{ ingredientId: 'miod', amount: 20, unit: 'g' },
|
{ ingredientId: 'bulka_grahamka', amount: 1, unit: 'szt.' },
|
||||||
|
{ ingredientId: 'szczypiorek', amount: 5, unit: 'g' },
|
||||||
],
|
],
|
||||||
steps: [
|
steps: [
|
||||||
'W garnuszku zagotuj mleko.',
|
'Na patelni rozgrzej oliwę na małej mocy palnika.',
|
||||||
'Wsyp płatki owsiane, zmniejsz ogień i gotuj 3–4 min, mieszając.',
|
'Wbij jajka bezpośrednio na patelnię. Smaż bez przykrycia, delikatnie mieszając łopatką, aż żółtka i białka stopniowo się połączą.',
|
||||||
'Przełóż do miski, polej miodem. Opcjonalnie dodaj owoce lub orzechy.',
|
'Dopraw solą i pieprzem. Kontynuuj smażenie, aż jajka się zetną, ale pozostaną lekko kremowe — ok. 3–4 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 2–3 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: {
|
serek_owoc: {
|
||||||
@@ -419,6 +415,7 @@ export const RECIPES = {
|
|||||||
description: 'Lekki, pożywny posiłek: serek z orzechami, truskawkami i borówkami.',
|
description: 'Lekki, pożywny posiłek: serek z orzechami, truskawkami i borówkami.',
|
||||||
minutes: 5,
|
minutes: 5,
|
||||||
thumbLabel: 'Serek',
|
thumbLabel: 'Serek',
|
||||||
|
image: 'images/recipes/serek_owoc.jpg',
|
||||||
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'],
|
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'],
|
||||||
tags: ['wegetariańskie', 'wysokobiałkowe', 'szybkie'],
|
tags: ['wegetariańskie', 'wysokobiałkowe', 'szybkie'],
|
||||||
nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 },
|
nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 },
|
||||||
|
|||||||
@@ -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="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-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="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">
|
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
|
||||||
<span class="text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>
|
${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>
|
||||||
<div class="min-w-0">
|
<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>
|
<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) {
|
function recipeCardHtml(r) {
|
||||||
return `
|
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}">
|
<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">
|
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
|
||||||
<span class="text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>
|
${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>
|
||||||
<div class="min-w-0 flex-1 py-0.5">
|
<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>
|
<p class="text-[13px] font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
|
||||||
@@ -872,9 +876,9 @@ function seedDemoIfEmpty(plans) {
|
|||||||
return {
|
return {
|
||||||
...plans,
|
...plans,
|
||||||
[todayKey]: {
|
[todayKey]: {
|
||||||
sniadanie: [{ id: newPlanEntryId(), recipeId: 'owsianka', servings: 1 }],
|
sniadanie: [{ id: newPlanEntryId(), recipeId: 'jajecznica', servings: 1 }],
|
||||||
obiad: [{ id: newPlanEntryId(), recipeId: 'salatka', servings: 1 }],
|
obiad: [{ id: newPlanEntryId(), recipeId: 'makaron_ricotta', servings: 1 }],
|
||||||
kolacja: [{ id: newPlanEntryId(), recipeId: 'makaron', servings: 1 }],
|
kolacja: [{ id: newPlanEntryId(), recipeId: 'kanapka_losos', servings: 1 }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ export function getRecipeDetailHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
|
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] relative overflow-hidden">
|
||||||
<span id="rd-hero-label" class="text-white font-medium text-[15px]"></span>
|
<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>
|
||||||
|
|
||||||
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden">
|
<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 = {};
|
currentSubstitutions = {};
|
||||||
expandedAlternatives.clear();
|
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-title').textContent = recipe.title;
|
||||||
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
||||||
updateKcalDisplay();
|
updateKcalDisplay();
|
||||||
@@ -190,10 +202,11 @@ function renderNutritionLine(nutrition) {
|
|||||||
|
|
||||||
/* ── ingredients tab with inline nutrition + summary ───── */
|
/* ── ingredients tab with inline nutrition + summary ───── */
|
||||||
|
|
||||||
function computeIngredientNutritionTotals(recipe) {
|
function computeEffectiveNutritionTotals(recipe) {
|
||||||
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
||||||
for (const ing of recipe.ingredients) {
|
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) {
|
if (n) {
|
||||||
kcal += n.kcal;
|
kcal += n.kcal;
|
||||||
protein += n.protein;
|
protein += n.protein;
|
||||||
@@ -210,14 +223,8 @@ function computeIngredientNutritionTotals(recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderNutritionSummary(recipe) {
|
function renderNutritionSummary(recipe) {
|
||||||
const n = recipe.nutritionPerServing;
|
|
||||||
const s = currentServings;
|
const s = currentServings;
|
||||||
const total = {
|
const total = computeEffectiveNutritionTotals(recipe);
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="mb-4 pt-1 pb-3 border-b border-gray-100">
|
<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 `
|
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-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
${extra?.prefix || ''}
|
${extra?.prefix || ''}
|
||||||
@@ -277,35 +284,52 @@ function renderIngredients(recipe) {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const rows = recipe.ingredients.map((ing) => {
|
const rows = recipe.ingredients.map((ing) => {
|
||||||
const def = INGREDIENTS[ing.ingredientId];
|
const origId = ing.ingredientId;
|
||||||
const name = def?.name || ing.ingredientId;
|
|
||||||
const scaledAmount = ing.amount * currentServings;
|
|
||||||
const nutrition = nutritionForAmount(ing.ingredientId, scaledAmount);
|
|
||||||
|
|
||||||
const hasAlts = ing.alternatives && ing.alternatives.length > 0;
|
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
|
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 = '';
|
let altListHtml = '';
|
||||||
if (hasAlts) {
|
if (hasAlts && isExpanded) {
|
||||||
const altCards = ing.alternatives.map((altId) => {
|
const allOptions = [origId, ...ing.alternatives];
|
||||||
const altDef = INGREDIENTS[altId];
|
const optionCards = allOptions.map((altId) => {
|
||||||
const altName = altDef?.name || altId;
|
const def = INGREDIENTS[altId];
|
||||||
|
const altName = def?.name || altId;
|
||||||
|
const isSelected = effectiveId === altId;
|
||||||
|
const isOriginal = altId === origId;
|
||||||
const altNutrition = nutritionForAmount(altId, scaledAmount);
|
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, {
|
return renderIngredientCard(altName, scaledAmount, ing.unit, altNutrition, {
|
||||||
cls: 'bg-gray-50',
|
cls: isSelected ? 'bg-gray-50' : 'bg-white hover:bg-gray-50 cursor-pointer',
|
||||||
border: 'border-gray-100',
|
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 = `
|
altListHtml = `
|
||||||
<div class="rd-alt-list ${isExpanded ? '' : 'hidden'} mt-1.5 space-y-1.5" data-original-id="${escapeHtml(ing.ingredientId)}">
|
<div class="mt-1.5 space-y-1.5 rd-alt-options" data-original-id="${escapeHtml(origId)}">
|
||||||
<p class="text-[10px] text-gray-400 font-medium mb-1 pl-1">Można zamienić na:</p>
|
${optionCards.join('')}
|
||||||
${altCards.join('')}
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,12 +348,23 @@ function renderIngredients(recipe) {
|
|||||||
} else {
|
} else {
|
||||||
expandedAlternatives.add(origId);
|
expandedAlternatives.add(origId);
|
||||||
}
|
}
|
||||||
const list = container.querySelector(`.rd-alt-list[data-original-id="${origId}"]`);
|
renderIngredients(recipe);
|
||||||
if (list) list.classList.toggle('hidden');
|
});
|
||||||
btn.classList.toggle('bg-gray-100');
|
});
|
||||||
btn.classList.toggle('text-gray-400');
|
|
||||||
btn.classList.toggle('bg-amber-50');
|
container.querySelectorAll('.rd-alt-options').forEach((group) => {
|
||||||
btn.classList.toggle('text-amber-500');
|
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;
|
if (!recipe) return;
|
||||||
|
|
||||||
document.getElementById('rd-planner-recipe-name').textContent = recipe.title;
|
document.getElementById('rd-planner-recipe-name').textContent = recipe.title;
|
||||||
currentSubstitutions = {};
|
|
||||||
expandedVariantGroups.clear();
|
expandedVariantGroups.clear();
|
||||||
|
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ function renderRecipeCard(recipe) {
|
|||||||
const labels = slotLabelsFor(recipe);
|
const labels = slotLabelsFor(recipe);
|
||||||
return `
|
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 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">
|
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
|
||||||
<span class="text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>
|
${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>
|
||||||
<div class="p-3 flex flex-col flex-1">
|
<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>
|
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">${escapeHtml(recipe.title)}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user