45 Commits

Author SHA1 Message Date
gru
e22c7e7dd2 Update deploy/varnish/default.vcl.template 2026-02-25 23:03:23 +01:00
gru
3cbeab37fb Update deploy/varnish/default.vcl.template 2026-02-25 23:00:40 +01:00
gru
3f26f5452f Update deploy/varnish/default.vcl.template 2026-02-25 22:58:34 +01:00
gru
98a52f3c25 Update deploy/varnish/default.vcl.template 2026-02-25 15:14:42 +01:00
gru
1705320ada Update deploy/varnish/default.vcl.template 2026-02-25 15:13:59 +01:00
gru
9fab8046f6 Update deploy/varnish/default.vcl.template 2026-02-25 15:10:48 +01:00
gru
eec49e2bd5 Update deploy/varnish/default.vcl.template 2026-02-25 15:07:36 +01:00
gru
cfc644e612 Update deploy/varnish/default.vcl.template 2026-02-25 15:02:50 +01:00
gru
ec67dacbbc Update app.py 2026-02-25 14:55:23 +01:00
gru
af9cef7b5b Update .env.example 2026-02-25 14:48:09 +01:00
gru
3a6ad5fd73 Update app.py 2026-02-25 14:21:47 +01:00
gru
bb3c9680a8 Update app.py 2026-02-25 14:19:57 +01:00
gru
1be0c7b9fc Update app.py 2026-02-25 14:16:31 +01:00
gru
8b5c843371 Update app.py 2026-02-25 14:10:26 +01:00
gru
b0a57b72e0 Update app.py 2026-02-25 14:06:06 +01:00
gru
8a462f6610 Update app.py 2026-02-25 14:03:00 +01:00
gru
f042653b86 Update app.py 2026-02-25 13:55:38 +01:00
gru
3cb08ad968 Update deploy/varnish/default.vcl.template 2026-02-25 13:54:33 +01:00
gru
c8b8d70c81 Update requirements.txt 2026-02-25 13:54:06 +01:00
gru
1d6bec5b8b Update docker-compose.yml 2026-02-25 00:29:08 +01:00
gru
1c623d49e3 Update docker-compose.yml 2026-02-25 00:23:21 +01:00
gru
8f08bf740a Update docker-compose.yml 2026-02-25 00:19:15 +01:00
gru
e8c6119def Update deploy/varnish/default.vcl.template 2026-02-25 00:11:05 +01:00
Mateusz Gruszczyński
4d5242a479 fix flask session socktio after flask-session upgrade 2026-02-20 23:57:37 +01:00
gru
4e1b200ab3 Update deploy/varnish/default.vcl.template 2026-02-20 23:48:59 +01:00
gru
859feba09e Update deploy/varnish/default.vcl.template 2026-02-20 23:44:19 +01:00
gru
8f0caf6c98 Update deploy/varnish/default.vcl.template 2026-02-20 23:42:04 +01:00
gru
95e3af4f76 Update deploy/varnish/default.vcl.template 2026-02-19 16:24:52 +01:00
gru
cf28a311ed Update deploy/varnish/default.vcl.template 2026-02-19 16:22:31 +01:00
gru
bbe8c559eb Update deploy/varnish/default.vcl.template 2026-02-19 16:19:27 +01:00
gru
28afbb4279 Update deploy/varnish/default.vcl.template 2026-02-19 16:16:00 +01:00
gru
fd7ca2fe6e Update README.md 2026-02-02 09:23:14 +01:00
gru
99ccd937a4 Update templates/base.html 2026-02-02 09:21:59 +01:00
Mateusz Gruszczyński
d5a2d1b309 kropka kategorii na malych ekranach 2026-01-21 11:15:04 +01:00
Mateusz Gruszczyński
34cfde795a kropka kategorii na malych ekranach 2026-01-21 11:11:22 +01:00
Mateusz Gruszczyński
43b5312e35 kropka kategorii na malych ekranach 2026-01-21 11:00:45 +01:00
Mateusz Gruszczyński
af40974018 kropka kategorii na malych ekranach 2026-01-21 10:58:01 +01:00
Mateusz Gruszczyński
a4d17492d2 kropka kategorii na malych ekranach 2026-01-21 10:55:50 +01:00
Mateusz Gruszczyński
a4403a0d33 poprawka dla malych ekranow 2026-01-13 11:25:55 +01:00
Mateusz Gruszczyński
218191a718 poprawka dla malych ekranow 2026-01-13 10:24:16 +01:00
Mateusz Gruszczyński
721387c994 poprawka dla malych ekranow 2026-01-13 09:23:39 +01:00
Mateusz Gruszczyński
3901cc152e poprawka dla malych ekranow 2026-01-13 09:03:05 +01:00
Mateusz Gruszczyński
177fde9e4b poprawka dla malych ekranow 2026-01-13 08:51:52 +01:00
Mateusz Gruszczyński
dc2ece32a0 poprawka dla malych ekranow 2026-01-13 08:34:57 +01:00
Mateusz Gruszczyński
71233ebb75 poprawka dla malych ekranow 2026-01-13 08:26:51 +01:00
9 changed files with 480 additions and 214 deletions

View File

@@ -153,12 +153,12 @@ LIB_JS_CACHE_CONTROL="max-age=86400"
# LIB_CSS_CACHE_CONTROL:
# Nagłówki Cache-Control dla bibliotek CSS (/static/lib/css/)
# Domyślnie: "max-age=86400"
LIB_CSS_CACHE_CONTROL="max-age=86400"
LIB_CSS_CACHE_CONTROL="max-age=3600"
# UPLOADS_CACHE_CONTROL:
# Nagłówki Cache-Control dla wgrywanych plików (/uploads/)
# Domyślnie: "max-age=2592000, immutable"
UPLOADS_CACHE_CONTROL="max-age=2592000, immutable"
UPLOADS_CACHE_CONTROL="max-age=3600, immutable"
# DEFAULT_CATEGORIES:
# Lista domyślnych kategorii tworzonych automatycznie przy starcie aplikacji,

View File

@@ -21,7 +21,7 @@ Prosta aplikacja webowa do zarządzania listami zakupów z obsługą użytkownik
1. Sklonuj repozytorium:
```bash
git https://gitea.linuxiarz.pl/gru/lista_zakupowa_live.git
git https://git.linuxiarz.pl/gru/lista_zakupowa_live.git
cd lista_zakupowa_live
```

165
app.py
View File

@@ -164,7 +164,15 @@ APP_VERSION = commit
app.config["APP_VERSION"] = APP_VERSION
db = SQLAlchemy(app)
socketio = SocketIO(app, async_mode="eventlet")
# old
#socketio = SocketIO(app, async_mode="eventlet")
# new flask
#socketio = SocketIO(app, async_mode="eventlet", manage_session=False)
# gevent
socketio = SocketIO(app, async_mode='gevent')
login_manager = LoginManager(app)
login_manager.login_view = "login"
@@ -1562,7 +1570,8 @@ def apply_headers(response):
# --- statyczne pliki (nagłówki z .env) ---
if request.path.startswith(("/static/", "/uploads/")):
response.headers["Vary"] = "Accept-Encoding"
response.headers.pop('Vary', None) # fix bug with backslash
response.headers['Vary'] = 'Accept-Encoding'
return response
# --- healthcheck ---
@@ -4203,6 +4212,158 @@ def robots_txt():
return content, 200, {"Content-Type": "text/plain"}
from flask import render_template_string
@app.route('/admin/debug-socket')
@login_required
@admin_required
def debug_socket():
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Socket Debug</title>
<script src="{{ url_for('static_bp.serve_js_lib', filename='socket.io.min.js') }}?v={{ APP_VERSION }}"></script>
<style>
body { font-family: monospace; background: #1e1e1e; color: #fff; padding: 20px; }
#log { height: 400px; overflow-y: scroll; background: #2d2d2d; padding: 15px; border-radius: 8px; margin: 10px 0; white-space: pre-wrap; }
button { background: #007bff; color: white; border: none; padding: 10px 20px; margin: 5px; border-radius: 5px; cursor: pointer; }
button:hover { background: #0056b3; }
.status { font-size: 18px; font-weight: bold; margin: 10px 0; }
.connected { color: #28a745; }
.disconnected { color: #dc3545; }
</style>
</head>
<body>
<h1>Socket.IO Debug Tool</h1>
<div id="status" class="status disconnected">Rozlaczony</div>
<div id="info">
Transport: <span id="transport">-</span> |
Ping: <span id="ping">-</span>ms |
SID: <span id="sid">-</span>
</div>
<button onclick="connect()">Polacz</button>
<button onclick="disconnect()">Rozlacz</button>
<button onclick="emitTest()">Emit Test</button>
<button onclick="forcePolling()">Force Polling</button>
<h3>Logi:</h3>
<div id="log"></div>
<script>
let socket;
let logLines = 0;
let isPollingOnly = true;
function log(msg, color = '#fff') {
const logEl = document.getElementById('log');
const time = new Date().toLocaleTimeString();
logEl.innerHTML += `[${time}] ${msg}\n`;
logEl.scrollTop = logEl.scrollHeight;
logLines++;
if (logLines > 200) {
const lines = logEl.innerHTML.split('\\n');
logEl.innerHTML = lines.slice(-200).join('\\n');
logLines = 200;
}
}
function updateStatus(connected) {
const status = document.getElementById('status');
status.textContent = connected ? 'Polaczony' : 'Rozlaczony';
status.className = `status ${connected ? 'connected' : 'disconnected'}`;
}
function connect() {
if (socket) {
socket.disconnect();
socket = null;
}
const transports = isPollingOnly ? ['polling'] : ['polling', 'websocket'];
log(`Polaczenie z: ${transports.join(', ')}`);
socket = io('', {
transports: transports,
timeout: 20000,
autoConnect: false,
forceNew: true
});
socket.on('connect', function() {
log('CONNECTED OK');
updateStatus(true);
try {
const transport = socket.io.engine.transport.name;
document.getElementById('transport').textContent = transport;
document.getElementById('sid').textContent = socket.id.substring(0,8) + '...';
} catch(e) {
log('Transport info error: ' + e.message);
}
socket.emit('requestfulllist', {listid: 1});
});
socket.on('disconnect', function(reason) {
log('DISCONNECTED: ' + reason);
updateStatus(false);
});
socket.on('connect_error', function(err) {
log('CONNECT ERROR: ' + err.message + ' (' + (err.type || 'unknown') + ')');
});
socket.onAny(function(event, ...args) {
log('RECV ' + event + ': ' + JSON.stringify(args).substring(0,100));
});
socket.connect();
}
function disconnect() {
if (socket) {
socket.disconnect();
socket = null;
}
}
function emitTest() {
if (!socket || !socket.connected) {
log('Niepolaczony!');
return;
}
const now = Date.now();
socket.emit('pingtest', now);
log('SENT pingtest ' + now);
}
function forcePolling() {
isPollingOnly = !isPollingOnly;
log('Polling only: ' + isPollingOnly);
connect();
}
// STATUS check co 30s
setInterval(function() {
if (socket && socket.connected) {
const transport = socket.io.engine ? socket.io.engine.transport.name : 'unknown';
log('STATUS OK: ' + transport + ' | SID: ' + (socket.id ? socket.id.substring(0,8) : 'none'));
emitTest();
} else {
log('STATUS: Offline');
}
}, 30000);
// Start
connect();
</script>
</body>
</html>
''')
# =========================================================================================
# SOCKET.IO
# =========================================================================================

View File

@@ -65,7 +65,7 @@ sub vcl_recv {
# }
# ---- STATYCZNE agresywny cache + ignorujemy sesję ----
if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
if (req.url ~ "^/static/" || req.url ~ "^/uploads/" || req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
unset req.http.Cookie;
unset req.http.Authorization;
return (hash);
@@ -141,8 +141,7 @@ sub vcl_backend_response {
}
# Nie cache'uj statyków, jeśli status ≠ 200
if (bereq.url ~ "^/static/" ||
bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)($|\?)") {
if (bereq.url ~ "^/static/" || bereq.url ~ "^/uploads/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)($|\?)") {
if (beresp.status != 200) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
@@ -170,7 +169,7 @@ sub vcl_backend_response {
}
# ---- STATYCZNE: zdejmij Set-Cookie i Vary: Cookie, zapewnij TTL ----
if (bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
if (bereq.url ~ "^/static/" || bereq.url ~ "^/uploads/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
unset beresp.http.Set-Cookie;
# Jeśli backend dodał Vary: Cookie, usuńmy ten element (nie wpływa na statyki)
@@ -261,4 +260,4 @@ sub vcl_synth {
# ===== PURGE HANDLER =====
sub vcl_purge {
return (synth(200, "Purged"));
}
}

View File

@@ -4,6 +4,11 @@ services:
container_name: lista-zakupow-app
expose:
- "${APP_PORT:-8000}"
# temporary
#ports:
# - "9281:${APP_PORT:-8000}"
healthcheck:
test:
[

View File

@@ -3,7 +3,8 @@ Flask-SQLAlchemy
Flask-Login
Flask-SocketIO
Flask-Compress
eventlet
#eventlet
gevent-websocket
Werkzeug
Pillow
psutil

View File

@@ -856,48 +856,66 @@ td select.tom-dark {
.sens-mid { background: rgba(13,110,253,.25); color: #9ec5fe; } /* niebieski */
.sens-high { background: rgba(220,53,69,.25); color: #f1aeb5; } /* czerwony */
/* Responsive buttons - hide text on narrow screens (iPhone 11, iPhone 17 Pro) */
/* =========================================================
COMPACT: przyciski akcji na listach
- Desktop: standard Bootstrap
- <=576px: kompakt
========================================================= */
/* <=420px: tylko emoji */
@media (max-width: 420px) {
.btn-group-compact .btn-text {
display: none !important;
}
.btn-group-compact .btn {
padding: 0.375rem 0.5rem;
min-width: auto;
}
.btn-group-compact .btn-text {
display: none !important;
}
.btn-group-compact .btn {
padding: 0.22rem 0.45rem;
min-width: auto;
font-size: 0.9rem;
line-height: 1.1;
}
}
/* Medium-narrow screens - smaller font */
/* 421576px: lekko ciaśniej, ale tekst zostaje */
@media (min-width: 421px) and (max-width: 576px) {
.btn-group-compact .btn {
padding: 0.25rem 0.5rem;
font-size: 0.82rem;
line-height: 1.1;
}
.btn-group-compact .btn-text {
font-size: 0.75rem;
}
}
/* Medium-narrow screens */
@media (min-width: 421px) and (max-width: 576px) {
.btn-group-compact .btn {
padding: 0.375rem 0.45rem;
font-size: 0.8rem;
padding: 0.24rem 0.45rem; /* ciaśniej */
font-size: 0.82rem;
line-height: 1.1;
}
.btn-group-compact .btn-text {
font-size: 0.75rem;
}
}
/* ================================================
RESPONSIVE NAVBAR - ukryj tekst na małych ekranach
================================================ */
/* ================================================
RESPONSIVE NAVBAR - zachowaj teksty, ale mniejsze
RESPONSIVE NAVBAR
================================================ */
/* Wąskie ekrany (np. iPhone 11) */
@media (max-width: 420px) {
/* Navbar: zmniejsz odstępy w poziomie */
.navbar .container-fluid {
gap: 6px;
gap: 4px;
}
/* Logo: zostaje "Lista Zakupów", ale mniejsze */
.navbar-brand-compact {
font-size: 1.05rem !important;
font-size: 0.9rem !important;
margin-right: 0.25rem;
white-space: nowrap;
}
@@ -905,10 +923,9 @@ td select.tom-dark {
font-size: 0.95em;
}
/* Info o użytkowniku: zostaje "Zalogowany:", ale mniejsze + bez zawijania */
.user-info-compact {
font-size: 0.72rem !important;
line-height: 1.1;
line-height: 0.9;
white-space: nowrap;
}
.user-info-compact .badge {
@@ -916,7 +933,6 @@ td select.tom-dark {
padding: 0.2rem 0.45rem;
}
/* Przyciski po prawej: tylko emoji (tekst chowamy) */
.nav-buttons-compact .nav-btn-text {
display: none !important;
}
@@ -931,6 +947,7 @@ td select.tom-dark {
}
}
/* Małe ekrany (np. 421-576px) */
@media (min-width: 421px) and (max-width: 576px) {
.navbar .container-fluid {
@@ -950,7 +967,6 @@ td select.tom-dark {
font-size: 0.75rem;
}
/* Tu tekst w przyciskach może zostać (ale mniejszy) */
.nav-buttons-compact {
flex-wrap: nowrap;
}
@@ -963,4 +979,58 @@ td select.tom-dark {
}
}
@media (max-width: 420px) {
.user-label-desktop { display: none !important; }
.user-label-mobile { display: inline !important; }
}
@media (min-width: 421px) {
.user-label-desktop { display: inline !important; }
.user-label-mobile { display: none !important; }
}
.category-dot-pure {
display: inline-block !important;
width: 14px !important;
height: 14px !important;
border-radius: 50% !important;
border: 2px solid rgba(255, 255, 255, 0.8) !important;
background-clip: content-box, border-box !important;
vertical-align: middle !important;
margin-right: 3px !important;
opacity: 1 !important;
padding: 0 !important;
line-height: 1 !important;
font-size: 0 !important;
text-indent: -9999px !important;
overflow: hidden !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.4) !important;
}
.category-dot-pure::before,
.category-dot-pure::after {
content: none !important;
}
/* Hover efekt */
.category-dot:hover {
transform: scale(1.3) !important;
box-shadow: 0 2px 6px rgba(0,0,0,0.4) !important;
}
.list-title {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 70% !important;
display: inline-block !important;
}
/* Bardzo małe ekrany */
@media (max-width: 420px) {
.list-title {
max-width: 60% !important;
}
}

View File

@@ -1,146 +1,167 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Live Lista Zakupów{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('favicon') }}">
{# --- Bootstrap i główny css zawsze --- #}
<link href="{{ url_for('static_bp.serve_css', filename='style.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
{# --- Style CSS ładowane tylko dla niezablokowanych --- #}
{% set exclude_paths = ['/system-auth'] %}
{% if (exclude_paths | select("in", request.path) | list | length == 0)
and has_authorized_cookie
and not is_blocked %}
<link href="{{ url_for('static_bp.serve_css_lib', filename='glightbox.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='sort_table.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='glightbox.min.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='sort_table.min.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
{% endif %}
{# --- Cropper CSS tylko dla wybranych podstron --- #}
{% set substrings_cropper = ['/admin/receipts', '/edit_my_list'] %}
{% if substrings_cropper | select("in", request.path) | list | length > 0 %}
<link href="{{ url_for('static_bp.serve_css_lib', filename='cropper.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='cropper.min.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
{% endif %}
{# --- Tom Select CSS tylko dla wybranych podstron --- #}
{% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %}
{% if substrings_tomselect | select("in", request.path) | list | length > 0 %}
<link href="{{ url_for('static_bp.serve_css_lib', filename='tom-select.bootstrap5.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='tom-select.bootstrap5.min.css') }}?v={{ APP_VERSION }}" rel="stylesheet">
{% endif %}
</head>
<body class="bg-dark text-white">
<nav class="navbar navbar-dark bg-dark mb-3">
<div class="container-fluid">
<a class="navbar-brand navbar-brand-compact fw-bold fs-4 text-success" href="/">
🛒 <span class="text-warning navbar-brand-text">Lista</span> <span class="navbar-brand-text">Zakupów</span>
</a>
{% if has_authorized_cookie and not is_blocked %}
{% if current_user.is_authenticated %}
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1 user-info-label">Zalogowany:</span>
<span class="badge rounded-pill bg-success">{{ current_user.username }}</span>
</div>
{% else %}
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1 user-info-label">Przeglądasz jako</span>
<span class="badge rounded-pill bg-info">niezalogowany/a</span>
</div>
{% endif %}
{% endif %}
{% if current_user.is_authenticated %}
<!-- Desktop/tablet: "Zalogowany:" -->
<div class="d-none d-sm-flex justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1">Zalogowany:</span>
<span class="badge rounded-pill bg-success">{{ current_user.username }}</span>
</div>
<!-- Mobile: 👤 zamiast "Zalogowany:" -->
<div class="d-flex d-sm-none justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1" aria-label="Zalogowany">👤</span>
<span class="badge rounded-pill bg-success">{{ current_user.username }}</span>
</div>
{% else %}
<!-- Desktop/tablet: tekst -->
<div class="d-none d-sm-flex justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1 user-info-label">Przeglądasz jako</span>
<span class="badge rounded-pill bg-info">niezalogowany/a</span>
</div>
<!-- Mobile: ikonka zamiast tekstu -->
<div class="d-flex d-sm-none justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
<span class="me-1" aria-label="Niezalogowany">👥</span>
<span class="badge rounded-pill bg-info">gość</span>
</div>
{% endif %}
{% endif %}
{% if not is_blocked and request.endpoint and request.endpoint != 'system_auth' %}
<div class="d-flex align-items-center gap-2 flex-wrap nav-buttons-compact">
{% if current_user.is_authenticated %}
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip"
title="Panel admina">
⚙️<span class="nav-btn-text ms-1">Panel</span>
</a>
{% endif %}
<a href="{{ url_for('expenses') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip"
title="Wydatki">
📊<span class="nav-btn-text ms-1">Wydatki</span>
</a>
<a href="{{ url_for('logout') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip"
title="Wyloguj">
🚪<span class="nav-btn-text ms-1">Wyloguj</span>
</a>
<!-- Desktop/tablet: bez tooltipów -->
<div class="d-none d-sm-flex align-items-center gap-2 flex-wrap nav-buttons-compact">
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-light btn-sm">
⚙️<span class="nav-btn-text ms-1">Panel</span>
</a>
{% endif %}
<a href="{{ url_for('expenses') }}" class="btn btn-outline-light btn-sm">
📊<span class="nav-btn-text ms-1">Wydatki</span>
</a>
<a href="{{ url_for('logout') }}" class="btn btn-outline-light btn-sm">
🚪<span class="nav-btn-text ms-1">Wyloguj</span>
</a>
</div>
<!-- Mobile: tooltipy (bo tekst przycisków znika CSS-em) -->
<div class="d-flex d-sm-none align-items-center gap-2 flex-wrap nav-buttons-compact">
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip" title="Panel admina">
⚙️<span class="nav-btn-text ms-1">Panel</span>
</a>
{% endif %}
<a href="{{ url_for('expenses') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip" title="Wydatki">
📊<span class="nav-btn-text ms-1">Wydatki</span>
</a>
<a href="{{ url_for('logout') }}"
class="btn btn-outline-light btn-sm"
data-bs-toggle="tooltip" title="Wyloguj">
🚪<span class="nav-btn-text ms-1">Wyloguj</span>
</a>
</div>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-outline-light btn-sm">🔑 Zaloguj</a>
<div class="d-flex align-items-center gap-2 flex-wrap nav-buttons-compact">
<a href="{{ url_for('login') }}" class="btn btn-outline-light btn-sm">🔑 Zaloguj</a>
</div>
{% endif %}
</div>
{% endif %}
</div>
</nav>
<div class="container px-2">
{% block content %}{% endblock %}
</div>
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
<footer class="text-center text-secondary small mt-5 mb-3">
<hr class="text-secondary">
<p class="mb-0">© 2025 <strong>linuxiarz.pl</strong> · <a href="https://gitea.linuxiarz.pl/gru/lista_zakupowa_live"
target="_blank" class="link-success text-decoration-none"> source code</a>
<p class="mb-0">© 2025 <strong>linuxiarz.pl</strong> ·
<a href="https://git.linuxiarz.pl/gru/lista_zakupowa_live" target="_blank" class="link-success text-decoration-none">
source code
</a>
</p>
<div class="small">v{{ APP_VERSION }}</div>
</footer>
<script src="{{ url_for('static_bp.serve_js_lib', filename='bootstrap.bundle.min.js') }}"></script>
{% if not is_blocked %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Initialize tooltips
// Tooltips tylko na mobile (bo tylko tam dodajemy data-bs-toggle="tooltip")
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
tooltipTriggerList.forEach(function (el) {
new bootstrap.Tooltip(el);
});
{% 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 %}
});
{% 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>
{% if request.endpoint != 'system_auth' %}
<script src="{{ url_for('static_bp.serve_js_lib', filename='glightbox.min.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js_lib', filename='socket.io.min.js') }}?v={{ APP_VERSION }}"></script>
@@ -149,32 +170,24 @@
<script src="{{ url_for('static_bp.serve_js', filename='live.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='sockets.js') }}?v={{ APP_VERSION }}"></script>
{% endif %}
<script src="{{ url_for('static_bp.serve_js', filename='toasts.js') }}?v={{ APP_VERSION }}"></script>
<script>
let lightbox = GLightbox({
selector: '.glightbox'
});
let lightbox = GLightbox({ selector: '.glightbox' });
</script>
{% set substrings = ['/admin/receipts', '/edit_my_list'] %}
{% if substrings | select("in", request.path) | list | length > 0 %}
<script src="{{ url_for('static_bp.serve_js_lib', filename='cropper.min.js') }}?v={{ APP_VERSION }}"></script>
{% endif %}
{% set substrings = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %}
{% if substrings | select("in", request.path) | list | length > 0 %}
<script
src="{{ url_for('static_bp.serve_js_lib', filename='tom-select.complete.min.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js_lib', filename='tom-select.complete.min.js') }}?v={{ APP_VERSION }}"></script>
{% endif %}
{% endif %}
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -2,20 +2,17 @@
{% block title %}Twoje listy zakupów{% endblock %}
{% block content %}
{% if not current_user.is_authenticated %}
<div class="alert alert-info text-center" role="alert">
Nie jesteś zalogowany/a. Możesz przeglądać tylko listy publiczne.
</div>
{% endif %}
{% if current_user.is_authenticated %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">Stwórz nową listę</h2>
</div>
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body">
<form action="{{ url_for('create_list') }}" method="post">
@@ -34,16 +31,11 @@
</div>
{% endif %}
{% set month_names = ["styczeń","luty","marzec","kwiecień","maj","czerwiec","lipiec","sierpień","wrzesień","październik","listopad","grudzień"] %}
{% set month_names = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień",
"październik", "listopad", "grudzień"] %}
<!-- Pulpit: zwykły <select> -->
<div class="d-none d-md-flex justify-content-end align-items-center flex-wrap gap-2 mb-3">
<label for="monthSelect" class="text-white small mb-0">📅 Wybierz miesiąc:</label>
<select id="monthSelect" class="form-select form-select-sm bg-dark text-white border-secondary"
style="min-width: 180px;">
<select id="monthSelect" class="form-select form-select-sm bg-dark text-white border-secondary" style="min-width: 180px;">
{% for m in month_options %}
{% set year, month = m.split('-') %}
<option value="{{ m }}" {% if selected_month==m %}selected{% endif %}>
@@ -56,108 +48,116 @@
</select>
</div>
<!-- Telefon: przycisk otwierający modal -->
<div class="d-md-none mb-3">
<button class="btn btn-outline-light w-100" data-bs-toggle="modal" data-bs-target="#monthPickerModal">
📅 Wybierz miesiąc
</button>
</div>
{% if current_user.is_authenticated %}
<h3 class="mt-4 d-flex justify-content-between align-items-center flex-wrap">
Twoje listy
<button type="button" class="btn btn-sm btn-outline-light ms-2" data-bs-toggle="modal"
data-bs-target="#archivedModal">
<button type="button" class="btn btn-sm btn-outline-light ms-2" data-bs-toggle="modal" data-bs-target="#archivedModal">
🗄️ Zarchiwizowane
</button>
</h3>
{% if user_lists %}
<ul class="list-group mb-4">
{% for l in user_lists %}
{% set purchased_count = l.purchased_count %}
{% set total_count = l.total_count %}
{% 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 }} (Autor: Ty)
{% for cat in l.category_badges %}
<span class="badge rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
font-size: 0.56rem;
opacity: 0.85;">
{{ cat.name }}
<!-- Desktop/tablet: zwykły tekst -->
<span class="d-none d-sm-inline">
{{ l.title }} (Autor: Ty)
</span>
<!-- Mobile: klikalny tytuł -->
<a class="d-inline d-sm-none text-white text-decoration-none"
href="{{ url_for('view_list', list_id=l.id) }}">
{{ l.title }}
</a>
{% for cat in l.category_badges %}
<!-- DESKTOP: nazwa -->
<span class="badge rounded-pill text-dark ms-1 d-none d-sm-inline fw-semibold"
style="background-color: {{ cat.color }}; font-size: 0.7rem; opacity: 0.9; padding: 0.3em 0.6em;">
{{ cat.name }}
</span>
<!-- MOBILE -->
<span class="ms-1 d-sm-none category-dot-pure"
style="background-color: {{ cat.color }};"
title="{{ cat.name }}" aria-label="Kategoria: {{ cat.name }}"></span>
{% endfor %}
</span>
<div class="btn-group btn-group-compact mt-2 mt-md-0" role="group">
<a href="{{ url_for('view_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="Otwórz">
<div class="btn-group btn-group-compact mt-2 mt-md-0 d-none d-sm-flex" role="group">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center">
📂 <span class="btn-text ms-1">Otwórz</span>
</a>
<a href="{{ url_for('shared_list', token=l.share_token) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="Odznaczaj">
<a href="{{ url_for('shared_list', token=l.share_token) }}" class="btn btn-sm btn-outline-light d-flex align-items-center">
✏️ <span class="btn-text ms-1">Odznaczaj</span>
</a>
<a href="{{ url_for('copy_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="Kopiuj">
<a href="{{ url_for('copy_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center">
📋 <span class="btn-text ms-1">Kopiuj</span>
</a>
<a href="{{ url_for('toggle_visibility', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="{% if l.is_public %}Ukryj{% else %}Odkryj{% endif %}">
<a href="{{ url_for('toggle_visibility', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center">
{% if l.is_public %}🙈 <span class="btn-text ms-1">Ukryj</span>{% else %}🐵 <span class="btn-text ms-1">Odkryj</span>{% endif %}
</a>
<a href="{{ url_for('edit_my_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="Ustawienia">
<a href="{{ url_for('edit_my_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center">
⚙️ <span class="btn-text ms-1">Ustawienia</span>
</a>
</div>
<div class="btn-group btn-group-compact mt-2 mt-md-0 d-flex d-sm-none" role="group">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="Otwórz">
📂 <span class="btn-text ms-1">Otwórz</span>
</a>
<a href="{{ url_for('shared_list', token=l.share_token) }}" class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="Odznaczaj">
✏️ <span class="btn-text ms-1">Odznaczaj</span>
</a>
<a href="{{ url_for('copy_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="Kopiuj">
📋 <span class="btn-text ms-1">Kopiuj</span>
</a>
<a href="{{ url_for('toggle_visibility', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="{% if l.is_public %}Ukryj{% else %}Odkryj{% endif %}">
{% if l.is_public %}🙈 <span class="btn-text ms-1">Ukryj</span>{% else %}🐵 <span class="btn-text ms-1">Odkryj</span>{% endif %}
</a>
<a href="{{ url_for('edit_my_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="Ustawienia">
⚙️ <span class="btn-text ms-1">Ustawienia</span>
</a>
</div>
</div>
<div class="progress progress-dark progress-thin mt-2 position-relative">
{# Kupione #}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
{# Niekupione #}
{% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
<div class="progress-bar bg-warning" role="progressbar"
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
{# Pozostałe #}
<div class="progress-bar bg-transparent" role="progressbar"
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
<span class="progress-label small fw-bold
{% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
<span class="progress-label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
{% if l.total_expense > 0 %}
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
{% endif %}
{% if l.total_expense > 0 %} — 💸 {{ '%.2f'|format(l.total_expense) }} PLN{% endif %}
</span>
</div>
</li>
{% endfor %}
</ul>
@@ -166,12 +166,13 @@
{% endif %}
{% endif %}
<h3 class="mt-4"> {% if current_user.is_authenticated %}Udostępnione i publiczne listy innych użytkowników {% else %}
Publiczne listy innych użytkowników {% endif %}</h3>
<h3 class="mt-4">
{% if current_user.is_authenticated %}
Udostępnione i publiczne listy innych użytkowników
{% else %}
Publiczne listy innych użytkowników
{% endif %}
</h3>
{% set lists_to_show = accessible_lists %}
{% if lists_to_show %}
@@ -180,44 +181,64 @@
{% set purchased_count = l.purchased_count %}
{% set total_count = l.total_count %}
{% 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 }} (Autor: {{ l.owner.username if l.owner else '—' }})
{% for cat in l.category_badges %}
<span class="badge rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
font-size: 0.56rem; opacity: 0.85;">
{{ cat.name }}
<!-- Desktop/tablet: zwykły tekst -->
<span class="d-none d-sm-inline">
{{ l.title }} (Autor: {{ l.owner.username if l.owner else '—' }})
</span>
<!-- Mobile: klikalny tytuł -> shared_list -->
<a class="d-inline d-sm-none fw-bold list-title text-white text-decoration-none"
href="{{ url_for('view_list', list_id=l.id) }}">
{{ l.title }}
</a>
{% for cat in l.category_badges %}
<!-- DESKTOP: nazwa -->
<span class="badge rounded-pill text-dark ms-1 d-none d-sm-inline fw-semibold"
style="background-color: {{ cat.color }}; font-size: 0.7rem; opacity: 0.9; padding: 0.3em 0.6em;">
{{ cat.name }}
</span>
<!-- MOBILE -->
<span class="ms-1 d-sm-none category-dot-pure"
style="background-color: {{ cat.color }};"
title="{{ cat.name }}" aria-label="Kategoria: {{ cat.name }}"></span>
{% endfor %}
</span>
<div class="btn-group btn-group-compact mt-2 mt-md-0 d-none d-sm-flex" role="group">
<a href="{{ url_for('shared_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center">
✏️ <span class="btn-text ms-1">Odznaczaj</span>
</a>
</div>
<a href="{{ url_for('shared_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip"
title="Odznaczaj">
✏️ <span class="btn-text ms-1">Odznaczaj</span>
</a>
<div class="btn-group btn-group-compact mt-2 mt-md-0 d-flex d-sm-none" role="group">
<a href="{{ url_for('shared_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center"
data-bs-toggle="tooltip" title="Odznaczaj">
✏️ <span class="btn-text ms-1">Odznaczaj</span>
</a>
</div>
</div>
<div class="progress progress-dark progress-thin mt-2 position-relative">
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
{% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
<div class="progress-bar bg-warning" role="progressbar"
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-transparent" role="progressbar"
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
<span class="progress-label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
@@ -231,7 +252,6 @@
<p><span class="badge rounded-pill bg-secondary opacity-75">Brak list do wyświetlenia</span></p>
{% endif %}
<div class="modal fade" id="archivedModal" tabindex="-1" aria-labelledby="archivedModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
@@ -267,7 +287,6 @@
</div>
</div>
<div class="modal fade" id="monthPickerModal" tabindex="-1" aria-labelledby="monthPickerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-white">
@@ -294,11 +313,9 @@
</div>
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='toggle_button.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='select_month.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}
{% endblock %}
{% endblock %}