feat: update language support and enhance API functionality

- Changed the default language in `index.html` from Russian to English, updating the title and button aria-labels for improved accessibility.
- Refactored the `buildFetchOptions` function in `api.js` to include an optional external abort signal, enhancing request management.
- Updated `fetchDuties` and `fetchCalendarEvents` to support request cancellation using the new abort signal, improving error handling.
- Added unit tests for the API functions to ensure proper functionality, including handling of 403 errors and request cancellations.
- Enhanced CSS styles for duty markers to improve visual consistency.
- Removed unused code and improved the overall structure of the JavaScript files for better maintainability.
This commit is contained in:
2026-03-02 12:40:49 +03:00
parent b906bfa777
commit a4d8d085c6
17 changed files with 822 additions and 181 deletions

View File

@@ -4,8 +4,7 @@
import { initTheme, applyTheme } from "./theme.js";
import { getLang, t, weekdayLabels } from "./i18n.js";
import { getInitData } from "./auth.js";
import { isLocalhost } from "./auth.js";
import { getInitData, isLocalhost } from "./auth.js";
import { RETRY_DELAY_MS, RETRY_AFTER_ACCESS_DENIED_MS } from "./constants.js";
import {
state,
@@ -24,6 +23,7 @@ import {
renderCalendar
} from "./calendar.js";
import { initDayDetail } from "./dayDetail.js";
import { initHints } from "./hints.js";
import { renderDutyList } from "./dutyList.js";
import {
firstDayOfMonth,
@@ -106,10 +106,18 @@ function requireTelegramOrLocalhost(onAllowed) {
if (loadingEl) loadingEl.classList.add("hidden");
}
/** AbortController for the in-flight loadMonth request; aborted when a new load starts. */
let loadMonthAbortController = null;
/**
* Load current month: fetch duties and events, render calendar and duty list.
* Stale requests are cancelled when the user navigates to another month before they complete.
*/
async function loadMonth() {
if (loadMonthAbortController) loadMonthAbortController.abort();
loadMonthAbortController = new AbortController();
const signal = loadMonthAbortController.signal;
hideAccessDenied();
setNavEnabled(false);
if (loadingEl) loadingEl.classList.remove("hidden");
@@ -122,8 +130,8 @@ async function loadMonth() {
const from = localDateString(start);
const to = localDateString(gridEnd);
try {
const dutiesPromise = fetchDuties(from, to);
const eventsPromise = fetchCalendarEvents(from, to);
const dutiesPromise = fetchDuties(from, to, signal);
const eventsPromise = fetchCalendarEvents(from, to, signal);
const duties = await dutiesPromise;
const events = await eventsPromise;
const byDate = dutiesByDate(duties);
@@ -156,15 +164,18 @@ async function loadMonth() {
}, 60000);
}
} catch (e) {
if (e.name === "AbortError") {
return;
}
if (e.message === "ACCESS_DENIED") {
showAccessDenied(e.serverDetail);
setNavEnabled(true);
if (
window.Telegram &&
window.Telegram.WebApp &&
!window._initDataRetried
!state.initDataRetried
) {
window._initDataRetried = true;
state.initDataRetried = true;
setTimeout(loadMonth, RETRY_AFTER_ACCESS_DENIED_MS);
}
return;
@@ -204,9 +215,9 @@ if (nextBtn) {
"touchstart",
(e) => {
if (e.changedTouches.length === 0) return;
const t = e.changedTouches[0];
startX = t.clientX;
startY = t.clientY;
const touch = e.changedTouches[0];
startX = touch.clientX;
startY = touch.clientY;
},
{ passive: true }
);
@@ -216,9 +227,9 @@ if (nextBtn) {
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;
const touch = e.changedTouches[0];
const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY;
if (Math.abs(deltaX) <= SWIPE_THRESHOLD) return;
if (Math.abs(deltaY) > Math.abs(deltaX)) return;
if (deltaX > SWIPE_THRESHOLD) {
@@ -237,8 +248,8 @@ if (nextBtn) {
function bindStickyScrollShadow() {
const stickyEl = document.getElementById("calendarSticky");
if (!stickyEl || document._stickyScrollBound) return;
document._stickyScrollBound = true;
if (!stickyEl || state.stickyScrollBound) return;
state.stickyScrollBound = true;
function updateScrolled() {
stickyEl.classList.toggle("is-scrolled", window.scrollY > 0);
}
@@ -250,6 +261,7 @@ runWhenReady(() => {
requireTelegramOrLocalhost(() => {
bindStickyScrollShadow();
initDayDetail();
initHints();
loadMonth();
});
});