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