hyapp-h5/achievement/index.html
2026-06-26 18:26:12 +08:00

1266 lines
48 KiB
HTML
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.

<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="utf-8" />
<meta name="google" content="notranslate" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Achievement</title>
<link rel="stylesheet" href="../common/theme.css" />
<style>
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--app-scale: 1;
--achievement-bg: #130a2b;
--achievement-card-border: #fff09b;
--achievement-card-purple: #5f1e68;
--achievement-card-muted: #130828;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: var(--achievement-bg);
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
outline: none;
}
.app-viewport {
position: relative;
width: 375px;
min-height: 812px;
margin: 0 auto;
transform: scale(var(--app-scale));
transform-origin: top center;
}
.page {
position: relative;
width: 375px;
min-height: 812px;
overflow: hidden;
background: var(--achievement-bg);
}
.stage-bg {
position: absolute;
left: 0;
top: 0;
width: 375px;
height: 667px;
object-fit: cover;
pointer-events: none;
}
.stage-shade {
position: absolute;
left: 0;
right: 0;
top: 212px;
height: 455px;
background: linear-gradient(
180deg,
rgba(19, 10, 43, 0) 0%,
var(--achievement-bg) 20%,
var(--achievement-bg) 100%
);
pointer-events: none;
}
.nav {
position: absolute;
left: 0;
top: 0;
z-index: 6;
width: 375px;
height: 44px;
}
.back {
position: absolute;
left: 16px;
top: 10px;
width: 24px;
height: 24px;
}
.back::before {
content: '';
position: absolute;
left: 8px;
top: 5px;
width: 12px;
height: 12px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(45deg);
}
.profile {
position: absolute;
left: 107px;
top: 40px;
z-index: 2;
width: 162px;
height: 162px;
}
.profile-avatar {
position: absolute;
left: 36px;
top: 36px;
z-index: 1;
width: 90px;
height: 90px;
border-radius: 50%;
object-fit: cover;
}
.profile-frame {
position: absolute;
inset: 0;
z-index: 2;
width: 162px;
height: 162px;
pointer-events: none;
}
.profile-frame img,
.profile-frame video,
.profile-frame canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.user-name {
position: absolute;
left: 0;
top: 188px;
z-index: 3;
width: 375px;
overflow: hidden;
padding: 0 32px;
font-size: 18px;
line-height: 18px;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
}
.earned {
position: absolute;
left: 0;
top: 218px;
z-index: 3;
width: 375px;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
line-height: 14px;
text-align: center;
}
.earned strong {
color: #fff;
font-weight: 400;
}
.tabs {
position: absolute;
left: 0;
top: 252px;
z-index: 5;
display: grid;
grid-template-columns: 1fr 1fr;
width: 375px;
height: 34px;
padding: 0 40px;
}
.tab {
position: relative;
height: 34px;
color: rgba(255, 255, 255, 0.5);
font-size: 18px;
line-height: 18px;
text-align: center;
}
.tab.active {
color: #fff;
}
.tab.active::after {
content: '';
position: absolute;
left: 50%;
top: 28px;
width: 16px;
height: 3px;
margin-left: -8px;
border-radius: 32px;
background: #fff;
}
.content {
position: relative;
z-index: 4;
min-height: 812px;
padding-top: 299px;
padding-bottom: 92px;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(3, 104px);
gap: 12px;
width: 336px;
margin: 0 auto;
}
.badge-card {
position: relative;
width: 104px;
height: 116px;
overflow: hidden;
border: 1px solid var(--achievement-card-border);
background: var(--achievement-card-purple);
}
.badge-card:focus,
.badge-card:focus-visible,
.badge-card:active {
outline: none;
box-shadow: none;
transform: none;
}
.badge-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
166deg,
rgba(165, 76, 178, 0.92) 36%,
rgba(165, 76, 178, 0) 63%
);
mix-blend-mode: plus-lighter;
pointer-events: none;
}
.badge-card.locked {
background: var(--achievement-card-muted);
}
.badge-card.locked::before {
background: linear-gradient(
166deg,
rgba(70, 42, 120, 0.92) 36%,
rgba(59, 36, 100, 0) 63%
);
}
.badge-card.locked img {
filter: grayscale(1) brightness(0.58);
opacity: 0.72;
}
.badge-card.locked .badge-title {
color: #8f879a;
}
.badge-card img {
position: absolute;
left: 12px;
top: 10px;
width: 80px;
height: 80px;
object-fit: contain;
}
.badge-title {
position: absolute;
left: 4px;
right: 4px;
top: 94px;
z-index: 2;
overflow: hidden;
color: #fff;
font-size: 12px;
line-height: 12px;
text-align: center;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
.empty-state {
display: none;
width: 375px;
padding-top: 60px;
text-align: center;
}
.empty-state.active {
display: block;
}
.empty-state img {
display: block;
width: 146px;
height: 143px;
margin: 0 auto 24px;
object-fit: contain;
}
.empty-text,
.footer-text {
color: rgba(255, 255, 255, 0.6);
font-size: 10px;
line-height: 10px;
text-align: center;
}
.footer-text {
margin-top: 24px;
}
.achievement-loading {
position: absolute;
left: 0;
top: 320px;
z-index: 7;
width: 375px;
color: rgba(255, 255, 255, 0.72);
}
.achievement-loading .hy-loading__mascot {
width: 76px;
height: 76px;
}
.achievement-loading .hy-loading__spinner {
border-color: rgba(255, 255, 255, 0.22);
border-top-color: #ffffff;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(34px * var(--app-scale));
z-index: 80;
max-width: 300px;
transform: translateX(-50%);
padding: 8px 12px;
border-radius: 16px;
background: rgba(0, 0, 0, 0.62);
color: #fff;
font-size: 12px;
line-height: 16px;
text-align: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.toast.show {
opacity: 1;
}
.achievement-modal {
position: fixed;
left: 50%;
top: 0;
z-index: 50;
display: none;
width: 375px;
height: 812px;
overflow: hidden;
background: rgba(0, 0, 0, 0.7);
transform: translateX(-50%) scale(var(--app-scale));
transform-origin: top center;
}
.achievement-modal.active {
display: block;
}
.modal-badge {
position: absolute;
left: 86px;
top: 180px;
width: 203px;
height: 203px;
object-fit: contain;
}
.modal-badge.locked {
filter: grayscale(1) brightness(0.56);
opacity: 0.72;
}
.modal-title {
position: absolute;
left: 0;
top: 407px;
width: 375px;
padding: 0 24px;
overflow: hidden;
font-size: 24px;
line-height: 24px;
text-align: center;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
.modal-status {
position: absolute;
left: 0;
top: 447px;
display: flex;
align-items: center;
justify-content: center;
width: 375px;
height: 16px;
color: #d3d3d3;
font-size: 12px;
line-height: 12px;
}
.modal-status.locked {
color: #8f879a;
}
.modal-status.unlocked {
color: #d3d3d3;
}
.modal-status img {
width: 16px;
height: 16px;
margin-right: 4px;
}
.modal-desc {
position: absolute;
left: 20px;
top: 475px;
width: 335px;
max-height: 72px;
overflow: hidden;
color: #fff;
font-size: 12px;
line-height: 16px;
text-align: center;
}
.collection-panel {
position: absolute;
left: 0;
top: 607px;
width: 375px;
height: 205px;
border: 2px solid var(--achievement-card-border);
background: #2a1351;
background-image: linear-gradient(
173deg,
rgba(70, 42, 120, 0.9) 36%,
rgba(59, 36, 100, 0) 63%
);
}
.panel-border {
position: absolute;
left: 0;
top: 595px;
width: 375px;
height: 31px;
object-fit: cover;
pointer-events: none;
}
.collection-title {
position: absolute;
left: 0;
top: 635px;
width: 375px;
font-size: 16px;
font-weight: 600;
line-height: 16px;
text-align: center;
}
.collector-list {
position: absolute;
left: 65px;
top: 675px;
display: flex;
align-items: center;
height: 45px;
}
.collector-list img {
width: 45px;
height: 45px;
margin-left: -5px;
border-radius: 50%;
object-fit: cover;
}
.collector-list img:first-child {
margin-left: 0;
}
.collector-more {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
margin-left: -5px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.24);
color: #fff;
font-size: 16px;
line-height: 16px;
}
.owner-text {
position: absolute;
left: 0;
top: 744px;
width: 375px;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
line-height: 12px;
text-align: center;
}
</style>
</head>
<body>
<div id="appViewport" class="app-viewport">
<main id="page" class="page" aria-label="Achievement">
<img class="stage-bg" src="assets/figma/bg-stage.png" alt="" />
<div class="stage-shade"></div>
<nav class="nav" aria-label="Achievement navigation">
<button
id="backButton"
class="back"
aria-label="Back"
data-i18n-aria="achievement.back"
></button>
</nav>
<section class="profile" aria-label="User">
<img id="profileAvatar" class="profile-avatar" alt="" />
<div
id="profileFrame"
class="profile-frame"
aria-hidden="true"
></div>
</section>
<div id="userName" class="user-name"></div>
<div id="earnedText" class="earned">
Earn <strong id="earnedCount">0</strong> achievements
</div>
<div class="tabs" role="tablist" aria-label="Achievement tabs">
<button
class="tab active"
role="tab"
data-tab="honors"
data-i18n="achievement.honors"
>
Honors
</button>
<button
class="tab"
role="tab"
data-tab="event"
data-i18n="achievement.event"
>
Event
</button>
</div>
<section class="content" aria-live="polite">
<div id="badgeGrid" class="badge-grid"></div>
<div id="emptyState" class="empty-state">
<img src="assets/figma/empty-cat.png" alt="" />
<div
class="empty-text"
data-i18n="achievement.stayTuned"
>
Stay Tuned For More Achievements!
</div>
</div>
<div
id="footerText"
class="footer-text"
data-i18n="achievement.stayTuned"
>
Stay Tuned For More Achievements!
</div>
</section>
<div
id="loading"
class="hy-loading achievement-loading"
aria-hidden="true"
>
<div class="hy-loading__mascot" aria-hidden="true"></div>
<div class="hy-loading__spinner" aria-hidden="true"></div>
<div
class="hy-loading__text"
data-i18n="achievement.loading"
>
Loading...
</div>
</div>
</main>
</div>
<div id="toast" class="toast"></div>
<div id="achievementModal" class="achievement-modal" aria-hidden="true">
<img id="modalBadge" class="modal-badge" alt="" />
<div id="modalTitle" class="modal-title"></div>
<div id="modalStatus" class="modal-status"></div>
<div id="modalDesc" class="modal-desc"></div>
<div class="collection-panel"></div>
<img
class="panel-border"
src="assets/figma/modal-border.png"
alt=""
/>
<div class="collection-title" data-i18n="achievement.collection">
Achievement Collection
</div>
<div id="collectorList" class="collector-list"></div>
<div id="ownerText" class="owner-text"></div>
</div>
<script src="../common/api.js"></script>
<script src="../common/toast.js"></script>
<script src="../common/params.js"></script>
<script src="../common/jsbridge.js"></script>
<script src="../common/i18n.js"></script>
<script src="../common/effect-player.js"></script>
<script>
(function () {
var ASSET = 'assets/figma/';
var tabs = {
honors: { collection_type: 'ordinary' },
event: { collection_type: 'event' },
};
var state = {
tab:
new URLSearchParams(window.location.search).get(
'tab'
) || 'honors',
loading: false,
toastTimer: 0,
profile: null,
appearance: {},
profileFrameURL: '',
profileFramePlayer: null,
achievements: { honors: [], event: [] },
loaded: { honors: false, event: false },
selected: null,
};
function byId(id) {
return document.getElementById(id);
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function replaceToken(text, key, value) {
return String(text || '').replace(
new RegExp('\\{' + key + '\\}', 'g'),
String(value)
);
}
function escapeHTML(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function validTab(value) {
return tabs[value] ? value : 'honors';
}
function showToast(message) {
var toast = byId('toast');
toast.textContent = message;
toast.classList.add('show');
window.clearTimeout(state.toastTimer);
state.toastTimer = window.setTimeout(function () {
toast.classList.remove('show');
}, 1800);
}
function resizeApp() {
var page = byId('page');
var contentHeight = Math.max(
812,
page ? page.scrollHeight : 812
);
var baseWidth = 375;
var maxRenderedWidth = 768;
var scale = Math.min(
window.innerWidth / baseWidth,
maxRenderedWidth / baseWidth
);
document.documentElement.style.setProperty(
'--app-scale',
String(scale)
);
byId('appViewport').style.height = contentHeight + 'px';
document.body.style.minHeight =
Math.ceil(contentHeight * scale) + 'px';
}
function profileSource(current) {
var user = current && current.user;
var data =
(user && (user.profile || user.user || user)) || {};
return data && data.profile && !data.nickname
? data.profile
: data;
}
function profileName(profile) {
return (
profile.nickname ||
profile.nick_name ||
profile.nickName ||
profile.username ||
profile.user_name ||
profile.name ||
profile.display_name ||
profile.displayName ||
profile.display_user_id ||
profile.displayUserId ||
profile.user_id ||
profile.userId ||
''
);
}
function profileAvatar(profile) {
return (
profile.avatar ||
profile.user_avatar ||
profile.userAvatar ||
profile.avatar_url ||
profile.avatarUrl ||
profile.profile_avatar ||
profile.profileAvatar ||
''
);
}
function normalizeAppearance(data) {
return (
(data &&
(data.appearance ||
data.user_appearance ||
data.userAppearance ||
data.profile_appearance ||
data.profileAppearance ||
data)) ||
{}
);
}
function frameURL(frameData) {
frameData = frameData || {};
if (typeof frameData === 'string') return frameData;
return (
frameData.asset_url ||
frameData.preview_url ||
frameData.animation_url ||
frameData.url ||
frameData.resource_url ||
frameData.resourceUrl ||
''
);
}
function profileFrameData(profile) {
profile = profile || {};
var appearance = state.appearance || {};
return (
appearance.avatar_frame ||
appearance.avatarFrame ||
appearance.profile_frame ||
appearance.profileFrame ||
profile.avatar_frame ||
profile.avatarFrame ||
profile.profile_frame ||
profile.profileFrame ||
profile.frame ||
{}
);
}
function setImage(node, src, fallback) {
var next = src || fallback || '';
node.onerror = function () {
if (fallback && node.getAttribute('src') !== fallback) {
node.src = fallback;
}
};
if (!next) {
node.removeAttribute('src');
return;
}
if (node.getAttribute('src') !== next) node.src = next;
}
function renderStaticFrame(node, src) {
if (!src) {
node.innerHTML = '';
return;
}
node.innerHTML =
'<img src="' + escapeHTML(src) + '" alt="">';
var img = node.querySelector('img');
img.onerror = function () {
img.onerror = null;
state.profileFrameURL = '';
renderStaticFrame(node, '');
};
}
function isDynamicEffect(url) {
if (
!window.HyAppEffectPlayer ||
!window.HyAppEffectPlayer.inferType
) {
return false;
}
return Boolean(window.HyAppEffectPlayer.inferType(url));
}
function renderProfileFrame(profile) {
var node = byId('profileFrame');
if (!node) return;
var url = frameURL(profileFrameData(profile));
if (state.profileFrameURL === url) return;
state.profileFrameURL = url;
if (state.profileFramePlayer) {
state.profileFramePlayer.destroy();
state.profileFramePlayer = null;
}
if (isDynamicEffect(url)) {
state.profileFramePlayer =
window.HyAppEffectPlayer.create(node, {
fit: 'contain',
waitForWindowLoad: false,
});
state.profileFramePlayer.play(url).catch(function () {
state.profileFrameURL = '';
renderStaticFrame(node, '');
});
return;
}
renderStaticFrame(node, url);
}
function parseJSON(text) {
try {
return text ? JSON.parse(text) : {};
} catch (_) {
return {};
}
}
function definitionOf(item) {
return (item && item.definition) || {};
}
function configOf(item) {
return parseJSON(definitionOf(item).display_config_json);
}
function resourceOf(item) {
var def = definitionOf(item);
return def.primary_badge_resource || {};
}
function resourceImage(resource, unlocked) {
return (
resource.preview_url ||
resource.asset_url ||
resource.animation_url ||
resource.url ||
''
);
}
function isUnlocked(item) {
var status = String(item && item.user_status).toLowerCase();
return (
status === 'unlocked' ||
status === 'completed' ||
Number((item && item.unlocked_at_ms) || 0) > 0
);
}
function titleOf(item) {
var resource = resourceOf(item);
var def = definitionOf(item);
return def.title || resource.name || '';
}
function descriptionOf(item) {
var config = configOf(item);
var def = definitionOf(item);
return (
config.unlock_text ||
config.unlock_method ||
config.how_to_get ||
def.description ||
t(
'achievement.defaultHowTo',
'Complete the required achievement goal to collect this badge.'
)
);
}
function ownerCountOf(item) {
var config = configOf(item);
var candidates = [
item && item.owner_count,
item && item.collector_count,
definitionOf(item).owner_count,
config.owner_count,
config.collector_count,
config.collected_count,
];
for (var i = 0; i < candidates.length; i += 1) {
var value = Number(candidates[i] || 0);
if (value > 0) return value;
}
return 0;
}
function collectorAvatarsOf(item) {
var config = configOf(item);
var list = Array.isArray(config.collectors)
? config.collectors
: Array.isArray(config.owner_avatars)
? config.owner_avatars
: [];
return list
.map(function (owner) {
return typeof owner === 'string'
? owner
: owner &&
(owner.avatar ||
owner.avatar_url ||
owner.avatarUrl);
})
.filter(Boolean)
.slice(0, 5);
}
function formatDate(ms) {
var value = Number(ms || 0);
if (!value) return '';
try {
var date = new Date(value);
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(
2,
'0'
);
var day = String(date.getDate()).padStart(2, '0');
return year + '/' + month + '/' + day;
} catch (_) {
return new Date(value)
.toISOString()
.slice(0, 10)
.replace(/-/g, '/');
}
}
function unlockedCount() {
return []
.concat(state.achievements.honors)
.concat(state.achievements.event)
.filter(isUnlocked).length;
}
function earnedText(count) {
var text = t(
'achievement.earned',
'Earn {count} achievements'
);
return replaceToken(
text,
'count',
'<strong>' + count + '</strong>'
);
}
function renderHeader() {
var profile = state.profile || {};
byId('userName').textContent = profileName(profile);
setImage(byId('profileAvatar'), profileAvatar(profile), '');
renderProfileFrame(profile);
byId('earnedText').innerHTML = earnedText(unlockedCount());
}
function cardHTML(item, index) {
var unlocked = isUnlocked(item);
var resource = resourceOf(item);
var img = resourceImage(resource, unlocked);
return (
'<button class="badge-card ' +
(unlocked ? 'unlocked' : 'locked') +
'" data-index="' +
index +
'" aria-label="' +
escapeHTML(titleOf(item)) +
'">' +
(img
? '<img src="' + escapeHTML(img) + '" alt="">'
: '') +
'<div class="badge-title">' +
escapeHTML(titleOf(item)) +
'</div>' +
'</button>'
);
}
function renderContent() {
state.tab = validTab(state.tab);
var list = state.achievements[state.tab] || [];
document.querySelectorAll('.tab').forEach(function (node) {
var active = node.dataset.tab === state.tab;
node.classList.toggle('active', active);
node.setAttribute(
'aria-selected',
active ? 'true' : 'false'
);
});
byId('badgeGrid').innerHTML = list.map(cardHTML).join('');
var empty = !state.loading && list.length === 0;
byId('emptyState').classList.toggle('active', empty);
byId('footerText').style.display = empty ? 'none' : 'block';
byId('loading').classList.toggle('active', state.loading);
byId('loading').setAttribute(
'aria-hidden',
state.loading ? 'false' : 'true'
);
resizeApp();
}
function render() {
renderHeader();
renderContent();
}
function normalizeAchievements(data) {
return ((data && data.achievements) || []).filter(
function (item) {
return item && item.definition;
}
);
}
function achievementQuery(name, page) {
var cfg = tabs[name] || tabs.honors;
return {
collection_type: cfg.collection_type,
collection_id: cfg.collection_id || '',
page: page,
page_size: 60,
};
}
function fetchAchievementList(name, page, acc) {
var nextPage = page || 1;
var items = acc || [];
return window.HyAppAPI.achievement
.list(achievementQuery(name, nextPage))
.then(function (data) {
var current = normalizeAchievements(data);
var merged = items.concat(current);
var total = Number(
data && data.total ? data.total : 0
);
// 后端成就列表是分页接口H5 需要完整 tab 数据来同时渲染列表和顶部已获得数量,所以按 total 继续补页。
if (current.length > 0 && total > merged.length) {
return fetchAchievementList(
name,
nextPage + 1,
merged
);
}
return merged;
});
}
function loadTab(name) {
state.tab = validTab(name);
if (window.history && window.history.replaceState) {
var url = new URL(window.location.href);
url.searchParams.set('tab', state.tab);
window.history.replaceState(null, '', url.toString());
}
render();
if (state.loaded[state.tab]) return Promise.resolve();
if (
!window.HyAppAPI ||
!window.HyAppAPI.achievement ||
!window.HyAppAPI.getAccessToken()
) {
state.loaded[state.tab] = true;
render();
return Promise.resolve();
}
state.loading = true;
render();
// Gateway 只开放 collection_type / collection_id 过滤tab 不拼 achievement_type避免把活动类型误当集合类型传给后端。
return fetchAchievementList(state.tab)
.then(function (data) {
state.achievements[state.tab] = data;
state.loaded[state.tab] = true;
})
.catch(function (error) {
state.loaded[state.tab] = true;
showToast(
(error && error.message) ||
t('achievement.loadFailed', 'Load failed')
);
})
.finally(function () {
state.loading = false;
render();
});
}
function loadUserProfile() {
var tasks = [];
var hasToken =
window.HyAppAPI &&
window.HyAppAPI.getAccessToken &&
window.HyAppAPI.getAccessToken();
if (window.HyAppParams && window.HyAppParams.loadUser) {
tasks.push(
window.HyAppParams.loadUser()
.then(function (current) {
state.profile = profileSource(current);
renderHeader();
return current;
})
.catch(function () {
return null;
})
);
}
if (
hasToken &&
window.HyAppAPI.user &&
window.HyAppAPI.user.appearance
) {
tasks.push(
window.HyAppAPI.user
.appearance()
.then(function (data) {
state.appearance =
normalizeAppearance(data);
renderHeader();
return data;
})
.catch(function () {
state.appearance = {};
renderHeader();
return null;
})
);
}
if (!tasks.length) {
return Promise.resolve(null);
}
// 用户资料和装扮是两个独立接口:任一失败都不能阻断成就页,成功的那部分立即刷新头部。
return Promise.all(tasks);
}
function loadCounts() {
var active = state.tab;
var other = active === 'honors' ? 'event' : 'honors';
if (state.loaded[other]) return Promise.resolve();
if (
!window.HyAppAPI ||
!window.HyAppAPI.achievement ||
!window.HyAppAPI.getAccessToken()
) {
state.loaded[other] = true;
renderHeader();
return Promise.resolve();
}
// Earned count spans both tabs, but preloading the hidden tab must not mutate the visible tab or browser URL.
return fetchAchievementList(other)
.then(function (data) {
state.achievements[other] = data;
state.loaded[other] = true;
renderHeader();
})
.catch(function () {
state.loaded[other] = true;
renderHeader();
});
}
function renderCollectors(item) {
var custom = collectorAvatarsOf(item);
byId('collectorList').innerHTML =
custom
.slice(0, 5)
.map(function (src) {
return (
'<img src="' + escapeHTML(src) + '" alt="">'
);
})
.join('') +
(custom.length
? '<div class="collector-more">...</div>'
: '');
}
function openModal(item) {
state.selected = item;
var unlocked = isUnlocked(item);
var resource = resourceOf(item);
setImage(
byId('modalBadge'),
resourceImage(resource, unlocked),
''
);
byId('modalBadge').classList.toggle('locked', !unlocked);
byId('modalTitle').textContent = titleOf(item);
byId('modalDesc').textContent = descriptionOf(item);
byId('modalStatus').classList.toggle('unlocked', unlocked);
byId('modalStatus').classList.toggle('locked', !unlocked);
if (unlocked) {
var date = formatDate(item.unlocked_at_ms);
byId('modalStatus').textContent = date
? replaceToken(
t(
'achievement.collectedWithDate',
'Collected on {date}'
),
'date',
date
)
: t('achievement.collected', 'Collected');
} else {
byId('modalStatus').textContent = t(
'achievement.notCollected',
'Not Collected'
);
}
var owners = ownerCountOf(item);
byId('ownerText').textContent = owners
? replaceToken(
t(
'achievement.ownerCount',
'Only {count} people own it'
),
'count',
owners
)
: t(
'achievement.ownerUpdating',
'Collection records are updating'
);
renderCollectors(item);
byId('achievementModal').classList.add('active');
byId('achievementModal').setAttribute(
'aria-hidden',
'false'
);
}
function closeModal() {
state.selected = null;
byId('achievementModal').classList.remove('active');
byId('achievementModal').setAttribute(
'aria-hidden',
'true'
);
}
byId('backButton').addEventListener('click', function () {
if (state.selected) {
closeModal();
return;
}
window.HyAppBridge.back();
});
document
.querySelector('.tabs')
.addEventListener('click', function (event) {
var target = event.target.closest('[data-tab]');
if (!target) return;
loadTab(target.dataset.tab);
});
byId('badgeGrid').addEventListener('click', function (event) {
var target = event.target.closest('[data-index]');
if (!target) return;
target.blur();
var list = state.achievements[state.tab] || [];
var item = list[Number(target.dataset.index)];
if (item) openModal(item);
});
byId('achievementModal').addEventListener(
'click',
function (event) {
if (event.target === byId('achievementModal'))
closeModal();
}
);
window.addEventListener('resize', resizeApp);
window.addEventListener('orientationchange', function () {
window.requestAnimationFrame(resizeApp);
});
window.addEventListener('hyapp:i18n-ready', render);
render();
window.HyAppBridge.ready({ page: 'achievement' });
loadTab(state.tab).finally(function () {
loadUserProfile();
loadCounts();
});
})();
</script>
</body>
</html>