From b02bd27aa1fa073bad64a414db1ccac3c0f83b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 2 Jul 2025 23:51:19 +0200 Subject: [PATCH] duzo zmian i funkcji --- alters.txt | 5 ++ app.py | 72 ++++++++++++---- static/js/live.js | 138 ++++++++++++++++++++++++------- static/js/toasts.js | 12 +++ templates/admin/admin_panel.html | 12 ++- templates/base.html | 19 ++++- templates/dashboard.html | 44 ---------- templates/index.html | 49 ++++++----- templates/list.html | 22 ++++- templates/list_guest.html | 37 +++++++-- 10 files changed, 285 insertions(+), 125 deletions(-) create mode 100644 alters.txt create mode 100644 static/js/toasts.js delete mode 100644 templates/dashboard.html diff --git a/alters.txt b/alters.txt new file mode 100644 index 0000000..f6e273f --- /dev/null +++ b/alters.txt @@ -0,0 +1,5 @@ +# SUGEROWANE PRODUKTY +CREATE TABLE IF NOT EXISTS suggested_product ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL +); \ No newline at end of file diff --git a/app.py b/app.py index 7883cc6..2e2e5c4 100644 --- a/app.py +++ b/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/') @@ -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/') @@ -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, diff --git a/static/js/live.js b/static/js/live.js index abd54bd..3e3658e 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -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 = `
- ${data.name} + ${data.name}
`; - 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 = `
${message}
`; toastContainer.appendChild(toast); - setTimeout(() => { toast.remove(); }, 4000); + setTimeout(() => { toast.remove(); }, 1750); } diff --git a/static/js/toasts.js b/static/js/toasts.js new file mode 100644 index 0000000..b823406 --- /dev/null +++ b/static/js/toasts.js @@ -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 = `
${message}
`; + + toastContainer.appendChild(toast); + setTimeout(() => { toast.remove(); }, 4000); +} diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 72b8f99..962c3c9 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -28,7 +28,8 @@ ID Tytuł - Właściciel (ID) + Utworzono + Właściciel (ID / nazwa) Akcje @@ -37,7 +38,14 @@ {{ l.id }} {{ l.title }} - {{ l.owner_id }} + {{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }} + + {% if l.owner_id %} + {{ l.owner_id }} / {{ l.owner.username if l.owner else 'Brak użytkownika' }} + {% else %} + - + {% endif %} + 🗑️ Usuń diff --git a/templates/base.html b/templates/base.html index 9314f0c..8c8f5ef 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,7 +6,8 @@ {% block title %}Live Lista Zakupów{% endblock %} - + + @@ -25,10 +26,26 @@ +
{% block content %}{% endblock %}
+ + diff --git a/templates/dashboard.html b/templates/dashboard.html deleted file mode 100644 index bf9d2af..0000000 --- a/templates/dashboard.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Twoje listy{% endblock %} -{% block content %} - -
-

📝 Twoje listy zakupowe

- -
- -{% if lists %} -
- - - - - - - - - - - {% for l in lists %} - - - - - - - {% endfor %} - -
IDTytułData utworzeniaAkcje
{{ l.id }}{{ l.title }}{{ l.created_at.strftime('%Y-%m-%d %H:%M') }} - 📄 Otwórz - 📋 Kopiuj -
-
-{% else %} -

Nie masz jeszcze żadnych list. Kliknij „Utwórz nową listę”, aby dodać pierwszą!

-{% endif %} - -{% endblock %} diff --git a/templates/index.html b/templates/index.html index ae4db59..cad1524 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,24 +4,21 @@

Twoje listy zakupów

- ← Powrót do panelu + ← Powrót do panelu
-
-
- + +
+ +
-
-
- - -
-
-
- +
+ +
+
@@ -29,15 +26,23 @@ {% if lists %}
    {% for l in lists %} -
  • - {{ l.title }} -
    - {% if current_user.is_authenticated %} - 📄 Otwórz - 📋 Kopiuj - {% else %} - 📄 Otwórz - {% 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 %} +
  • +
    + {{ l.title }} +
    + {% if current_user.is_authenticated %} + 📄 Otwórz + 📋 Kopiuj + {% else %} + 📄 Otwórz + {% endif %} +
    +
    +
    +
    {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
  • {% endfor %} @@ -46,4 +51,4 @@

    Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!

    {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/list.html b/templates/list.html index 6d41daf..368ca3c 100644 --- a/templates/list.html +++ b/templates/list.html @@ -19,12 +19,17 @@
+ +
+
0%
+
+
    {% for item in items %} -
  • +
  • - - {{ item.name }} + + {{ item.name }}
    @@ -34,7 +39,7 @@ {% endfor %}
-
+
@@ -44,4 +49,13 @@ + + {% endblock %} diff --git a/templates/list_guest.html b/templates/list_guest.html index 3a0e8ad..067e86a 100644 --- a/templates/list_guest.html +++ b/templates/list_guest.html @@ -9,10 +9,10 @@
    {% for item in items %} -
  • +
  • - {{ item.name }} + {{ item.name }}
  • {% endfor %} @@ -28,14 +28,32 @@