- Updated CSS for `.day` and `.access-denied` to improve layout and visual consistency. - Introduced a new function `dutyOverlapsLocalRange` in `dateUtils.js` to check duty overlaps within a specified date range. - Refactored `dutyItemHtml` in `dutyList.js` to utilize `formatTimeLocal` for time formatting, enhancing readability. - Added utility functions in `hints.js` for parsing duty marker data and building time prefixes, streamlining hint rendering logic. - Improved the `showAccessDenied` function in `ui.js` to display detailed server messages when access is denied.
234 lines
6.6 KiB
JavaScript
234 lines
6.6 KiB
JavaScript
/**
|
||
* Entry point: theme init, auth gate, loadMonth, nav and swipe handlers.
|
||
*/
|
||
|
||
import { initTheme, applyTheme } from "./theme.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
|
||
} 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 { renderDutyList } from "./dutyList.js";
|
||
import {
|
||
firstDayOfMonth,
|
||
lastDayOfMonth,
|
||
getMonday,
|
||
localDateString,
|
||
dutyOverlapsLocalRange
|
||
} from "./dateUtils.js";
|
||
|
||
initTheme();
|
||
|
||
/**
|
||
* 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();
|
||
if (loadingEl) loadingEl.classList.add("hidden");
|
||
}, RETRY_DELAY_MS);
|
||
return;
|
||
}
|
||
showAccessDenied();
|
||
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 || "Не удалось загрузить данные.");
|
||
setNavEnabled(true);
|
||
return;
|
||
}
|
||
if (loadingEl) loadingEl.classList.add("hidden");
|
||
setNavEnabled(true);
|
||
}
|
||
|
||
if (prevBtn) {
|
||
prevBtn.addEventListener("click", () => {
|
||
if (accessDeniedEl && !accessDeniedEl.hidden) return;
|
||
state.current.setMonth(state.current.getMonth() - 1);
|
||
loadMonth();
|
||
});
|
||
}
|
||
if (nextBtn) {
|
||
nextBtn.addEventListener("click", () => {
|
||
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 (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();
|
||
loadMonth();
|
||
});
|
||
});
|