Compare commits
2 Commits
a299783a6c
...
f02d3b8085
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02d3b8085 | ||
|
|
3347df1911 |
@@ -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,179 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* final hotfix 2026-03-17b: password toggle parity on login/system-auth/admin-users */
|
||||
.ui-password-group {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ui-password-group > .form-control {
|
||||
flex: 1 1 auto !important;
|
||||
width: 1% !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 42px !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 !important;
|
||||
-webkit-appearance: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
flex: 0 0 46px !important;
|
||||
width: 46px !important;
|
||||
min-width: 46px !important;
|
||||
min-height: 42px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
cursor: pointer !important;
|
||||
background-color: var(--dark-700) !important;
|
||||
background-image: none !important;
|
||||
color: var(--text-strong) !important;
|
||||
border: 1px solid var(--dark-300) !important;
|
||||
border-left: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-top-right-radius: 14px !important;
|
||||
border-bottom-right-radius: 14px !important;
|
||||
box-shadow: none !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.ui-password-group > .ui-password-toggle:hover,
|
||||
.ui-password-group > .ui-password-toggle:focus,
|
||||
.ui-password-group > .ui-password-toggle:focus-visible {
|
||||
background-color: var(--dark-800) !important;
|
||||
color: #fff !important;
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 .25rem rgba(24, 64, 118, .18) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.ui-password-group > .ui-password-toggle.is-active {
|
||||
background-color: var(--dark-800) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.ui-password-group > .ui-password-toggle > * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.endpoint-login .ui-password-group > .ui-password-toggle,
|
||||
.endpoint-system_auth .ui-password-group > .ui-password-toggle,
|
||||
.endpoint-user_management .ui-password-group > .ui-password-toggle,
|
||||
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle {
|
||||
background-color: var(--dark-700) !important;
|
||||
color: var(--text-strong) !important;
|
||||
border-color: var(--dark-300) !important;
|
||||
}
|
||||
|
||||
.endpoint-login .ui-password-group > .ui-password-toggle:hover,
|
||||
.endpoint-login .ui-password-group > .ui-password-toggle:focus,
|
||||
.endpoint-login .ui-password-group > .ui-password-toggle:focus-visible,
|
||||
.endpoint-system_auth .ui-password-group > .ui-password-toggle:hover,
|
||||
.endpoint-system_auth .ui-password-group > .ui-password-toggle:focus,
|
||||
.endpoint-system_auth .ui-password-group > .ui-password-toggle:focus-visible,
|
||||
.endpoint-user_management .ui-password-group > .ui-password-toggle:hover,
|
||||
.endpoint-user_management .ui-password-group > .ui-password-toggle:focus,
|
||||
.endpoint-user_management .ui-password-group > .ui-password-toggle:focus-visible,
|
||||
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:hover,
|
||||
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:focus,
|
||||
.endpoint-user_management .modal .ui-password-group > .ui-password-toggle:focus-visible {
|
||||
background-color: var(--dark-800) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.ui-password-group > .ui-password-toggle {
|
||||
flex-basis: 44px !important;
|
||||
width: 44px !important;
|
||||
min-width: 44px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
enhancePasswordFields();
|
||||
observePasswordFields();
|
||||
enhanceSearchableTables();
|
||||
wireCopyButtons();
|
||||
wireUnsavedWarnings();
|
||||
@@ -8,36 +9,78 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
initResponsiveCategoryBadges();
|
||||
});
|
||||
|
||||
function enhancePasswordFields() {
|
||||
document.querySelectorAll('input[type="password"]').forEach(function (input) {
|
||||
if (input.dataset.uiPasswordReady === '1') return;
|
||||
if (input.closest('[data-ui-skip-toggle="true"]')) return;
|
||||
function initPasswordField(input) {
|
||||
if (!input || input.dataset.uiPasswordReady === '1') return;
|
||||
if (input.closest('[data-ui-skip-toggle="true"]')) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'ui-password-toggle';
|
||||
btn.setAttribute('aria-label', 'Pokaż lub ukryj hasło');
|
||||
btn.textContent = '👁';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'ui-password-toggle';
|
||||
btn.setAttribute('aria-label', 'Pokaż lub ukryj hasło');
|
||||
btn.setAttribute('aria-pressed', 'false');
|
||||
btn.title = 'Pokaż hasło';
|
||||
btn.innerHTML = '<span aria-hidden="true">👁</span>';
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
const visible = input.type === 'text';
|
||||
input.type = visible ? 'password' : 'text';
|
||||
btn.textContent = visible ? '👁' : '🙈';
|
||||
btn.classList.toggle('is-active', !visible);
|
||||
});
|
||||
const syncState = function () {
|
||||
const visible = input.type === 'text';
|
||||
btn.innerHTML = visible ? '<span aria-hidden="true">🙈</span>' : '<span aria-hidden="true">👁</span>';
|
||||
btn.classList.toggle('is-active', visible);
|
||||
btn.setAttribute('aria-pressed', visible ? 'true' : 'false');
|
||||
btn.title = visible ? 'Ukryj hasło' : 'Pokaż hasło';
|
||||
};
|
||||
|
||||
if (input.parentElement && input.parentElement.classList.contains('input-group')) {
|
||||
input.parentElement.appendChild(btn);
|
||||
} else {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'input-group ui-password-group';
|
||||
input.parentNode.insertBefore(wrapper, input);
|
||||
wrapper.appendChild(input);
|
||||
wrapper.appendChild(btn);
|
||||
btn.addEventListener('click', function () {
|
||||
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) {}
|
||||
}
|
||||
|
||||
input.dataset.uiPasswordReady = '1';
|
||||
});
|
||||
|
||||
const parent = input.parentElement;
|
||||
if (parent && parent.classList.contains('input-group')) {
|
||||
parent.classList.add('ui-password-group');
|
||||
if (!parent.querySelector(':scope > .ui-password-toggle')) {
|
||||
parent.appendChild(btn);
|
||||
}
|
||||
} else {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'input-group ui-password-group';
|
||||
input.parentNode.insertBefore(wrapper, input);
|
||||
wrapper.appendChild(input);
|
||||
wrapper.appendChild(btn);
|
||||
}
|
||||
|
||||
input.dataset.uiPasswordReady = '1';
|
||||
syncState();
|
||||
}
|
||||
|
||||
function enhancePasswordFields(root) {
|
||||
const scope = root && root.querySelectorAll ? root : document;
|
||||
if (scope.matches && scope.matches('input[type="password"]')) {
|
||||
initPasswordField(scope);
|
||||
}
|
||||
scope.querySelectorAll('input[type="password"]').forEach(initPasswordField);
|
||||
}
|
||||
|
||||
function observePasswordFields() {
|
||||
if (window.__uiPasswordObserverReady) return;
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
mutation.addedNodes.forEach(function (node) {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
enhancePasswordFields(node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.__uiPasswordObserverReady = true;
|
||||
}
|
||||
|
||||
function enhanceSearchableTables() {
|
||||
|
||||
@@ -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 = `<input id="checkbox-${item.id}" class="large-checkbox" type="checkbox" ${item.purchased ? 'checked' : ''} ${(item.not_purchased || isArchived) ? 'disabled' : ''}>`;
|
||||
|
||||
@@ -326,9 +363,13 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
|
||||
let actionButtons = '';
|
||||
|
||||
if (canEditListItem) {
|
||||
const dragHandleButton = window.isSorting
|
||||
? `<button type="button" class="${iconBtn} drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" ${isArchived ? 'disabled' : ''}>☰</button>`
|
||||
: '';
|
||||
|
||||
actionButtons += `
|
||||
<button type="button" class="${iconBtn} drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" ${isArchived ? 'disabled' : ''}>☰</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="editItem(${item.id}, ${nameForEdit}, ${quantity})"`}>✏️</button>
|
||||
${dragHandleButton}
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${JSON.stringify(String(item.name || ''))}, ${quantity})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
|
||||
}
|
||||
|
||||
@@ -340,7 +381,12 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
|
||||
<button type="button" class="${iconBtn}" ${canMarkNotPurchased ? `onclick="markNotPurchasedModal(event, ${item.id})"` : 'disabled'}>⚠️</button>`;
|
||||
}
|
||||
|
||||
if (canShowShareActions) {
|
||||
if (temporaryShareUndo) {
|
||||
actionButtons += `
|
||||
<button type="button" class="${iconBtn} shopping-action-btn--countdown" disabled data-countdown-for="${item.id}">${countdownSeconds}s</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${nameForEdit}, ${quantity})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
|
||||
} else if (canShowShareActions) {
|
||||
actionButtons += `
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="openNoteModal(event, ${item.id})"`}>📝</button>`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<a href="{{ request.url_root }}share/{{ list.share_token }}" class="btn btn-outline-primary btn-sm w-100 mb-3" {% if not
|
||||
list.is_public %}disabled{% endif %}>
|
||||
✅ Otwórz tryb zakupowy / odznaczania produktów
|
||||
✅ Otwórz tryb odznaczania
|
||||
</a>
|
||||
|
||||
<div id="share-card" class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
@@ -68,9 +68,8 @@
|
||||
|
||||
<!-- Progress bar (dynamic) -->
|
||||
<h5 id="progress-title" class="mb-2">
|
||||
📊 Postęp listy —
|
||||
<span id="purchased-count">{{ purchased_count }}</span>/
|
||||
<span id="total-count">{{ total_count }}</span> kupionych
|
||||
Postęp listy —
|
||||
<span id="purchased-count">{{ purchased_count }}</span>/<span id="total-count">{{ total_count }}</span> kupionych
|
||||
(<span id="percent-value">{{ percent|int }}</span>%)
|
||||
</h5>
|
||||
|
||||
@@ -83,7 +82,7 @@
|
||||
title="Pozostałe do kupienia"></div>
|
||||
<span id="progress-label" class="progress-label small fw-bold"></span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% if total_expense > 0 %}
|
||||
<div id="total-expense2" class="text-success fw-bold mb-3">
|
||||
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN
|
||||
@@ -129,9 +128,8 @@
|
||||
</div>
|
||||
<div class="list-item-actions shopping-item-actions" role="group">
|
||||
{% if not is_share %}
|
||||
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn drag-handle" title="Przesuń produkt" aria-label="Przesuń produkt" {% if list.is_archived %}disabled{% endif %}>☰</button>
|
||||
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
|
||||
%}onclick="editItem({{ item.id }}, '{{ item.name }}', {{ item.quantity or 1 }})" {% endif %}>✏️</button>
|
||||
%}onclick='openEditItemModal(event, {{ item.id }}, {{ item.name|tojson }}, {{ item.quantity or 1 }})' {% endif %}>✏️</button>
|
||||
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
|
||||
%}onclick="deleteItem({{ item.id }})" {% endif %}>🗑️</button>
|
||||
{% endif %}
|
||||
@@ -154,6 +152,38 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="modal fade" id="editItemModal" tabindex="-1" aria-labelledby="editItemModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<form id="editItemForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editItemModalLabel">Edytuj produkt</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editItemId">
|
||||
<div class="mb-3">
|
||||
<label for="editItemName" class="form-label">Nazwa produktu</label>
|
||||
<input type="text" id="editItemName" class="form-control bg-dark text-white border-secondary" maxlength="255" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="editItemQuantity" class="form-label">Ilość</label>
|
||||
<input type="number" id="editItemQuantity" class="form-control bg-dark text-white border-secondary" min="1" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer justify-content-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light"><span class="shopping-btn-icon" aria-hidden="true">💾</span><span class="shopping-btn-label">Zapisz</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not list.is_archived %}
|
||||
<div class="list-action-block mb-3">
|
||||
<div class="list-action-row mb-2">
|
||||
@@ -379,6 +409,27 @@
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='category_modal.js') }}?v={{ APP_VERSION }}"></script>
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const editItemForm = document.getElementById('editItemForm');
|
||||
if (!editItemForm) return;
|
||||
|
||||
editItemForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const itemId = parseInt(document.getElementById('editItemId').value, 10);
|
||||
const itemName = document.getElementById('editItemName').value;
|
||||
const itemQuantity = document.getElementById('editItemQuantity').value;
|
||||
|
||||
editItem(itemId, itemName, itemQuantity);
|
||||
|
||||
const modalEl = document.getElementById('editItemModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
1
shopping_app/uploads
Symbolic link
1
shopping_app/uploads
Symbolic link
@@ -0,0 +1 @@
|
||||
../uploads
|
||||
Reference in New Issue
Block a user