diff --git a/app.py b/app.py index 9a88a37..f4c6925 100644 --- a/app.py +++ b/app.py @@ -44,7 +44,8 @@ PROTECTED_JS_FILES = { "expenses.js", "toggle_button.js", "user_management.js", - "mass_add.js" + "mass_add.js", + "functions.js" } os.makedirs(UPLOAD_FOLDER, exist_ok=True) @@ -1141,7 +1142,7 @@ def handle_join(data): emit('user_joined', {'username': username}, to=room) emit('user_list', {'users': list(active_users[room])}, to=room) emit('joined_confirmation', {'room': room, 'list_title': list_title}) - + @socketio.on('disconnect') def handle_disconnect(sid): global active_users diff --git a/static/js/functions.js b/static/js/functions.js new file mode 100644 index 0000000..7625209 --- /dev/null +++ b/static/js/functions.js @@ -0,0 +1,240 @@ +function updateItemState(itemId, isChecked) { + const checkbox = document.querySelector(`#item-${itemId} input[type='checkbox']`); + if (checkbox) { + checkbox.checked = isChecked; + checkbox.disabled = false; + const li = checkbox.closest('li'); + li.classList.remove('opacity-50', 'bg-light', 'text-dark', 'bg-success', 'text-white'); + + if (isChecked) { + li.classList.add('bg-success', 'text-white'); + } else { + li.classList.add('item-not-checked'); + } + + const sp = li.querySelector('.spinner-border'); + if (sp) sp.remove(); + } + updateProgressBar(); +} + +function updateProgressBar() { + const items = document.querySelectorAll('#items li'); + const total = items.length; + const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length; + const percent = total > 0 ? Math.round((purchased / total) * 100) : 0; + + const progressBar = document.getElementById('progress-bar'); + if (progressBar) { + progressBar.style.width = `${percent}%`; + progressBar.setAttribute('aria-valuenow', percent); + progressBar.textContent = `${percent}%`; + } +} + +function addItem(listId) { + const name = document.getElementById('newItem').value; + const quantityInput = document.getElementById('newQuantity'); + let quantity = 1; + + if (quantityInput) { + quantity = parseInt(quantityInput.value); + if (isNaN(quantity) || quantity < 1) { + quantity = 1; + } + } + + if (name.trim() === '') return; + + socket.emit('add_item', { list_id: listId, name: name, quantity: quantity }); + + document.getElementById('newItem').value = ''; + if (quantityInput) quantityInput.value = 1; + document.getElementById('newItem').focus(); +} + +function deleteItem(id) { + if (confirm('Na pewno usunąć produkt?')) { + socket.emit('delete_item', { item_id: id }); + } +} + +function editItem(id, oldName, oldQuantity) { + const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName); + if (newName === null) return; + + const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity); + if (newQuantityStr === null) return; + + const finalName = newName.trim() !== '' ? newName.trim() : oldName; + + let newQuantity = parseInt(newQuantityStr); + if (isNaN(newQuantity) || newQuantity < 1) { + newQuantity = oldQuantity; + } + + socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity }); +} + +function submitExpense(listId) { + const amountInput = document.getElementById('expenseAmount'); + const amount = parseFloat(amountInput.value); + if (isNaN(amount) || amount <= 0) { + showToast('Podaj poprawną kwotę!'); + return; + } + socket.emit('add_expense', { + list_id: listId, + amount: amount + }); + amountInput.value = ''; +} + +function copyLink(link) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(link).then(() => { + showToast('Link skopiowany do schowka!'); + }).catch((err) => { + console.error('Błąd clipboard API:', err); + fallbackCopyText(link); + }); + } else { + fallbackCopyText(link); + } +} + +function fallbackCopyText(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.top = 0; + textarea.style.left = 0; + textarea.style.opacity = 0; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + try { + const successful = document.execCommand('copy'); + if (successful) { + showToast('Link skopiowany do schowka!'); + } else { + showToast('Nie udało się skopiować linku', 'warning'); + } + } catch (err) { + console.error('Fallback błąd kopiowania:', err); + showToast('Nie udało się skopiować linku', 'warning'); + } + + document.body.removeChild(textarea); +} + +function showToast(message, type = 'primary') { + const toastContainer = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-bg-${type} border-0 show`; + toast.setAttribute('role', 'alert'); + toast.innerHTML = `
${message}
`; + toastContainer.appendChild(toast); + setTimeout(() => { toast.remove(); }, 1750); +} + +function isListDifferent(oldItems, newItems) { + if (oldItems.length !== newItems.length) return true; + + const oldIds = Array.from(oldItems).map(li => parseInt(li.id.replace('item-', ''), 10)).sort(); + const newIds = newItems.map(i => i.id).sort(); + + for (let i = 0; i < newIds.length; i++) { + if (oldIds[i] !== newIds[i]) { + return true; + } + } + return false; +} + +function updateListSmoothly(newItems) { + const itemsContainer = document.getElementById('items'); + const existingItemsMap = new Map(); + + Array.from(itemsContainer.querySelectorAll('li')).forEach(li => { + const id = parseInt(li.id.replace('item-', ''), 10); + existingItemsMap.set(id, li); + }); + + const fragment = document.createDocumentFragment(); + + newItems.forEach(item => { + let li = existingItemsMap.get(item.id); + let quantityBadge = ''; + if (item.quantity && item.quantity > 1) { + quantityBadge = `x${item.quantity}`; + } + + if (li) { + // Checkbox + const checkbox = li.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = item.purchased; + checkbox.disabled = false; // Zdejmij disabled jeśli było + } + + // Klasy + li.classList.remove('bg-success', 'text-white', 'item-not-checked', 'opacity-50'); + if (item.purchased) { + li.classList.add('bg-success', 'text-white'); + } else { + li.classList.add('item-not-checked'); + } + + // Nazwa + const nameSpan = li.querySelector(`#name-${item.id}`); + const expectedName = `${item.name} ${quantityBadge}`.trim(); + if (nameSpan && nameSpan.innerHTML.trim() !== expectedName) { + nameSpan.innerHTML = expectedName; + } + + // Notatka + let noteEl = li.querySelector('small'); + if (item.note) { + if (!noteEl) { + const newNote = document.createElement('small'); + newNote.className = 'text-danger ms-4'; + newNote.innerHTML = `[ ${item.note} ]`; + nameSpan.insertAdjacentElement('afterend', newNote); + } else { + noteEl.innerHTML = `[ ${item.note} ]`; + } + } else if (noteEl) { + noteEl.remove(); + } + + // Usuń spinner jeśli był + const sp = li.querySelector('.spinner-border'); + if (sp) sp.remove(); + + } else { + // Twórz nowy element + li = document.createElement('li'); + li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap ${item.purchased ? 'bg-success text-white' : 'item-not-checked'}`; + li.id = `item-${item.id}`; + + li.innerHTML = ` +
+ + ${item.name} ${quantityBadge} + ${item.note ? `[ ${item.note} ]` : ''} +
+ + `; + } + + fragment.appendChild(li); + }); + + // Wyczyść i wstaw nowy porządek + itemsContainer.innerHTML = ''; + itemsContainer.appendChild(fragment); + + updateProgressBar(); +} diff --git a/static/js/live.js b/static/js/live.js index 9e5bcdc..4394a0d 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -204,11 +204,11 @@ function setupList(listId, username) { socket.on('user_joined', function(data) { - showToast(`${data.username} dołączył do listy 👥`, 'info'); + showToast(`${data.username} dołączył do listy`, 'info'); }); socket.on('user_left', function(data) { - showToast(`${data.username} opuścił listę ❌`, 'warning'); + showToast(`${data.username} opuścił listę`, 'warning'); }); socket.on('user_list', function(data) { @@ -217,210 +217,16 @@ socket.on('user_list', function(data) { }); socket.on('full_list', function(data) { + const itemsContainer = document.getElementById('items'); + const oldItems = Array.from(itemsContainer.querySelectorAll('li')); + + if (isListDifferent(oldItems, data.items)) { updateListSmoothly(data.items); - showToast('🔄 Lista została zaktualizowana', 'info'); + showToast('Lista została zaktualizowana', 'info'); + } else { + updateListSmoothly(data.items); + } }); } -function updateItemState(itemId, isChecked) { - const checkbox = document.querySelector(`#item-${itemId} input[type='checkbox']`); - if (checkbox) { - checkbox.checked = isChecked; - checkbox.disabled = false; - const li = checkbox.closest('li'); - li.classList.remove('opacity-50', 'bg-light', 'text-dark', 'bg-success', 'text-white'); - - if (isChecked) { - li.classList.add('bg-success', 'text-white'); - } else { - li.classList.add('item-not-checked'); - } - - const sp = li.querySelector('.spinner-border'); - if (sp) sp.remove(); - } - updateProgressBar(); -} - -function updateProgressBar() { - const items = document.querySelectorAll('#items li'); - const total = items.length; - const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length; - const percent = total > 0 ? Math.round((purchased / total) * 100) : 0; - - const progressBar = document.getElementById('progress-bar'); - if (progressBar) { - progressBar.style.width = `${percent}%`; - progressBar.setAttribute('aria-valuenow', percent); - progressBar.textContent = `${percent}%`; - } -} - -function addItem(listId) { - const name = document.getElementById('newItem').value; - const quantityInput = document.getElementById('newQuantity'); - let quantity = 1; - - if (quantityInput) { - quantity = parseInt(quantityInput.value); - if (isNaN(quantity) || quantity < 1) { - quantity = 1; - } - } - - if (name.trim() === '') return; - - socket.emit('add_item', { list_id: listId, name: name, quantity: quantity }); - - document.getElementById('newItem').value = ''; - if (quantityInput) quantityInput.value = 1; - document.getElementById('newItem').focus(); -} - -function deleteItem(id) { - if (confirm('Na pewno usunąć produkt?')) { - socket.emit('delete_item', { item_id: id }); - } -} - -function editItem(id, oldName, oldQuantity) { - const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName); - if (newName === null) return; - - const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity); - if (newQuantityStr === null) return; - - const finalName = newName.trim() !== '' ? newName.trim() : oldName; - - let newQuantity = parseInt(newQuantityStr); - if (isNaN(newQuantity) || newQuantity < 1) { - newQuantity = oldQuantity; - } - - socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity }); -} - -function submitExpense(listId) { - const amountInput = document.getElementById('expenseAmount'); - const amount = parseFloat(amountInput.value); - if (isNaN(amount) || amount <= 0) { - showToast('Podaj poprawną kwotę!'); - return; - } - socket.emit('add_expense', { - list_id: listId, - amount: amount - }); - amountInput.value = ''; -} - -function copyLink(link) { - if (navigator.share) { - navigator.share({ - title: 'Lista zakupów', - text: 'Udostępniam Ci moją listę zakupów:', - url: link - }).catch((err) => console.log('Udostępnianie anulowane', err)); - } else { - navigator.clipboard.writeText(link).then(() => { - showToast('Link skopiowany do schowka!'); - }); - } -} - -function showToast(message, type = 'primary') { - const toastContainer = document.getElementById('toast-container'); - const toast = document.createElement('div'); - toast.className = `toast align-items-center text-bg-${type} border-0 show`; - toast.setAttribute('role', 'alert'); - toast.innerHTML = `
${message}
`; - toastContainer.appendChild(toast); - setTimeout(() => { toast.remove(); }, 1750); -} - -function updateListSmoothly(newItems) { - const itemsContainer = document.getElementById('items'); - const existingItemsMap = new Map(); - - Array.from(itemsContainer.querySelectorAll('li')).forEach(li => { - const id = parseInt(li.id.replace('item-', ''), 10); - existingItemsMap.set(id, li); - }); - - const fragment = document.createDocumentFragment(); - - newItems.forEach(item => { - let li = existingItemsMap.get(item.id); - let quantityBadge = ''; - if (item.quantity && item.quantity > 1) { - quantityBadge = `x${item.quantity}`; - } - - if (li) { - // Checkbox - const checkbox = li.querySelector('input[type="checkbox"]'); - if (checkbox) { - checkbox.checked = item.purchased; - checkbox.disabled = false; // Zdejmij disabled jeśli było - } - - // Klasy - li.classList.remove('bg-success', 'text-white', 'item-not-checked', 'opacity-50'); - if (item.purchased) { - li.classList.add('bg-success', 'text-white'); - } else { - li.classList.add('item-not-checked'); - } - - // Nazwa - const nameSpan = li.querySelector(`#name-${item.id}`); - const expectedName = `${item.name} ${quantityBadge}`.trim(); - if (nameSpan && nameSpan.innerHTML.trim() !== expectedName) { - nameSpan.innerHTML = expectedName; - } - - // Notatka - let noteEl = li.querySelector('small'); - if (item.note) { - if (!noteEl) { - const newNote = document.createElement('small'); - newNote.className = 'text-danger ms-4'; - newNote.innerHTML = `[ ${item.note} ]`; - nameSpan.insertAdjacentElement('afterend', newNote); - } else { - noteEl.innerHTML = `[ ${item.note} ]`; - } - } else if (noteEl) { - noteEl.remove(); - } - - // Usuń spinner jeśli był - const sp = li.querySelector('.spinner-border'); - if (sp) sp.remove(); - - } else { - // Twórz nowy element - li = document.createElement('li'); - li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap ${item.purchased ? 'bg-success text-white' : 'item-not-checked'}`; - li.id = `item-${item.id}`; - - li.innerHTML = ` -
- - ${item.name} ${quantityBadge} - ${item.note ? `[ ${item.note} ]` : ''} -
- - `; - } - - fragment.appendChild(li); - }); - - // Wyczyść i wstaw nowy porządek - itemsContainer.innerHTML = ''; - itemsContainer.appendChild(fragment); - - updateProgressBar(); -} diff --git a/templates/base.html b/templates/base.html index 5f259ea..2557e46 100644 --- a/templates/base.html +++ b/templates/base.html @@ -72,6 +72,7 @@ {% if request.endpoint != 'system_auth' %} + diff --git a/templates/main.html b/templates/main.html index 6e9e6e3..0c4aa41 100644 --- a/templates/main.html +++ b/templates/main.html @@ -137,7 +137,6 @@ {% block scripts %} - {% endblock %}