Files
duty-teller/webapp/js/main.js
Nikolay Tatarinov dd14d48824 style: enhance layout and functionality of duty markers and calendar
- 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.
2026-02-19 15:40:34 +03:00

234 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
});
});