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

434 lines
14 KiB
JavaScript

(function () {
'use strict';
var DESIGN_WIDTH = 1080;
var DESIGN_HEIGHT = 1985;
var TABLE_TOP = 1005;
var TABLE_CONTENT_TOP = 1020;
var ROW_HEIGHT = 103;
var MIN_TIER_SLOTS = 7;
var TABLE_BOTTOM_SPACE = 141;
var MAX_PAGE_WIDTH = 430;
var ASSET_BASE = './fami/assets/layers/';
var params = new URLSearchParams(window.location.search || '');
var useMock = params.get('mock') === '1';
document.documentElement.classList.toggle('rr-mock', useMock);
var stageHeight = DESIGN_HEIGHT;
var currentData = null;
var loadState = 'loading';
var resizeFrame = 0;
var app = document.getElementById('rrApp');
var stageShell = document.getElementById('rrStageShell');
var stage = document.getElementById('rrStage');
var table = document.getElementById('rrTable');
var tierTable = document.getElementById('rrTierTable');
var tableDivider = document.getElementById('rrTableDivider');
var tableSides = Array.prototype.slice.call(
document.querySelectorAll('.rr-table-side-ornament')
);
var footerShell = document.getElementById('rrFooterShell');
var statusNode = document.getElementById('rrStatus');
var avatar = document.getElementById('rrUserAvatar');
var mockData = {
targetReward: 1000,
user: {
name: 'Namename...',
id: '123456789',
avatar: ASSET_BASE + 'avatar-mock.webp',
},
currentCoinSpent: 100,
expectedReward: 100,
tiers: Array.from({ length: 7 }, function (_, index) {
return {
id: 'figma-tier-' + String(index + 1),
turnover: 7000000,
reward: 350000,
};
}),
};
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
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 number(value, fallback) {
var parsed = Number(value);
return Number.isFinite(parsed) ? parsed : Number(fallback || 0);
}
function unwrap(payload) {
if (
payload &&
typeof payload === 'object' &&
Object.prototype.hasOwnProperty.call(payload, 'data')
) {
return payload.data || {};
}
return payload || {};
}
function trimFixed(value) {
var fixed = value >= 10 ? value.toFixed(0) : value.toFixed(1);
return fixed.replace(/\.0$/, '');
}
function compactNumber(value) {
var amount = number(value, 0);
if (amount >= 1000000000) return trimFixed(amount / 1000000000) + 'B';
if (amount >= 1000000) return trimFixed(amount / 1000000) + 'M';
if (amount >= 1000) return trimFixed(amount / 1000) + 'k';
return String(Math.floor(amount));
}
function plainWholeNumber(value) {
return String(Math.floor(number(value, 0)));
}
function wholeNumber(value) {
var lang =
window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en';
return new Intl.NumberFormat(lang, {
maximumFractionDigits: 0,
}).format(Math.floor(number(value, 0)));
}
function normalizeTier(tier, index) {
tier = tier || {};
return {
id:
first(tier, ['tier_id', 'tierId', 'id', 'tier_code']) ||
'tier-' + String(index + 1),
turnover: number(
first(tier, [
'threshold_coin_spent',
'thresholdCoinSpent',
'turnover',
]),
0
),
reward: number(
first(tier, [
'reward_coin_amount',
'rewardCoinAmount',
'reward',
]),
0
),
};
}
function normalizeAPIData(payload) {
var data = unwrap(payload);
var user = data.user || {};
var room = data.room || {};
var currentCoinSpent = number(
first(data, ['current_coin_spent', 'currentCoinSpent']),
0
);
var expectedReward = number(
first(data, [
'expected_reward_coin_amount',
'expectedRewardCoinAmount',
]),
0
);
var tiers = Array.isArray(data.tiers) ? data.tiers : [];
return {
enabled: data.enabled !== false,
targetReward: expectedReward,
currentCoinSpent: currentCoinSpent,
expectedReward: expectedReward,
user: {
name:
first(user, ['username', 'name']) || first(room, ['title']),
id:
first(user, [
'display_user_id',
'displayUserId',
'user_id',
'userId',
]) || first(room, ['owner_user_id', 'ownerUserId']),
avatar:
first(user, ['avatar']) ||
first(room, ['cover_url', 'coverUrl']),
},
tiers: tiers.map(normalizeTier),
};
}
function createCell(className, text) {
var cell = document.createElement('div');
cell.className = 'rr-tier-cell' + (className ? ' ' + className : '');
cell.textContent = text;
return cell;
}
function createHeaderRow() {
var row = document.createElement('div');
var turnover = createCell(
'',
t('roomReward.turnover', 'Room turnover')
);
var reward = createCell(
'',
t('roomReward.receiveReward', 'Receive a reward')
);
row.className = 'rr-tier-row rr-tier-row-head';
turnover.setAttribute('data-i18n', 'roomReward.turnover');
reward.setAttribute('data-i18n', 'roomReward.receiveReward');
row.appendChild(turnover);
row.appendChild(reward);
return row;
}
function createTierRow(tier) {
var row = document.createElement('div');
row.className = 'rr-tier-row';
if (!tier) {
row.classList.add('rr-tier-placeholder');
row.appendChild(createCell('', '—'));
row.appendChild(createCell('', '—'));
return row;
}
row.appendChild(createCell('', compactNumber(tier.turnover)));
var rewardCell = createCell('rr-tier-reward', '');
var amount = document.createElement('span');
var coin = document.createElement('img');
amount.textContent = compactNumber(tier.reward);
coin.src = ASSET_BASE + 'coin-tier.webp';
coin.alt = '';
coin.setAttribute('aria-hidden', 'true');
coin.draggable = false;
rewardCell.appendChild(amount);
rewardCell.appendChild(coin);
row.appendChild(rewardCell);
return row;
}
function renderTable(tiers) {
tiers = Array.isArray(tiers) ? tiers : [];
var slotCount = Math.max(tiers.length, MIN_TIER_SLOTS);
var fragment = document.createDocumentFragment();
fragment.appendChild(createHeaderRow());
for (var index = 0; index < slotCount; index += 1) {
fragment.appendChild(createTierRow(tiers[index] || null));
}
tierTable.replaceChildren(fragment);
tierTable.setAttribute(
'aria-busy',
loadState === 'loading' ? 'true' : 'false'
);
syncTableLayout(slotCount + 1);
}
function syncTableLayout(rowCount) {
var rowsHeight = Math.max(rowCount, 1) * ROW_HEIGHT;
// The Figma frame leaves 141 design pixels after the final row. Reusing that
// relationship for extra API tiers keeps the fixed footer overlap identical
// instead of hiding an arbitrary amount of the last dynamic row.
stageHeight = Math.max(
DESIGN_HEIGHT,
TABLE_CONTENT_TOP + rowsHeight + TABLE_BOTTOM_SPACE
);
var tableHeight = stageHeight - TABLE_TOP;
var sideHeight = tableHeight - 15;
var sideImageHeight = Math.max(1003, tableHeight + 23);
document.documentElement.style.setProperty(
'--rr-stage-height',
stageHeight + 'px'
);
stage.style.height = stageHeight + 'px';
table.style.height = tableHeight + 'px';
tierTable.style.minHeight = rowsHeight + 'px';
tableDivider.style.height = rowsHeight + 'px';
tableSides.forEach(function (side) {
side.style.height = sideHeight + 'px';
var image = side.querySelector('img');
if (image) image.style.height = sideImageHeight + 'px';
});
syncViewport();
}
function userID(value) {
return t('roomReward.idPrefix', 'ID:') + ' ' + (value || '—');
}
function setAvatar(source) {
source = String(source || '').trim();
if (!source) {
avatar.hidden = true;
avatar.removeAttribute('src');
return;
}
avatar.hidden = false;
avatar.src = source;
}
function renderUser(data) {
var user = data && data.user ? data.user : {};
document.getElementById('rrTargetValue').textContent = data
? plainWholeNumber(data.targetReward)
: '—';
document.getElementById('rrUserName').textContent = data
? user.name || t('roomReward.roomOwner', 'Room owner')
: '—';
document.getElementById('rrUserId').textContent = userID(
data ? user.id : ''
);
document.getElementById('rrContributionValue').textContent = data
? wholeNumber(data.currentCoinSpent)
: '—';
document.getElementById('rrRewardValue').textContent = data
? wholeNumber(data.expectedReward)
: '—';
setAvatar(data ? user.avatar : '');
}
function setStatus(nextState) {
loadState = nextState;
var key = '';
var fallback = '';
if (nextState === 'loading') {
key = 'roomReward.loading';
fallback = 'Loading...';
} else if (nextState === 'empty') {
key = 'roomReward.empty';
fallback = 'No reward tiers yet';
} else if (nextState === 'error') {
key = 'roomReward.loadFailed';
fallback = 'Load failed';
}
if (key) {
statusNode.setAttribute('data-i18n', key);
statusNode.textContent = t(key, fallback);
} else {
statusNode.removeAttribute('data-i18n');
statusNode.textContent = '';
}
tierTable.setAttribute(
'aria-busy',
nextState === 'loading' ? 'true' : 'false'
);
app.setAttribute('data-state', nextState);
}
function render(data) {
currentData = data;
var hasTiers = Boolean(data && data.tiers && data.tiers.length);
setStatus(hasTiers ? 'ready' : 'empty');
renderTable(data ? data.tiers : []);
renderUser(data);
}
function loadData() {
if (useMock) return Promise.resolve(mockData);
// Mock data is opt-in only. A production API failure must remain visible as
// an error state; silently substituting Figma fixture numbers would present
// fabricated room turnover and reward balances to the user.
if (!window.HyAppAPI || !window.HyAppAPI.get) {
return Promise.reject(new Error('room_reward_api_unavailable'));
}
return window.HyAppAPI.get(
'/api/v1/activities/room-turnover-reward'
).then(normalizeAPIData);
}
function syncViewport() {
var viewportWidth =
document.documentElement.clientWidth || window.innerWidth || 375;
var pageWidth = Math.min(viewportWidth, MAX_PAGE_WIDTH);
var scale = pageWidth / DESIGN_WIDTH;
var shellHeight = stageHeight * scale;
var footerHeight = 169 * scale;
document.documentElement.style.setProperty('--rr-scale', String(scale));
document.documentElement.style.setProperty(
'--rr-page-width',
pageWidth + 'px'
);
stageShell.style.width = pageWidth + 'px';
stageShell.style.height = shellHeight + 'px';
footerShell.style.width = pageWidth + 'px';
footerShell.style.height = footerHeight + 'px';
}
function scheduleViewportSync() {
if (resizeFrame) window.cancelAnimationFrame(resizeFrame);
resizeFrame = window.requestAnimationFrame(function () {
resizeFrame = 0;
syncViewport();
});
}
function syncLanguageText() {
document.title = t('roomReward.pageTitle', 'Room Reward Event');
if (currentData) renderUser(currentData);
if (loadState !== 'ready') setStatus(loadState);
}
function init() {
setStatus('loading');
renderTable([]);
renderUser(null);
syncViewport();
avatar.addEventListener('error', function () {
// Broken remote media must not fall through to the Figma mock avatar in
// production; hiding it keeps the identity state honest and stable.
avatar.hidden = true;
});
window.addEventListener('resize', scheduleViewportSync, {
passive: true,
});
window.addEventListener('orientationchange', scheduleViewportSync, {
passive: true,
});
window.addEventListener('hyapp:i18n-ready', syncLanguageText);
loadData()
.then(render)
.catch(function (error) {
console.warn('[fami-room-reward] load failed', error);
currentData = null;
setStatus('error');
renderTable([]);
renderUser(null);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();