Compare commits
43 Commits
dc2ece32a0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e22c7e7dd2 | |||
| 3cbeab37fb | |||
| 3f26f5452f | |||
| 98a52f3c25 | |||
| 1705320ada | |||
| 9fab8046f6 | |||
| eec49e2bd5 | |||
| cfc644e612 | |||
| ec67dacbbc | |||
| af9cef7b5b | |||
| 3a6ad5fd73 | |||
| bb3c9680a8 | |||
| 1be0c7b9fc | |||
| 8b5c843371 | |||
| b0a57b72e0 | |||
| 8a462f6610 | |||
| f042653b86 | |||
| 3cb08ad968 | |||
| c8b8d70c81 | |||
| 1d6bec5b8b | |||
| 1c623d49e3 | |||
| 8f08bf740a | |||
| e8c6119def | |||
|
|
4d5242a479 | ||
| 4e1b200ab3 | |||
| 859feba09e | |||
| 8f0caf6c98 | |||
| 95e3af4f76 | |||
| cf28a311ed | |||
| bbe8c559eb | |||
| 28afbb4279 | |||
| fd7ca2fe6e | |||
| 99ccd937a4 | |||
|
|
d5a2d1b309 | ||
|
|
34cfde795a | ||
|
|
43b5312e35 | ||
|
|
af40974018 | ||
|
|
a4d17492d2 | ||
|
|
a4403a0d33 | ||
|
|
218191a718 | ||
|
|
721387c994 | ||
|
|
3901cc152e | ||
|
|
177fde9e4b |
@@ -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,
|
||||
|
||||
@@ -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
165
app.py
@@ -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
|
||||
# =========================================================================================
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ services:
|
||||
container_name: lista-zakupow-app
|
||||
expose:
|
||||
- "${APP_PORT:-8000}"
|
||||
|
||||
# temporary
|
||||
#ports:
|
||||
# - "9281:${APP_PORT:-8000}"
|
||||
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
|
||||
@@ -3,7 +3,8 @@ Flask-SQLAlchemy
|
||||
Flask-Login
|
||||
Flask-SocketIO
|
||||
Flask-Compress
|
||||
eventlet
|
||||
#eventlet
|
||||
gevent-websocket
|
||||
Werkzeug
|
||||
Pillow
|
||||
psutil
|
||||
|
||||
@@ -857,32 +857,36 @@ td select.tom-dark {
|
||||
.sens-high { background: rgba(220,53,69,.25); color: #f1aeb5; } /* czerwony */
|
||||
|
||||
/* =========================================================
|
||||
COMPACT: przyciski akcji na listach (jak navbar)
|
||||
(podmień za stary blok .btn-group-compact)
|
||||
COMPACT: przyciski akcji na listach
|
||||
- Desktop: standard Bootstrap
|
||||
- <=576px: kompakt
|
||||
========================================================= */
|
||||
|
||||
/* Bazowo (wszystkie ekrany): trochę ciaśniej niż Bootstrap */
|
||||
.btn-group-compact .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
/* <=420px: tylko emoji */
|
||||
@media (max-width: 420px) {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wąskie ekrany (iPhone 11 / węższe) */
|
||||
@media (max-width: 420px) {
|
||||
.btn-group-compact {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
/* 421–576px: 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 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-group-compact .btn {
|
||||
padding: 0.22rem 0.45rem; /* jak navbar */
|
||||
min-width: auto;
|
||||
font-size: 0.9rem; /* emoji czytelne */
|
||||
line-height: 1.1;
|
||||
}
|
||||
.btn-group-compact .btn-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium-narrow screens */
|
||||
@@ -900,17 +904,16 @@ td select.tom-dark {
|
||||
|
||||
|
||||
/* ================================================
|
||||
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: 4px;
|
||||
}
|
||||
|
||||
/* Logo: zostaje "Lista Zakupów", ale mniejsze */
|
||||
.navbar-brand-compact {
|
||||
font-size: 0.9rem !important;
|
||||
margin-right: 0.25rem;
|
||||
@@ -930,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;
|
||||
}
|
||||
@@ -965,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;
|
||||
}
|
||||
@@ -978,7 +979,6 @@ td select.tom-dark {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-only / Desktop-only helpery (jeśli nie chcesz polegać na d-sm-*) */
|
||||
@media (max-width: 420px) {
|
||||
.user-label-desktop { display: none !important; }
|
||||
.user-label-mobile { display: inline !important; }
|
||||
@@ -987,4 +987,50 @@ td select.tom-dark {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,18 @@
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center user-info-compact">
|
||||
<!-- 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 %}
|
||||
|
||||
@@ -124,7 +131,7 @@
|
||||
<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">
|
||||
<a href="https://git.linuxiarz.pl/gru/lista_zakupowa_live" target="_blank" class="link-success text-decoration-none">
|
||||
source code
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -31,10 +31,8 @@
|
||||
</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;">
|
||||
@@ -50,7 +48,6 @@
|
||||
</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
|
||||
@@ -75,15 +72,31 @@
|
||||
<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>
|
||||
|
||||
<!-- Desktop/tablet: bez tooltipów -->
|
||||
<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>
|
||||
@@ -102,30 +115,24 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: tooltipy (bo tekst znika CSS-em) -->
|
||||
<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"
|
||||
<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"
|
||||
<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"
|
||||
<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"
|
||||
<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"
|
||||
<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>
|
||||
@@ -134,21 +141,21 @@
|
||||
|
||||
<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) }}%)
|
||||
{% 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>
|
||||
@@ -178,39 +185,60 @@
|
||||
<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>
|
||||
|
||||
<!-- Desktop/tablet: bez tooltipów -->
|
||||
<a href="{{ url_for('shared_list', list_id=l.id) }}"
|
||||
class="btn btn-sm btn-outline-light d-none d-sm-flex align-items-center">
|
||||
✏️ <span class="btn-text ms-1">Odznaczaj</span>
|
||||
</a>
|
||||
<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>
|
||||
|
||||
<!-- Mobile: tooltipy -->
|
||||
<a href="{{ url_for('shared_list', list_id=l.id) }}"
|
||||
class="btn btn-sm btn-outline-light d-flex d-sm-none 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) }}%)
|
||||
@@ -239,7 +267,9 @@
|
||||
<span>{{ l.title }}</span>
|
||||
<form action="{{ url_for('edit_my_list', list_id=l.id) }}" method="post" class="d-contents">
|
||||
<input type="hidden" name="unarchive" value="1">
|
||||
<button type="submit" class="btn btn-sm btn-outline-light">♻️ Przywróć</button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light">
|
||||
♻️ Przywróć
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -268,7 +298,8 @@
|
||||
<div class="d-grid gap-2">
|
||||
{% for m in month_options %}
|
||||
{% set year, month = m.split('-') %}
|
||||
<a href="{{ url_for('main_page', m=m) }}" class="btn btn-outline-light {% if selected_month == m %}active{% endif %}">
|
||||
<a href="{{ url_for('main_page', m=m) }}"
|
||||
class="btn btn-outline-light {% if selected_month == m %}active{% endif %}">
|
||||
{{ month_names[month|int - 1] }} {{ year }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -287,4 +318,4 @@
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='select_month.js') }}?v={{ APP_VERSION }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user