${
badge
? `

`
: escapeHtml(String(item.rank))
}
${escapeHtml(item.name)}
${escapeHtml(item.score)}
`;
}).join("");
}
function parseEndAt() {
const raw = query.get("endAt") || DEFAULTS.endAt;
if (!raw) {
return null;
}
const timestamp = Date.parse(raw);
if (Number.isNaN(timestamp)) {
return null;
}
return timestamp;
}
function pad(number) {
return String(number).padStart(2, "0");
}
function updateCountdown() {
const endAt = parseEndAt();
if (!endAt) {
return;
}
const diff = Math.max(0, endAt - Date.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() {
const endAt = parseEndAt();
if (!endAt) {
return;
}
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 = new URL(path, state.apiBase);
Object.entries(searchParams || {}).forEach(([key, value]) => {
const raw = trimValue(value);
if (raw) {
url.searchParams.set(key, raw);
}
});
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 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)} valid 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 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 applyHomeData(body) {
if (!body || typeof body !== "object") {
return;
}
const stats = body.stats || {};
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.inviterTotalRechargeTasks?.[0]?.goldAmount ??
body.inviteeRechargeRules?.[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;
}
const shareTitle = trimValue(body.shareTitle);
if (shareTitle) {
document.title = shareTitle;
}
}
async function fetchHomeData() {
if (!state.authorization) {
return false;
}
const body = await requestJSON("/go/app/h5/invite-campaign/home", {
headers: {
Authorization: state.authorization,
},
});
applyHomeData(body);
return true;
}
async function fetchLandingData() {
if (!state.landingCode) {
return false;
}
const body = await requestJSON("/go/public/h5/invite-campaign/landing", {
searchParams: {
code: state.landingCode,
},
});
applyLandingData(body);
return true;
}
async function refreshPageData() {
try {
if (state.view === "gift") {
await fetchLandingData();
} else {
await fetchHomeData();
}
} catch (error) {
const message = trimValue(error?.message);
if (message) {
showToast(message);
}
} finally {
renderStats();
renderTasks();
}
}
async function claimTask(ruleId) {
if (!ruleId) {
return;
}
if (!state.authorization) {
showToast("Token is missing");
return;
}
state.pendingClaimRuleId = ruleId;
renderTasks();
try {
const body = await requestJSON("/go/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(() => {});
} catch (error) {
showToast(trimValue(error?.message) || "Claim failed");
} finally {
state.pendingClaimRuleId = 0;
renderStats();
renderTasks();
}
}
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", () => {
setView(button.getAttribute("data-nav-view") || "invite");
});
});
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 = state.downloadUrl;
});
}
async function init() {
renderStats();
renderTasks();
renderRankList();
bindEvents();
setView(state.view);
setupCountdown();
await refreshPageData();
}
init();