diff --git a/app.py b/app.py index f2c4349..59d11f5 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ import secrets import time import mimetypes from datetime import datetime, timedelta -from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort +from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort, session from markupsafe import Markup from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user @@ -832,18 +832,31 @@ def handle_edit_item(data): 'new_quantity': item.quantity }, to=str(item.list_id)) - @socketio.on('join_list') def handle_join(data): room = str(data['room']) username = data.get('username', 'Gość') join_room(room) + + if room not in active_users: + active_users[room] = set() + active_users[room].add(username) + shopping_list = ShoppingList.query.get(int(data['room'])) list_title = shopping_list.title if shopping_list else "Twoja lista" 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(): + for room, users in active_users.items(): + if current_user.username in users: + users.remove(current_user.username) + emit('user_left', {'username': current_user.username}, to=room) + emit('user_list', {'users': list(users)}, to=room) + @socketio.on('add_item') def handle_add_item(data): list_id = data['list_id'] diff --git a/static/js/live.js b/static/js/live.js index 423d36c..6df1dcc 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -196,6 +196,20 @@ function setupList(listId, username) { window.LIST_ID = listId; window.usernameForReconnect = username; + +socket.on('user_joined', function(data) { + showToast(`${data.username} dołączył do listy 👥`, 'info'); +}); + +socket.on('user_left', function(data) { + showToast(`${data.username} opuścił listę ❌`, 'warning'); +}); + +socket.on('user_list', function(data) { + const userList = data.users.join(', '); + showToast(`Obecni: ${userList}`, 'info'); +}); + } function updateItemState(itemId, isChecked) { diff --git a/static/js/live_backup.js b/static/js/live_backup.js deleted file mode 100644 index 84af0ef..0000000 --- a/static/js/live_backup.js +++ /dev/null @@ -1,319 +0,0 @@ -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 21a9581..604599e 100644 --- a/static/js/socket_reconnect.js +++ b/static/js/socket_reconnect.js @@ -43,6 +43,11 @@ socket.on('connect', function() { firstConnect = false; }); +socket.on('disconnect', function(reason) { + showToast('Utracono połączenie z serwerem...', 'warning'); + disableCheckboxes(true); +}); + socket.off('joined_confirmation'); socket.on('joined_confirmation', function(data) { if (wasReconnected) {