fixy w js

This commit is contained in:
Mateusz Gruszczyński
2025-07-09 16:44:03 +02:00
parent 4152ff4e13
commit cfd7f1d31c
5 changed files with 254 additions and 207 deletions

5
app.py
View File

@@ -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

240
static/js/functions.js Normal file
View File

@@ -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 = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
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 = `<span class="badge bg-secondary">x${item.quantity}</span>`;
}
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 = `[ <b>${item.note}</b> ]`;
nameSpan.insertAdjacentElement('afterend', newNote);
} else {
noteEl.innerHTML = `[ <b>${item.note}</b> ]`;
}
} 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 = `
<div class="d-flex align-items-center gap-3 flex-grow-1">
<input class="large-checkbox" type="checkbox" ${item.purchased ? 'checked' : ''}>
<span id="name-${item.id}" class="text-white">${item.name} ${quantityBadge}</span>
${item.note ? `<small class="text-danger ms-4">[ <b>${item.note}</b> ]</small>` : ''}
</div>
<button type="button" class="btn btn-sm btn-outline-info" onclick="openNoteModal(event, ${item.id})">📝</button>
`;
}
fragment.appendChild(li);
});
// Wyczyść i wstaw nowy porządek
itemsContainer.innerHTML = '';
itemsContainer.appendChild(fragment);
updateProgressBar();
}

View File

@@ -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 = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
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 = `<span class="badge bg-secondary">x${item.quantity}</span>`;
}
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 = `[ <b>${item.note}</b> ]`;
nameSpan.insertAdjacentElement('afterend', newNote);
} else {
noteEl.innerHTML = `[ <b>${item.note}</b> ]`;
}
} 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 = `
<div class="d-flex align-items-center gap-3 flex-grow-1">
<input class="large-checkbox" type="checkbox" ${item.purchased ? 'checked' : ''}>
<span id="name-${item.id}" class="text-white">${item.name} ${quantityBadge}</span>
${item.note ? `<small class="text-danger ms-4">[ <b>${item.note}</b> ]</small>` : ''}
</div>
<button type="button" class="btn btn-sm btn-outline-info" onclick="openNoteModal(event, ${item.id})">📝</button>
`;
}
fragment.appendChild(li);
});
// Wyczyść i wstaw nowy porządek
itemsContainer.innerHTML = '';
itemsContainer.appendChild(fragment);
updateProgressBar();
}

View File

@@ -72,6 +72,7 @@
<script src="{{ url_for('static_bp.serve_js_lib', filename='bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js_lib', filename='socket.io.min.js') }}"></script>
{% if request.endpoint != 'system_auth' %}
<script src="{{ url_for('static_bp.serve_js', filename='functions.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='live.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='hide_list.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='socket_reconnect.js') }}"></script>

View File

@@ -137,7 +137,6 @@
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='toggle_button.js') }}"></script>
{% endblock %}