From 3347df191115ff90820f0aeb9f547496fd0e8097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 17 Mar 2026 12:55:59 +0100 Subject: [PATCH] fixes --- shopping_app/static/css/style.css | 111 +++++++++++++++++++++++++++- shopping_app/static/js/app_ui.js | 33 +++++++-- shopping_app/static/js/functions.js | 72 ++++++++++++++---- shopping_app/static/js/live.js | 58 ++++++++------- shopping_app/static/js/sockets.js | 3 + shopping_app/static/js/sort_mode.js | 90 ++++++++++++---------- shopping_app/templates/list.html | 65 ++++++++++++++-- shopping_app/uploads | 1 + 8 files changed, 338 insertions(+), 95 deletions(-) create mode 120000 shopping_app/uploads diff --git a/shopping_app/static/css/style.css b/shopping_app/static/css/style.css index c9848d2..3b88f37 100644 --- a/shopping_app/static/css/style.css +++ b/shopping_app/static/css/style.css @@ -233,7 +233,7 @@ textarea.form-control:disabled { border-bottom-left-radius: 0; } -input.form-control { +.create-list-input-group > input.form-control { border-top-right-radius: 0; border-bottom-right-radius: 0; } @@ -1473,7 +1473,7 @@ h1, h2, h3, h4, h5, h6 { #total-expense1, #total-expense2, #total-expense { - background: rgba(255,255,255,0.08) !important; + background: transparent; color: #dfffea !important; } @@ -4885,6 +4885,14 @@ body.sorting-active .shopping-item-row .large-checkbox { padding: 0 .8rem !important; flex: 0 0 auto; } +.shopping-action-btn--countdown { + width: auto !important; + min-width: 3.2rem !important; + padding: 0 .65rem !important; + font-variant-numeric: tabular-nums; + opacity: 1 !important; +} + .endpoint-list_share .shopping-item-actions, .endpoint-shared_list .shopping-item-actions, @@ -4907,6 +4915,13 @@ body.sorting-active .shopping-item-row .large-checkbox { width: auto; min-width: 5.9rem; } +.endpoint-list_share .shopping-action-btn--countdown, +.endpoint-shared_list .shopping-action-btn--countdown, +.endpoint-list .shopping-action-btn--countdown { + width: auto; + min-width: 3.2rem; +} + @media (max-width: 575.98px) { .shopping-item-spinner { @@ -4933,6 +4948,14 @@ body.sorting-active .shopping-item-row .large-checkbox { padding: 0 .72rem !important; } } + .shopping-action-btn--countdown, + .endpoint-list_share .shopping-action-btn--countdown, + .endpoint-shared_list .shopping-action-btn--countdown, + .endpoint-list .shopping-action-btn--countdown { + min-width: 3rem; + padding: 0 .55rem !important; + } +} .endpoint-login .card .form-control, .endpoint-system_auth .card .form-control, @@ -5023,6 +5046,15 @@ body.sorting-active .shopping-item-row .large-checkbox { min-width: 5.9rem !important; padding: 0 .8rem !important; } +.endpoint-list_share .shopping-action-btn--countdown, +.endpoint-shared_list .shopping-action-btn--countdown, +.endpoint-view_list .shopping-action-btn--countdown, +.endpoint-list .shopping-action-btn--countdown { + width: auto !important; + min-width: 3.2rem !important; + padding: 0 .65rem !important; +} + .endpoint-list_share .shopping-action-btn > *, .endpoint-shared_list .shopping-action-btn > *, @@ -5051,7 +5083,82 @@ body.sorting-active .shopping-item-row .large-checkbox { padding: 0 .72rem !important; } } + .endpoint-list_share .shopping-action-btn--countdown, + .endpoint-shared_list .shopping-action-btn--countdown, + .endpoint-view_list .shopping-action-btn--countdown, + .endpoint-list .shopping-action-btn--countdown { + min-width: 3rem !important; + padding: 0 .55rem !important; + } +} body:not(.sorting-active) .drag-handle { display: none !important; } + + +/* final hotfix 2026-03-17: consistent password toggle on auth/admin */ +.ui-password-group { + display: flex !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 0 !important; + width: 100%; +} + +.ui-password-group > .form-control { + flex: 1 1 auto !important; + width: 1% !important; + min-width: 0 !important; + max-width: none !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-right: 0 !important; +} + +.ui-password-group > .ui-password-toggle { + appearance: none; + -webkit-appearance: none; + display: inline-flex !important; + align-items: center; + justify-content: center; + flex: 0 0 46px !important; + width: 46px !important; + min-width: 46px !important; + padding: 0 !important; + margin: 0 !important; + background: var(--dark-700) !important; + color: var(--text-strong) !important; + border: 1px solid var(--dark-300) !important; + border-left: 0 !important; + border-top-right-radius: 14px !important; + border-bottom-right-radius: 14px !important; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + box-shadow: none !important; + outline: none !important; + line-height: 1; + transition: background-color .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease; +} + +.ui-password-group > .ui-password-toggle:hover, +.ui-password-group > .ui-password-toggle:focus, +.ui-password-group > .ui-password-toggle:focus-visible { + background: var(--dark-800) !important; + color: #fff !important; + border-color: var(--primary) !important; + box-shadow: 0 0 0 .25rem rgba(24, 64, 118, .18) !important; +} + +.ui-password-group > .ui-password-toggle.is-active { + background: #2a3550 !important; + color: #fff !important; +} + +@media (max-width: 575.98px) { + .ui-password-group > .ui-password-toggle { + flex-basis: 44px !important; + width: 44px !important; + min-width: 44px !important; + } +} diff --git a/shopping_app/static/js/app_ui.js b/shopping_app/static/js/app_ui.js index 69e9e43..f5bf746 100644 --- a/shopping_app/static/js/app_ui.js +++ b/shopping_app/static/js/app_ui.js @@ -17,17 +17,35 @@ function enhancePasswordFields() { btn.type = 'button'; btn.className = 'ui-password-toggle'; btn.setAttribute('aria-label', 'Pokaż lub ukryj hasło'); - btn.textContent = '👁'; + btn.setAttribute('aria-pressed', 'false'); + btn.title = 'Pokaż hasło'; + btn.innerHTML = ''; + + const syncState = function () { + const visible = input.type === 'text'; + btn.innerHTML = visible ? '' : ''; + btn.classList.toggle('is-active', visible); + btn.setAttribute('aria-pressed', visible ? 'true' : 'false'); + btn.title = visible ? 'Ukryj hasło' : 'Pokaż hasło'; + }; btn.addEventListener('click', function () { - const visible = input.type === 'text'; - input.type = visible ? 'password' : 'text'; - btn.textContent = visible ? '👁' : '🙈'; - btn.classList.toggle('is-active', !visible); + const selectionStart = input.selectionStart; + const selectionEnd = input.selectionEnd; + input.type = input.type === 'password' ? 'text' : 'password'; + syncState(); + input.focus({ preventScroll: true }); + if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') { + try { + input.setSelectionRange(selectionStart, selectionEnd); + } catch (err) {} + } }); - if (input.parentElement && input.parentElement.classList.contains('input-group')) { - input.parentElement.appendChild(btn); + const parent = input.parentElement; + if (parent && parent.classList.contains('input-group')) { + parent.classList.add('ui-password-group'); + parent.appendChild(btn); } else { const wrapper = document.createElement('div'); wrapper.className = 'input-group ui-password-group'; @@ -37,6 +55,7 @@ function enhancePasswordFields() { } input.dataset.uiPasswordReady = '1'; + syncState(); }); } diff --git a/shopping_app/static/js/functions.js b/shopping_app/static/js/functions.js index 43c6e3b..d3d9d5c 100644 --- a/shopping_app/static/js/functions.js +++ b/shopping_app/static/js/functions.js @@ -86,22 +86,51 @@ function deleteItem(id) { } function editItem(id, oldName, oldQuantity) { - const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName); - if (newName === null) return; + const finalName = String(oldName ?? '').trim(); + let newQuantity = parseInt(oldQuantity, 10); - const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity); - if (newQuantityStr === null) return; + if (!finalName) { + showToast('Nazwa produktu nie może być pusta.', 'warning'); + return; + } - const finalName = newName.trim() !== '' ? newName.trim() : oldName; - - let newQuantity = parseInt(newQuantityStr); if (isNaN(newQuantity) || newQuantity < 1) { - newQuantity = oldQuantity; + newQuantity = 1; } socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity }); } +function openEditItemModal(event, id, oldName, oldQuantity) { + if (event && typeof event.stopPropagation === 'function') { + event.stopPropagation(); + } + + const modalEl = document.getElementById('editItemModal'); + const idInput = document.getElementById('editItemId'); + const nameInput = document.getElementById('editItemName'); + const quantityInput = document.getElementById('editItemQuantity'); + + if (!modalEl || !idInput || !nameInput || !quantityInput || typeof bootstrap === 'undefined') { + editItem(id, oldName, oldQuantity); + return; + } + + idInput.value = id; + nameInput.value = String(oldName ?? '').trim(); + + const parsedQuantity = parseInt(oldQuantity, 10); + quantityInput.value = !isNaN(parsedQuantity) && parsedQuantity > 0 ? parsedQuantity : 1; + + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + modal.show(); + + setTimeout(() => { + nameInput.focus(); + nameInput.select(); + }, 150); +} + function submitExpense(listId) { const amountInput = document.getElementById('expenseAmount'); const amount = parseFloat(amountInput.value); @@ -282,7 +311,15 @@ function isListDifferent(oldItems, newItems) { } -function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { +function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = false) { + const options = (typeof optionsOrShowEditOnly === 'object' && optionsOrShowEditOnly !== null) + ? optionsOrShowEditOnly + : { showEditOnly: !!optionsOrShowEditOnly }; + + const showEditOnly = !!options.showEditOnly; + const temporaryShareUndo = !!options.temporaryShareUndo; + const countdownSeconds = Math.max(0, parseInt(options.countdownSeconds, 10) || 15); + const li = document.createElement('li'); li.id = `item-${item.id}`; li.dataset.name = String(item.name || '').toLowerCase(); @@ -302,7 +339,7 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { : ''; const canEditListItem = !isShare; - const canShowShareActions = isShare && !showEditOnly; + const canShowShareActions = isShare && !showEditOnly && !temporaryShareUndo; const canMarkNotPurchased = !item.not_purchased && !isArchived; const checkboxHtml = ``; @@ -326,9 +363,13 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { let actionButtons = ''; if (canEditListItem) { + const dragHandleButton = window.isSorting + ? `` + : ''; + actionButtons += ` - - + ${dragHandleButton} + `; } @@ -340,7 +381,12 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { `; } - if (canShowShareActions) { + if (temporaryShareUndo) { + actionButtons += ` + + + `; + } else if (canShowShareActions) { actionButtons += ` `; } diff --git a/shopping_app/static/js/live.js b/shopping_app/static/js/live.js index 3c9ad5b..e134d5e 100644 --- a/shopping_app/static/js/live.js +++ b/shopping_app/static/js/live.js @@ -139,45 +139,48 @@ function setupList(listId, username) { note: '' }; - const li = renderItem(item, window.IS_SHARE, true); + const isOwnFreshShareItem = Boolean( + window.IS_SHARE && + data.added_by && + window.CURRENT_LIST_USERNAME && + String(data.added_by) === String(window.CURRENT_LIST_USERNAME) + ); + + const li = renderItem( + item, + window.IS_SHARE, + isOwnFreshShareItem ? { temporaryShareUndo: true, countdownSeconds: 15 } : false + ); + document.getElementById('items').appendChild(li); toggleEmptyPlaceholder(); updateProgressBar(); - if (window.IS_SHARE) { - const countdownId = `countdown-${data.id}`; - const countdownBtn = document.createElement('button'); - countdownBtn.type = 'button'; - countdownBtn.className = 'btn btn-outline-warning'; - countdownBtn.id = countdownId; - countdownBtn.disabled = true; - countdownBtn.textContent = '15s'; - - const btnGroup = li.querySelector('.btn-group'); - if (btnGroup) { - btnGroup.prepend(countdownBtn); - } - + if (isOwnFreshShareItem) { let seconds = 15; const intervalId = setInterval(() => { - const el = document.getElementById(countdownId); - if (el) { - seconds--; - el.textContent = `${seconds}s`; - if (seconds <= 0) { - el.remove(); - clearInterval(intervalId); - } - } else { + const currentItem = document.getElementById(`item-${data.id}`); + const countdownEl = currentItem?.querySelector(`[data-countdown-for="${data.id}"]`); + + if (!currentItem || !countdownEl) { clearInterval(intervalId); + return; } + + seconds -= 1; + if (seconds <= 0) { + clearInterval(intervalId); + return; + } + + countdownEl.textContent = `${seconds}s`; }, 1000); setTimeout(() => { + clearInterval(intervalId); const existing = document.getElementById(`item-${data.id}`); if (existing) { - const updated = renderItem(item, window.IS_SHARE); - existing.replaceWith(updated); + existing.replaceWith(renderItem(item, window.IS_SHARE)); } }, 15000); } @@ -218,7 +221,7 @@ function setupList(listId, username) { window.currentItems[idx].name = data.new_name; window.currentItems[idx].quantity = data.new_quantity; - const newItem = renderItem(window.currentItems[idx], true); + const newItem = renderItem(window.currentItems[idx], window.IS_SHARE); const oldItem = document.getElementById(`item-${data.item_id}`); if (oldItem && newItem) { oldItem.replaceWith(newItem); @@ -234,6 +237,7 @@ function setupList(listId, username) { // --- WAŻNE: zapisz dane do reconnect --- window.LIST_ID = listId; window.usernameForReconnect = username; + window.CURRENT_LIST_USERNAME = username; } diff --git a/shopping_app/static/js/sockets.js b/shopping_app/static/js/sockets.js index e9ef140..4ef5862 100644 --- a/shopping_app/static/js/sockets.js +++ b/shopping_app/static/js/sockets.js @@ -126,6 +126,9 @@ socket.on('full_list', function (data) { window.currentItems = data.items; updateListSmoothly(data.items); + if (typeof window.syncSortModeUI === 'function') { + window.syncSortModeUI(); + } toggleEmptyPlaceholder(); if (didReceiveFirstFullList && isDifferent) { diff --git a/shopping_app/static/js/sort_mode.js b/shopping_app/static/js/sort_mode.js index 6c5ea7d..dcad781 100644 --- a/shopping_app/static/js/sort_mode.js +++ b/shopping_app/static/js/sort_mode.js @@ -1,21 +1,52 @@ let sortable = null; -let isSorting = false; +window.isSorting = false; + +function syncSortModeUI() { + const active = !!window.isSorting; + const btn = document.getElementById('sort-toggle-btn'); + const itemsContainer = document.getElementById('items'); + + document.body.classList.toggle('sorting-active', active); + + if (btn) { + if (active) { + btn.textContent = '✔️ Zakończ sortowanie'; + btn.classList.remove('btn-outline-warning'); + btn.classList.add('btn-outline-success'); + } else { + btn.textContent = '✳️ Zmień kolejność'; + btn.classList.remove('btn-outline-success'); + btn.classList.add('btn-outline-warning'); + } + } + + if (itemsContainer && window.currentItems) { + updateListSmoothly(window.currentItems); + } + + document.querySelectorAll('.drag-handle').forEach(handle => { + handle.hidden = !active; + handle.setAttribute('aria-hidden', active ? 'false' : 'true'); + }); +} function enableSortMode() { - if (isSorting) return; - isSorting = true; - window.isSorting = true; + if (window.isSorting) return; const itemsContainer = document.getElementById('items'); const listId = window.LIST_ID; if (!itemsContainer || !listId) return; - if (window.currentItems) { - updateListSmoothly(window.currentItems); - } + window.isSorting = true; + syncSortModeUI(); setTimeout(() => { - if (sortable) sortable.destroy(); + if (!window.isSorting) return; + + if (sortable) { + sortable.destroy(); + sortable = null; + } sortable = Sortable.create(itemsContainer, { animation: 150, @@ -25,7 +56,7 @@ function enableSortMode() { preventOnFilter: false, onEnd: () => { const order = Array.from(itemsContainer.children) - .map(li => parseInt(li.id.replace('item-', ''))) + .map(li => parseInt(li.id.replace('item-', ''), 10)) .filter(id => !isNaN(id)); fetch('/reorder_items', { @@ -36,16 +67,14 @@ function enableSortMode() { showToast('Zapisano nową kolejność', 'success'); if (window.currentItems) { - window.currentItems = order.map(id => - window.currentItems.find(item => item.id === id) - ); + window.currentItems = order + .map(id => window.currentItems.find(item => item.id === id)) + .filter(Boolean); updateListSmoothly(window.currentItems); } }); } }); - - updateSortButtonUI(true); }, 50); } @@ -55,39 +84,22 @@ function disableSortMode() { sortable = null; } - isSorting = false; window.isSorting = false; - if (window.currentItems) { - updateListSmoothly(window.currentItems); - } - - updateSortButtonUI(false); - + syncSortModeUI(); } function toggleSortMode() { - isSorting ? disableSortMode() : enableSortMode(); -} - -function updateSortButtonUI(active) { - const btn = document.getElementById('sort-toggle-btn'); - document.body.classList.toggle('sorting-active', !!active); - if (!btn) return; - - if (active) { - btn.textContent = '✔️ Zakończ sortowanie'; - btn.classList.remove('btn-outline-warning'); - btn.classList.add('btn-outline-success'); + if (window.isSorting) { + disableSortMode(); } else { - btn.textContent = '✳️ Zmień kolejność'; - btn.classList.remove('btn-outline-success'); - btn.classList.add('btn-outline-warning'); + enableSortMode(); } } +window.toggleSortMode = toggleSortMode; +window.syncSortModeUI = syncSortModeUI; + document.addEventListener('DOMContentLoaded', () => { - isSorting = false; window.isSorting = false; - document.body.classList.remove('sorting-active'); - updateSortButtonUI(false); + syncSortModeUI(); }); diff --git a/shopping_app/templates/list.html b/shopping_app/templates/list.html index 8fa09f6..428b952 100644 --- a/shopping_app/templates/list.html +++ b/shopping_app/templates/list.html @@ -32,7 +32,7 @@ - ✅ Otwórz tryb zakupowy / odznaczania produktów + ✅ Otwórz tryb odznaczania
@@ -68,9 +68,8 @@
- 📊 Postęp listy — - {{ purchased_count }}/ - {{ total_count }} kupionych + Postęp listy — + {{ purchased_count }}/{{ total_count }} kupionych ({{ percent|int }}%)
@@ -83,7 +82,7 @@ title="Pozostałe do kupienia">
- +
{% if total_expense > 0 %}
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN @@ -129,9 +128,8 @@
{% if not is_share %} - + %}onclick='openEditItemModal(event, {{ item.id }}, {{ item.name|tojson }}, {{ item.quantity or 1 }})' {% endif %}>✏️ {% endif %} @@ -154,6 +152,38 @@ {% endfor %} + + {% if not list.is_archived %}
@@ -379,6 +409,27 @@ {% endblock %} diff --git a/shopping_app/uploads b/shopping_app/uploads new file mode 120000 index 0000000..1cca7b2 --- /dev/null +++ b/shopping_app/uploads @@ -0,0 +1 @@ +../uploads \ No newline at end of file