Split auth UI module and load FAQ from markdown assets
This commit is contained in:
72
wwwroot/js/auth-ui.js
Normal file
72
wwwroot/js/auth-ui.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { t } from "./i18n.js";
|
||||
import { state, getSavedUsername } from "./state.js";
|
||||
import { $, toast } from "./dom.js";
|
||||
|
||||
export function setAuthUI(isAuthed) {
|
||||
const main = document.querySelector("main");
|
||||
const statusBar = document.querySelector(".status-bar");
|
||||
const authCard = $("auth-card");
|
||||
[main, statusBar].forEach((el) =>
|
||||
el?.classList.toggle("hidden", !isAuthed),
|
||||
);
|
||||
if (authCard) authCard.classList.toggle("hidden", isAuthed);
|
||||
const adminToggle = $("admin-toggle");
|
||||
if (adminToggle)
|
||||
adminToggle.classList.toggle("hidden", !isAuthed || !state.me?.isAdmin);
|
||||
if (!isAuthed) {
|
||||
const adminCard = $("admin-card");
|
||||
if (adminCard) adminCard.classList.add("hidden");
|
||||
const loginUser = $("login-username");
|
||||
const cachedUser = getSavedUsername();
|
||||
if (
|
||||
loginUser &&
|
||||
cachedUser &&
|
||||
!loginUser.dataset.userEditing &&
|
||||
!loginUser.value
|
||||
) {
|
||||
loginUser.value = cachedUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAuthMode(mode) {
|
||||
state.authMode = mode;
|
||||
document.querySelectorAll(".auth-form").forEach((form) => {
|
||||
form.classList.toggle("hidden", form.dataset.mode !== mode);
|
||||
});
|
||||
const title = $("auth-title");
|
||||
const toggleBtn = $("auth-toggle");
|
||||
if (title) {
|
||||
title.textContent =
|
||||
mode === "login"
|
||||
? t("auth.loginHeading")
|
||||
: t("auth.registerHeading");
|
||||
}
|
||||
if (toggleBtn) {
|
||||
toggleBtn.textContent =
|
||||
mode === "login"
|
||||
? t("auth.switchToRegister")
|
||||
: t("auth.switchToLogin");
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAuthError(err, clearUserState) {
|
||||
if (err?.status === 401) {
|
||||
clearUserState();
|
||||
state.isAuthenticated = false;
|
||||
setAuthUI(false);
|
||||
return true;
|
||||
}
|
||||
toast(err?.message || t("toast.unexpected"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function renderWelcome() {
|
||||
const el = $("welcome-text");
|
||||
if (!el) return;
|
||||
const name =
|
||||
state.me?.displayName?.trim() ||
|
||||
state.me?.username ||
|
||||
t("auth.defaultName");
|
||||
el.textContent = t("auth.welcome", { name });
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const translationsAssetUrl = new URL(
|
||||
"../data/i18n/translations.json",
|
||||
import.meta.url,
|
||||
);
|
||||
const faqAssetUrl = new URL("../data/i18n/faq.json", import.meta.url);
|
||||
const faqBaseUrl = new URL("../data/i18n/faq/", import.meta.url);
|
||||
|
||||
let currentLang = defaultLang;
|
||||
const listeners = [];
|
||||
@@ -62,20 +62,6 @@ function validateTranslations(raw) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
function validateFaq(raw) {
|
||||
if (!isRecord(raw) || typeof raw[defaultLang] !== "string") {
|
||||
throw new Error(`Missing default FAQ language "${defaultLang}".`);
|
||||
}
|
||||
|
||||
Object.entries(raw).forEach(([lang, value]) => {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`Invalid FAQ value for "${lang}".`);
|
||||
}
|
||||
});
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
async function loadJson(url, label) {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
@@ -85,18 +71,61 @@ async function loadJson(url, label) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function loadText(url, label) {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${label}: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async function loadFaqMarkdownForLanguages(languages) {
|
||||
if (!Array.isArray(languages) || languages.length === 0) {
|
||||
throw new Error("No i18n languages available for FAQ loading.");
|
||||
}
|
||||
|
||||
const faqByLanguage = {};
|
||||
for (const lang of languages) {
|
||||
const faqUrl = new URL(`${lang}.md`, faqBaseUrl);
|
||||
try {
|
||||
faqByLanguage[lang] = await loadText(faqUrl, `faq.${lang}`);
|
||||
} catch (err) {
|
||||
if (lang === defaultLang) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = faqByLanguage[defaultLang];
|
||||
if (typeof fallback !== "string" || fallback.trim().length === 0) {
|
||||
throw new Error(`Missing default FAQ language "${defaultLang}".`);
|
||||
}
|
||||
|
||||
languages.forEach((lang) => {
|
||||
const value = faqByLanguage[lang];
|
||||
faqByLanguage[lang] =
|
||||
typeof value === "string" && value.trim().length > 0
|
||||
? value
|
||||
: fallback;
|
||||
});
|
||||
|
||||
return faqByLanguage;
|
||||
}
|
||||
|
||||
async function ensureAssetsLoaded() {
|
||||
if (assetsLoaded) return;
|
||||
if (assetsLoadingPromise) return assetsLoadingPromise;
|
||||
|
||||
assetsLoadingPromise = (async () => {
|
||||
const [translationsRaw, faqRaw] = await Promise.all([
|
||||
loadJson(translationsAssetUrl, "translations"),
|
||||
loadJson(faqAssetUrl, "faq"),
|
||||
]);
|
||||
|
||||
const translationsRaw = await loadJson(
|
||||
translationsAssetUrl,
|
||||
"translations",
|
||||
);
|
||||
translations = validateTranslations(translationsRaw);
|
||||
faqMarkdown = validateFaq(faqRaw);
|
||||
faqMarkdown = await loadFaqMarkdownForLanguages(
|
||||
Object.keys(translations),
|
||||
);
|
||||
assetsLoaded = true;
|
||||
})().finally(() => {
|
||||
assetsLoadingPromise = null;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { t } from "./i18n.js";
|
||||
import { state, getSavedUsername } from "./state.js";
|
||||
import { $, toast } from "./dom.js";
|
||||
import { state } from "./state.js";
|
||||
import { $ } from "./dom.js";
|
||||
import { configureUiRuntime } from "./ui-runtime.js";
|
||||
import {
|
||||
setAuthUI,
|
||||
setAuthMode,
|
||||
handleAuthError,
|
||||
renderWelcome,
|
||||
} from "./auth-ui.js";
|
||||
import {
|
||||
renderMySuggestions,
|
||||
renderAllSuggestions,
|
||||
@@ -25,65 +31,6 @@ import {
|
||||
openSuggestionsChangedModal,
|
||||
} from "./modals-ui.js";
|
||||
|
||||
export function setAuthUI(isAuthed) {
|
||||
const main = document.querySelector("main");
|
||||
const statusBar = document.querySelector(".status-bar");
|
||||
const authCard = $("auth-card");
|
||||
[main, statusBar].forEach((el) =>
|
||||
el?.classList.toggle("hidden", !isAuthed),
|
||||
);
|
||||
if (authCard) authCard.classList.toggle("hidden", isAuthed);
|
||||
const adminToggle = $("admin-toggle");
|
||||
if (adminToggle)
|
||||
adminToggle.classList.toggle("hidden", !isAuthed || !state.me?.isAdmin);
|
||||
if (!isAuthed) {
|
||||
const adminCard = $("admin-card");
|
||||
if (adminCard) adminCard.classList.add("hidden");
|
||||
const loginUser = $("login-username");
|
||||
const cachedUser = getSavedUsername();
|
||||
if (
|
||||
loginUser &&
|
||||
cachedUser &&
|
||||
!loginUser.dataset.userEditing &&
|
||||
!loginUser.value
|
||||
) {
|
||||
loginUser.value = cachedUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAuthMode(mode) {
|
||||
state.authMode = mode;
|
||||
document.querySelectorAll(".auth-form").forEach((form) => {
|
||||
form.classList.toggle("hidden", form.dataset.mode !== mode);
|
||||
});
|
||||
const title = $("auth-title");
|
||||
const toggleBtn = $("auth-toggle");
|
||||
if (title) {
|
||||
title.textContent =
|
||||
mode === "login"
|
||||
? t("auth.loginHeading")
|
||||
: t("auth.registerHeading");
|
||||
}
|
||||
if (toggleBtn) {
|
||||
toggleBtn.textContent =
|
||||
mode === "login"
|
||||
? t("auth.switchToRegister")
|
||||
: t("auth.switchToLogin");
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAuthError(err, clearUserState) {
|
||||
if (err?.status === 401) {
|
||||
clearUserState();
|
||||
state.isAuthenticated = false;
|
||||
setAuthUI(false);
|
||||
return true;
|
||||
}
|
||||
toast(err?.message || t("toast.unexpected"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function renderPhasePill() {
|
||||
document
|
||||
.querySelectorAll(".phase-view")
|
||||
@@ -108,19 +55,10 @@ export function renderCounts() {
|
||||
});
|
||||
}
|
||||
|
||||
export function renderWelcome() {
|
||||
const el = $("welcome-text");
|
||||
if (!el) return;
|
||||
const name =
|
||||
state.me?.displayName?.trim() ||
|
||||
state.me?.username ||
|
||||
t("auth.defaultName");
|
||||
el.textContent = t("auth.welcome", { name });
|
||||
}
|
||||
|
||||
export {
|
||||
buildCard,
|
||||
configureUiRuntime,
|
||||
handleAuthError,
|
||||
neutralEmoji,
|
||||
normalizeSuggestionForm,
|
||||
openConfirmModal,
|
||||
@@ -130,10 +68,13 @@ export {
|
||||
openSuggestionsChangedModal,
|
||||
renderAllSuggestions,
|
||||
renderMySuggestions,
|
||||
renderWelcome,
|
||||
renderPhaseTitles,
|
||||
renderResults,
|
||||
renderVotes,
|
||||
scoreToEmoji,
|
||||
setAuthMode,
|
||||
setAuthUI,
|
||||
syncVoteScores,
|
||||
updatePhaseNav,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user