fix in rooms
This commit is contained in:
@@ -546,8 +546,21 @@ def logout():
|
||||
def create_list():
|
||||
title = request.form.get("title")
|
||||
is_temporary = request.form.get("temporary") == "1"
|
||||
list_date_raw = (request.form.get("list_date") or "").strip()
|
||||
token = generate_share_token(8)
|
||||
|
||||
created_at_override = None
|
||||
if list_date_raw:
|
||||
try:
|
||||
# created_at jest w tym modelu DateTime bez strefy. Środek dnia
|
||||
# zapobiega przypadkowemu przesunięciu daty przy konwersjach DB/UI.
|
||||
created_at_override = datetime.strptime(list_date_raw, "%Y-%m-%d").replace(
|
||||
hour=12, minute=0, second=0, microsecond=0
|
||||
)
|
||||
except ValueError:
|
||||
flash("Nieprawidłowa data listy.", "danger")
|
||||
return redirect(url_for("main_page"))
|
||||
|
||||
expires_at = (
|
||||
datetime.now(timezone.utc) + timedelta(days=7) if is_temporary else None
|
||||
)
|
||||
@@ -559,6 +572,8 @@ def create_list():
|
||||
share_token=token,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
if created_at_override is not None:
|
||||
new_list.created_at = created_at_override
|
||||
db.session.add(new_list)
|
||||
db.session.commit()
|
||||
log_list_activity(new_list.id, 'list_created', actor=current_user, actor_name=current_user.username, details='Utworzono listę ręcznie')
|
||||
|
||||
+42
-35
@@ -354,50 +354,56 @@ def handle_add_item(data):
|
||||
def handle_check_item(data):
|
||||
item = db.session.get(Item, data["item_id"])
|
||||
|
||||
if item:
|
||||
item.purchased = True
|
||||
item.purchased_at = datetime.now(UTC)
|
||||
item.not_purchased = False
|
||||
item.not_purchased_reason = None
|
||||
log_list_activity(item.list_id, 'item_checked', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość')
|
||||
db.session.commit()
|
||||
if not item:
|
||||
return {"ok": False, "error": "item_not_found"}
|
||||
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
item.purchased = True
|
||||
item.purchased_at = datetime.now(UTC)
|
||||
item.not_purchased = False
|
||||
item.not_purchased_reason = None
|
||||
log_list_activity(item.list_id, 'item_checked', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość')
|
||||
db.session.commit()
|
||||
|
||||
emit("item_checked", {"item_id": item.id}, to=str(item.list_id))
|
||||
emit(
|
||||
"progress_updated",
|
||||
{
|
||||
"purchased_count": purchased_count,
|
||||
"total_count": total_count,
|
||||
"percent": percent,
|
||||
},
|
||||
to=str(item.list_id),
|
||||
)
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
|
||||
emit("item_checked", {"item_id": item.id}, to=str(item.list_id))
|
||||
emit(
|
||||
"progress_updated",
|
||||
{
|
||||
"purchased_count": purchased_count,
|
||||
"total_count": total_count,
|
||||
"percent": percent,
|
||||
},
|
||||
to=str(item.list_id),
|
||||
)
|
||||
return {"ok": True, "item_id": item.id, "purchased": True}
|
||||
|
||||
|
||||
@socketio.on("uncheck_item")
|
||||
def handle_uncheck_item(data):
|
||||
item = db.session.get(Item, data["item_id"])
|
||||
|
||||
if item:
|
||||
item.purchased = False
|
||||
item.purchased_at = None
|
||||
log_list_activity(item.list_id, 'item_unchecked', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość')
|
||||
db.session.commit()
|
||||
if not item:
|
||||
return {"ok": False, "error": "item_not_found"}
|
||||
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
item.purchased = False
|
||||
item.purchased_at = None
|
||||
log_list_activity(item.list_id, 'item_unchecked', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość')
|
||||
db.session.commit()
|
||||
|
||||
emit("item_unchecked", {"item_id": item.id}, to=str(item.list_id))
|
||||
emit(
|
||||
"progress_updated",
|
||||
{
|
||||
"purchased_count": purchased_count,
|
||||
"total_count": total_count,
|
||||
"percent": percent,
|
||||
},
|
||||
to=str(item.list_id),
|
||||
)
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
|
||||
emit("item_unchecked", {"item_id": item.id}, to=str(item.list_id))
|
||||
emit(
|
||||
"progress_updated",
|
||||
{
|
||||
"purchased_count": purchased_count,
|
||||
"total_count": total_count,
|
||||
"percent": percent,
|
||||
},
|
||||
to=str(item.list_id),
|
||||
)
|
||||
return {"ok": True, "item_id": item.id, "purchased": False}
|
||||
|
||||
|
||||
@socketio.on("request_full_list")
|
||||
@@ -406,7 +412,7 @@ def handle_request_full_list(data):
|
||||
|
||||
shopping_list = db.session.get(ShoppingList, list_id)
|
||||
if not shopping_list:
|
||||
return
|
||||
return {"ok": False, "error": "list_not_found"}
|
||||
|
||||
owner_id = shopping_list.owner_id
|
||||
|
||||
@@ -438,6 +444,7 @@ def handle_request_full_list(data):
|
||||
)
|
||||
|
||||
emit("full_list", {"items": items_data}, to=request.sid)
|
||||
return {"ok": True, "list_id": list_id}
|
||||
|
||||
|
||||
@socketio.on("update_note")
|
||||
|
||||
@@ -729,3 +729,77 @@ input[type="checkbox"].form-check-input,
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tworzenie listy: osobny, kompaktowy wybór daty obok "Tymczasowa". */
|
||||
.endpoint-main_page .create-list-input-group > .create-list-temp-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #tempToggle {
|
||||
min-width: 8.75rem;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle {
|
||||
flex: 0 0 auto !important;
|
||||
width: auto !important;
|
||||
min-width: 5.75rem;
|
||||
margin-left: -1px;
|
||||
padding-left: .8rem;
|
||||
padding-right: .8rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
border: 0 !important;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 0 14px 14px 0 !important;
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
color: var(--app-text) !important;
|
||||
box-shadow: none !important;
|
||||
transition: background-color .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle:hover,
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle:focus,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle:hover,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle:focus {
|
||||
background: rgba(255, 255, 255, 0.07) !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle.is-active,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle.is-active {
|
||||
background: rgba(13, 110, 253, 0.2) !important;
|
||||
color: #b9d4ff !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.endpoint-main_page .create-list-input-group > .create-list-temp-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #tempToggle {
|
||||
min-width: 7.25rem;
|
||||
padding-left: .65rem;
|
||||
padding-right: .65rem;
|
||||
font-size: .86rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle {
|
||||
min-width: 5rem;
|
||||
padding-left: .55rem;
|
||||
padding-right: .55rem;
|
||||
font-size: .86rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.endpoint-main_page .create-list-input-group > .create-list-temp-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #tempToggle {
|
||||
min-width: 6.7rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .create-list-input-group > .create-list-date-toggle,
|
||||
.endpoint-main_page .create-list-input-group > #listDateToggle {
|
||||
min-width: 4.7rem;
|
||||
padding-left: .45rem;
|
||||
padding-right: .45rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ function toggleEmptyPlaceholder() {
|
||||
}
|
||||
|
||||
function setupList(listId, username) {
|
||||
// Ustaw dane pokoju przed pierwszym emit — dzięki temu reconnect, który trafi
|
||||
// dokładnie w moment inicjalizacji widoku, nadal wie do jakiego pokoju wrócić.
|
||||
window.LIST_ID = listId;
|
||||
window.usernameForReconnect = username;
|
||||
window.CURRENT_LIST_USERNAME = username;
|
||||
|
||||
socket.emit('join_list', { room: listId, username: username });
|
||||
|
||||
const newItemInput = document.getElementById('newItem');
|
||||
@@ -81,11 +87,8 @@ function setupList(listId, username) {
|
||||
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 });
|
||||
}
|
||||
const intendedChecked = e.target.checked;
|
||||
const eventName = intendedChecked ? 'check_item' : 'uncheck_item';
|
||||
|
||||
e.target.disabled = true;
|
||||
li.classList.add('opacity-50', 'is-pending');
|
||||
@@ -98,6 +101,25 @@ function setupList(listId, username) {
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
li.appendChild(spinner);
|
||||
}
|
||||
|
||||
// ACK od serwera zabezpiecza przypadek, w którym po wznowieniu karty
|
||||
// transport wygląda na aktywny, ale faktycznie już nie działa. Nie czekamy wtedy
|
||||
// bez końca z zablokowanym checkboxem.
|
||||
socket.timeout(7000).emit(eventName, { item_id: id }, (err, response) => {
|
||||
if (!err && response && response.ok) {
|
||||
updateItemState(id, intendedChecked);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!err && response && response.ok === false) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.recoverListSocket === 'function') {
|
||||
window.recoverListSocket({ force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -262,11 +284,6 @@ function setupList(listId, username) {
|
||||
applyHidePurchased();
|
||||
});
|
||||
|
||||
// --- WAŻNE: zapisz dane do reconnect ---
|
||||
window.LIST_ID = listId;
|
||||
window.usernameForReconnect = username;
|
||||
window.CURRENT_LIST_USERNAME = username;
|
||||
|
||||
}
|
||||
|
||||
function unmarkNotPurchased(itemId) {
|
||||
|
||||
@@ -1,53 +1,177 @@
|
||||
let didReceiveFirstFullList = false;
|
||||
|
||||
// --- Automatyczny reconnect po powrocie do karty/przywróceniu internetu ---
|
||||
function reconnectIfNeeded() {
|
||||
if (!socket.connected) {
|
||||
socket.connect();
|
||||
}
|
||||
// Przeglądarki mobilne i desktopowe mogą zamrozić kartę lub zerwać transport
|
||||
// bez natychmiastowej zmiany socket.connected. Po wznowieniu odświeżamy transport,
|
||||
// ponownie dołączamy do pokoju i pobieramy aktualny stan listy.
|
||||
const WAKE_RECONNECT_THRESHOLD_MS = 2500;
|
||||
const WAKE_RECONNECT_DEBOUNCE_MS = 200;
|
||||
const WAKE_WATCHDOG_INTERVAL_MS = 10000;
|
||||
const WAKE_WATCHDOG_GAP_MS = 25000;
|
||||
let pageHiddenAt = null;
|
||||
let recoveryTimer = null;
|
||||
let recoveryForceRequested = false;
|
||||
let recoveryInProgress = false;
|
||||
let firstConnect = true;
|
||||
let wasReconnected = false;
|
||||
let lastWatchdogTick = Date.now();
|
||||
|
||||
function hasActiveListRoom() {
|
||||
return Boolean(window.LIST_ID && window.usernameForReconnect);
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden) {
|
||||
reconnectIfNeeded();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("focus", function () {
|
||||
reconnectIfNeeded();
|
||||
});
|
||||
|
||||
window.addEventListener("online", function () {
|
||||
reconnectIfNeeded();
|
||||
});
|
||||
|
||||
// --- Blokowanie checkboxów na czas reconnect ---
|
||||
function disableCheckboxes(disable) {
|
||||
document.querySelectorAll('#items input[type="checkbox"]').forEach(cb => {
|
||||
cb.disabled = disable;
|
||||
if (disable) {
|
||||
// Zapamiętaj stan biznesowy tylko przy pierwszym nałożeniu blokady transportu.
|
||||
// Dzięki temu checkboxy wyłączone np. przez archiwizację nie zostaną
|
||||
// przypadkiem odblokowane po ponownej synchronizacji.
|
||||
if (cb.dataset.socketDisabledBefore === undefined) {
|
||||
cb.dataset.socketDisabledBefore = cb.disabled ? '1' : '0';
|
||||
}
|
||||
cb.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cb.dataset.socketDisabledBefore !== undefined) {
|
||||
cb.disabled = cb.dataset.socketDisabledBefore === '1';
|
||||
delete cb.dataset.socketDisabledBefore;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Toasty przy rozłączeniu i połączeniu ---
|
||||
let firstConnect = true;
|
||||
let wasReconnected = false; // flaga do kontrolowania toasta
|
||||
function rejoinCurrentList() {
|
||||
if (!socket.connected || !hasActiveListRoom()) return;
|
||||
socket.emit('join_list', {
|
||||
room: window.LIST_ID,
|
||||
username: window.usernameForReconnect
|
||||
});
|
||||
}
|
||||
|
||||
function requestCurrentList({ verify = false } = {}) {
|
||||
if (!socket.connected || !window.LIST_ID) return;
|
||||
|
||||
if (!verify) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.timeout(4000).emit('request_full_list', { list_id: window.LIST_ID }, (err, response) => {
|
||||
if (!err && response && response.ok === true) return;
|
||||
recoverListSocket({ force: true });
|
||||
});
|
||||
}
|
||||
|
||||
function recoverListSocket({ force = false } = {}) {
|
||||
recoveryForceRequested = recoveryForceRequested || force;
|
||||
clearTimeout(recoveryTimer);
|
||||
recoveryTimer = setTimeout(() => {
|
||||
const shouldForce = recoveryForceRequested;
|
||||
recoveryForceRequested = false;
|
||||
|
||||
if (document.hidden) return;
|
||||
|
||||
if (!socket.connected) {
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
socket.connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldForce && hasActiveListRoom()) {
|
||||
if (recoveryInProgress) return;
|
||||
recoveryInProgress = true;
|
||||
disableCheckboxes(true);
|
||||
|
||||
// disconnect/connect czyści potencjalnie "martwy" transport po uśpieniu karty/procesu.
|
||||
socket.disconnect();
|
||||
setTimeout(() => socket.connect(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasActiveListRoom()) {
|
||||
rejoinCurrentList();
|
||||
requestCurrentList({ verify: true });
|
||||
}
|
||||
}, WAKE_RECONNECT_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Udostępnione dla akcji w live.js (np. timeout checkboxa).
|
||||
window.recoverListSocket = recoverListSocket;
|
||||
|
||||
// Powrót po zablokowaniu telefonu / zmianie aplikacji.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
pageHiddenAt = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddenFor = pageHiddenAt === null ? 0 : Date.now() - pageHiddenAt;
|
||||
pageHiddenAt = null;
|
||||
recoverListSocket({ force: hiddenFor >= WAKE_RECONNECT_THRESHOLD_MS });
|
||||
});
|
||||
|
||||
// BFCache może przywrócić stronę ze starym transportem Socket.IO.
|
||||
window.addEventListener('pageshow', event => {
|
||||
recoverListSocket({ force: Boolean(event.persisted) });
|
||||
});
|
||||
|
||||
window.addEventListener('pagehide', () => {
|
||||
pageHiddenAt = Date.now();
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
recoverListSocket({ force: false });
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
recoverListSocket({ force: hasActiveListRoom() });
|
||||
});
|
||||
|
||||
// Dodatkowe lifecycle events obsługiwane przez część przeglądarek mobilnych.
|
||||
document.addEventListener('freeze', () => {
|
||||
pageHiddenAt = Date.now();
|
||||
});
|
||||
|
||||
document.addEventListener('resume', () => {
|
||||
recoverListSocket({ force: hasActiveListRoom() });
|
||||
});
|
||||
|
||||
// Watchdog łapie także wznowienia, przy których przeglądarka nie wyśle poprawnie
|
||||
// visibilitychange/pageshow (np. po agresywnym uśpieniu procesu lub karty).
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const gap = now - lastWatchdogTick;
|
||||
lastWatchdogTick = now;
|
||||
|
||||
if (!document.hidden && gap >= WAKE_WATCHDOG_GAP_MS) {
|
||||
recoverListSocket({ force: hasActiveListRoom() });
|
||||
}
|
||||
}, WAKE_WATCHDOG_INTERVAL_MS);
|
||||
|
||||
socket.on('connect', function () {
|
||||
if (!firstConnect) {
|
||||
//showToast('Połączono z serwerem!', 'info');
|
||||
disableCheckboxes(true);
|
||||
wasReconnected = true;
|
||||
const isReconnect = !firstConnect;
|
||||
recoveryInProgress = false;
|
||||
|
||||
if (window.LIST_ID && window.usernameForReconnect) {
|
||||
socket.emit('join_list', { room: window.LIST_ID, username: window.usernameForReconnect });
|
||||
if (hasActiveListRoom()) {
|
||||
if (isReconnect) {
|
||||
disableCheckboxes(true);
|
||||
wasReconnected = true;
|
||||
}
|
||||
rejoinCurrentList();
|
||||
|
||||
// Nie polegamy wyłącznie na joined_confirmation — po wake-up lista ma się
|
||||
// zsynchronizować nawet jeśli pojedyncze zdarzenie zaginie.
|
||||
setTimeout(requestCurrentList, 250);
|
||||
}
|
||||
|
||||
firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', function (reason) {
|
||||
//showToast('Utracono połączenie z serwerem...', 'warning');
|
||||
disableCheckboxes(true);
|
||||
socket.on('disconnect', function () {
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
});
|
||||
|
||||
socket.on('connect_error', function () {
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
});
|
||||
|
||||
socket.off('joined_confirmation');
|
||||
@@ -56,10 +180,7 @@ socket.on('joined_confirmation', function (data) {
|
||||
showToast(`Lista: ${data.list_title} – ponownie dołączono.`, 'info');
|
||||
wasReconnected = false;
|
||||
}
|
||||
if (window.LIST_ID) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
}
|
||||
|
||||
requestCurrentList();
|
||||
});
|
||||
|
||||
socket.on('user_joined', function (data) {
|
||||
@@ -126,6 +247,8 @@ socket.on('full_list', function (data) {
|
||||
|
||||
window.currentItems = data.items;
|
||||
updateListSmoothly(data.items);
|
||||
disableCheckboxes(false);
|
||||
recoveryInProgress = false;
|
||||
if (typeof window.syncSortModeUI === 'function') {
|
||||
window.syncSortModeUI();
|
||||
}
|
||||
|
||||
@@ -1,30 +1,95 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const toggleBtn = document.getElementById("tempToggle");
|
||||
const hiddenInput = document.getElementById("temporaryHidden");
|
||||
if (!toggleBtn || !hiddenInput) return;
|
||||
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
new bootstrap.Tooltip(toggleBtn);
|
||||
}
|
||||
if (toggleBtn && hiddenInput) {
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
new bootstrap.Tooltip(toggleBtn);
|
||||
}
|
||||
|
||||
function updateToggle(isActive) {
|
||||
toggleBtn.classList.toggle("is-active", isActive);
|
||||
toggleBtn.textContent = isActive ? "Tymczasowa ✔" : "Tymczasowa";
|
||||
toggleBtn.setAttribute("aria-pressed", isActive ? "true" : "false");
|
||||
toggleBtn.setAttribute("title", isActive
|
||||
? "Lista tymczasowa będzie ważna przez 7 dni"
|
||||
: "Po zaznaczeniu lista będzie ważna tylko 7 dni");
|
||||
}
|
||||
function updateToggle(isActive) {
|
||||
toggleBtn.classList.toggle("is-active", isActive);
|
||||
toggleBtn.textContent = isActive ? "Tymczasowa ✔" : "Tymczasowa";
|
||||
toggleBtn.setAttribute("aria-pressed", isActive ? "true" : "false");
|
||||
toggleBtn.setAttribute("title", isActive
|
||||
? "Lista tymczasowa będzie ważna przez 7 dni"
|
||||
: "Po zaznaczeniu lista będzie ważna tylko 7 dni");
|
||||
}
|
||||
|
||||
let active = toggleBtn.getAttribute("data-active") === "1";
|
||||
hiddenInput.value = active ? "1" : "0";
|
||||
updateToggle(active);
|
||||
|
||||
toggleBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
active = !active;
|
||||
toggleBtn.setAttribute("data-active", active ? "1" : "0");
|
||||
let active = toggleBtn.getAttribute("data-active") === "1";
|
||||
hiddenInput.value = active ? "1" : "0";
|
||||
updateToggle(active);
|
||||
|
||||
toggleBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
active = !active;
|
||||
toggleBtn.setAttribute("data-active", active ? "1" : "0");
|
||||
hiddenInput.value = active ? "1" : "0";
|
||||
updateToggle(active);
|
||||
});
|
||||
}
|
||||
|
||||
const dateToggle = document.getElementById("listDateToggle");
|
||||
const dateHidden = document.getElementById("listDateHidden");
|
||||
const datePicker = document.getElementById("listDatePicker");
|
||||
const dateModalEl = document.getElementById("listDateModal");
|
||||
const applyDateBtn = document.getElementById("applyListDateBtn");
|
||||
const clearDateBtn = document.getElementById("clearListDateBtn");
|
||||
|
||||
if (!dateToggle || !dateHidden || !datePicker || !dateModalEl || !applyDateBtn || !clearDateBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
function localIsoDate(date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function displayDate(value) {
|
||||
const parts = String(value || "").split("-");
|
||||
if (parts.length !== 3) return value;
|
||||
return `${parts[2]}.${parts[1]}.${parts[0]}`;
|
||||
}
|
||||
|
||||
function updateDateButton() {
|
||||
const value = dateHidden.value;
|
||||
const hasDate = Boolean(value);
|
||||
dateToggle.classList.toggle("is-active", hasDate);
|
||||
dateToggle.textContent = hasDate ? "📅 Data ✓" : "📅 Data";
|
||||
dateToggle.setAttribute("aria-pressed", hasDate ? "true" : "false");
|
||||
dateToggle.setAttribute(
|
||||
"aria-label",
|
||||
hasDate ? `Data listy: ${displayDate(value)}` : "Wybierz datę listy"
|
||||
);
|
||||
dateToggle.setAttribute(
|
||||
"title",
|
||||
hasDate ? `Przypisana data: ${displayDate(value)}` : "Przypisz listę do wybranego dnia"
|
||||
);
|
||||
}
|
||||
|
||||
function closeDateModal() {
|
||||
if (typeof bootstrap === "undefined") return;
|
||||
bootstrap.Modal.getOrCreateInstance(dateModalEl).hide();
|
||||
}
|
||||
|
||||
dateModalEl.addEventListener("show.bs.modal", function () {
|
||||
datePicker.value = dateHidden.value || localIsoDate();
|
||||
});
|
||||
|
||||
applyDateBtn.addEventListener("click", function () {
|
||||
dateHidden.value = datePicker.value || "";
|
||||
updateDateButton();
|
||||
closeDateModal();
|
||||
});
|
||||
|
||||
clearDateBtn.addEventListener("click", function () {
|
||||
dateHidden.value = "";
|
||||
datePicker.value = "";
|
||||
updateDateButton();
|
||||
closeDateModal();
|
||||
});
|
||||
|
||||
updateDateButton();
|
||||
});
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
aria-pressed="false" aria-label="Przełącz listę tymczasową">
|
||||
<span class="create-list-temp-toggle__label">Tymczasowa</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary create-list-date-toggle" id="listDateToggle"
|
||||
data-bs-toggle="modal" data-bs-target="#listDateModal" title="Przypisz listę do wybranego dnia"
|
||||
aria-label="Wybierz datę listy">📅 Data</button>
|
||||
<input type="hidden" name="temporary" id="temporaryHidden" value="0">
|
||||
<input type="hidden" name="list_date" id="listDateHidden" value="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
@@ -46,6 +50,27 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal fade" id="listDateModal" tabindex="-1" aria-labelledby="listDateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="listDateModalLabel">📅 Data listy</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="listDatePicker" class="form-label">Przypisz listę do dnia</label>
|
||||
<input type="date" id="listDatePicker" class="form-control bg-dark text-white border-secondary">
|
||||
<div class="form-text text-secondary mt-2">Opcjonalne. Bez wyboru lista dostanie bieżącą datę.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" id="clearListDateBtn">Wyczyść</button>
|
||||
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="button" class="btn btn-success" id="applyListDateBtn">Ustaw datę</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% set month_names = ["styczeń","luty","marzec","kwiecień","maj","czerwiec","lipiec","sierpień","wrzesień","październik","listopad","grudzień"] %}
|
||||
|
||||
Reference in New Issue
Block a user