feat: enhance CI workflow and update webapp styles
Some checks failed
CI / lint-and-test (push) Failing after 45s

- Added Node.js setup and webapp testing steps to the CI workflow for improved integration.
- Updated HTML to link multiple CSS files for better modularity and organization of styles.
- Removed deprecated `style.css` and introduced new CSS files for base styles, calendar, day detail, hints, markers, states, and duty list to enhance maintainability and readability.
- Implemented new styles for improved presentation of duty information and user interactions.
- Added unit tests for new API functions and contact link rendering to ensure functionality and reliability.
This commit is contained in:
2026-03-02 17:20:33 +03:00
parent e3240d0981
commit 2fb553567f
29 changed files with 2212 additions and 1375 deletions

View File

@@ -3,9 +3,10 @@
* Fetches today's duties, finds the active one (start <= now < end), shows name, shift, contacts.
*/
import { currentDutyViewEl, state, loadingEl } from "./dom.js";
import { getCurrentDutyViewEl, state, getLoadingEl } from "./dom.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import { buildContactLinksHtml } from "./contactHtml.js";
import { fetchDuties } from "./api.js";
import {
localDateString,
@@ -13,6 +14,11 @@ import {
formatHHMM
} from "./dateUtils.js";
/** @type {(() => void)|null} Callback when user taps "Back to calendar". */
let onBackCallback = null;
/** @type {(() => void)|null} Handler registered with Telegram BackButton.onClick. */
let backButtonHandler = null;
/**
* Find the duty that is currently active (start <= now < end). Prefer event_type === "duty".
* @param {object[]} duties - List of duties with start_at, end_at, event_type
@@ -30,54 +36,6 @@ export function findCurrentDuty(duties) {
return null;
}
/**
* Build contact HTML (phone + Telegram) for current duty card, styled like day-detail.
* @param {'ru'|'en'} lang
* @param {object} d - Duty with optional phone, username
* @returns {string}
*/
function buildContactHtml(lang, d) {
const parts = [];
if (d.phone && String(d.phone).trim()) {
const p = String(d.phone).trim();
const label = t(lang, "contact.phone");
const safeHref =
"tel:" +
p.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
parts.push(
'<span class="current-duty-contact">' +
escapeHtml(label) +
": " +
'<a href="' +
safeHref +
'" class="current-duty-contact-link current-duty-contact-phone">' +
escapeHtml(p) +
"</a></span>"
);
}
if (d.username && String(d.username).trim()) {
const u = String(d.username).trim().replace(/^@+/, "");
if (u) {
const label = t(lang, "contact.telegram");
const display = "@" + u;
const href = "https://t.me/" + encodeURIComponent(u);
parts.push(
'<span class="current-duty-contact">' +
escapeHtml(label) +
": " +
'<a href="' +
escapeHtml(href) +
'" class="current-duty-contact-link current-duty-contact-username" target="_blank" rel="noopener noreferrer">' +
escapeHtml(display) +
"</a></span>"
);
}
}
return parts.length
? '<div class="current-duty-contact-row">' + parts.join(" ") + "</div>"
: "";
}
/**
* Render the current duty view content (card with duty or no-duty message).
* @param {object|null} duty - Active duty or null
@@ -120,7 +78,11 @@ export function renderCurrentDutyContent(duty, lang) {
" " +
endTime;
const shiftLabel = t(lang, "current_duty.shift");
const contactHtml = buildContactHtml(lang, duty);
const contactHtml = buildContactLinksHtml(lang, duty.phone, duty.username, {
classPrefix: "current-duty-contact",
showLabels: true,
separator: " "
});
return (
'<div class="current-duty-card">' +
@@ -149,16 +111,18 @@ export function renderCurrentDutyContent(duty, lang) {
* @param {() => void} onBack - Callback when user taps "Back to calendar"
*/
export async function showCurrentDutyView(onBack) {
const currentDutyViewEl = getCurrentDutyViewEl();
const container = currentDutyViewEl && currentDutyViewEl.closest(".container");
const calendarSticky = document.getElementById("calendarSticky");
const dutyList = document.getElementById("dutyList");
if (!currentDutyViewEl) return;
currentDutyViewEl._onBack = onBack;
onBackCallback = onBack;
currentDutyViewEl.classList.remove("hidden");
if (container) container.setAttribute("data-view", "currentDuty");
if (calendarSticky) calendarSticky.hidden = true;
if (dutyList) dutyList.hidden = true;
const loadingEl = getLoadingEl();
if (loadingEl) loadingEl.classList.add("hidden");
const lang = state.lang;
@@ -170,9 +134,9 @@ export async function showCurrentDutyView(onBack) {
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.BackButton) {
window.Telegram.WebApp.BackButton.show();
const handler = () => {
if (currentDutyViewEl._onBack) currentDutyViewEl._onBack();
if (onBackCallback) onBackCallback();
};
currentDutyViewEl._backButtonHandler = handler;
backButtonHandler = handler;
window.Telegram.WebApp.BackButton.onClick(handler);
}
@@ -205,9 +169,7 @@ export async function showCurrentDutyView(onBack) {
function handleCurrentDutyClick(e) {
const btn = e.target && e.target.closest("[data-action='back']");
if (!btn) return;
if (currentDutyViewEl && currentDutyViewEl._onBack) {
currentDutyViewEl._onBack();
}
if (onBackCallback) onBackCallback();
}
/**
@@ -215,24 +177,24 @@ function handleCurrentDutyClick(e) {
* Hides Telegram BackButton and calls loadMonth so calendar is populated.
*/
export function hideCurrentDutyView() {
const currentDutyViewEl = getCurrentDutyViewEl();
const container = currentDutyViewEl && currentDutyViewEl.closest(".container");
const calendarSticky = document.getElementById("calendarSticky");
const dutyList = document.getElementById("dutyList");
const backHandler = currentDutyViewEl && currentDutyViewEl._backButtonHandler;
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.BackButton) {
if (backHandler) {
window.Telegram.WebApp.BackButton.offClick(backHandler);
if (backButtonHandler) {
window.Telegram.WebApp.BackButton.offClick(backButtonHandler);
}
window.Telegram.WebApp.BackButton.hide();
}
if (currentDutyViewEl) {
currentDutyViewEl.removeEventListener("click", handleCurrentDutyClick);
currentDutyViewEl._onBack = null;
currentDutyViewEl._backButtonHandler = null;
currentDutyViewEl.classList.add("hidden");
currentDutyViewEl.innerHTML = "";
}
onBackCallback = null;
backButtonHandler = null;
if (container) container.removeAttribute("data-view");
if (calendarSticky) calendarSticky.hidden = false;
if (dutyList) dutyList.hidden = false;