Redesign bought items on shopping list
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s
This commit is contained in:
@@ -57,7 +57,7 @@ const CALENDAR_DIM_TEXT = 'rgb(var(--text-faint-rgb))';
|
|||||||
const CALENDAR_DIM_OPACITY = '0.58';
|
const CALENDAR_DIM_OPACITY = '0.58';
|
||||||
|
|
||||||
/* ── module state ── */
|
/* ── module state ── */
|
||||||
let boughtSectionOpen = false;
|
let boughtPopupOpen = false;
|
||||||
let expandedIngredientId = null;
|
let expandedIngredientId = null;
|
||||||
let expandedAmount = 0;
|
let expandedAmount = 0;
|
||||||
let calendarOpen = false;
|
let calendarOpen = false;
|
||||||
@@ -122,13 +122,17 @@ export function getShoppingListHTML() {
|
|||||||
<!-- ── header ── -->
|
<!-- ── header ── -->
|
||||||
<div class="shrink-0 px-4 pt-5 pb-0">
|
<div class="shrink-0 px-4 pt-5 pb-0">
|
||||||
|
|
||||||
<!-- title row + pill (position:relative anchors the popup) -->
|
<!-- title row + pill + bought button (position:relative anchors the popups) -->
|
||||||
<div class="flex items-center gap-2 mb-3" style="position:relative;">
|
<div class="flex items-center gap-2 mb-4" style="position:relative;">
|
||||||
<h1 class="flex-1 text-[18px] font-bold" style="color:rgb(var(--text-emphasis-rgb));">Lista zakupów</h1>
|
<h1 class="flex-1 text-[18px] font-bold" style="color:rgb(var(--text-emphasis-rgb));">Lista zakupów</h1>
|
||||||
<button type="button" id="sl-range-pill" class="min-w-0 max-w-[10rem] h-10 rounded-full flex items-center gap-1.5 px-2.5 transition-all shrink" style="background:rgb(var(--card-rgb)); border:1px solid rgb(var(--border-card-rgb)); box-shadow:var(--shadow-shell);">
|
<button type="button" id="sl-range-pill" class="min-w-0 max-w-[10rem] h-10 rounded-full flex items-center gap-1.5 px-2.5 transition-all shrink" style="background:rgb(var(--card-rgb)); border:1px solid rgb(var(--border-card-rgb)); box-shadow:var(--shadow-shell);">
|
||||||
<span id="sl-range-label" class="min-w-0 flex-1 text-left text-[13px] font-normal truncate" style="color:rgb(var(--text-body-rgb));"></span>
|
<span id="sl-range-label" class="min-w-0 flex-1 text-left text-[13px] font-normal truncate" style="color:rgb(var(--text-body-rgb));"></span>
|
||||||
<i id="sl-range-chevron" class="fas fa-chevron-down text-[10px] shrink-0 transition-transform duration-200" style="color:rgb(var(--text-dim-rgb));"></i>
|
<i id="sl-range-chevron" class="fas fa-chevron-down text-[10px] shrink-0 transition-transform duration-200" style="color:rgb(var(--text-dim-rgb));"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" id="sl-bought-btn" class="relative h-10 w-10 rounded-full flex items-center justify-center transition-all shrink-0" style="background:rgb(var(--card-rgb)); border:1px solid rgb(var(--border-card-rgb)); box-shadow:var(--shadow-shell);" aria-label="Kupione">
|
||||||
|
<i class="fas fa-check text-[13px]" style="color:rgb(var(--text-body-rgb));"></i>
|
||||||
|
<span id="sl-bought-badge" class="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 rounded-full text-[9px] font-bold items-center justify-center" style="background:rgb(var(--success-rgb)); color:rgb(var(--on-accent-rgb)); display:none;">0</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- popup calendar (absolute, overlays content below) -->
|
<!-- popup calendar (absolute, overlays content below) -->
|
||||||
<div id="sl-calendar-popup" style="position:absolute; top:calc(100% + 0.5rem); left:0; right:0; z-index:50; pointer-events:none; opacity:0; transform:translateY(-6px) scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease;">
|
<div id="sl-calendar-popup" style="position:absolute; top:calc(100% + 0.5rem); left:0; right:0; z-index:50; pointer-events:none; opacity:0; transform:translateY(-6px) scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease;">
|
||||||
@@ -151,34 +155,25 @@ export function getShoppingListHTML() {
|
|||||||
<div id="sl-cal-grid" class="grid grid-cols-7 gap-1.5"></div>
|
<div id="sl-cal-grid" class="grid grid-cols-7 gap-1.5"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- progress -->
|
<!-- popup bought (absolute, overlays content below) -->
|
||||||
<div id="sl-progress-wrap" class="mb-3">
|
<div id="sl-bought-popup" style="position:absolute; top:calc(100% + 0.5rem); left:0; right:0; z-index:50; pointer-events:none; opacity:0; transform:translateY(-6px) scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease;">
|
||||||
<span class="text-[12px] font-semibold block mb-1.5" style="color:rgb(var(--text-dim-rgb));">Kupione</span>
|
<div class="rounded-[1.35rem] px-3 py-3" style="background:rgb(var(--sunken-rgb)); border:1px solid rgb(var(--border-input-rgb)); box-shadow:var(--shadow-shell);">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center justify-between mb-2 px-1">
|
||||||
<div class="flex-1 h-1.5 rounded-full overflow-hidden" style="background:rgb(var(--card-rgb));">
|
<span class="text-[11px] font-bold uppercase tracking-wider" style="color:rgb(var(--text-dim-rgb));">Kupione (<span id="sl-bought-popup-count">0</span>)</span>
|
||||||
<div id="sl-progress-bar" class="h-full rounded-full transition-all duration-300" style="background:rgb(var(--success-rgb)); width:0%;"></div>
|
<button type="button" id="sl-clear-session" class="h-8 px-2 rounded-full text-[11px] font-semibold transition-colors" style="background:transparent; border:none; color:rgb(var(--text-muted-rgb));">
|
||||||
</div>
|
Wyczyść
|
||||||
<button type="button" id="sl-clear-session" class="flex items-center justify-center flex-shrink-0" style="color:rgb(var(--text-subdued-rgb));" aria-label="Wyczyść kupione">
|
|
||||||
<i class="fas fa-trash-can" style="font-size:14px;" aria-hidden="true"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sl-bought-list" class="max-h-[50vh] overflow-y-auto no-scrollbar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── scrollable list ── -->
|
<!-- ── scrollable list ── -->
|
||||||
<div id="sl-scroll" class="flex-1 overflow-y-auto no-scrollbar px-4 pt-3 pb-24" style="background:rgb(var(--app-bg-rgb)) !important;">
|
<div id="sl-scroll" class="flex-1 overflow-y-auto no-scrollbar px-4 pt-3 pb-24" style="background:rgb(var(--app-bg-rgb)) !important;">
|
||||||
<div id="sl-board"></div>
|
<div id="sl-board"></div>
|
||||||
|
|
||||||
<!-- bought section -->
|
|
||||||
<div id="sl-bought-section" class="mt-3 hidden">
|
|
||||||
<button type="button" id="sl-bought-toggle" class="flex items-center gap-2 w-full py-2 px-1 text-left" style="color:rgb(var(--text-dim-rgb));">
|
|
||||||
<i id="sl-bought-chevron" class="fas fa-chevron-right text-[10px] transition-transform duration-200"></i>
|
|
||||||
<span class="text-[11px] font-bold uppercase tracking-wider">Kupione (<span id="sl-bought-count">0</span>)</span>
|
|
||||||
</button>
|
|
||||||
<div id="sl-bought-list" class="hidden mt-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -302,6 +297,7 @@ function updatePillLabel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCalendar() {
|
function openCalendar() {
|
||||||
|
if (boughtPopupOpen) closeBoughtPopup();
|
||||||
calendarOpen = true;
|
calendarOpen = true;
|
||||||
calendarMonth = startOfMonth(new Date());
|
calendarMonth = startOfMonth(new Date());
|
||||||
const popup = document.getElementById('sl-calendar-popup');
|
const popup = document.getElementById('sl-calendar-popup');
|
||||||
@@ -341,6 +337,41 @@ function toggleCalendar() {
|
|||||||
calendarOpen ? closeCalendar() : openCalendar();
|
calendarOpen ? closeCalendar() : openCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openBoughtPopup() {
|
||||||
|
if (calendarOpen) closeCalendar();
|
||||||
|
boughtPopupOpen = true;
|
||||||
|
const popup = document.getElementById('sl-bought-popup');
|
||||||
|
const btn = document.getElementById('sl-bought-btn');
|
||||||
|
if (popup) {
|
||||||
|
popup.style.pointerEvents = 'auto';
|
||||||
|
popup.style.opacity = '1';
|
||||||
|
popup.style.transform = 'translateY(0) scale(1)';
|
||||||
|
}
|
||||||
|
if (btn) {
|
||||||
|
btn.style.background = 'rgb(var(--sunken-rgb))';
|
||||||
|
btn.style.borderColor = 'rgb(var(--border-input-rgb))';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBoughtPopup() {
|
||||||
|
boughtPopupOpen = false;
|
||||||
|
const popup = document.getElementById('sl-bought-popup');
|
||||||
|
const btn = document.getElementById('sl-bought-btn');
|
||||||
|
if (popup) {
|
||||||
|
popup.style.pointerEvents = 'none';
|
||||||
|
popup.style.opacity = '0';
|
||||||
|
popup.style.transform = 'translateY(-6px) scale(0.98)';
|
||||||
|
}
|
||||||
|
if (btn) {
|
||||||
|
btn.style.background = 'rgb(var(--card-rgb))';
|
||||||
|
btn.style.borderColor = 'rgb(var(--border-card-rgb))';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBoughtPopup() {
|
||||||
|
boughtPopupOpen ? closeBoughtPopup() : openBoughtPopup();
|
||||||
|
}
|
||||||
|
|
||||||
/* ══════════════════════ ITEM ROWS ══════════════════════ */
|
/* ══════════════════════ ITEM ROWS ══════════════════════ */
|
||||||
|
|
||||||
function activeItemHtml(item) {
|
function activeItemHtml(item) {
|
||||||
@@ -397,23 +428,20 @@ function boughtItemHtml(entry) {
|
|||||||
const image = def?.image;
|
const image = def?.image;
|
||||||
const mediaFit = image?.endsWith('.svg') ? 'object-contain' : 'object-cover';
|
const mediaFit = image?.endsWith('.svg') ? 'object-contain' : 'object-cover';
|
||||||
const mediaHtml = image
|
const mediaHtml = image
|
||||||
? `<img src="${esc(image)}" alt="" class="w-8 h-8 rounded-lg ${mediaFit} shrink-0 opacity-50">`
|
? `<img src="${esc(image)}" alt="" class="w-8 h-8 rounded-lg ${mediaFit} shrink-0">`
|
||||||
: `<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 opacity-50" style="background:rgb(var(--card-soft-rgb));"><i class="fas ${icon} text-xs" style="color:rgb(var(--text-faint-rgb));"></i></div>`;
|
: `<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style="background:rgb(var(--card-soft-rgb));"><i class="fas ${icon} text-xs" style="color:rgb(var(--text-faint-rgb));"></i></div>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="background:rgb(var(--danger-rgb)); box-shadow:var(--shadow-card);">
|
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="background:rgb(var(--danger-rgb)); box-shadow:var(--shadow-card);">
|
||||||
<div class="sl-swipe-bg-undo absolute inset-0 flex items-center pl-5 justify-start">
|
<div class="sl-swipe-bg-undo absolute inset-0 flex items-center pl-5 justify-start">
|
||||||
<i class="fas fa-rotate-left text-white text-lg"></i>
|
<i class="fas fa-rotate-left text-white text-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="sl-swipe-inner flex items-center gap-3 py-1.5 px-3 rounded-xl" style="background:rgb(var(--card-soft-rgb)); position:relative;" data-entry-id="${esc(entry.id)}">
|
<div class="sl-swipe-inner flex items-center gap-3 py-1.5 px-3 rounded-xl" style="background:rgb(var(--card-rgb)); position:relative;" data-entry-id="${esc(entry.id)}">
|
||||||
<div class="shrink-0 w-6 h-6 rounded-full flex items-center justify-center" style="background:rgb(var(--success-rgb));">
|
|
||||||
<i class="fas fa-check text-[10px]" style="color:rgb(var(--on-accent-rgb));"></i>
|
|
||||||
</div>
|
|
||||||
${mediaHtml}
|
${mediaHtml}
|
||||||
<div class="flex-1 min-w-0 opacity-60">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="block text-[13px] font-medium leading-tight truncate line-through" style="color:rgb(var(--text-body-rgb));">${esc(entry.name)}</span>
|
<span class="block text-[13px] font-medium leading-tight truncate" style="color:rgb(var(--text-body-rgb));">${esc(entry.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[13px] tabular-nums shrink-0 opacity-60" style="color:rgb(var(--text-muted-rgb));">${esc(formatQty(entry.addedAmount))} ${esc(unitLabel(entry.unit))}</span>
|
<span class="text-[13px] font-semibold tabular-nums shrink-0" style="color:rgb(var(--text-muted-rgb));">${esc(formatQty(entry.addedAmount))} ${esc(unitLabel(entry.unit))}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -516,14 +544,15 @@ function undoBoughtEntry(entryId) {
|
|||||||
|
|
||||||
/* ══════════════════════ RENDERING ══════════════════════ */
|
/* ══════════════════════ RENDERING ══════════════════════ */
|
||||||
|
|
||||||
function renderProgress(activeItems, sessionLog) {
|
function renderBoughtBadge(count) {
|
||||||
const touchedIds = new Set(sessionLog.map((e) => e.ingredientId));
|
const badge = document.getElementById('sl-bought-badge');
|
||||||
const bought = touchedIds.size;
|
if (!badge) return;
|
||||||
const untouched = activeItems.filter((i) => !touchedIds.has(i.ingredientId)).length;
|
if (count > 0) {
|
||||||
const total = bought + untouched;
|
badge.textContent = String(count);
|
||||||
|
badge.style.display = 'flex';
|
||||||
const barEl = document.getElementById('sl-progress-bar');
|
} else {
|
||||||
if (barEl) barEl.style.width = total > 0 ? `${Math.round((bought / total) * 100)}%` : '0%';
|
badge.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBoard() {
|
function renderBoard() {
|
||||||
@@ -576,22 +605,17 @@ function renderBoard() {
|
|||||||
|
|
||||||
function renderBought() {
|
function renderBought() {
|
||||||
const sessionLog = getSessionLog();
|
const sessionLog = getSessionLog();
|
||||||
const section = document.getElementById('sl-bought-section');
|
const countEl = document.getElementById('sl-bought-popup-count');
|
||||||
const countEl = document.getElementById('sl-bought-count');
|
|
||||||
const list = document.getElementById('sl-bought-list');
|
const list = document.getElementById('sl-bought-list');
|
||||||
if (!section || !list || !countEl) return;
|
if (!list || !countEl) return;
|
||||||
|
|
||||||
if (sessionLog.length === 0) { section.classList.add('hidden'); return; }
|
|
||||||
|
|
||||||
section.classList.remove('hidden');
|
|
||||||
countEl.textContent = String(sessionLog.length);
|
countEl.textContent = String(sessionLog.length);
|
||||||
|
|
||||||
const chevron = document.getElementById('sl-bought-chevron');
|
if (sessionLog.length === 0) {
|
||||||
if (chevron) chevron.style.transform = boughtSectionOpen ? 'rotate(90deg)' : '';
|
list.innerHTML = `<div class="py-6 text-center text-[12px]" style="color:rgb(var(--text-dim-rgb));">Brak kupionych</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!boughtSectionOpen) { list.classList.add('hidden'); return; }
|
|
||||||
|
|
||||||
list.classList.remove('hidden');
|
|
||||||
list.innerHTML = [...sessionLog].reverse().map((e) => boughtItemHtml(e)).join('');
|
list.innerHTML = [...sessionLog].reverse().map((e) => boughtItemHtml(e)).join('');
|
||||||
list.querySelectorAll('.sl-swipe-wrap').forEach((wrap) => {
|
list.querySelectorAll('.sl-swipe-wrap').forEach((wrap) => {
|
||||||
const entryId = wrap.querySelector('.sl-swipe-inner')?.dataset.entryId;
|
const entryId = wrap.querySelector('.sl-swipe-inner')?.dataset.entryId;
|
||||||
@@ -601,9 +625,8 @@ function renderBought() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
const activeItems = computeActiveItems();
|
|
||||||
const sessionLog = getSessionLog();
|
const sessionLog = getSessionLog();
|
||||||
renderProgress(activeItems, sessionLog);
|
renderBoughtBadge(sessionLog.length);
|
||||||
renderBoard();
|
renderBoard();
|
||||||
renderBought();
|
renderBought();
|
||||||
}
|
}
|
||||||
@@ -663,13 +686,26 @@ export function setupShoppingList() {
|
|||||||
toggleCalendar();
|
toggleCalendar();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('sl-bought-btn')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleBoughtPopup();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!calendarOpen) return;
|
if (calendarOpen) {
|
||||||
const popup = document.getElementById('sl-calendar-popup');
|
const popup = document.getElementById('sl-calendar-popup');
|
||||||
const pill = document.getElementById('sl-range-pill');
|
const pill = document.getElementById('sl-range-pill');
|
||||||
if (popup && !popup.contains(e.target) && pill && !pill.contains(e.target)) {
|
if (popup && !popup.contains(e.target) && pill && !pill.contains(e.target)) {
|
||||||
closeCalendar();
|
closeCalendar();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (boughtPopupOpen) {
|
||||||
|
const popup = document.getElementById('sl-bought-popup');
|
||||||
|
const btn = document.getElementById('sl-bought-btn');
|
||||||
|
if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) {
|
||||||
|
closeBoughtPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('sl-cal-prev')?.addEventListener('click', (e) => {
|
document.getElementById('sl-cal-prev')?.addEventListener('click', (e) => {
|
||||||
@@ -685,15 +721,11 @@ export function setupShoppingList() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('sl-clear-session')?.addEventListener('click', () => {
|
document.getElementById('sl-clear-session')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
clearSessionLog();
|
clearSessionLog();
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('sl-bought-toggle')?.addEventListener('click', () => {
|
|
||||||
boughtSectionOpen = !boughtSectionOpen;
|
|
||||||
renderBought();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.refreshShoppingList = refreshShoppingList;
|
window.refreshShoppingList = refreshShoppingList;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user