434 lines
14 KiB
JavaScript
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();
|
|
}
|
|
})();
|