(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(); } })();