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

@@ -2,9 +2,10 @@
* Duty list (timeline) rendering.
*/
import { dutyListEl, state } from "./dom.js";
import { getDutyListEl, state } from "./dom.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import { buildContactLinksHtml } from "./contactHtml.js";
import {
localDateString,
firstDayOfMonth,
@@ -14,37 +15,6 @@ import {
formatDateKey
} from "./dateUtils.js";
/**
* Build HTML for contact links (phone, Telegram) for a duty. Returns empty string if none.
* @param {'ru'|'en'} lang
* @param {object} d - Duty with optional phone, username
* @returns {string}
*/
function dutyCardContactHtml(lang, d) {
const parts = [];
if (d.phone && String(d.phone).trim()) {
const p = String(d.phone).trim();
const safeHref = "tel:" + p.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
parts.push(
'<a href="' + safeHref + '" class="duty-contact-link duty-contact-phone">' +
escapeHtml(p) + "</a>"
);
}
if (d.username && String(d.username).trim()) {
const u = String(d.username).trim().replace(/^@+/, "");
if (u) {
const href = "https://t.me/" + encodeURIComponent(u);
parts.push(
'<a href="' + href.replace(/"/g, "&quot;") + '" class="duty-contact-link duty-contact-username" target="_blank" rel="noopener noreferrer">@' +
escapeHtml(u) + "</a>"
);
}
}
return parts.length
? '<div class="duty-contact-row">' + parts.join(" · ") + "</div>"
: "";
}
/** Phone icon SVG for flip button (show contacts). */
const ICON_PHONE =
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>';
@@ -79,7 +49,11 @@ export function dutyTimelineCardHtml(d, isCurrent) {
? t(lang, "duty.now_on_duty")
: (t(lang, "event_type." + (d.event_type || "duty")));
const extraClass = isCurrent ? " duty-item--current" : "";
const contactHtml = dutyCardContactHtml(lang, d);
const contactHtml = buildContactLinksHtml(lang, d.phone, d.username, {
classPrefix: "duty-contact",
showLabels: false,
separator: " · "
});
const hasContacts = Boolean(
(d.phone && String(d.phone).trim()) ||
(d.username && String(d.username).trim())
@@ -174,12 +148,12 @@ export function dutyItemHtml(d, typeLabelOverride, showUntilEnd, extraClass) {
'</span> <span class="name">' +
escapeHtml(d.full_name) +
'</span><div class="time">' +
timeOrRange +
escapeHtml(timeOrRange) +
"</div></div>"
);
}
/** Whether the delegated flip-button click listener has been attached to dutyListEl. */
/** Whether the delegated flip-button click listener has been attached to duty list element. */
let flipListenerAttached = false;
/**
@@ -187,6 +161,7 @@ let flipListenerAttached = false;
* @param {object[]} duties - Duties (only duty type used for timeline)
*/
export function renderDutyList(duties) {
const dutyListEl = getDutyListEl();
if (!dutyListEl) return;
if (!flipListenerAttached) {
@@ -277,8 +252,9 @@ export function renderDutyList(duties) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
const currentDutyCard = dutyListEl.querySelector(".duty-item--current");
const todayBlock = dutyListEl.querySelector(".duty-timeline-day--today");
const listEl = getDutyListEl();
const currentDutyCard = listEl ? listEl.querySelector(".duty-item--current") : null;
const todayBlock = listEl ? listEl.querySelector(".duty-timeline-day--today") : null;
if (currentDutyCard) {
scrollToEl(currentDutyCard);
} else if (todayBlock) {