All checks were successful
CI / lint-and-test (push) Successful in 14s
- Added functionality to lock the background scroll when the day detail panel is open, improving user interaction by preventing background movement. - Implemented logic to restore the scroll position when the panel is closed, ensuring a seamless user experience. - Updated CSS to support the new behavior, enhancing the visual consistency of the day detail panel. - Added early return checks in navigation button event listeners to prevent actions while the day detail panel is open, improving performance and usability.
255 lines
7.5 KiB
JavaScript
255 lines
7.5 KiB
JavaScript
/**
|
|
* Entry point: theme init, auth gate, loadMonth, nav and swipe handlers.
|
|
*/
|
|
|
|
import { initTheme, applyTheme } from "./theme.js";
|
|
import { getLang, t, weekdayLabels } from "./i18n.js";
|
|
import { getInitData } from "./auth.js";
|
|
import { isLocalhost } from "./auth.js";
|
|
import { RETRY_DELAY_MS, RETRY_AFTER_ACCESS_DENIED_MS } from "./constants.js";
|
|
import {
|
|
state,
|
|
accessDeniedEl,
|
|
prevBtn,
|
|
nextBtn,
|
|
loadingEl,
|
|
errorEl,
|
|
weekdaysEl
|
|
} from "./dom.js";
|
|
import { showAccessDenied, hideAccessDenied, showError, setNavEnabled } from "./ui.js";
|
|
import { fetchDuties, fetchCalendarEvents } from "./api.js";
|
|
import {
|
|
dutiesByDate,
|
|
calendarEventsByDate,
|
|
renderCalendar
|
|
} from "./calendar.js";
|
|
import { initDayDetail } from "./dayDetail.js";
|
|
import { renderDutyList } from "./dutyList.js";
|
|
import {
|
|
firstDayOfMonth,
|
|
lastDayOfMonth,
|
|
getMonday,
|
|
localDateString,
|
|
dutyOverlapsLocalRange
|
|
} from "./dateUtils.js";
|
|
|
|
initTheme();
|
|
|
|
state.lang = getLang();
|
|
document.documentElement.lang = state.lang;
|
|
document.title = t(state.lang, "app.title");
|
|
if (loadingEl) loadingEl.textContent = t(state.lang, "loading");
|
|
const dayLabels = weekdayLabels(state.lang);
|
|
if (weekdaysEl) {
|
|
const spans = weekdaysEl.querySelectorAll("span");
|
|
spans.forEach((span, i) => {
|
|
if (dayLabels[i]) span.textContent = dayLabels[i];
|
|
});
|
|
}
|
|
if (prevBtn) prevBtn.setAttribute("aria-label", t(state.lang, "nav.prev_month"));
|
|
if (nextBtn) nextBtn.setAttribute("aria-label", t(state.lang, "nav.next_month"));
|
|
|
|
/**
|
|
* Run callback when Telegram WebApp is ready (or immediately outside Telegram).
|
|
* Expands and applies theme when in TWA.
|
|
* @param {() => void} cb
|
|
*/
|
|
function runWhenReady(cb) {
|
|
if (window.Telegram && window.Telegram.WebApp) {
|
|
if (window.Telegram.WebApp.ready) {
|
|
window.Telegram.WebApp.ready();
|
|
}
|
|
if (window.Telegram.WebApp.expand) {
|
|
window.Telegram.WebApp.expand();
|
|
}
|
|
applyTheme();
|
|
if (window.Telegram.WebApp.onEvent) {
|
|
window.Telegram.WebApp.onEvent("theme_changed", applyTheme);
|
|
}
|
|
requestAnimationFrame(() => applyTheme());
|
|
setTimeout(cb, 0);
|
|
} else {
|
|
cb();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If allowed (initData or localhost), call onAllowed(); otherwise show access denied.
|
|
* When inside Telegram WebApp but initData is empty, retry once after a short delay.
|
|
* @param {() => void} onAllowed
|
|
*/
|
|
function requireTelegramOrLocalhost(onAllowed) {
|
|
let initData = getInitData();
|
|
const isLocal = isLocalhost();
|
|
if (initData) {
|
|
onAllowed();
|
|
return;
|
|
}
|
|
if (isLocal) {
|
|
onAllowed();
|
|
return;
|
|
}
|
|
if (window.Telegram && window.Telegram.WebApp) {
|
|
setTimeout(() => {
|
|
initData = getInitData();
|
|
if (initData) {
|
|
onAllowed();
|
|
return;
|
|
}
|
|
showAccessDenied(undefined);
|
|
if (loadingEl) loadingEl.classList.add("hidden");
|
|
}, RETRY_DELAY_MS);
|
|
return;
|
|
}
|
|
showAccessDenied(undefined);
|
|
if (loadingEl) loadingEl.classList.add("hidden");
|
|
}
|
|
|
|
/**
|
|
* Load current month: fetch duties and events, render calendar and duty list.
|
|
*/
|
|
async function loadMonth() {
|
|
hideAccessDenied();
|
|
setNavEnabled(false);
|
|
if (loadingEl) loadingEl.classList.remove("hidden");
|
|
if (errorEl) errorEl.hidden = true;
|
|
const current = state.current;
|
|
const first = firstDayOfMonth(current);
|
|
const start = getMonday(first);
|
|
const gridEnd = new Date(start);
|
|
gridEnd.setDate(gridEnd.getDate() + 41);
|
|
const from = localDateString(start);
|
|
const to = localDateString(gridEnd);
|
|
try {
|
|
const dutiesPromise = fetchDuties(from, to);
|
|
const eventsPromise = fetchCalendarEvents(from, to);
|
|
const duties = await dutiesPromise;
|
|
const events = await eventsPromise;
|
|
const byDate = dutiesByDate(duties);
|
|
const calendarByDate = calendarEventsByDate(events);
|
|
renderCalendar(current.getFullYear(), current.getMonth(), byDate, calendarByDate);
|
|
const last = lastDayOfMonth(current);
|
|
const firstKey = localDateString(first);
|
|
const lastKey = localDateString(last);
|
|
const dutiesInMonth = duties.filter((d) =>
|
|
dutyOverlapsLocalRange(d, firstKey, lastKey)
|
|
);
|
|
state.lastDutiesForList = dutiesInMonth;
|
|
renderDutyList(dutiesInMonth);
|
|
if (state.todayRefreshInterval) {
|
|
clearInterval(state.todayRefreshInterval);
|
|
}
|
|
state.todayRefreshInterval = null;
|
|
const viewedMonth = current.getFullYear() * 12 + current.getMonth();
|
|
const thisMonth =
|
|
new Date().getFullYear() * 12 + new Date().getMonth();
|
|
if (viewedMonth === thisMonth) {
|
|
state.todayRefreshInterval = setInterval(() => {
|
|
if (
|
|
current.getFullYear() * 12 + current.getMonth() !==
|
|
new Date().getFullYear() * 12 + new Date().getMonth()
|
|
) {
|
|
return;
|
|
}
|
|
renderDutyList(state.lastDutiesForList);
|
|
}, 60000);
|
|
}
|
|
} catch (e) {
|
|
if (e.message === "ACCESS_DENIED") {
|
|
showAccessDenied(e.serverDetail);
|
|
setNavEnabled(true);
|
|
if (
|
|
window.Telegram &&
|
|
window.Telegram.WebApp &&
|
|
!window._initDataRetried
|
|
) {
|
|
window._initDataRetried = true;
|
|
setTimeout(loadMonth, RETRY_AFTER_ACCESS_DENIED_MS);
|
|
}
|
|
return;
|
|
}
|
|
showError(e.message || t(state.lang, "error_generic"));
|
|
setNavEnabled(true);
|
|
return;
|
|
}
|
|
if (loadingEl) loadingEl.classList.add("hidden");
|
|
setNavEnabled(true);
|
|
}
|
|
|
|
if (prevBtn) {
|
|
prevBtn.addEventListener("click", () => {
|
|
if (document.body.classList.contains("day-detail-sheet-open")) return;
|
|
if (accessDeniedEl && !accessDeniedEl.hidden) return;
|
|
state.current.setMonth(state.current.getMonth() - 1);
|
|
loadMonth();
|
|
});
|
|
}
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener("click", () => {
|
|
if (document.body.classList.contains("day-detail-sheet-open")) return;
|
|
if (accessDeniedEl && !accessDeniedEl.hidden) return;
|
|
state.current.setMonth(state.current.getMonth() + 1);
|
|
loadMonth();
|
|
});
|
|
}
|
|
|
|
(function bindSwipeMonth() {
|
|
const swipeEl = document.getElementById("calendarSticky");
|
|
if (!swipeEl) return;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
const SWIPE_THRESHOLD = 50;
|
|
swipeEl.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
if (e.changedTouches.length === 0) return;
|
|
const t = e.changedTouches[0];
|
|
startX = t.clientX;
|
|
startY = t.clientY;
|
|
},
|
|
{ passive: true }
|
|
);
|
|
swipeEl.addEventListener(
|
|
"touchend",
|
|
(e) => {
|
|
if (e.changedTouches.length === 0) return;
|
|
if (document.body.classList.contains("day-detail-sheet-open")) return;
|
|
if (accessDeniedEl && !accessDeniedEl.hidden) return;
|
|
const t = e.changedTouches[0];
|
|
const deltaX = t.clientX - startX;
|
|
const deltaY = t.clientY - startY;
|
|
if (Math.abs(deltaX) <= SWIPE_THRESHOLD) return;
|
|
if (Math.abs(deltaY) > Math.abs(deltaX)) return;
|
|
if (deltaX > SWIPE_THRESHOLD) {
|
|
if (prevBtn && prevBtn.disabled) return;
|
|
state.current.setMonth(state.current.getMonth() - 1);
|
|
loadMonth();
|
|
} else if (deltaX < -SWIPE_THRESHOLD) {
|
|
if (nextBtn && nextBtn.disabled) return;
|
|
state.current.setMonth(state.current.getMonth() + 1);
|
|
loadMonth();
|
|
}
|
|
},
|
|
{ passive: true }
|
|
);
|
|
})();
|
|
|
|
function bindStickyScrollShadow() {
|
|
const stickyEl = document.getElementById("calendarSticky");
|
|
if (!stickyEl || document._stickyScrollBound) return;
|
|
document._stickyScrollBound = true;
|
|
function updateScrolled() {
|
|
stickyEl.classList.toggle("is-scrolled", window.scrollY > 0);
|
|
}
|
|
window.addEventListener("scroll", updateScrolled, { passive: true });
|
|
updateScrolled();
|
|
}
|
|
|
|
runWhenReady(() => {
|
|
requireTelegramOrLocalhost(() => {
|
|
bindStickyScrollShadow();
|
|
initDayDetail();
|
|
loadMonth();
|
|
});
|
|
});
|