From 9bd6627fe2d796caaf282b95be8176b2e0665582 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 17 Apr 2026 19:38:15 +0200 Subject: [PATCH] Design work on pantry --- js/ui/ingredientCard.js | 277 ++++++++++++++++++++----------------- js/ui/mealPlanEditor.js | 2 +- js/views/Pantry.js | 22 ++- js/views/RecipeDetailV2.js | 2 +- 4 files changed, 163 insertions(+), 140 deletions(-) diff --git a/js/ui/ingredientCard.js b/js/ui/ingredientCard.js index 719420b..46d96e1 100644 --- a/js/ui/ingredientCard.js +++ b/js/ui/ingredientCard.js @@ -160,7 +160,7 @@ export function getIngredientCardHTML({ return `
-
+
@@ -215,30 +215,41 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze state.shopDraftQty = null; } - function getCurrentShoppingItem(def) { + function getShoppingItemFor(def, product) { const shopping = loadShoppingState(); const kitchen = shopping.lists.find((list) => list.id === KITCHEN_LIST_ID && list.type === 'kitchen'); if (!kitchen || kitchen.type !== 'kitchen') return null; const unit = unitLabel(def.pantryUnit); + const targetProductId = product?.id || ''; return kitchen.items.find((item) => { if (item.checked) return false; - return item.ingredientId === state.ingredientId + return item.ingredientId === def.id && item.unit === unit - && (item.productId || '') === (state.productId || ''); + && (item.productId || '') === targetProductId; }) || null; } function renderHeader(def, product, pantry) { const hasProducts = ingredientHasProducts(def.id); + const isListMode = hasProducts && state.allowProductSelection && !state.productId; + const isBackAvailable = hasProducts && state.allowProductSelection && state.productId; const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; - const image = product?.image || def.image; + const heroEl = el('hero'); const img = el('img'); const fallback = el('fallback'); const fallbackIcon = el('fallback-icon'); + + if (heroEl) { + heroEl.style.height = ''; + heroEl.style.background = '#393937'; + } + if (img && fallback) { + const image = isListMode ? def.image : (product?.image || def.image); + const altName = isListMode ? def.name : (product?.name || def.name); if (image) { img.src = image; - img.alt = product?.name || def.name; + img.alt = altName; img.classList.remove('hidden'); fallback.classList.add('hidden'); } else { @@ -248,22 +259,20 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze } } - const totalQty = getPantryTotal(def.id, pantry); const categoryEl = el('category'); const nameEl = el('name'); const subtitleEl = el('subtitle'); const backBtn = el('back'); - if (categoryEl) categoryEl.textContent = product?.brand || CATEGORY_LABELS[def.category] || def.category; - if (nameEl) nameEl.textContent = product?.name || def.name; + const displayProduct = isListMode ? null : product; + if (categoryEl) categoryEl.textContent = displayProduct?.brand || CATEGORY_LABELS[def.category] || def.category; + if (nameEl) nameEl.textContent = displayProduct?.name || def.name; if (subtitleEl) { let subtitle = ''; - if (product) { - subtitle = [def.name, product.packLabel].filter(Boolean).join(' • '); - } else if (hasProducts) { - subtitle = `${productCountLabel(getProductsForIngredient(def.id).length)} • ${formatQtyWithUnit(totalQty, def.pantryUnit)} na stanie`; - } else if (def.purchasePack?.label) { + if (displayProduct) { + subtitle = [def.name, displayProduct.packLabel].filter(Boolean).join(' • '); + } else if (!hasProducts && def.purchasePack?.label) { subtitle = def.purchasePack.label; } @@ -276,20 +285,25 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze } if (backBtn) { - backBtn.classList.toggle('hidden', !(hasProducts && state.productId && state.allowProductSelection)); + backBtn.classList.toggle('hidden', !isBackAvailable); } } function renderNutrition(def, product) { const wrap = el('nutrition'); if (!wrap) return; + const hasProducts = ingredientHasProducts(def.id); + const isListMode = hasProducts && state.allowProductSelection && !state.productId; + if (isListMode) { + wrap.innerHTML = ''; + return; + } const nutrition = product?.nutritionPer100g || def.nutritionPer100g; if (!nutrition) { wrap.innerHTML = ''; return; } - const hasProducts = ingredientHasProducts(def.id); const unitScope = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g'; const hint = product ? '' @@ -321,40 +335,11 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
`; } - function renderStock() { - const wrap = el('stock'); - if (!wrap || !state.ingredientId) return; - const def = INGREDIENTS[state.ingredientId]; - if (!def) return; - - const pantry = loadPantry(); - const hasProducts = ingredientHasProducts(state.ingredientId); - const product = state.productId ? PRODUCTS[state.productId] : null; - const totalQty = getPantryTotal(state.ingredientId, pantry); + function renderStockEditorInto(wrap, def, product, pantry) { + const totalQty = getPantryTotal(def.id, pantry); const unit = unitLabel(def.pantryUnit); - - if (hasProducts && !product) { - const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length; - const helperChip = state.allowProductSelection - ? 'Wybierz produkt' - : ''; - wrap.innerHTML = ` -

Zapas

-
-
-
-

Stan łączny

-

${esc(formatQty(totalQty))} ${esc(unit)}

-

${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma zapas

-
- ${helperChip} -
-
`; - return; - } - const qty = product - ? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0) + ? (getPantryProducts(def.id, pantry).find((i) => i.productId === product.id)?.qty || 0) : totalQty; const { step, usesPackStep } = getQtyStepMeta(def, product); const packSize = product?.packSize || def.purchasePack?.amount || 0; @@ -374,12 +359,12 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze wrap.innerHTML = `

Zapas

-
-
-
-

${esc(stockValueLabel)}

- ${stockSubLabel ? `

${esc(stockSubLabel)}

` : ''} -
+
+
+
+

${esc(stockValueLabel)}

+ ${stockSubLabel ? `

${esc(stockSubLabel)}

` : ''} +
@@ -443,10 +428,10 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze const nextQty = usesPackStep ? normalizeQty((Number(input?.value) || 0) * step) : normalizeQty(input?.value ?? state.stockDraftQty ?? qty); - if (state.productId) { - setPantryProductQty(state.ingredientId, state.productId, nextQty); + if (product) { + setPantryProductQty(def.id, product.id, nextQty); } else { - setPantryQty(state.ingredientId, nextQty); + setPantryQty(def.id, nextQty); } state.stockEditorOpen = false; state.stockDraftQty = null; @@ -455,76 +440,13 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze }); } - function productRowHtml(ingredientId, productId, pantry, selectedProductId) { - const def = INGREDIENTS[ingredientId]; - const product = PRODUCTS[productId]; - const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; - const qty = getPantryProducts(ingredientId, pantry).find((i) => i.productId === productId)?.qty || 0; - const isSelected = selectedProductId === productId; - return ``; - } - - function renderProducts() { - const wrap = el('products'); - if (!wrap || !state.ingredientId) return; - if (!ingredientHasProducts(state.ingredientId) || !state.allowProductSelection) { - wrap.innerHTML = ''; - return; - } - - const pantry = loadPantry(); - const products = sortProductsByStock(getProductsForIngredient(state.ingredientId), getPantryProducts(state.ingredientId, pantry)); - const selectedProductId = state.selectedProductId || state.productId; - const subtitle = state.productId - ? 'Wróć lub wybierz inny wariant.' - : 'Wybierz wariant, aby zobaczyć szczegóły.'; - - wrap.innerHTML = ` -

Produkty

-

${esc(subtitle)}

-
- ${products.map((product) => productRowHtml(state.ingredientId, product.id, pantry, selectedProductId)).join('')} -
`; - - wrap.querySelectorAll('.ingredient-card-product-row').forEach((btn) => { - btn.addEventListener('click', () => { - const nextProductId = btn.dataset.productId || null; - state.productId = nextProductId; - state.selectedProductId = nextProductId; - resetInlineEditors(); - if (nextProductId) state.onProductChange?.(nextProductId); - render(); - state.onAfterChange?.(); - }); - }); - } - - function renderShop() { - const wrap = el('shop'); - if (!wrap || !state.ingredientId) return; - const def = INGREDIENTS[state.ingredientId]; - if (!def) return; - - const product = state.productId ? PRODUCTS[state.productId] : null; + function renderShopEditorInto(wrap, def, product) { const { step, usesPackStep } = getQtyStepMeta(def, product); const packSize = product?.packSize || def.purchasePack?.amount; const packLabel = product?.packLabel || def.purchasePack?.label; const usesPacks = Boolean(packSize && packSize > 0); const defaultAmount = step; - const shoppingItem = getCurrentShoppingItem(def); + const shoppingItem = getShoppingItemFor(def, product); const hasShoppingItem = Boolean(shoppingItem); const shoppingAmount = shoppingItem?.amount || 0; const draftQty = state.shopEditorOpen @@ -542,7 +464,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze ? formatPreciseQty(draftQty / step) : formatPreciseQty(draftQty); const shopInputUnit = usesPackStep ? 'opak.' : unitLabel(def.pantryUnit); - const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień'; + const actionLabel = state.shopEditorOpen ? 'Anuluj' : (hasShoppingItem ? 'Zmień' : 'Dodaj'); wrap.innerHTML = `

Lista zakupów

@@ -632,14 +554,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze } else if (nextAmount > 0) { const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`) : undefined; const line = { - ingredientId: state.ingredientId, + ingredientId: def.id, amount: nextAmount, unit: unitLabel(def.pantryUnit), name: product?.name || def.name, category: def.category, sourceNote: note || state.sourceNote || defaultSourceNote, }; - if (state.productId) line.productId = state.productId; + if (product) line.productId = product.id; addOrMergeShoppingLines([line]); toastText = `Dodano ${product?.name || def.name}.`; } @@ -652,6 +574,111 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze }); } + function renderStock() { + const wrap = el('stock'); + if (!wrap || !state.ingredientId) return; + const def = INGREDIENTS[state.ingredientId]; + if (!def) return; + + const hasProducts = ingredientHasProducts(state.ingredientId); + const isListMode = hasProducts && state.allowProductSelection && !state.productId; + if (isListMode) { + wrap.innerHTML = ''; + return; + } + + const pantry = loadPantry(); + const product = state.productId ? PRODUCTS[state.productId] : null; + renderStockEditorInto(wrap, def, product, pantry); + } + + function productRowHtml(ingredientId, productId, pantry, kitchenItems) { + const def = INGREDIENTS[ingredientId]; + const product = PRODUCTS[productId]; + const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; + const unit = unitLabel(def.pantryUnit); + const pantryQty = getPantryProducts(ingredientId, pantry).find((i) => i.productId === productId)?.qty || 0; + const shoppingItem = kitchenItems.find((item) => !item.checked + && item.ingredientId === ingredientId + && item.unit === unit + && (item.productId || '') === productId); + const shoppingAmount = shoppingItem?.amount || 0; + + const pantryLabel = pantryQty > 0 ? `${formatQty(pantryQty)} ${unit}` : '—'; + const pantryColor = pantryQty > 0 ? '#ddd6ca' : '#6d6c67'; + const shoppingLabel = shoppingAmount > 0 ? `${formatQty(shoppingAmount)} ${unit}` : ''; + + return ``; + } + + function renderProducts() { + const wrap = el('products'); + if (!wrap || !state.ingredientId) return; + const hasProducts = ingredientHasProducts(state.ingredientId); + const isListMode = hasProducts && state.allowProductSelection && !state.productId; + if (!isListMode) { + wrap.innerHTML = ''; + return; + } + + const pantry = loadPantry(); + const products = sortProductsByStock(getProductsForIngredient(state.ingredientId), getPantryProducts(state.ingredientId, pantry)); + const shopping = loadShoppingState(); + const kitchen = shopping.lists.find((list) => list.id === KITCHEN_LIST_ID && list.type === 'kitchen'); + const kitchenItems = (kitchen && kitchen.type === 'kitchen') ? kitchen.items : []; + + wrap.innerHTML = ` +

Produkty

+
+ ${products.map((product) => productRowHtml(state.ingredientId, product.id, pantry, kitchenItems)).join('')} +
`; + + wrap.querySelectorAll('.ingredient-card-product-row').forEach((btn) => { + btn.addEventListener('click', () => { + const nextProductId = btn.dataset.productId || null; + if (!nextProductId) return; + state.productId = nextProductId; + state.selectedProductId = nextProductId; + resetInlineEditors(); + state.onProductChange?.(nextProductId); + render(); + state.onAfterChange?.(); + }); + }); + } + + function renderShop() { + const wrap = el('shop'); + if (!wrap || !state.ingredientId) return; + const def = INGREDIENTS[state.ingredientId]; + if (!def) return; + + const hasProducts = ingredientHasProducts(state.ingredientId); + const isListMode = hasProducts && state.allowProductSelection && !state.productId; + if (isListMode) { + wrap.innerHTML = ''; + return; + } + + const product = state.productId ? PRODUCTS[state.productId] : null; + renderShopEditorInto(wrap, def, product); + } + function render() { if (!state.ingredientId) return; const def = INGREDIENTS[state.ingredientId]; diff --git a/js/ui/mealPlanEditor.js b/js/ui/mealPlanEditor.js index 66cb7e7..80515aa 100644 --- a/js/ui/mealPlanEditor.js +++ b/js/ui/mealPlanEditor.js @@ -24,7 +24,7 @@ import { renderCalendarGrid, syncCalendarTodayButton, } from './mealCalendar.js?v=11'; -import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-107'; +import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260417-113'; function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); diff --git a/js/views/Pantry.js b/js/views/Pantry.js index 6609f5f..3848b43 100644 --- a/js/views/Pantry.js +++ b/js/views/Pantry.js @@ -13,7 +13,7 @@ import { renderCalendarGrid, syncCalendarTodayButton, } from '../ui/mealCalendar.js?v=11'; -import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107'; +import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260417-113'; /* ── helpers ── */ @@ -253,12 +253,8 @@ export function getPantryHTML() {
- - ${getIngredientCardHTML({ - idBase: 'pv2-card', - overlayClass: 'absolute inset-0 z-[60] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5', - })} -
`; +
+ ${getIngredientCardHTML({ idBase: 'pv2-card' })}`; } /* ══════════════════════ HORIZON SELECTOR ══════════════════════ */ @@ -574,11 +570,11 @@ function shortfallTileHtml(item) {

${esc(item.name)}

-
-
+
+
- ${esc(formatQty(item.pantryQty))} / ${esc(formatQty(item.needed))} ${esc(unitLabel(item.unit))} + ${esc(formatQty(item.pantryQty))}/${esc(formatQty(item.needed))} ${esc(unitLabel(item.unit))}
`; } @@ -593,11 +589,11 @@ function sufficientTileHtml(item) {

${esc(item.name)}

-
-
+
+
- ${esc(formatQty(item.pantryQty))} / ${esc(formatQty(item.needed))} ${esc(unitLabel(item.unit))} + ${esc(formatQty(item.pantryQty))}/${esc(formatQty(item.needed))} ${esc(unitLabel(item.unit))}
`; } diff --git a/js/views/RecipeDetailV2.js b/js/views/RecipeDetailV2.js index b4af620..fbf574a 100644 --- a/js/views/RecipeDetailV2.js +++ b/js/views/RecipeDetailV2.js @@ -1,5 +1,5 @@ import { RECIPES, INGREDIENTS, PRODUCTS } from '../data/catalog.js?v=8'; -import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107'; +import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260417-113'; function escapeHtml(s) { return String(s)