2026-07-17 11:54:43 +08:00

1197 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
'use strict';
var ASSET_BASE = './huwaa/assets/layers/';
var RANK_LIMIT = 5;
var DAY_MS = 24 * 60 * 60 * 1000;
var DESIGN_HEIGHT = { history: 2309, weekly: 2309, rewards: 3200 };
var DESIGN_FRAME = {
history: '818:1999',
weekly: '818:2066',
rewards: '818:2132',
};
var params = currentParams();
var useMock = params.get('mock') === '1';
var page = document.getElementById('weekStarPage');
var stage = document.getElementById('weekStarStage');
var stageWrap = document.getElementById('weekStarStageWrap');
var tabs = all('[data-tab-target]');
var panels = all('[data-panel]');
var state = {
// Once the user switches sections, the hash is the canonical tab state. It
// must win over a stale entry `?tab=` value while the hash-query token stays intact.
tab: normalizeTab(tabFromHash() || params.get('tab')),
current: null,
weeklyEntries: [],
historyCycles: [],
loadingCurrent: true,
loadingWeekly: true,
loadingHistory: true,
currentError: '',
weeklyError: '',
historyError: '',
requestSerial: 0,
};
function all(selector, root) {
return Array.prototype.slice.call(
(root || document).querySelectorAll(selector)
);
}
function currentParams() {
if (
window.HyAppParams &&
window.HyAppParams.current &&
window.HyAppParams.current.raw
) {
return window.HyAppParams.current.raw;
}
var result = new URLSearchParams(window.location.search || '');
var hash = window.location.hash || '';
if (hash.indexOf('?') >= 0) {
new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
function (value, key) {
if (!result.has(key)) result.set(key, value);
}
);
}
return result;
}
function text(value) {
return String(
value === undefined || value === null ? '' : value
).trim();
}
function number(value, fallback) {
var result = Number(value);
return Number.isFinite(result) ? result : Number(fallback || 0);
}
function first(source, keys) {
source = source || {};
for (var index = 0; index < keys.length; index += 1) {
var value = source[keys[index]];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return '';
}
function envelope(payload) {
if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) {
return payload.data || {};
}
return payload || {};
}
function parseJSON(value) {
if (!value || typeof value !== 'string') return {};
try {
return JSON.parse(value);
} catch (error) {
return {};
}
}
function t(key, fallback, values) {
var translated =
window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
Object.keys(values || {}).forEach(function (name) {
translated = translated.replace(
new RegExp('\\{' + name + '\\}', 'g'),
String(values[name])
);
});
return translated;
}
function escapeHTML(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttr(value) {
return escapeHTML(value);
}
function resolveAssetURL(value) {
var raw = text(value);
if (!raw) return '';
if (
/^(https?:)?\/\//i.test(raw) ||
/^(data|blob):/i.test(raw) ||
raw.charAt(0) === '/' ||
raw.indexOf('./') === 0 ||
raw.indexOf('../') === 0
) {
return raw;
}
// Backend resource records may return a path relative to the API host. Resolving
// against the API base avoids silently requesting that path from this H5 directory.
try {
var base =
window.HyAppAPI && window.HyAppAPI.baseURL
? window.HyAppAPI.baseURL()
: window.location.origin + '/';
return new URL(raw, base).toString();
} catch (error) {
return '';
}
}
function formatNumber(value) {
var lang =
window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en';
return new Intl.NumberFormat(lang).format(number(value));
}
function formatScore(value) {
var raw = text(value);
if (!raw) return '0';
var numeric = Number(raw);
return Number.isFinite(numeric) ? String(numeric) : raw;
}
function pad(value) {
return String(value).padStart(2, '0');
}
function timezoneOffsetHours(cycle) {
var explicit = first(cycle, [
'utc_offset_hours',
'utcOffsetHours',
'timezone_offset_hours',
'timezoneOffsetHours',
]);
if (explicit !== '') return number(explicit, 3);
var label = text(first(cycle, ['timezone', 'time_zone', 'timeZone']));
var match = label.match(/(?:utc|gmt)\s*([+-]\d+(?:\.\d+)?)/i);
// The Figma campaign explicitly uses UTC+3. The API can override this through
// its cycle metadata; the design offset is only the fallback display contract.
return match ? number(match[1], 3) : 3;
}
function shiftedDate(value, offsetHours) {
var timestamp = number(value);
if (!timestamp) return null;
return new Date(timestamp + offsetHours * 60 * 60 * 1000);
}
function formatDateTime(value, offsetHours) {
var date = shiftedDate(value, offsetHours);
if (!date) return '—';
return [
date.getUTCFullYear(),
'-',
pad(date.getUTCMonth() + 1),
'-',
pad(date.getUTCDate()),
' ',
pad(date.getUTCHours()),
':',
pad(date.getUTCMinutes()),
].join('');
}
function formatDate(value, offsetHours) {
var date = shiftedDate(value, offsetHours);
if (!date) return '—';
return [
date.getUTCFullYear(),
'/',
pad(date.getUTCMonth() + 1),
'/',
pad(date.getUTCDate()),
].join('');
}
function formatOffset(offsetHours) {
return (offsetHours >= 0 ? '+' : '') + String(offsetHours);
}
function cycleRange(cycle) {
cycle = cycle || {};
var custom = text(
first(cycle, [
'display_time',
'displayTime',
'time_range',
'timeRange',
])
);
if (custom) return custom;
var offset = timezoneOffsetHours(cycle);
return (
formatDateTime(first(cycle, ['start_ms', 'startMs']), offset) +
'-' +
formatDateTime(first(cycle, ['end_ms', 'endMs']), offset) +
'(utc' +
formatOffset(offset) +
')'
);
}
function historyRange(cycle) {
cycle = cycle || {};
var offset = timezoneOffsetHours(cycle);
return (
formatDate(first(cycle, ['start_ms', 'startMs']), offset) +
'-' +
formatDate(first(cycle, ['end_ms', 'endMs']), offset)
);
}
function currentCycle() {
var data = envelope(state.current);
return data.cycle || data.current_cycle || data.currentCycle || null;
}
function errorKey(error) {
var status = number(
first(error, ['status', 'status_code', 'statusCode'])
);
var code = text(
first(error, ['code', 'error_code', 'errorCode'])
).toUpperCase();
var message = text(error && error.message).toLowerCase();
if (
status === 401 ||
code === 'UNAUTHORIZED' ||
message.indexOf('unauthor') >= 0
) {
return 'huwaa_weekly_star.login_required';
}
if (code === 'PROFILE_REQUIRED') {
return 'huwaa_weekly_star.profile_required';
}
return 'huwaa_weekly_star.request_failed';
}
function normalizeGift(item) {
item = item || {};
var resource = item.resource || item.gift || {};
var metadata = parseJSON(
first(item, ['metadata_json', 'metadataJson']) ||
first(resource, ['metadata_json', 'metadataJson'])
);
return {
id: text(
first(item, [
'gift_id',
'giftId',
'resource_id',
'resourceId',
'id',
]) || first(resource, ['id', 'resource_id', 'resourceId'])
),
name: text(
first(item, ['display_name', 'displayName', 'name']) ||
first(resource, ['display_name', 'displayName', 'name'])
),
image: resolveAssetURL(
first(metadata, [
'preview_url',
'previewUrl',
'asset_url',
'assetUrl',
]) ||
first(item, [
'image_url',
'imageUrl',
'cover_url',
'coverUrl',
'preview_url',
'previewUrl',
'icon_url',
'iconUrl',
'asset_url',
'assetUrl',
]) ||
first(resource, [
'preview_url',
'previewUrl',
'cover_url',
'coverUrl',
'asset_url',
'assetUrl',
])
),
};
}
function normalizeEntries(entries) {
if (!Array.isArray(entries)) return [];
return entries.slice(0, RANK_LIMIT).map(function (entry, index) {
entry = entry || {};
var user = entry.user || entry.profile || {};
var rank = number(
first(entry, ['rank_no', 'rankNo', 'rank']),
index + 1
);
var name = text(
first(user, [
'username',
'nickname',
'display_name',
'displayName',
'display_user_id',
'displayUserId',
]) || first(entry, ['username', 'name', 'user_id', 'userId'])
);
return {
rank: rank || index + 1,
userID: text(
first(entry, ['user_id', 'userId']) ||
first(user, ['user_id', 'userId', 'id'])
),
name: name || t('huwaa_weekly_star.unknown_user', 'User'),
score: first(entry, ['score', 'points', 'value']) || 0,
avatar: resolveAssetURL(
first(user, [
'avatar',
'avatar_url',
'avatarUrl',
'profile_image',
'profileImage',
]) || first(entry, ['avatar', 'avatar_url', 'avatarUrl'])
),
};
});
}
function rewardImage(item, resource, metadata) {
return resolveAssetURL(
first(metadata, [
'preview_url',
'previewUrl',
'asset_url',
'assetUrl',
]) ||
first(item, [
'image',
'image_url',
'imageUrl',
'cover_url',
'coverUrl',
'preview_url',
'previewUrl',
'icon_url',
'iconUrl',
'asset_url',
'assetUrl',
]) ||
first(resource, [
'preview_url',
'previewUrl',
'cover_url',
'coverUrl',
'asset_url',
'assetUrl',
'animation_url',
'animationUrl',
])
);
}
function normalizeRewardItem(item, group) {
item = item || {};
group = group || {};
var resource = item.resource || {};
var metadata = parseJSON(
first(item, ['metadata_json', 'metadataJson']) ||
first(resource, ['metadata_json', 'metadataJson'])
);
var walletType = text(
first(item, ['wallet_asset_type', 'walletAssetType']) ||
first(resource, ['wallet_asset_type', 'walletAssetType'])
).replace(/_/g, ' ');
var walletAmount = number(
first(item, [
'wallet_asset_amount',
'walletAssetAmount',
'amount',
]) || first(resource, ['wallet_asset_amount', 'walletAssetAmount'])
);
var quantity = number(first(item, ['quantity']));
var durationMS = number(first(item, ['duration_ms', 'durationMs']));
var durationDays = number(
first(item, [
'duration_days',
'durationDays',
'valid_days',
'validDays',
]) || first(resource, ['duration_days', 'durationDays'])
);
if (!durationDays && durationMS > 0) {
// Gateway resource-group items expose validity as duration_ms. Keep the
// conversion aligned with the backend's whole-day reward contract.
durationDays = Math.floor(durationMS / DAY_MS);
}
var labelKey = text(first(item, ['i18n_key', 'i18nKey']));
var label = labelKey
? t(labelKey, '')
: text(
first(item, [
'display_name',
'displayName',
'label',
'name',
]) ||
first(resource, [
'display_name',
'displayName',
'name',
]) ||
walletType ||
first(group, ['display_name', 'displayName', 'name'])
);
if (durationDays > 0) {
label = t('huwaa_weekly_star.reward_days', '{name} ({days} days)', {
name: label || t('huwaa_weekly_star.reward', 'Reward'),
days: durationDays,
});
}
if (quantity > 1) {
label =
(label || t('huwaa_weekly_star.reward', 'Reward')) +
' ×' +
formatNumber(quantity);
} else if (walletAmount > 0) {
label =
(label ||
walletType ||
t('huwaa_weekly_star.reward', 'Reward')) +
' ×' +
formatNumber(walletAmount);
}
return {
label: label || t('huwaa_weekly_star.reward', 'Reward'),
image: rewardImage(item, resource, metadata),
};
}
function normalizeRewardGroups(cycle) {
cycle = cycle || {};
var source =
cycle.rewards ||
cycle.reward_groups ||
cycle.rewardGroups ||
cycle.rank_rewards ||
cycle.rankRewards ||
[];
if (!Array.isArray(source)) return [];
return source
.map(function (group, index) {
group = group || {};
var nested =
group.resource_group ||
group.resourceGroup ||
group.reward_resource_group ||
group.rewardResourceGroup ||
{};
var items =
group.items ||
group.resources ||
nested.items ||
nested.resources ||
[];
// Some endpoints flatten a single visual resource onto the rank reward.
// It is only promoted when an actual image/name exists; a bare group ID
// must remain an empty state rather than a fabricated usable reward.
if (!Array.isArray(items)) items = [];
if (
!items.length &&
(rewardImage(group, group.resource || {}, {}) ||
first(group, ['display_name', 'displayName', 'name']))
) {
items = [group];
}
return {
rank: number(
first(group, ['rank_no', 'rankNo', 'rank']),
index + 1
),
items: items.slice(0, 3).map(function (item) {
return normalizeRewardItem(item, group);
}),
};
})
.filter(function (group) {
return group.rank >= 1 && group.rank <= 3;
})
.sort(function (left, right) {
return left.rank - right.rank;
});
}
function renderGifts(cycle) {
var source = cycle && Array.isArray(cycle.gifts) ? cycle.gifts : [];
var gifts = source.slice(0, 3).map(normalizeGift);
var lefts = [61, 411, 761];
var root = document.getElementById('weekStarGifts');
root.innerHTML = lefts
.map(function (left, index) {
var gift = gifts[index] || {};
var label =
gift.name ||
t('huwaa_weekly_star.gift_label', 'Activity gift {index}', {
index: index + 1,
});
return [
'<div class="week-star-gift',
gift.image ? ' has-image' : '',
'" role="listitem" aria-label="',
escapeAttr(label),
'" style="--gift-left:',
left,
'px">',
'<img class="week-star-gift-frame" src="',
ASSET_BASE,
'gift-frame.webp" alt="" aria-hidden="true" />',
'<img class="week-star-gift-frame week-star-gift-frame-glow" src="',
ASSET_BASE,
'gift-frame.webp" alt="" aria-hidden="true" />',
gift.image
? '<img class="week-star-gift-image" data-image-fallback="gift" src="' +
escapeAttr(gift.image) +
'" alt="" />'
: '',
'<span class="week-star-gift-placeholder" aria-hidden="true">—</span>',
'</div>',
].join('');
})
.join('');
bindImageFallbacks(root);
}
function medalForRank(rank) {
if (rank === 1) return ASSET_BASE + 'rank-1.webp';
if (rank === 2) return ASSET_BASE + 'rank-2.webp';
if (rank === 3) return ASSET_BASE + 'rank-3.webp';
return '';
}
function rankInitial(name) {
var characters = Array.from(text(name));
return characters.length ? characters[0] : '?';
}
function stateHTML(top, message, retry) {
return [
'<div class="week-star-state" style="top:',
top,
'px">',
'<span>',
escapeHTML(message),
'</span>',
retry
? '<button class="week-star-state-retry" type="button" data-retry="true">' +
escapeHTML(t('huwaa_weekly_star.retry', 'Retry')) +
'</button>'
: '',
'</div>',
].join('');
}
function isRetryable(error) {
return error === 'huwaa_weekly_star.request_failed';
}
function renderRankList(options) {
var root = document.getElementById(options.rootID);
var entries = normalizeEntries(options.entries);
if (!entries.length) {
var message = options.loading
? t('huwaa_weekly_star.loading', 'Loading…')
: options.error
? t(options.error, 'Unable to load the ranking.')
: t(options.emptyKey, options.emptyFallback);
root.innerHTML = stateHTML(
options.top,
message,
!options.loading && isRetryable(options.error)
);
return;
}
root.innerHTML = entries
.map(function (entry, index) {
var medal = medalForRank(entry.rank);
var tag = entry.userID ? 'button' : 'article';
var className =
index % 2 === (options.firstDark ? 0 : 1)
? 'is-dark'
: 'is-purple';
var aria = t(
'huwaa_weekly_star.rank_label',
'Rank {rank}, {name}, {score}',
{
rank: entry.rank,
name: entry.name,
score: formatScore(entry.score),
}
);
return [
'<',
tag,
tag === 'button' ? ' type="button"' : '',
' class="week-star-rank-row ',
className,
'" style="top:',
options.top + index * 140,
'px"',
entry.userID
? ' data-user-id="' + escapeAttr(entry.userID) + '"'
: '',
' aria-label="',
escapeAttr(aria),
'">',
medal
? '<img class="week-star-rank-medal' +
(entry.rank === 3 ? ' is-rank-three' : '') +
'" src="' +
medal +
'" alt="" aria-hidden="true" />'
: '<span class="week-star-rank-number" aria-hidden="true">' +
escapeHTML(entry.rank) +
'</span>',
'<span class="week-star-rank-avatar',
entry.avatar ? '' : ' is-fallback',
'">',
entry.avatar
? '<img data-image-fallback="avatar" src="' +
escapeAttr(entry.avatar) +
'" alt="" />'
: '',
'<span aria-hidden="true">',
escapeHTML(rankInitial(entry.name)),
'</span>',
'</span>',
'<span class="week-star-rank-name" dir="auto">',
escapeHTML(entry.name),
'</span>',
'<span class="week-star-rank-score">',
escapeHTML(formatScore(entry.score)),
'</span>',
'</',
tag,
'>',
].join('');
})
.join('');
bindImageFallbacks(root);
}
function historyCycleData() {
var record = state.historyCycles[0] || {};
return {
cycle: record.cycle || record,
entries:
record.top_entries ||
record.topEntries ||
record.entries ||
record.leaderboard ||
[],
};
}
function renderHistory() {
var history = historyCycleData();
document.getElementById('weekStarHistoryDate').textContent =
history.cycle &&
first(history.cycle, ['start_ms', 'startMs']) &&
first(history.cycle, ['end_ms', 'endMs'])
? historyRange(history.cycle)
: t(
'huwaa_weekly_star.previous_top_three',
'Previous Top 3 Records'
);
renderRankList({
rootID: 'weekStarHistoryList',
top: 1607,
entries: history.entries,
loading: state.loadingHistory,
error: state.historyError,
emptyKey: 'huwaa_weekly_star.no_history',
emptyFallback: 'No previous ranking records.',
firstDark: false,
});
}
function renderWeekly() {
renderRankList({
rootID: 'weekStarWeeklyList',
top: 1496,
entries: state.weeklyEntries,
loading: state.loadingWeekly,
error: state.weeklyError,
emptyKey: 'huwaa_weekly_star.no_ranking',
emptyFallback: 'No ranking data yet.',
firstDark: true,
});
}
function renderRewardItem(item) {
return [
'<article class="week-star-reward-item',
item.image ? ' has-image' : '',
'">',
'<img class="week-star-reward-item-frame" src="',
ASSET_BASE,
'reward-item-frame.webp" alt="" aria-hidden="true" />',
'<img class="week-star-reward-item-glow" src="',
ASSET_BASE,
'reward-item-glow.webp" alt="" aria-hidden="true" />',
item.image
? '<img class="week-star-reward-item-image" data-image-fallback="reward" src="' +
escapeAttr(item.image) +
'" alt="" />'
: '',
'<span class="week-star-reward-item-placeholder" aria-hidden="true">—</span>',
'<span class="week-star-reward-label" dir="auto">',
escapeHTML(item.label),
'</span>',
'</article>',
].join('');
}
function renderRewards(cycle) {
var root = document.getElementById('weekStarRewardGroups');
if (state.loadingCurrent) {
root.innerHTML = stateHTML(
1705,
t('huwaa_weekly_star.loading', 'Loading…'),
false
);
return;
}
if (state.currentError) {
root.innerHTML = stateHTML(
1705,
t(state.currentError, 'Unable to load rewards.'),
isRetryable(state.currentError)
);
return;
}
var groups = normalizeRewardGroups(cycle);
if (!groups.length) {
root.innerHTML = stateHTML(
1705,
t('huwaa_weekly_star.no_rewards', 'No reward data yet.'),
false
);
return;
}
var positions = {
1: { heading: 1505, items: 1705 },
2: { heading: 2068, items: 2203 },
3: { heading: 2566, items: 2701 },
};
root.innerHTML = groups
.map(function (group) {
var position = positions[group.rank];
var itemsHTML = group.items.length
? group.items.map(renderRewardItem).join('')
: '<div class="week-star-reward-inline-empty">' +
escapeHTML(
t(
'huwaa_weekly_star.no_rank_reward',
'No reward data.'
)
) +
'</div>';
return [
'<section class="week-star-reward-group" style="top:',
position.heading,
'px" aria-label="TOP ',
group.rank,
'">',
'<h2 class="week-star-reward-heading">',
'<img src="',
ASSET_BASE,
'reward-heading.webp" alt="" aria-hidden="true" />',
'<span>TOP',
group.rank,
'</span>',
'</h2>',
'<div class="week-star-reward-items" style="top:',
position.items - position.heading,
'px">',
itemsHTML,
'</div>',
'</section>',
].join('');
})
.join('');
bindImageFallbacks(root);
}
function renderHeader() {
var cycle = currentCycle();
var date = document.getElementById('weekStarDate');
if (state.loadingCurrent) {
date.textContent = '—';
} else if (state.currentError) {
date.textContent = '—';
} else if (cycle) {
date.textContent = cycleRange(cycle);
} else {
date.textContent = t(
'huwaa_weekly_star.no_active_cycle',
'No active cycle'
);
}
renderGifts(cycle || {});
renderRewards(cycle || {});
}
function bindImageFallbacks(root) {
all('[data-image-fallback]', root).forEach(function (image) {
if (image.dataset.fallbackBound === 'true') return;
image.dataset.fallbackBound = 'true';
function fallback() {
var type = image.getAttribute('data-image-fallback');
var parent = image.parentElement;
if (type === 'avatar' && parent) {
parent.classList.add('is-fallback');
} else if (parent) {
parent.classList.remove('has-image');
}
image.hidden = true;
}
image.addEventListener('error', fallback, { once: true });
if (image.complete && image.naturalWidth === 0) fallback();
});
}
function renderAll() {
renderHeader();
renderHistory();
renderWeekly();
setTab(state.tab, false, false);
}
function tabFromHash() {
var hash = text((window.location.hash || '').replace(/^#/, ''));
if (!hash) return '';
var route = hash.split('?')[0].replace(/^\/+|\/+$/g, '');
var pieces = route.split('/');
return pieces[pieces.length - 1];
}
function normalizeTab(value) {
return value === 'weekly' || value === 'rewards' || value === 'history'
? value
: 'history';
}
function replaceTabHash(tab) {
if (!window.history || !window.history.replaceState) return;
var hash = window.location.hash || '';
var hashQuery =
hash.indexOf('?') >= 0
? '?' + hash.split('?').slice(1).join('?')
: '';
// Native containers sometimes inject the token after `#?...`; preserving that
// suffix is mandatory because replacing the entire hash would log the user out.
var next =
window.location.pathname +
window.location.search +
'#' +
tab +
hashQuery;
window.history.replaceState(null, '', next);
}
function updateStageHeight() {
var viewportWidth = Math.min(window.innerWidth, 430);
var height = DESIGN_HEIGHT[state.tab] || DESIGN_HEIGHT.history;
var scaledHeight = Math.ceil((height * viewportWidth) / 1080);
document.documentElement.style.setProperty(
'--huwaa-week-star-scale',
String(viewportWidth / 1080)
);
stage.style.height = height + 'px';
stageWrap.style.height = scaledHeight + 'px';
page.style.minHeight = scaledHeight + 'px';
}
function setTab(tab, updateURL, announce) {
state.tab = normalizeTab(tab);
page.setAttribute('data-tab', state.tab);
stage.setAttribute('data-node-id', DESIGN_FRAME[state.tab]);
tabs.forEach(function (button) {
var active = button.getAttribute('data-tab-target') === state.tab;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
button.tabIndex = active ? 0 : -1;
});
panels.forEach(function (panel) {
panel.hidden = panel.getAttribute('data-panel') !== state.tab;
});
updateStageHeight();
if (updateURL) replaceTabHash(state.tab);
if (announce) {
var selected = tabs.find(function (button) {
return button.getAttribute('data-tab-target') === state.tab;
});
document.getElementById('weekStarAnnouncer').textContent = selected
? selected.textContent.trim()
: '';
}
}
function mockState() {
var sample = ASSET_BASE + 'sample-gift.webp';
var mockEntries = [1, 2, 3, 4, 5].map(function (rank) {
return {
rank_no: rank,
score: 337700,
user: {
id: 'mock-user-' + rank,
username: rank === 1 ? 'hanneem' : 'star' + rank,
avatar: '',
},
};
});
var rewardGroups = [1, 2, 3].map(function (rank) {
return {
rank_no: rank,
items: [1, 2, 3].map(function () {
return {
i18n_key: 'huwaa_weekly_star.frame',
duration_days: 7,
image_url: sample,
};
}),
};
});
return {
current: {
cycle: {
cycle_id: 'figma-mock',
start_ms: Date.UTC(2025, 11, 27, 21, 0, 0),
end_ms: Date.UTC(2026, 0, 3, 20, 59, 0),
utc_offset_hours: 3,
gifts: [
{ image_url: sample },
{ image_url: sample },
{ image_url: sample },
],
rewards: rewardGroups,
},
},
entries: mockEntries,
history: [
{
cycle: {
start_ms: Date.UTC(2025, 11, 6, 21, 0, 0),
end_ms: Date.UTC(2025, 11, 14, 20, 59, 0),
utc_offset_hours: 3,
},
top_entries: mockEntries,
},
],
};
}
function loadMock() {
var mock = mockState();
state.current = mock.current;
state.weeklyEntries = mock.entries;
state.historyCycles = mock.history;
state.loadingCurrent = false;
state.loadingWeekly = false;
state.loadingHistory = false;
state.currentError = '';
state.weeklyError = '';
state.historyError = '';
renderAll();
bridgeReady();
}
function tokenAvailable() {
return !!(
window.HyAppAPI &&
window.HyAppAPI.getAccessToken &&
window.HyAppAPI.getAccessToken()
);
}
function loadReal() {
var serial = ++state.requestSerial;
state.loadingCurrent = true;
state.loadingWeekly = true;
state.loadingHistory = true;
state.currentError = '';
state.weeklyError = '';
state.historyError = '';
renderAll();
if (!window.HyAppAPI || !window.HyAppAPI.weeklyStar) {
state.loadingCurrent = false;
state.loadingWeekly = false;
state.loadingHistory = false;
state.currentError = 'huwaa_weekly_star.request_failed';
state.weeklyError = state.currentError;
state.historyError = state.currentError;
renderAll();
bridgeReady();
return;
}
if (!tokenAvailable()) {
// Production weekly-star endpoints require the authenticated user context.
// Do not send three doomed requests or silently replace the response with mock data.
state.loadingCurrent = false;
state.loadingWeekly = false;
state.loadingHistory = false;
state.currentError = 'huwaa_weekly_star.login_required';
state.weeklyError = state.currentError;
state.historyError = state.currentError;
renderAll();
bridgeReady();
return;
}
// Current/leaderboard and history are intentionally isolated. A history outage
// must not erase a successfully loaded current leaderboard, and vice versa.
var currentFlow = window.HyAppAPI.weeklyStar
.current()
.then(function (payload) {
if (serial !== state.requestSerial) return;
var data = envelope(payload);
state.current = data;
state.weeklyEntries = data.top_entries || data.topEntries || [];
state.loadingCurrent = false;
state.loadingWeekly = false;
state.currentError = '';
state.weeklyError = '';
renderHeader();
renderWeekly();
})
.catch(function (error) {
if (serial !== state.requestSerial) return;
state.current = null;
state.weeklyEntries = [];
state.loadingCurrent = false;
state.loadingWeekly = false;
state.currentError = errorKey(error);
state.weeklyError = state.currentError;
renderHeader();
renderWeekly();
});
var historyFlow = window.HyAppAPI.weeklyStar
.history(1)
.then(function (payload) {
if (serial !== state.requestSerial) return;
var data = envelope(payload);
state.historyCycles = Array.isArray(data)
? data
: data.cycles || data.items || data.list || [];
state.historyError = '';
})
.catch(function (error) {
if (serial !== state.requestSerial) return;
state.historyCycles = [];
state.historyError = errorKey(error);
})
.then(function () {
if (serial !== state.requestSerial) return;
state.loadingHistory = false;
renderHistory();
});
Promise.all([currentFlow, historyFlow]).then(function () {
if (serial === state.requestSerial) bridgeReady();
});
}
function bridgeReady() {
if (!window.HyAppBridge || !window.HyAppBridge.ready) return;
window.HyAppBridge.ready({
page: 'huwaa_weekly_star',
app_code: 'huwaa',
mock: useMock,
});
}
function activateRelativeTab(currentIndex, direction) {
var nextIndex = (currentIndex + direction + tabs.length) % tabs.length;
var next = tabs[nextIndex];
setTab(next.getAttribute('data-tab-target'), true, true);
next.focus();
}
function bindEvents() {
tabs.forEach(function (button, index) {
button.addEventListener('click', function () {
setTab(button.getAttribute('data-tab-target'), true, true);
});
button.addEventListener('keydown', function (event) {
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
event.preventDefault();
activateRelativeTab(index, 1);
} else if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowUp'
) {
event.preventDefault();
activateRelativeTab(index, -1);
} else if (event.key === 'Home') {
event.preventDefault();
setTab('history', true, true);
tabs[0].focus();
} else if (event.key === 'End') {
event.preventDefault();
setTab('rewards', true, true);
tabs[tabs.length - 1].focus();
}
});
});
document.addEventListener('click', function (event) {
var retry = event.target.closest('[data-retry]');
if (retry) {
loadReal();
return;
}
var row = event.target.closest('[data-user-id]');
if (row && window.HyAppBridge && window.HyAppBridge.openUser) {
window.HyAppBridge.openUser(row.getAttribute('data-user-id'));
}
});
window.addEventListener('resize', updateStageHeight);
window.addEventListener('hashchange', function () {
setTab(normalizeTab(tabFromHash()), false, false);
});
window.addEventListener('hyapp:i18n-ready', function () {
// Dynamic API rows are not covered by common/i18n.js's static DOM pass.
// Re-rendering keeps empty/error/reward labels synchronized after locale load.
renderAll();
});
}
function init() {
bindEvents();
setTab(state.tab, false, false);
if (useMock) loadMock();
else loadReal();
}
init();
})();