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