diff --git a/app.py b/app.py index 63af4a1..b6c08ee 100644 --- a/app.py +++ b/app.py @@ -839,6 +839,7 @@ def handle_join(data): username = data.get('username', 'Gość') join_room(room) emit('user_joined', {'username': username}, to=room) + emit('joined_confirmation', {'room': room}) @socketio.on('add_item') def handle_add_item(data): diff --git a/static/js/live.js b/static/js/live.js index b5c0ee4..79d3245 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -327,3 +327,4 @@ function showToast(message, type = 'primary') { toastContainer.appendChild(toast); setTimeout(() => { toast.remove(); }, 1750); } + diff --git a/static/js/live_backup.js b/static/js/live_backup.js new file mode 100644 index 0000000..84af0ef --- /dev/null +++ b/static/js/live_backup.js @@ -0,0 +1,319 @@ +const socket = io(); + +function setupList(listId, username) { + socket.emit('join_list', { room: listId, username: username }); + + const newItemInput = document.getElementById('newItem'); + + const parentDiv = newItemInput.closest('.input-group'); + if (parentDiv) { + parentDiv.classList.add('position-relative'); + } + + const suggestionsBox = document.createElement('div'); + suggestionsBox.className = 'list-group position-absolute w-100'; + suggestionsBox.style.top = '100%'; + suggestionsBox.style.left = '0'; + suggestionsBox.style.zIndex = '9999'; + newItemInput.parentNode.appendChild(suggestionsBox); + + newItemInput.addEventListener('input', async function () { + const query = this.value; + if (query.length < 2) { + suggestionsBox.innerHTML = ''; + return; + } + + const res = await fetch(`/suggest_products?q=${encodeURIComponent(query)}`); + const data = await res.json(); + + suggestionsBox.innerHTML = ''; + data.suggestions.forEach(s => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'list-group-item list-group-item-action'; + item.textContent = s; + item.onclick = () => { + newItemInput.value = s; + suggestionsBox.innerHTML = ''; + }; + suggestionsBox.appendChild(item); + }); + }); + + newItemInput.addEventListener('blur', () => { + setTimeout(() => { suggestionsBox.innerHTML = ''; }, 200); + }); + + newItemInput.focus(); + newItemInput.addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + addItem(listId); + } + }); + + const itemsContainer = document.getElementById('items'); + + itemsContainer.addEventListener('change', function (e) { + if (e.target && e.target.type === 'checkbox') { + const li = e.target.closest('li'); + if (li) { + const id = parseInt(li.id.replace('item-', ''), 10); + + if (e.target.checked) { + socket.emit('check_item', { item_id: id }); + } else { + socket.emit('uncheck_item', { item_id: id }); + } + + e.target.disabled = true; + li.classList.add('opacity-50'); + + let existingSpinner = li.querySelector('.spinner-border'); + if (!existingSpinner) { + const spinner = document.createElement('span'); + spinner.className = 'spinner-border spinner-border-sm ms-2'; + spinner.setAttribute('role', 'status'); + spinner.setAttribute('aria-hidden', 'true'); + e.target.parentElement.appendChild(spinner); + } + } + } + }); + + socket.on('item_checked', data => { + updateItemState(data.item_id, true); + }); + + socket.on('item_unchecked', data => { + updateItemState(data.item_id, false); + }); + + socket.on('expense_added', data => { + const badgeEl = document.getElementById('total-expense1'); + if (badgeEl) { + badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`; + badgeEl.classList.remove('bg-secondary'); + badgeEl.classList.add('bg-success'); + badgeEl.style.display = ''; + } + + const summaryEl = document.getElementById('total-expense2'); + if (summaryEl) { + summaryEl.innerHTML = `💸 Łącznie wydano: ${data.total.toFixed(2)} PLN`; + } + + showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`); + }); + + socket.on('item_added', data => { + showToast(`${data.added_by} dodał: ${data.name}`); + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap item-not-checked'; + li.id = `item-${data.id}`; + + let quantityBadge = ''; + if (data.quantity && data.quantity > 1) { + quantityBadge = `x${data.quantity}`; + } + + li.innerHTML = ` +
+ + ${data.name} ${quantityBadge} +
+
+ + +
+ `; + + document.getElementById('items').appendChild(li); + updateProgressBar(); + }); + + socket.on('item_deleted', data => { + const li = document.getElementById(`item-${data.item_id}`); + if (li) { + li.remove(); + } + showToast('Usunięto produkt'); + updateProgressBar(); + }); + + socket.on('progress_updated', function(data) { + const progressBar = document.getElementById('progress-bar'); + if (progressBar) { + progressBar.style.width = data.percent + '%'; + progressBar.setAttribute('aria-valuenow', data.percent); + progressBar.textContent = `${Math.round(data.percent)}%`; + } + + const progressTitle = document.getElementById('progress-title'); + if (progressTitle) { + progressTitle.textContent = `📊 Postęp listy — ${data.purchased_count}/${data.total_count} kupionych (${Math.round(data.percent)}%)`; + } + }); + + socket.on('note_updated', data => { + const itemEl = document.getElementById(`item-${data.item_id}`); + if (itemEl) { + let noteEl = itemEl.querySelector('small'); + if (noteEl) { + noteEl.innerHTML = `[ Notatka: ${data.note} ]`; + } else { + const newNote = document.createElement('small'); + newNote.className = 'text-danger ms-4'; + newNote.innerHTML = `[ Notatka: ${data.note} ]`; + + const flexColumn = itemEl.querySelector('.d-flex.flex-column'); + if (flexColumn) { + flexColumn.appendChild(newNote); + } else { + itemEl.appendChild(newNote); + } + } + } + showToast('Notatka zaktualizowana!'); + }); + + socket.on('item_edited', data => { + const nameSpan = document.getElementById(`name-${data.item_id}`); + if (nameSpan) { + let quantityBadge = ''; + if (data.new_quantity && data.new_quantity > 1) { + quantityBadge = ` x${data.new_quantity}`; + } + nameSpan.innerHTML = `${data.new_name}${quantityBadge}`; + } + showToast(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`); + }); + + updateProgressBar(); + + // --- NOWE: zapisz dane do reconnect --- + window.LIST_ID = listId; + window.usernameForReconnect = username; + + socket.on('joined_confirmation', function(data) { + console.log('Dołączono do pokoju:', data.room); + showToast('Pokój ponownie dołączony ✅', 'info'); + }); +} + +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); +} diff --git a/static/js/socket_reconnect.js b/static/js/socket_reconnect.js index c4ea4b4..3ec1098 100644 --- a/static/js/socket_reconnect.js +++ b/static/js/socket_reconnect.js @@ -27,6 +27,7 @@ socket.on('connect', function() { showToast('Połączono z serwerem! 🔄', 'info'); // Automatyczne ponowne dołączenie do pokoju if (window.LIST_ID && window.usernameForReconnect) { + console.log('Ponownie wysyłam join_list:', window.LIST_ID, window.usernameForReconnect); socket.emit('join_list', { room: window.LIST_ID, username: window.usernameForReconnect }); } }