2026-04-27 19:11:53 +08:00

952 lines
26 KiB
JavaScript

const query = new URLSearchParams(window.location.search);
const LOCAL_INVITE_API_BASE = "http://127.0.0.1:2900/";
const LOCAL_PROXY_API_BASE = "/api/";
const LOCAL_FILE_PROXY_API_BASE = "http://127.0.0.1:8800/api/";
const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/";
const ONLINE_INVITE_API_PREFIX = "/go";
const DEFAULT_AVATAR = "./assets/avatar-sample.png";
const DEFAULT_RESET_PERIOD_MS = 30 * 86400 * 1000;
const DEFAULT_TASK_NOTES = {
transport: "(Only the highest-level bonuses may be awarded)",
quota: "(You'll receive a reward upon reaching each level)",
};
const DEFAULTS = {
view: "invite",
inviteLink: "",
inviteCode: "",
invitees: "0",
eligibleVoters: "0",
rechargeTotal: "0",
myRewards: "0",
friendName: "Name",
downloadUrl: "",
inviterBindReward: 0,
rechargeReward: 0,
giftReward: 0,
};
const FALLBACK_TASKS = {
transport: [],
quota: [],
};
const state = {
view: resolveInitialView(),
apiBase: resolveAPIBase(),
apiPrefix: resolveAPIPrefix(),
authorization: resolveAuthorization(),
landingCode: resolveLandingCode(),
inviteLink: query.get("inviteLink") || DEFAULTS.inviteLink,
inviteCode: query.get("inviteCode") || DEFAULTS.inviteCode,
invitees: query.get("invitees") || DEFAULTS.invitees,
eligibleVoters: query.get("eligible") || DEFAULTS.eligibleVoters,
rechargeTotal: query.get("recharge") || DEFAULTS.rechargeTotal,
myRewards: query.get("rewards") || DEFAULTS.myRewards,
friendName: query.get("friendName") || DEFAULTS.friendName,
inviterAvatar: DEFAULT_AVATAR,
downloadUrl: query.get("downloadUrl") || DEFAULTS.downloadUrl,
inviterBindReward: query.get("inviterReward") || DEFAULTS.inviterBindReward,
rechargeReward: query.get("rechargeReward") || DEFAULTS.rechargeReward,
giftReward: query.get("giftReward") || DEFAULTS.giftReward,
taskSet: normalizeTaskSet(query.get("taskSet") || "quota"),
tasks: {
transport: cloneTasks(FALLBACK_TASKS.transport),
quota: cloneTasks(FALLBACK_TASKS.quota),
},
rankItems: [],
serverOffsetMs: 0,
countdownEndAtMs: Date.now() + DEFAULT_RESET_PERIOD_MS,
countdownTimer: null,
pendingClaimRuleId: 0,
};
const els = {
views: [...document.querySelectorAll("[data-view]")],
inviteLinkText: document.getElementById("inviteLinkText"),
inviteCodeText: document.getElementById("inviteCodeText"),
statInvitees: document.getElementById("statInvitees"),
statEligible: document.getElementById("statEligible"),
statRecharge: document.getElementById("statRecharge"),
statRewards: document.getElementById("statRewards"),
taskList: document.getElementById("taskList"),
taskNote: document.getElementById("taskNote"),
taskTabs: [...document.querySelectorAll("[data-task-set]")],
navButtons: [...document.querySelectorAll("[data-nav-view]")],
copyButtons: [...document.querySelectorAll("[data-copy-target]")],
rankList: document.getElementById("rankList"),
giftTitle: document.getElementById("giftTitle"),
giftAvatarImage: document.getElementById("giftAvatarImage"),
downloadButton: document.getElementById("downloadButton"),
inviteRewardAmounts: [
...document.querySelectorAll('.reward-panel .reward-amount'),
],
giftRewardAmount: document.querySelector('.gift-panel .reward-amount'),
toast: document.getElementById("pageToast"),
countdown: {
days: document.querySelector('[data-countdown="days"]'),
hours: document.querySelector('[data-countdown="hours"]'),
minutes: document.querySelector('[data-countdown="minutes"]'),
seconds: document.querySelector('[data-countdown="seconds"]'),
},
};
function normalizeView(value) {
if (value === "rank" || value === "gift") {
return value;
}
return "invite";
}
function normalizeTaskSet(value) {
return value === "transport" ? "transport" : "quota";
}
function trimValue(value) {
return value == null ? "" : String(value).trim();
}
function resolveLandingCode() {
return trimValue(
query.get("code") ||
query.get("landingCode") ||
query.get("invitationCode") ||
query.get("inviteCode"),
);
}
function hasAuthorizationInput() {
return Boolean(
trimValue(query.get("authorization")) ||
trimValue(query.get("Authorization")) ||
trimValue(query.get("token")) ||
trimValue(query.get("auth")) ||
(typeof window.__APP_INVITE_AUTHORIZATION__ === "string" &&
trimValue(window.__APP_INVITE_AUTHORIZATION__)) ||
(typeof window.__APP_INVITE_TOKEN__ === "string" && trimValue(window.__APP_INVITE_TOKEN__)),
);
}
function resolveInitialView() {
const explicitView = trimValue(query.get("view"));
if (explicitView) {
return normalizeView(explicitView);
}
if (resolveLandingCode() && !hasAuthorizationInput()) {
return "gift";
}
return DEFAULTS.view;
}
function normalizeBaseURL(base) {
const value = trimValue(base);
if (!value) {
return resolveDefaultAPIBase();
}
return value.endsWith("/") ? value : `${value}/`;
}
function isLocalHost(hostname = window.location.hostname) {
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}
function isOnlineAPIBase(base) {
try {
return new URL(base, window.location.href).origin === new URL(ONLINE_INVITE_API_BASE).origin;
} catch {
return false;
}
}
function isAbsoluteURL(value) {
return /^https?:\/\//i.test(trimValue(value));
}
function resolveDefaultAPIBase() {
if (window.location.protocol === "file:") {
return LOCAL_FILE_PROXY_API_BASE;
}
if (window.location.protocol === "http:" && isLocalHost()) {
return LOCAL_PROXY_API_BASE;
}
if (window.location.protocol === "https:" && !isLocalHost()) {
return ONLINE_INVITE_API_BASE;
}
return LOCAL_INVITE_API_BASE;
}
function resolveAPIBase() {
const queryBase = trimValue(query.get("apiBase"));
if (queryBase) {
return normalizeBaseURL(queryBase);
}
if (typeof window.__APP_INVITE_API_BASE__ === "string") {
return normalizeBaseURL(window.__APP_INVITE_API_BASE__);
}
return normalizeBaseURL(resolveDefaultAPIBase());
}
function resolveAPIPrefix() {
const queryPrefix = trimValue(query.get("apiPrefix"));
if (queryPrefix) {
return queryPrefix;
}
if (typeof window.__APP_INVITE_API_PREFIX__ === "string") {
return trimValue(window.__APP_INVITE_API_PREFIX__);
}
return isOnlineAPIBase(resolveAPIBase()) ? ONLINE_INVITE_API_PREFIX : "";
}
function resolveAuthorization() {
const candidates = [
query.get("authorization"),
query.get("Authorization"),
query.get("token"),
query.get("auth"),
typeof window.__APP_INVITE_AUTHORIZATION__ === "string"
? window.__APP_INVITE_AUTHORIZATION__
: "",
typeof window.__APP_INVITE_TOKEN__ === "string" ? window.__APP_INVITE_TOKEN__ : "",
];
const value = candidates.find((item) => trimValue(item));
if (!value) {
return "";
}
const normalized = trimValue(value);
if (/^bearer\s+/i.test(normalized)) {
return normalized;
}
return `Bearer ${normalized}`;
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (match) => {
const entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
return entityMap[match] || match;
});
}
function cloneTasks(list) {
return list.map((item) => ({ ...item }));
}
function showToast(text) {
if (!text) {
return;
}
els.toast.textContent = text;
els.toast.classList.add("show");
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
els.toast.classList.remove("show");
}, 1800);
}
function loadImage(image) {
if (!image || image.getAttribute("src")) {
return;
}
const src = image.getAttribute("data-src");
if (src) {
image.setAttribute("src", src);
}
}
function setImageSource(image, src, fallbackSrc = "") {
if (!(image instanceof HTMLImageElement)) {
return;
}
const nextSrc = trimValue(src) || fallbackSrc;
if (!nextSrc) {
return;
}
image.setAttribute("src", nextSrc);
image.setAttribute("data-src", nextSrc);
}
function hydrateViewAssets(view) {
const targets = {
invite: [
document.getElementById("inviteHeroImage"),
...document.querySelectorAll('.view[data-view="invite"] .lazy-image'),
],
rank: [
...document.querySelectorAll('.view[data-view="rank"] [data-badge-src]'),
...document.querySelectorAll('.view[data-view="rank"] [data-avatar-src]'),
],
gift: [
document.getElementById("giftBgImage"),
document.getElementById("giftAvatarImage"),
document.getElementById("phoneHomeImage"),
document.getElementById("phoneGiftImage"),
...document.querySelectorAll('.view[data-view="gift"] .lazy-image'),
],
};
(targets[view] || []).forEach((item) => {
if (item instanceof HTMLImageElement) {
loadImage(item);
}
});
}
function setView(view) {
state.view = normalizeView(view);
els.views.forEach((section) => {
section.classList.toggle("active", section.getAttribute("data-view") === state.view);
});
hydrateViewAssets(state.view);
const url = new URL(window.location.href);
if (state.view === DEFAULTS.view) {
url.searchParams.delete("view");
} else {
url.searchParams.set("view", state.view);
}
window.history.replaceState({}, "", url.toString());
}
function formatMetric(value) {
const raw = trimValue(value);
if (!raw) {
return "--";
}
if (/^-?\d+(\.\d+)?$/.test(raw)) {
return new Intl.NumberFormat("en-US").format(Number(raw));
}
return raw;
}
function formatSignedReward(value) {
const raw = trimValue(value);
if (!raw) {
return "+0";
}
if (raw.startsWith("+")) {
return raw;
}
return `+${formatMetric(raw)}`;
}
function renderStats() {
els.inviteLinkText.textContent = trimValue(state.inviteLink) || "--";
els.inviteCodeText.textContent = trimValue(state.inviteCode) || "--";
els.statInvitees.textContent = formatMetric(state.invitees);
els.statEligible.textContent = formatMetric(state.eligibleVoters);
els.statRecharge.textContent = formatMetric(state.rechargeTotal);
els.statRewards.textContent = formatMetric(state.myRewards);
els.giftTitle.innerHTML = `Your friend [${escapeHtml(state.friendName)}]<br />has invited you to join`;
setImageSource(els.giftAvatarImage, state.inviterAvatar, DEFAULT_AVATAR);
if (els.inviteRewardAmounts[0]) {
els.inviteRewardAmounts[0].textContent = formatSignedReward(state.inviterBindReward);
}
if (els.inviteRewardAmounts[1]) {
els.inviteRewardAmounts[1].textContent = formatSignedReward(state.rechargeReward);
}
if (els.giftRewardAmount) {
els.giftRewardAmount.textContent = formatSignedReward(state.giftReward);
}
}
function renderTasks() {
const current = state.tasks[state.taskSet] || cloneTasks(FALLBACK_TASKS[state.taskSet]);
els.taskTabs.forEach((tab) => {
const active = tab.getAttribute("data-task-set") === state.taskSet;
tab.classList.toggle("active", active);
tab.classList.toggle("passive", !active);
});
if (current.length === 0) {
els.taskList.innerHTML = `
<article class="task-card">
<div>
<p class="task-title">No tasks yet</p>
<p class="task-desc">Task data will appear after the activity config is loaded.</p>
</div>
<button type="button" class="claim-button is-pending disabled" disabled>
Incomplete
</button>
</article>
`;
els.taskNote.textContent = DEFAULT_TASK_NOTES[state.taskSet] || "";
return;
}
els.taskList.innerHTML = current
.map((item) => {
const isLoading = Number(item.ruleId) > 0 && state.pendingClaimRuleId === Number(item.ruleId);
const disabled = item.disabled || isLoading || Number(item.ruleId) <= 0;
const buttonLabel = isLoading ? "Loading..." : item.buttonLabel;
let buttonStateClass = " is-pending";
if (isLoading) {
buttonStateClass = " is-loading";
} else if (item.claimed) {
buttonStateClass = " is-claimed";
} else if (item.achieved && !item.claimed) {
buttonStateClass = " is-claimable";
}
return `
<article class="task-card">
<div>
<p class="task-title">${escapeHtml(item.title)}</p>
<p class="task-desc">${escapeHtml(item.desc)}</p>
</div>
<button
type="button"
class="claim-button${buttonStateClass}${disabled ? " disabled" : ""}"
data-rule-id="${Number(item.ruleId) || 0}"
${disabled ? "disabled" : ""}
>
${escapeHtml(buttonLabel)}
</button>
</article>
`;
})
.join("");
els.taskNote.textContent = DEFAULT_TASK_NOTES[state.taskSet] || "";
}
function renderRankList() {
const list = state.rankItems;
if (!Array.isArray(list) || list.length === 0) {
els.rankList.innerHTML = `
<article
class="rank-row"
style="background-image:url('./assets/rank-row.svg')"
>
<div class="rank-badge">--</div>
<div class="avatar">
<img src="${DEFAULT_AVATAR}" alt="" />
</div>
<div class="rank-name">No ranking data yet</div>
<div class="rank-score">0 people</div>
</article>
`;
return;
}
els.rankList.innerHTML = list.map((item) => {
const badgeMap = {
1: "./assets/rank-top1.png",
2: "./assets/rank-top2.png",
3: "./assets/rank-top3.png",
};
const badge = badgeMap[item.rank];
return `
<article
class="rank-row"
style="background-image:url('./assets/rank-row.svg')"
>
<div class="rank-badge">
${
badge
? `<img src="${badge}" alt="Rank ${item.rank}" />`
: escapeHtml(String(item.rank))
}
</div>
<div class="avatar">
<img src="${escapeHtml(item.avatar || DEFAULT_AVATAR)}" alt="${escapeHtml(item.name)}" />
</div>
<div class="rank-name">${escapeHtml(item.name)}</div>
<div class="rank-score">${escapeHtml(item.score)}</div>
</article>
`;
}).join("");
}
function pad(number) {
return String(number).padStart(2, "0");
}
function updateCountdown() {
const now = Date.now() + state.serverOffsetMs;
const diff = Math.max(0, Number(state.countdownEndAtMs || 0) - now);
const seconds = Math.floor(diff / 1000);
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
els.countdown.days.textContent = pad(days);
els.countdown.hours.textContent = pad(hours);
els.countdown.minutes.textContent = pad(minutes);
els.countdown.seconds.textContent = pad(secs);
}
function setupCountdown() {
updateCountdown();
state.countdownTimer = window.setInterval(updateCountdown, 1000);
}
async function copyText(text) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const input = document.createElement("textarea");
input.value = text;
input.setAttribute("readonly", "");
input.style.position = "absolute";
input.style.left = "-9999px";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
function extractErrorMessage(payload) {
const candidates = [
payload?.errorMsg,
payload?.message,
payload?.errorCodeName,
payload?.code,
];
return candidates.find((item) => trimValue(item)) || "";
}
async function requestJSON(path, options = {}) {
const { method = "GET", headers = {}, body, searchParams } = options;
const url = buildAPIURL(path, searchParams);
const response = await fetch(url.toString(), {
method,
headers: {
Accept: "application/json",
...headers,
},
body,
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok || payload?.status === false) {
const message = extractErrorMessage(payload) || `Request failed (${response.status})`;
throw new Error(message);
}
return payload?.body || payload?.data || payload || null;
}
function joinURLPath(...parts) {
return `/${parts
.map((part) => trimValue(part).replace(/^\/+|\/+$/g, ""))
.filter(Boolean)
.join("/")}`;
}
function buildAPIURL(path, searchParams = {}) {
const apiPath = joinURLPath(state.apiPrefix, path);
let url;
if (isAbsoluteURL(state.apiBase)) {
url = new URL(apiPath.replace(/^\/+/, ""), state.apiBase);
} else {
url = new URL(joinURLPath(state.apiBase, apiPath), window.location.href);
}
Object.entries(searchParams || {}).forEach(([key, value]) => {
const raw = trimValue(value);
if (raw) {
url.searchParams.set(key, raw);
}
});
return url;
}
function normalizeTaskList(list, taskSet) {
if (!Array.isArray(list) || list.length === 0) {
return cloneTasks(FALLBACK_TASKS[taskSet]);
}
return list.map((item, index) => {
const threshold = Number(item?.threshold || 0);
const goldAmount = Number(item?.goldAmount || 0);
const achieved = Boolean(item?.achieved);
const claimed = Boolean(item?.claimed);
const desc =
taskSet === "quota"
? `Invite ${formatMetric(threshold)} friends to claim ${formatMetric(goldAmount)} coins`
: `Reach ${formatMetric(threshold)} recharge coins to claim ${formatMetric(goldAmount)} coins`;
return {
title: `Milestone ${index + 1}`,
desc,
buttonLabel: claimed ? "Claimed" : achieved ? "Claim" : "Incomplete",
disabled: claimed || !achieved,
claimed,
achieved,
ruleId: Number(item?.ruleId) || 0,
};
});
}
function normalizeRankList(list) {
if (!Array.isArray(list)) {
return [];
}
return list.map((item, index) => {
const rank = Number(item?.rank || index + 1);
const inviteCount = Number(item?.inviteCount || 0);
const rewardGoldCoins = Number(item?.rewardGoldCoins || 0);
const userName = trimValue(item?.nickname) || `User ${trimValue(item?.userId) || rank}`;
const scoreParts = [`${formatMetric(inviteCount)} people`];
if (rewardGoldCoins > 0) {
scoreParts.push(`${formatMetric(rewardGoldCoins)} coins`);
}
return {
rank,
name: userName,
avatar: trimValue(item?.avatar) || DEFAULT_AVATAR,
score: scoreParts.join(" · "),
};
});
}
function markTaskClaimed(ruleId) {
["quota", "transport"].forEach((taskSet) => {
state.tasks[taskSet] = (state.tasks[taskSet] || []).map((item) => {
if (Number(item.ruleId) !== Number(ruleId)) {
return item;
}
return {
...item,
claimed: true,
disabled: true,
buttonLabel: "Claimed",
};
});
});
}
function applyRankData(body) {
if (!body || typeof body !== "object") {
state.rankItems = [];
return;
}
state.rankItems = normalizeRankList(body.items);
}
function applyHomeData(body) {
if (!body || typeof body !== "object") {
return;
}
const stats = body.stats || {};
if (body.serverTimeMs) {
state.serverOffsetMs = Number(body.serverTimeMs) - Date.now();
}
if (body.periodEndAtMs) {
state.countdownEndAtMs = Number(body.periodEndAtMs);
}
state.inviteLink = trimValue(body.inviteLink) || state.inviteLink;
state.inviteCode = trimValue(body.inviteCode) || state.inviteCode;
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
state.invitees = stats.totalInvitedUsers ?? state.invitees;
state.eligibleVoters = stats.totalValidUsers ?? state.eligibleVoters;
state.rechargeTotal = stats.totalRechargeCoins ?? state.rechargeTotal;
state.myRewards = stats.rewardGoldCoins ?? state.myRewards;
const inviterBindReward = body.inviterBindReward?.goldAmount;
if (inviterBindReward != null) {
state.inviterBindReward = inviterBindReward;
}
const rechargeReward =
body.inviteeRechargeRules?.[0]?.goldAmount ??
body.inviterTotalRechargeTasks?.[0]?.goldAmount;
if (rechargeReward != null) {
state.rechargeReward = rechargeReward;
}
state.tasks.quota = normalizeTaskList(body.inviterValidCountTasks, "quota");
state.tasks.transport = normalizeTaskList(body.inviterTotalRechargeTasks, "transport");
const shareTitle = trimValue(body.shareTitle);
if (shareTitle) {
document.title = shareTitle;
}
}
function applyLandingData(body) {
if (!body || typeof body !== "object") {
return;
}
state.friendName = trimValue(body.inviterNickname) || state.friendName;
state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR;
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward;
const inviteCode = trimValue(body.inviteCode);
if (inviteCode) {
state.landingCode = inviteCode;
state.inviteCode = inviteCode;
}
const shareTitle = trimValue(body.shareTitle);
if (shareTitle) {
document.title = shareTitle;
}
}
function buildDownloadURL(downloadUrl, inviteCode) {
const rawURL = trimValue(downloadUrl);
const code = trimValue(inviteCode);
if (!rawURL || !code) {
return rawURL;
}
try {
const url = new URL(rawURL, window.location.href);
if (url.hostname.includes("play.google.com")) {
const referrer = new URLSearchParams(url.searchParams.get("referrer") || "");
setPlayInstallReferrer(referrer, code);
removeTopLevelInviteCodeParams(url.searchParams);
url.searchParams.set("referrer", referrer.toString());
return url.toString();
}
setInviteCodeParams(url.searchParams, code);
return url.toString();
} catch {
const separator = rawURL.includes("?") ? "&" : "?";
const params = new URLSearchParams();
setInviteCodeParams(params, code);
return `${rawURL}${separator}${params.toString()}`;
}
}
function setInviteCodeParams(searchParams, code) {
searchParams.set("code", code);
searchParams.set("invitationCode", code);
searchParams.set("inviteCode", code);
}
function setPlayInstallReferrer(searchParams, code) {
searchParams.set("invite_code", code);
searchParams.set("source", "share");
}
function removeTopLevelInviteCodeParams(searchParams) {
["code", "invitationCode", "inviteCode", "invite_code"].forEach((name) => {
searchParams.delete(name);
});
}
async function fetchHomeData() {
if (!state.authorization) {
return false;
}
const body = await requestJSON("/app/h5/invite-campaign/home", {
headers: {
Authorization: state.authorization,
},
});
applyHomeData(body);
return true;
}
async function fetchRankData() {
if (!state.authorization) {
return false;
}
const body = await requestJSON("/app/h5/invite-campaign/rank", {
headers: {
Authorization: state.authorization,
},
searchParams: {
limit: 50,
},
});
applyRankData(body);
return true;
}
async function fetchLandingData() {
if (!state.landingCode) {
return false;
}
const body = await requestJSON("/public/h5/invite-campaign/landing", {
searchParams: {
code: state.landingCode,
},
});
applyLandingData(body);
return true;
}
async function refreshPageData() {
try {
if (state.view === "gift") {
await fetchLandingData();
} else {
const hasHome = await fetchHomeData();
if (hasHome) {
await fetchRankData();
}
}
} catch (error) {
const message = trimValue(error?.message);
if (message) {
showToast(message);
}
} finally {
renderStats();
renderTasks();
renderRankList();
}
}
async function claimTask(ruleId) {
if (!ruleId) {
return;
}
if (!state.authorization) {
showToast("Token is missing");
return;
}
state.pendingClaimRuleId = ruleId;
renderTasks();
try {
const body = await requestJSON("/app/h5/invite-campaign/tasks/claim", {
method: "POST",
headers: {
Authorization: state.authorization,
"Content-Type": "application/json",
},
body: JSON.stringify({ ruleId }),
});
markTaskClaimed(ruleId);
showToast(body?.alreadyClaimed ? "Already claimed" : "Claim succeeded");
await fetchHomeData().catch(() => {});
await fetchRankData().catch(() => {});
} catch (error) {
showToast(trimValue(error?.message) || "Claim failed");
} finally {
state.pendingClaimRuleId = 0;
renderStats();
renderTasks();
renderRankList();
}
}
function bindEvents() {
els.taskTabs.forEach((button) => {
button.addEventListener("click", () => {
state.taskSet = normalizeTaskSet(button.getAttribute("data-task-set"));
renderTasks();
});
});
els.navButtons.forEach((button) => {
button.addEventListener("click", () => {
const nextView = button.getAttribute("data-nav-view") || "invite";
setView(nextView);
if (nextView === "rank") {
fetchRankData()
.then(renderRankList)
.catch((error) => showToast(trimValue(error?.message) || "Rank load failed"));
}
});
});
els.copyButtons.forEach((button) => {
button.addEventListener("click", async () => {
const target = button.getAttribute("data-copy-target");
const value = target === "invite-code" ? state.inviteCode : state.inviteLink;
try {
await copyText(trimValue(value));
showToast(target === "invite-code" ? "Invitation code copied" : "Invitation link copied");
} catch (error) {
showToast("Copy failed");
}
});
});
els.taskList.addEventListener("click", (event) => {
const button = event.target.closest("[data-rule-id]");
if (!(button instanceof HTMLButtonElement)) {
return;
}
const ruleId = Number(button.getAttribute("data-rule-id") || 0);
if (!ruleId || button.disabled) {
return;
}
claimTask(ruleId);
});
els.downloadButton.addEventListener("click", () => {
if (!state.downloadUrl) {
showToast("Download link is not configured");
return;
}
window.location.href = buildDownloadURL(state.downloadUrl, state.landingCode || state.inviteCode);
});
}
async function init() {
renderStats();
renderTasks();
renderRankList();
bindEvents();
setView(state.view);
setupCountdown();
await refreshPageData();
}
init();