1197 lines
41 KiB
JavaScript
1197 lines
41 KiB
JavaScript
(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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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();
|
||
})();
|