- Introduced a new `dayDetail.js` module to manage the day detail panel functionality, allowing users to view detailed information about duties and events for each calendar day. - Enhanced the calendar rendering in `calendar.js` to include visual indicators for duties and events, improving user interaction and experience. - Updated CSS in `style.css` to style the day detail panel and its components, ensuring a responsive design for both desktop and mobile views. - Refactored `hints.js` to export the `getDutyMarkerRows` function, facilitating better integration with the new day detail features. - Added localization support for the day detail panel in `i18n.js`, including new translations for close button and event titles. - Enhanced the initialization process in `main.js` to set up the day detail panel on application load.
252 lines
7.3 KiB
JavaScript
252 lines
7.3 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 (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();
|
|
initDayDetail();
|
|
loadMonth();
|
|
});
|
|
});
|