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 = ``;
+ 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 });
}
}