Files
duty-teller/webapp/js/main.js
Nikolay Tatarinov c9cf86a8f6 refactor: restructure web application with modular JavaScript and remove legacy code
- Deleted the `app.js` file and migrated its functionality to a modular structure with multiple JavaScript files for better organization and maintainability.
- Updated `index.html` to reference the new `main.js` module, ensuring proper loading of the application.
- Introduced new utility modules for API requests, authentication, calendar handling, and DOM manipulation to enhance code clarity and separation of concerns.
- Enhanced CSS styles for improved layout and theming consistency across the application.
- Added comprehensive comments and documentation to new modules to facilitate future development and understanding.
2026-02-19 15:24:52 +03:00

235 lines
6.7 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
} 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) => {
const byDateLocal = dutiesByDate([d]);
return Object.keys(byDateLocal).some(
(key) => key >= firstKey && key <= 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;
if (prevBtn && prevBtn.disabled) 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) {
state.current.setMonth(state.current.getMonth() - 1);
loadMonth();
} else if (deltaX < -SWIPE_THRESHOLD) {
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();
});
});