2026-06-11 16:10:32 +08:00

736 lines
24 KiB
JavaScript

(function () {
var INVITE_CODE_KEYS = [
'invite_code',
'inviteCode',
'code',
'invitationCode',
];
var SHARE_NAME_KEYS = ['name', 'nickname', 'inviterName', 'userName'];
var SHARE_AVATAR_KEYS = ['avatar', 'avatarUrl', 'avatar_url', 'headUrl'];
var PASS_THROUGH_KEYS = [
'env',
'app_code',
'appCode',
'lang',
'downloadUrl',
'download_url',
'androidUrl',
'android_url',
'iosUrl',
'ios_url',
'url',
];
var rankRows = Array.from({ length: 30 }, function (_, index) {
return {
rank: index + 1,
name: 'hanneem',
score: '123',
};
});
var params = new URLSearchParams(window.location.search);
var state = {
inviteCode: firstParam(INVITE_CODE_KEYS),
inviterName: firstParam(SHARE_NAME_KEYS),
inviterAvatar: firstParam(SHARE_AVATAR_KEYS),
inviteLink: '',
inviteRewardStatus: null,
claiming: {},
};
function $(selector) {
return document.querySelector(selector);
}
function firstParam(keys) {
for (var i = 0; i < keys.length; i += 1) {
var value = String(params.get(keys[i]) || '').trim();
if (value) return value;
}
return '';
}
function textValue(value) {
return String(value || '').trim();
}
function readPath(source, path) {
var value = source;
for (var i = 0; i < path.length; i += 1) {
if (!value || typeof value !== 'object') return '';
value = value[path[i]];
}
return textValue(value);
}
function firstPath(source, paths) {
for (var i = 0; i < paths.length; i += 1) {
var value = readPath(source, paths[i]);
if (value) return value;
}
return '';
}
function currentLandingURL() {
return new URL('../invite-landing/', window.location.href);
}
function setInviteCodeParams(searchParams, inviteCode) {
searchParams.set('invite_code', inviteCode);
searchParams.set('inviteCode', inviteCode);
searchParams.set('code', inviteCode);
}
function buildInviteLink() {
var code = textValue(state.inviteCode);
if (!code) return '';
var url = currentLandingURL();
PASS_THROUGH_KEYS.forEach(function (key) {
var value = textValue(params.get(key));
if (value) url.searchParams.set(key, value);
});
setInviteCodeParams(url.searchParams, code);
if (state.inviterName) {
url.searchParams.set('name', state.inviterName);
url.searchParams.set('nickname', state.inviterName);
}
if (state.inviterAvatar) {
url.searchParams.set('avatar', state.inviterAvatar);
url.searchParams.set('avatar_url', state.inviterAvatar);
}
return url.toString();
}
function renderInviteShare() {
state.inviteLink = buildInviteLink();
var link = $('#inviteLink');
var code = $('#inviteCode');
if (link) link.textContent = state.inviteLink || '--';
if (code) code.textContent = state.inviteCode || '--';
}
function readOverviewPayload(payload) {
return payload && payload.user ? payload.user : payload;
}
function applyCurrentUser(payload) {
var data = readOverviewPayload(payload || {});
var profile = data.profile || data;
var inviteCode = firstPath(data, [
['invite', 'my_invite_code'],
['invite', 'myInviteCode'],
['profile', 'invite', 'my_invite_code'],
['profile', 'invite', 'myInviteCode'],
['my_invite_code'],
['myInviteCode'],
['invite_code'],
['inviteCode'],
]);
var name = firstPath(data, [
['profile', 'username'],
['profile', 'nickname'],
['username'],
['nickname'],
]);
var avatar = firstPath(data, [
['profile', 'avatar'],
['profile', 'avatar_url'],
['avatar'],
['avatar_url'],
]);
if (inviteCode) state.inviteCode = inviteCode;
if (name) state.inviterName = name;
if (avatar) state.inviterAvatar = avatar;
var inviteOverview = data.invite || profile.invite || {};
var inviteCount = inviteOverview.invite_count;
var validInviteCount = inviteOverview.valid_invite_count;
if (
inviteCount !== undefined &&
inviteCount !== null &&
$('#statInvitees')
) {
$('#statInvitees').textContent = inviteCount;
}
if (
validInviteCount !== undefined &&
validInviteCount !== null &&
$('#statEligible')
) {
$('#statEligible').textContent = validInviteCount;
}
renderInviteShare();
}
function loadCurrentUser() {
var api = window.HyAppAPI || {};
var userAPI = api.user || api.users;
var hasToken = api.getAccessToken && api.getAccessToken();
var userPromise = Promise.resolve();
if (hasToken && api.get) {
userPromise = window.HyAppAPI.get(
'/api/v1/users/me/invite-overview'
)
.then(applyCurrentUser)
.catch(function () {});
}
if (window.HyAppParams && window.HyAppParams.loadUser) {
return userPromise
.then(function () {
return window.HyAppParams.loadUser();
})
.then(function (current) {
if (current && current.user) applyCurrentUser(current.user);
});
}
if (userAPI) {
if (userAPI.overview) {
return userPromise
.then(function () {
return userAPI.overview();
})
.then(applyCurrentUser);
}
if (userAPI.me) {
return userPromise
.then(function () {
return userAPI.me();
})
.then(applyCurrentUser);
}
}
return userPromise;
}
function toast(message) {
if (window.HyToast && window.HyToast.show) {
window.HyToast.show(message);
return;
}
window.alert(message);
}
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
function formatPeople(value) {
return t('invite.peopleSuffix', '{value} people').replace(
'{value}',
value
);
}
function formatNumber(value) {
return new Intl.NumberFormat(
window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en'
).format(Number(value || 0));
}
function numberValue(value) {
var number = Number(value || 0);
return Number.isFinite(number) ? number : 0;
}
function field(source, snakeKey, camelKey) {
if (!source || typeof source !== 'object') return undefined;
if (Object.prototype.hasOwnProperty.call(source, snakeKey)) {
return source[snakeKey];
}
if (
camelKey &&
Object.prototype.hasOwnProperty.call(source, camelKey)
) {
return source[camelKey];
}
return undefined;
}
function setText(selector, value) {
var node = $(selector);
if (node) node.textContent = value;
}
function pad(value) {
return String(Math.max(0, value)).padStart(2, '0');
}
function updateCountdown() {
var now = Date.now();
var end = new Date();
end.setDate(end.getDate() + 1);
end.setHours(0, 0, 0, 0);
var diff = Math.max(0, end.getTime() - now);
var totalSeconds = Math.floor(diff / 1000);
var days = Math.floor(totalSeconds / 86400);
var hours = Math.floor((totalSeconds % 86400) / 3600);
var minutes = Math.floor((totalSeconds % 3600) / 60);
var seconds = totalSeconds % 60;
$('#days').textContent = pad(days);
$('#hours').textContent = pad(hours);
$('#minutes').textContent = pad(minutes);
$('#seconds').textContent = pad(seconds);
}
function renderRank() {
var list = $('#rankList');
list.textContent = '';
rankRows.forEach(function (item) {
var row = document.createElement('article');
var rank = document.createElement('span');
var medal = document.createElement('img');
var avatar = document.createElement('img');
var name = document.createElement('strong');
var score = document.createElement('span');
row.className = 'rank-row';
rank.className = 'rank-no';
medal.className = 'rank-medal';
avatar.className = 'rank-avatar';
name.className = 'rank-name';
score.className = 'rank-score';
if (item.rank <= 3) {
medal.src = './assets/medal-' + item.rank + '.png';
medal.alt = 'Rank ' + item.rank;
rank.textContent = '';
} else {
medal.alt = '';
rank.textContent = item.rank;
}
avatar.src = './assets/rank-avatar.png';
avatar.alt = '';
name.textContent = item.name;
score.textContent = formatPeople(item.score);
row.appendChild(rank);
row.appendChild(
item.rank <= 3 ? medal : document.createElement('span')
);
row.appendChild(avatar);
row.appendChild(name);
row.appendChild(score);
list.appendChild(row);
});
}
function normalizeInviteRewardStatus(payload) {
if (!payload || typeof payload !== 'object') return null;
return payload.status && typeof payload.status === 'object'
? payload.status
: payload;
}
function claimKey(rewardType, tierID) {
return rewardType + ':' + tierID;
}
function tierID(tier) {
return numberValue(field(tier, 'tier_id', 'tierId'));
}
function tierRewardType(tier) {
return String(field(tier, 'reward_type', 'rewardType') || '');
}
function tierName(tier, index, rewardType) {
return (
textValue(field(tier, 'tier_name', 'tierName')) ||
textValue(field(tier, 'tier_code', 'tierCode')) ||
(rewardType === 'recharge'
? t('invite.rechargeMilestone', 'Recharge milestone {index}')
: t(
'invite.validInviteMilestone',
'Invitation milestone {index}'
)
).replace('{index}', index + 1)
);
}
function thresholdValue(tier, rewardType) {
return rewardType === 'valid_invite'
? numberValue(
field(
tier,
'threshold_valid_invite_count',
'thresholdValidInviteCount'
)
)
: numberValue(
field(tier, 'threshold_coin_amount', 'thresholdCoinAmount')
);
}
function rewardCoinAmount(tier) {
return numberValue(
field(tier, 'reward_coin_amount', 'rewardCoinAmount')
);
}
function tierClaimed(tier) {
return Boolean(field(tier, 'claimed', 'claimed'));
}
function tierReached(tier) {
return Boolean(field(tier, 'reached', 'reached'));
}
function tierClaimable(tier, status) {
var rewardType = tierRewardType(tier);
var progress = (status && field(status, 'progress', 'progress')) || {};
if (
!Boolean(field(status || {}, 'enabled', 'enabled')) ||
tierClaimed(tier)
) {
return false;
}
if (rewardType === 'recharge') {
return (
tierID(tier) ===
numberValue(
field(
progress,
'recharge_claimable_tier_id',
'rechargeClaimableTierId'
)
)
);
}
return Boolean(field(tier, 'claimable', 'claimable'));
}
function taskDescription(tier, rewardType) {
var threshold = formatNumber(thresholdValue(tier, rewardType));
var reward = formatNumber(rewardCoinAmount(tier));
if (rewardType === 'valid_invite') {
return t(
'invite.validInviteTaskDescription',
'Invite {threshold} valid friends, reward {reward} coins'
)
.replace('{threshold}', threshold)
.replace('{reward}', reward);
}
return t(
'invite.rechargeTaskDescription',
'Invitees recharge {threshold} coins, reward {reward} coins'
)
.replace('{threshold}', threshold)
.replace('{reward}', reward);
}
function emptyTaskNode(message) {
var node = document.createElement('p');
node.className = 'task-empty';
node.textContent = message;
return node;
}
function buildClaimButton(tier, status) {
var rewardType = tierRewardType(tier);
var id = tierID(tier);
var key = claimKey(rewardType, id);
var claimed = tierClaimed(tier);
var ready = tierClaimable(tier, status);
var button = document.createElement('button');
button.className = 'claim-button';
if (ready) button.classList.add('is-ready');
button.type = 'button';
button.disabled = !ready || Boolean(state.claiming[key]);
button.textContent = state.claiming[key]
? t('invite.claiming', 'Claiming')
: claimed
? t('invite.claimed', 'Claimed')
: t('invite.claim', 'Claim');
button.addEventListener('click', function () {
claimInviteReward(tier);
});
return button;
}
function renderRewardTier(container, tier, index, status) {
var rewardType = tierRewardType(tier);
var article = document.createElement('article');
var content = document.createElement('div');
var title = document.createElement('h3');
var desc = document.createElement('p');
article.className =
rewardType === 'recharge' ? 'milestone' : 'quota-card';
article.classList.toggle('is-done', tierClaimed(tier));
article.classList.toggle(
'is-current',
tierReached(tier) && !tierClaimed(tier)
);
title.textContent = tierName(tier, index, rewardType);
desc.textContent = taskDescription(tier, rewardType);
content.appendChild(title);
content.appendChild(desc);
article.appendChild(content);
article.appendChild(buildClaimButton(tier, status));
container.appendChild(article);
}
function renderTaskList(selector, status, rewardType) {
var container = $(selector);
if (!container) return;
container.textContent = '';
var tiers = ((status && field(status, 'tiers', 'tiers')) || [])
.filter(function (tier) {
return (
tierRewardType(tier) === rewardType &&
String(field(tier, 'status', 'status') || 'active') !==
'inactive'
);
})
.sort(function (left, right) {
var sortLeft = numberValue(
field(left, 'sort_order', 'sortOrder')
);
var sortRight = numberValue(
field(right, 'sort_order', 'sortOrder')
);
if (sortLeft === sortRight) {
return (
thresholdValue(left, rewardType) -
thresholdValue(right, rewardType)
);
}
return sortLeft - sortRight;
});
if (!tiers.length) {
container.appendChild(
emptyTaskNode(t('invite.noRewardTasks', 'No reward tasks'))
);
return;
}
tiers.forEach(function (tier, index) {
renderRewardTier(container, tier, index, status);
});
}
function grantedRewardTotal(status) {
return ((status && field(status, 'claims', 'claims')) || []).reduce(
function (total, claim) {
if (
String(field(claim, 'status', 'status') || '') !== 'granted'
) {
return total;
}
return (
total +
numberValue(
field(claim, 'reward_coin_amount', 'rewardCoinAmount')
)
);
},
0
);
}
function renderInviteActivityReward(status) {
state.inviteRewardStatus = normalizeInviteRewardStatus(status);
var current = state.inviteRewardStatus || {};
var progress = field(current, 'progress', 'progress') || {};
if (state.inviteRewardStatus) {
setText(
'#statTotalRecharged',
formatNumber(
field(
progress,
'total_recharge_coin_amount',
'totalRechargeCoinAmount'
)
)
);
setText(
'#statEligible',
formatNumber(
field(progress, 'valid_invite_count', 'validInviteCount')
)
);
setText(
'#statMyRewards',
formatNumber(grantedRewardTotal(current))
);
}
renderTaskList('#transportTimeline', current, 'recharge');
renderTaskList('#quotaList', current, 'valid_invite');
}
function loadInviteActivityReward() {
var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward;
if (!api || !api.status || !window.HyAppAPI.getAccessToken()) {
renderInviteActivityReward(null);
return Promise.resolve();
}
return api
.status()
.then(function (status) {
renderInviteActivityReward(status);
})
.catch(function () {
renderInviteActivityReward(null);
});
}
function claimInviteReward(tier) {
var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward;
var rewardType = tierRewardType(tier);
var id = tierID(tier);
var key = claimKey(rewardType, id);
if (!api || !api.claim || state.claiming[key]) return;
if (!tierClaimable(tier, state.inviteRewardStatus)) {
toast(t('invite.notAvailable', 'Not available yet'));
return;
}
state.claiming[key] = true;
renderInviteActivityReward(state.inviteRewardStatus);
api.claim({
reward_type: rewardType,
tier_id: id,
command_id:
'invite_reward_' +
rewardType +
'_' +
id +
'_' +
Date.now() +
'_' +
Math.random().toString(36).slice(2),
})
.then(function (payload) {
delete state.claiming[key];
renderInviteActivityReward(
normalizeInviteRewardStatus(payload && payload.status)
);
toast(t('invite.claimSuccess', 'Claimed'));
})
.catch(function (error) {
delete state.claiming[key];
renderInviteActivityReward(state.inviteRewardStatus);
toast(
(error && error.message) ||
t('invite.claimFailed', 'Claim failed')
);
});
}
function setTab(tabName) {
var monthly = $('#monthlyTask');
var isQuota = tabName === 'quota';
monthly.classList.toggle('is-quota', isQuota);
document.querySelectorAll('.task-tab').forEach(function (button) {
var active = button.getAttribute('data-tab') === tabName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
$('#transportPane').classList.toggle('is-active', !isQuota);
$('#quotaPane').classList.toggle('is-active', isQuota);
$('#rankPanel').hidden = !isQuota;
}
function copyTextFrom(selector) {
var node = $(selector);
var text = node ? node.textContent.trim() : '';
if (!text || text === '--') {
toast(t('invite.copyFailed', 'Copy failed'));
return;
}
function fallbackCopy() {
var input = document.createElement('textarea');
input.value = text;
input.setAttribute('readonly', 'readonly');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '0';
document.body.appendChild(input);
input.select();
var ok = false;
try {
ok = document.execCommand('copy');
} catch (error) {
ok = false;
}
document.body.removeChild(input);
toast(
ok
? t('invite.copied', 'Copied')
: t('invite.copyFailed', 'Copy failed')
);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(text)
.then(function () {
toast(t('invite.copied', 'Copied'));
})
.catch(function () {
fallbackCopy();
});
return;
}
fallbackCopy();
}
function copyInviteLink() {
copyTextFrom('#inviteLink');
}
function copyInviteCode() {
copyTextFrom('#inviteCode');
}
function bindEvents() {
document.querySelectorAll('.task-tab').forEach(function (button) {
button.addEventListener('click', function () {
setTab(button.getAttribute('data-tab') || 'transport');
});
});
$('#copyLinkButton').addEventListener('click', copyInviteLink);
$('#copyCodeButton').addEventListener('click', copyInviteCode);
document
.querySelectorAll('.copy-task-button')
.forEach(function (button) {
button.addEventListener('click', copyInviteLink);
});
}
function init() {
renderInviteShare();
renderRank();
updateCountdown();
setInterval(updateCountdown, 1000);
bindEvents();
renderInviteActivityReward(null);
Promise.all([
loadCurrentUser().catch(function () {
renderInviteShare();
}),
loadInviteActivityReward(),
]);
}
window.addEventListener('hyapp:i18n-ready', function () {
renderRank();
renderInviteActivityReward(state.inviteRewardStatus);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();