duzo zmian i funkcji
This commit is contained in:
5
alters.txt
Normal file
5
alters.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# SUGEROWANE PRODUKTY
|
||||
CREATE TABLE IF NOT EXISTS suggested_product (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL
|
||||
);
|
72
app.py
72
app.py
@ -1,7 +1,8 @@
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||
from flask_socketio import SocketIO, emit, join_room
|
||||
@ -19,6 +20,21 @@ socketio = SocketIO(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
static_bp = Blueprint('static_bp', __name__)
|
||||
|
||||
@static_bp.route('/static/js/live.js')
|
||||
def serve_live_js():
|
||||
response = send_from_directory('static/js', 'live.js')
|
||||
response.cache_control.no_cache = True
|
||||
response.cache_control.no_store = True
|
||||
response.cache_control.must_revalidate = True
|
||||
response.expires = 0
|
||||
response.pragma = 'no-cache'
|
||||
return response
|
||||
|
||||
app.register_blueprint(static_bp)
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||
@ -33,6 +49,7 @@ class ShoppingList(db.Model):
|
||||
is_temporary = db.Column(db.Boolean, default=False)
|
||||
share_token = db.Column(db.String(64), unique=True, nullable=True)
|
||||
expires_at = db.Column(db.DateTime, nullable=True)
|
||||
owner = db.relationship('User', backref='lists', lazy=True)
|
||||
|
||||
class Item(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -43,10 +60,18 @@ class Item(db.Model):
|
||||
purchased = db.Column(db.Boolean, default=False)
|
||||
purchased_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
class SuggestedProduct(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(150), unique=True, nullable=False)
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
@app.context_processor
|
||||
def inject_time():
|
||||
return dict(time=time)
|
||||
|
||||
@app.before_request
|
||||
def require_system_password():
|
||||
if 'authorized' not in request.cookies \
|
||||
@ -83,6 +108,10 @@ def system_auth():
|
||||
@app.route('/')
|
||||
def index_guest():
|
||||
lists = ShoppingList.query.all()
|
||||
for l in lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
l.total_count = len(items)
|
||||
l.purchased_count = len([i for i in items if i.purchased])
|
||||
return render_template('index.html', lists=lists)
|
||||
|
||||
@app.errorhandler(404)
|
||||
@ -95,21 +124,16 @@ def login():
|
||||
user = User.query.filter_by(username=request.form['username']).first()
|
||||
if user and check_password_hash(user.password_hash, request.form['password']):
|
||||
login_user(user)
|
||||
flash('Zalogowano pomyślnie')
|
||||
return redirect(url_for('dashboard'))
|
||||
flash('Nieprawidłowy login lub hasło')
|
||||
flash('Zalogowano pomyślnie', 'success')
|
||||
return redirect(url_for('index_guest'))
|
||||
flash('Nieprawidłowy login lub hasło', 'danger')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
lists = ShoppingList.query.filter_by(owner_id=current_user.id).all()
|
||||
return render_template('dashboard.html', lists=lists)
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('Wylogowano pomyślnie', 'success')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/create', methods=['POST'])
|
||||
@ -122,6 +146,7 @@ def create_list():
|
||||
new_list = ShoppingList(title=title, owner_id=current_user.id, is_temporary=is_temporary, share_token=token, expires_at=expires_at)
|
||||
db.session.add(new_list)
|
||||
db.session.commit()
|
||||
flash('Utworzono nową listę', 'success')
|
||||
return redirect(url_for('view_list', list_id=new_list.id))
|
||||
|
||||
@app.route('/list/<int:list_id>')
|
||||
@ -156,8 +181,17 @@ def copy_list(list_id):
|
||||
copy_item = Item(list_id=new_list.id, name=item.name)
|
||||
db.session.add(copy_item)
|
||||
db.session.commit()
|
||||
flash('Skopiowano listę', 'success')
|
||||
return redirect(url_for('view_list', list_id=new_list.id))
|
||||
|
||||
@app.route('/suggest_products')
|
||||
def suggest_products():
|
||||
query = request.args.get('q', '')
|
||||
suggestions = []
|
||||
if query:
|
||||
suggestions = SuggestedProduct.query.filter(SuggestedProduct.name.ilike(f'%{query}%')).limit(5).all()
|
||||
return {'suggestions': [s.name for s in suggestions]}
|
||||
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
def admin_panel():
|
||||
@ -166,7 +200,7 @@ def admin_panel():
|
||||
user_count = User.query.count()
|
||||
list_count = ShoppingList.query.count()
|
||||
item_count = Item.query.count()
|
||||
all_lists = ShoppingList.query.all()
|
||||
all_lists = ShoppingList.query.options(db.joinedload(ShoppingList.owner)).all()
|
||||
return render_template('admin/admin_panel.html', user_count=user_count, list_count=list_count, item_count=item_count, all_lists=all_lists)
|
||||
|
||||
@app.route('/admin/delete_list/<int:list_id>')
|
||||
@ -175,11 +209,10 @@ def delete_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
list_to_delete = ShoppingList.query.get_or_404(list_id)
|
||||
# Usuń wszystkie powiązane produkty
|
||||
Item.query.filter_by(list_id=list_to_delete.id).delete()
|
||||
db.session.delete(list_to_delete)
|
||||
db.session.commit()
|
||||
flash(f'Usunięto listę: {list_to_delete.title}')
|
||||
flash(f'Usunięto listę: {list_to_delete.title}', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/delete_all_lists')
|
||||
@ -190,7 +223,7 @@ def delete_all_lists():
|
||||
Item.query.delete()
|
||||
ShoppingList.query.delete()
|
||||
db.session.commit()
|
||||
flash('Usunięto wszystkie listy')
|
||||
flash('Usunięto wszystkie listy', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_user', methods=['GET', 'POST'])
|
||||
@ -204,7 +237,7 @@ def add_user():
|
||||
new_user = User(username=username, password_hash=password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash('Dodano nowego użytkownika')
|
||||
flash('Dodano nowego użytkownika', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
return render_template('admin/add_user.html')
|
||||
|
||||
@ -230,7 +263,7 @@ def reset_password(user_id):
|
||||
new_password = generate_password_hash(request.form['password'])
|
||||
user.password_hash = new_password
|
||||
db.session.commit()
|
||||
flash('Hasło zresetowane')
|
||||
flash('Hasło zresetowane', 'success')
|
||||
return redirect(url_for('list_users'))
|
||||
return render_template('admin/reset_password.html', user=user)
|
||||
|
||||
@ -242,7 +275,7 @@ def delete_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash('Użytkownik usunięty')
|
||||
flash('Użytkownik usunięty', 'success')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
@socketio.on('delete_item')
|
||||
@ -280,6 +313,11 @@ def handle_add_item(data):
|
||||
added_by=current_user.id if current_user.is_authenticated else None
|
||||
)
|
||||
db.session.add(new_item)
|
||||
|
||||
if not SuggestedProduct.query.filter_by(name=name).first():
|
||||
new_suggestion = SuggestedProduct(name=name)
|
||||
db.session.add(new_suggestion)
|
||||
|
||||
db.session.commit()
|
||||
emit('item_added', {
|
||||
'id': new_item.id,
|
||||
|
@ -4,65 +4,110 @@ function setupList(listId, username) {
|
||||
socket.emit('join_list', { room: listId, username: username });
|
||||
|
||||
const newItemInput = document.getElementById('newItem');
|
||||
if (newItemInput) {
|
||||
newItemInput.focus();
|
||||
newItemInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addItem(listId);
|
||||
}
|
||||
});
|
||||
|
||||
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 = '';
|
||||
newItemInput.focus();
|
||||
};
|
||||
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');
|
||||
|
||||
// Delegacja zdarzenia checkboxów
|
||||
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('user_joined', data => showToast(`${data.username} dołączył do listy`));
|
||||
socket.on('item_checked', data => {
|
||||
updateItemState(data.item_id, true);
|
||||
});
|
||||
|
||||
socket.on('item_unchecked', data => {
|
||||
updateItemState(data.item_id, false);
|
||||
});
|
||||
|
||||
socket.on('item_added', data => {
|
||||
showToast(`${data.added_by} dodał: ${data.name}`);
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap';
|
||||
li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap bg-light text-dark';
|
||||
li.id = `item-${data.id}`;
|
||||
li.innerHTML = `
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<input type="checkbox">
|
||||
<span id="name-${data.id}">${data.name}</span>
|
||||
<span id="name-${data.id}" class="text-white">${data.name}</span>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<button class="btn btn-sm btn-outline-warning me-1" onclick="editItem(${data.id}, '${data.name}')">✏️ Edytuj</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem(${data.id})">🗑️ Usuń</button>
|
||||
</div>
|
||||
`;
|
||||
itemsContainer.appendChild(li);
|
||||
});
|
||||
|
||||
socket.on('item_checked', data => {
|
||||
const checkbox = document.querySelector(`#item-${data.item_id} input[type='checkbox']`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('item_unchecked', data => {
|
||||
const checkbox = document.querySelector(`#item-${data.item_id} input[type='checkbox']`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
document.getElementById('items').appendChild(li);
|
||||
updateProgressBar();
|
||||
});
|
||||
|
||||
socket.on('item_deleted', data => {
|
||||
@ -71,6 +116,7 @@ function setupList(listId, username) {
|
||||
li.remove();
|
||||
}
|
||||
showToast('Usunięto produkt');
|
||||
updateProgressBar();
|
||||
});
|
||||
|
||||
socket.on('item_edited', data => {
|
||||
@ -80,6 +126,42 @@ function setupList(listId, username) {
|
||||
}
|
||||
showToast(`Zmieniono nazwę na: ${data.new_name}`);
|
||||
});
|
||||
|
||||
updateProgressBar();
|
||||
}
|
||||
|
||||
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('bg-light', 'text-dark');
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -124,5 +206,5 @@ function showToast(message) {
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
|
||||
toastContainer.appendChild(toast);
|
||||
setTimeout(() => { toast.remove(); }, 4000);
|
||||
setTimeout(() => { toast.remove(); }, 1750);
|
||||
}
|
||||
|
12
static/js/toasts.js
Normal file
12
static/js/toasts.js
Normal file
@ -0,0 +1,12 @@
|
||||
function showToast(message, type = 'primary') {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) return;
|
||||
|
||||
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(); }, 4000);
|
||||
}
|
@ -28,7 +28,8 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Właściciel (ID)</th>
|
||||
<th>Utworzono</th>
|
||||
<th>Właściciel (ID / nazwa)</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -37,7 +38,14 @@
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold">{{ l.title }}</td>
|
||||
<td>{{ l.owner_id }}</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
<td>
|
||||
{% if l.owner_id %}
|
||||
{{ l.owner_id }} / {{ l.owner.username if l.owner else 'Brak użytkownika' }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
|
@ -6,7 +6,8 @@
|
||||
<title>{% block title %}Live Lista Zakupów{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/live.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_live_js') }}?v={{ time.time() | int }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/toasts.js') }}"></script>
|
||||
</head>
|
||||
<body class="bg-dark text-white">
|
||||
|
||||
@ -25,10 +26,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<div class="container px-2">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
{% set cat = 'info' if not category else ('danger' if category == 'error' else category) %}
|
||||
{% if message == 'Please log in to access this page.' %}
|
||||
showToast("Aby uzyskać dostęp do tej strony, musisz być zalogowany.", "danger");
|
||||
{% else %}
|
||||
showToast({{ message|tojson }}, "{{ cat }}");
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,44 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Twoje listy{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">📝 Twoje listy zakupowe</h2>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-light">👀 Tryb gościa</a>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-success">➕ Utwórz nową listę</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-secondary">🚪 Wyloguj</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Data utworzenia</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold">{{ l.title }}</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="{{ url_for('copy_list', list_id=l.id) }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Nie masz jeszcze żadnych list. Kliknij „Utwórz nową listę”, aby dodać pierwszą!</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -4,24 +4,21 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Twoje listy zakupów</h2>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post" class="row g-2 align-items-center">
|
||||
<div class="col-sm-5">
|
||||
<input type="text" name="title" placeholder="Nazwa listy" required class="form-control">
|
||||
<form action="/create" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Nazwa listy</label>
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę listy" required class="form-control">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="temporary" class="form-check-input" id="tempCheck">
|
||||
<label for="tempCheck" class="form-check-label">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-success">➕ Utwórz nową listę</button>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="temporary" class="form-check-input" id="tempCheck">
|
||||
<label for="tempCheck" class="form-check-label">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,15 +26,23 @@
|
||||
{% if lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in lists %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap">
|
||||
<span class="fw-bold">{{ l.title }}</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
{% else %}
|
||||
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
{% endif %}
|
||||
{% set purchased_count = l.purchased_count if l.purchased_count is defined else 0 %}
|
||||
{% set total_count = l.total_count if l.total_count is defined else 0 %}
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }}</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
{% else %}
|
||||
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">{{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@ -46,4 +51,4 @@
|
||||
<p>Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -19,12 +19,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar (dynamic) -->
|
||||
<div class="progress mb-3">
|
||||
<div id="progress-bar" class="progress-bar bg-success" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap" id="item-{{ item.id }}">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<input type="checkbox">
|
||||
<span id="name-{{ item.id }}">{{ item.name }}</span>
|
||||
<input type="checkbox" {% if item.purchased %}checked{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<button class="btn btn-sm btn-outline-warning me-1" onclick="editItem({{ item.id }}, '{{ item.name }}')">✏️ Edytuj</button>
|
||||
@ -34,7 +39,7 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group mb-2 position-relative">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
@ -44,4 +49,13 @@
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bg-success {
|
||||
background-color: #1e7e34 !important;
|
||||
}
|
||||
.bg-light {
|
||||
background-color: #2c2f33 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
@ -9,10 +9,10 @@
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap clickable-item" id="item-{{ item.id }}">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center gap-3 flex-grow-1">
|
||||
<input type="checkbox" class="form-check-input large-checkbox" {% if item.purchased %}checked{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="flex-grow-1">{{ item.name }}</span>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@ -28,14 +28,32 @@
|
||||
<script>
|
||||
setupList({{ list.id }}, 'Gość');
|
||||
|
||||
// Dodanie kliknięcia na cały wiersz
|
||||
document.querySelectorAll('.clickable-item').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
// Ignoruj kliknięcia w input (żeby nie duplikować akcji)
|
||||
if (e.target.tagName.toLowerCase() !== 'input') {
|
||||
const checkbox = this.querySelector('input[type="checkbox"]');
|
||||
checkbox.checked = !checkbox.checked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
|
||||
if (checkbox.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkbox.checked) {
|
||||
socket.emit('uncheck_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
|
||||
} else {
|
||||
socket.emit('check_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
|
||||
}
|
||||
|
||||
checkbox.disabled = true;
|
||||
this.classList.add('opacity-50');
|
||||
|
||||
let existingSpinner = this.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');
|
||||
checkbox.parentElement.appendChild(spinner);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -49,6 +67,11 @@
|
||||
.clickable-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.bg-success {
|
||||
background-color: #1e7e34 !important;
|
||||
}
|
||||
.bg-light {
|
||||
background-color: #2c2f33 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user