compact UI and add localization switch
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-19 05:05:24 +09:00
parent 5e5582664a
commit 58ff6ff614
6 changed files with 761 additions and 65 deletions

View File

@@ -4,6 +4,8 @@ tg?.expand();
const textNodes = new WeakMap();
const attrOriginals = new WeakMap();
let translationObserver = null;
let translationTimer = null;
const i18n = {
en: {
@@ -83,6 +85,14 @@ const i18n = {
"Марка": "Make",
"Модель": "Model",
"Добавить авто": "Add vehicle",
"Добавить запись": "Add entry",
"Расход": "Expense",
"дата, пробег, литры, цена": "date, odometer, liters, price",
"работа, стоимость, следующий срок": "work, cost, next due",
"страховка, штраф, парковка, прочее": "insurance, fine, parking, other",
"фото или файл": "photo or file",
"Дополнительно": "More options",
"Напоминание о следующем ТО": "Next maintenance reminder",
"За весь срок": "All time",
"За месяц": "This month",
"За день": "Per day",
@@ -215,6 +225,14 @@ const i18n = {
"Марка": "브랜드",
"Модель": "모델",
"Добавить авто": "차량 추가",
"Добавить запись": "기록 추가",
"Расход": "지출",
"дата, пробег, литры, цена": "날짜, 주행거리, 리터, 가격",
"работа, стоимость, следующий срок": "작업, 비용, 다음 예정",
"страховка, штраф, парковка, прочее": "보험, 벌금, 주차, 기타",
"фото или файл": "사진 또는 파일",
"Дополнительно": "추가 옵션",
"Напоминание о следующем ТО": "다음 정비 알림",
"За весь срок": "전체",
"За месяц": "월",
"За день": "일 평균",
@@ -304,6 +322,15 @@ function applyTranslations(root = document.body) {
});
}
function observeTranslations(root = document.body) {
if (translationObserver || !root) return;
translationObserver = new MutationObserver(() => {
window.clearTimeout(translationTimer);
translationTimer = window.setTimeout(() => applyTranslations(root), 40);
});
translationObserver.observe(root, { childList: true, subtree: true });
}
const state = {
user: null,
@@ -582,6 +609,7 @@ async function ensureUser() {
method: "POST",
body: JSON.stringify({ init_data: tg.initData }),
});
localStorage.setItem("carpassLocale", state.user.locale || "ru");
hideAuthOverlay();
updateRoleVisibility();
return;
@@ -590,6 +618,7 @@ async function ensureUser() {
const devId = localStorage.getItem("driversDevTelegramId") || "1";
localStorage.setItem("driversDevTelegramId", devId);
state.user = await api("/users/me");
localStorage.setItem("carpassLocale", state.user.locale || "ru");
hideAuthOverlay();
updateRoleVisibility();
return;
@@ -598,6 +627,36 @@ async function ensureUser() {
throw new Error("Требуется вход через Telegram");
}
function installLocaleSwitch() {
const topActions = document.querySelector(".topbar .top-actions");
if (!topActions || document.querySelector("#globalLocaleSelect")) return;
const select = document.createElement("select");
select.id = "globalLocaleSelect";
select.className = "locale-switch";
select.setAttribute("aria-label", "Язык");
select.innerHTML = `
<option value="ru">RU</option>
<option value="en">EN</option>
<option value="ko">KO</option>
`;
select.value = state.user?.locale || "ru";
select.addEventListener("change", async () => {
await runAction(select, "Сохраняю...", async () => {
state.user = await api(`/users/${state.user.id}/preferences`, {
method: "PATCH",
body: JSON.stringify({ locale: select.value, currency: state.user.currency }),
});
localStorage.setItem("carpassLocale", state.user.locale || "ru");
document.querySelector("#localeSelect").value = state.user.locale || "ru";
applyTranslations();
renderCars();
renderStats(state.latestStats);
toast("Сохранено");
});
});
topActions.prepend(select);
}
function hideAuthOverlay() {
document.querySelector("#authOverlay")?.classList.add("hidden");
document.body.classList.remove("auth-required");
@@ -2671,6 +2730,9 @@ document.querySelector("#settingsForm").addEventListener("submit", async (event)
method: "PATCH",
body: JSON.stringify({ locale: data.locale, currency: data.currency }),
});
localStorage.setItem("carpassLocale", state.user.locale || "ru");
const globalLocale = document.querySelector("#globalLocaleSelect");
if (globalLocale) globalLocale.value = state.user.locale || "ru";
applyTranslations();
initCarCatalog();
await loadSelectedCar();
@@ -2700,6 +2762,7 @@ document.querySelector("#fuelForm").addEventListener("submit", async (event) =>
});
form.reset();
form.entry_date.value = today();
form.is_full_tank.checked = true;
await loadSelectedCar();
toast("Сохранено");
haptic("success");
@@ -2802,6 +2865,20 @@ function mountEntryForms() {
}
}
function fillEntryDefaults(sectionId) {
const car = selectedCar();
const odometer = car?.current_odometer || "";
const sections = sectionId ? [document.querySelector(`#${sectionId}`)] : [...document.querySelectorAll(".drawer-section")];
sections.filter(Boolean).forEach((section) => {
section.querySelectorAll('input[name="entry_date"]').forEach((input) => {
if (!input.value) input.value = today();
});
section.querySelectorAll('input[name="odometer"]').forEach((input) => {
if (!input.value && odometer) input.value = odometer;
});
});
}
async function openDrawerSection(sectionId, options = {}) {
if (!canOpenDrawerSection(sectionId)) {
toast("Этот раздел недоступен для вашей роли", "error");
@@ -2817,6 +2894,7 @@ async function openDrawerSection(sectionId, options = {}) {
button.classList.toggle("active", button.dataset.menuSection === sectionId);
});
mountEntryForms();
fillEntryDefaults(sectionId);
if (sectionId === "carProfileSection") fillCarProfileForm();
if (sectionId === "settingsSection") {
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
@@ -2891,7 +2969,18 @@ document.querySelector("#addCarQuickBtn").addEventListener("click", () => {
});
document.querySelector("#addRecordPrimaryBtn").addEventListener("click", () => {
openDrawerSection("expensesSection");
openDrawerSection("quickAddSection");
});
document.querySelectorAll("[data-quick-entry]").forEach((button) => {
button.addEventListener("click", async () => {
haptic();
if (button.dataset.quickEntry === "scan") {
openScanModal();
return;
}
await openDrawerSection(button.dataset.quickEntry);
});
});
document.querySelectorAll("[data-menu-section]").forEach((button) => {
@@ -3043,6 +3132,8 @@ initPwa();
Promise.all([loadAuthConfig()])
.then(() => Promise.all([ensureUser(), loadCatalog()]))
.then(() => {
installLocaleSwitch();
observeTranslations();
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
document.querySelector("#currencySelect").value = state.user?.currency || "RUB";
document.querySelector("#expenseForm").currency.value = state.user?.currency || "RUB";