diff --git a/activity/invite/assets/generated/copy-icon.png b/activity/invite/assets/generated/copy-icon.png new file mode 100644 index 0000000..73a2c52 Binary files /dev/null and b/activity/invite/assets/generated/copy-icon.png differ diff --git a/activity/invite/assets/generated/invite-input-bg.png b/activity/invite/assets/generated/invite-input-bg.png new file mode 100644 index 0000000..70fb482 Binary files /dev/null and b/activity/invite/assets/generated/invite-input-bg.png differ diff --git a/activity/invite/assets/generated/inviter-card-bg.png b/activity/invite/assets/generated/inviter-card-bg.png new file mode 100644 index 0000000..d2539e4 Binary files /dev/null and b/activity/invite/assets/generated/inviter-card-bg.png differ diff --git a/activity/invite/assets/generated/modal-bg.png b/activity/invite/assets/generated/modal-bg.png new file mode 100644 index 0000000..16619c3 Binary files /dev/null and b/activity/invite/assets/generated/modal-bg.png differ diff --git a/activity/invite/assets/generated/modal-bottom.png b/activity/invite/assets/generated/modal-bottom.png new file mode 100644 index 0000000..9470b07 Binary files /dev/null and b/activity/invite/assets/generated/modal-bottom.png differ diff --git a/activity/invite/assets/generated/modal-button-active.png b/activity/invite/assets/generated/modal-button-active.png new file mode 100644 index 0000000..d8f4945 Binary files /dev/null and b/activity/invite/assets/generated/modal-button-active.png differ diff --git a/activity/invite/assets/generated/modal-button-inactive.png b/activity/invite/assets/generated/modal-button-inactive.png new file mode 100644 index 0000000..f7af1c7 Binary files /dev/null and b/activity/invite/assets/generated/modal-button-inactive.png differ diff --git a/activity/invite/assets/generated/modal-middle.png b/activity/invite/assets/generated/modal-middle.png new file mode 100644 index 0000000..bf97032 Binary files /dev/null and b/activity/invite/assets/generated/modal-middle.png differ diff --git a/activity/invite/assets/generated/modal-top.png b/activity/invite/assets/generated/modal-top.png new file mode 100644 index 0000000..5230b29 Binary files /dev/null and b/activity/invite/assets/generated/modal-top.png differ diff --git a/activity/invite/assets/generated/panel-a-monthly-bg.png b/activity/invite/assets/generated/panel-a-monthly-bg.png new file mode 100644 index 0000000..a51294e Binary files /dev/null and b/activity/invite/assets/generated/panel-a-monthly-bg.png differ diff --git a/activity/invite/assets/generated/panel-a-monthly-quota-bg.png b/activity/invite/assets/generated/panel-a-monthly-quota-bg.png new file mode 100644 index 0000000..1e57e64 Binary files /dev/null and b/activity/invite/assets/generated/panel-a-monthly-quota-bg.png differ diff --git a/activity/invite/assets/generated/panel-a-stats-bg.png b/activity/invite/assets/generated/panel-a-stats-bg.png new file mode 100644 index 0000000..8773b7b Binary files /dev/null and b/activity/invite/assets/generated/panel-a-stats-bg.png differ diff --git a/activity/invite/assets/generated/panel-b-rank-bg.png b/activity/invite/assets/generated/panel-b-rank-bg.png new file mode 100644 index 0000000..5614799 Binary files /dev/null and b/activity/invite/assets/generated/panel-b-rank-bg.png differ diff --git a/activity/invite/assets/generated/reward-success-visual.png b/activity/invite/assets/generated/reward-success-visual.png new file mode 100644 index 0000000..c42217f Binary files /dev/null and b/activity/invite/assets/generated/reward-success-visual.png differ diff --git a/activity/invite/assets/generated/reward-topup-visual.png b/activity/invite/assets/generated/reward-topup-visual.png new file mode 100644 index 0000000..230b0f2 Binary files /dev/null and b/activity/invite/assets/generated/reward-topup-visual.png differ diff --git a/activity/invite/assets/generated/search-icon.png b/activity/invite/assets/generated/search-icon.png new file mode 100644 index 0000000..230817b Binary files /dev/null and b/activity/invite/assets/generated/search-icon.png differ diff --git a/activity/invite/index.html b/activity/invite/index.html index 9435a1d..1ced9be 100644 --- a/activity/invite/index.html +++ b/activity/invite/index.html @@ -7,7 +7,7 @@ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> Invite Friends - +
-
+
- - +2000 - - Invitation received + +1000 + + Successful Invite
+ -
+
- - +1000 - - Top up via friends - + 1% + Friend's Top-Up
@@ -116,31 +106,39 @@ (Copy your invitation link and share it with your friends)

-
- + - + diff --git a/activity/invite/script.js b/activity/invite/script.js index 672babf..296a109 100644 --- a/activity/invite/script.js +++ b/activity/invite/script.js @@ -26,10 +26,15 @@ inviterName: firstParam(SHARE_NAME_KEYS), inviterAvatar: firstParam(SHARE_AVATAR_KEYS), inviteLink: '', + currentUser: null, + pendingInviteCode: '', + searchedInviter: null, inviteRewardStatus: null, rankRows: [], rankLoaded: false, rankLoading: false, + searchingInviter: false, + bindingInviter: false, claiming: {}, }; @@ -49,6 +54,10 @@ return String(value || '').trim(); } + function normalizeInviteCodeInput(value) { + return textValue(value).toUpperCase(); + } + function readPath(source, path) { var value = source; for (var i = 0; i < path.length; i += 1) { @@ -66,6 +75,25 @@ return ''; } + function currentUserPayload(data, profile) { + var target = profile || data || {}; + return Boolean( + target.user_id || + target.userId || + target.display_user_id || + target.displayUserId || + target.username || + target.nickname || + target.profile_completed !== undefined || + target.profileCompleted !== undefined + ); + } + + function mergeCurrentUser(profile) { + if (!profile || typeof profile !== 'object') return; + state.currentUser = Object.assign({}, state.currentUser || {}, profile); + } + function currentLandingURL() { return new URL('../invite-landing/', window.location.href); } @@ -112,6 +140,7 @@ function applyCurrentUser(payload) { var data = readOverviewPayload(payload || {}); var profile = data.profile || data; + if (currentUserPayload(data, profile)) mergeCurrentUser(profile); var inviteCode = firstPath(data, [ ['invite', 'my_invite_code'], ['invite', 'myInviteCode'], @@ -171,6 +200,13 @@ .then(applyCurrentUser) .catch(function () {}); } + function loadMeProfile() { + if (!hasToken || !userAPI || !userAPI.me) return Promise.resolve(); + return userAPI + .me() + .then(applyCurrentUser) + .catch(function () {}); + } if (window.HyAppParams && window.HyAppParams.loadUser) { return userPromise @@ -179,7 +215,8 @@ }) .then(function (current) { if (current && current.user) applyCurrentUser(current.user); - }); + }) + .then(loadMeProfile); } if (userAPI) { if (userAPI.overview) { @@ -187,17 +224,14 @@ .then(function () { return userAPI.overview(); }) - .then(applyCurrentUser); + .then(applyCurrentUser) + .then(loadMeProfile); } if (userAPI.me) { - return userPromise - .then(function () { - return userAPI.me(); - }) - .then(applyCurrentUser); + return userPromise.then(loadMeProfile); } } - return userPromise; + return userPromise.then(loadMeProfile); } function toast(message) { @@ -583,6 +617,20 @@ state.inviteRewardStatus = normalizeInviteRewardStatus(status); var current = state.inviteRewardStatus || {}; var progress = field(current, 'progress', 'progress') || {}; + var perInviteReward = numberValue( + field( + current, + 'per_invite_inviter_reward_coin_amount', + 'perInviteInviterRewardCoinAmount' + ) + ); + + if (perInviteReward > 0) { + setText( + '#rewardInviteAmount', + '+' + formatNumber(perInviteReward) + '(Max40,000)' + ); + } if (state.inviteRewardStatus) { setText( @@ -766,6 +814,247 @@ copyTextFrom('#inviteCode'); } + function firstObjectValue(source, keys) { + source = source || {}; + for (var i = 0; i < keys.length; i += 1) { + var value = source[keys[i]]; + if (value !== undefined && value !== null && value !== '') { + return value; + } + } + return ''; + } + + function openBindModal() { + var api = window.HyAppAPI || {}; + if (!api.getAccessToken || !api.getAccessToken()) { + toast(t('invite.loginRequired', 'Please log in first')); + return; + } + state.pendingInviteCode = ''; + state.searchedInviter = null; + state.searchingInviter = false; + state.bindingInviter = false; + var input = $('#bindInviteInput'); + if (input) input.value = ''; + showBindInput(); + $('#bindInviteModal').hidden = false; + window.setTimeout(function () { + if (input) input.focus(); + }, 0); + } + + function closeBindModal() { + $('#bindInviteModal').hidden = true; + state.searchingInviter = false; + state.bindingInviter = false; + renderBindActions(); + } + + function showBindInput() { + $('#bindInputCard').hidden = false; + $('#bindResultCard').hidden = true; + state.searchedInviter = null; + renderBindActions(); + } + + function showBindResult(inviter) { + var avatar = $('#inviterAvatar'); + var name = $('#inviterName'); + var displayID = $('#inviterDisplayId'); + if (avatar) { + avatar.src = inviter.avatar || './assets/rank-avatar.png'; + avatar.onerror = function () { + avatar.onerror = null; + avatar.src = './assets/rank-avatar.png'; + }; + } + if (name) name.textContent = inviter.name || '--'; + if (displayID) displayID.textContent = inviter.displayUserID || '--'; + $('#bindInputCard').hidden = true; + $('#bindResultCard').hidden = false; + renderBindActions(); + } + + function renderBindActions() { + var cancel = $('#bindCancelButton'); + var confirm = $('#confirmBindButton'); + var input = $('#bindInviteInput'); + var busy = state.searchingInviter || state.bindingInviter; + if (confirm) { + confirm.disabled = busy; + confirm.textContent = state.bindingInviter + ? t('invite.binding', 'Binding...') + : state.searchingInviter + ? t('invite.searchingInviter', 'Searching...') + : t('invite.confirm', 'Confirm'); + } + if (cancel) cancel.disabled = busy; + if (input) input.disabled = busy; + } + + function normalizeInviter(payload, fallbackDisplayUserID) { + payload = payload || {}; + var wrapper = payload.inviter || payload.Inviter || payload; + var identity = payload.identity || payload.Identity || payload; + var profile = + payload.profile || + payload.Profile || + wrapper.profile || + wrapper.Profile || + wrapper; + var userID = + textValue(firstObjectValue(profile, ['user_id', 'userId'])) || + textValue(firstObjectValue(identity, ['user_id', 'userId'])); + var displayUserID = + textValue( + firstObjectValue(profile, [ + 'pretty_display_user_id', + 'prettyDisplayUserId', + 'display_user_id', + 'displayUserId', + ]) + ) || + textValue( + firstObjectValue(identity, [ + 'pretty_display_user_id', + 'prettyDisplayUserId', + 'display_user_id', + 'displayUserId', + ]) + ) || + fallbackDisplayUserID; + var name = + textValue( + firstObjectValue(profile, ['username', 'nickname', 'name']) + ) || displayUserID; + var avatar = textValue( + firstObjectValue(profile, ['avatar', 'avatar_url', 'avatarUrl']) + ); + if (!userID && !displayUserID) return null; + return { + userID: userID, + displayUserID: displayUserID, + name: name, + avatar: avatar, + }; + } + + function isCurrentUser(inviter) { + var current = state.currentUser || {}; + var currentUserID = textValue( + firstObjectValue(current, ['user_id', 'userId']) + ); + var currentDisplayID = textValue( + firstObjectValue(current, [ + 'pretty_display_user_id', + 'prettyDisplayUserId', + 'display_user_id', + 'displayUserId', + ]) + ); + return Boolean( + (currentUserID && currentUserID === inviter.userID) || + (currentDisplayID && currentDisplayID === inviter.displayUserID) + ); + } + + function searchInviter() { + if (state.searchingInviter) return; + var input = $('#bindInviteInput'); + var inviteCode = normalizeInviteCodeInput(input && input.value); + var api = window.HyAppAPI && window.HyAppAPI.user; + if (input && input.value !== inviteCode) input.value = inviteCode; + if (!inviteCode) { + toast( + t('invite.searchRequired', 'Please enter an invitation code') + ); + return; + } + if (!api || !api.searchInviteReferrer) { + toast(t('invite.bindFailed', 'Bind failed')); + return; + } + state.searchingInviter = true; + state.pendingInviteCode = inviteCode; + renderBindActions(); + // 搜索入口只提交邀请码,由 server 解析归属并按当前用户国家过滤,前端不再用可被篡改的用户 ID 作为绑定入参。 + api.searchInviteReferrer(inviteCode) + .then(function (payload) { + var inviter = normalizeInviter(payload, ''); + state.searchingInviter = false; + if (!inviter) { + renderBindActions(); + toast(t('invite.inviterNotFound', 'Inviter not found')); + return; + } + if (isCurrentUser(inviter)) { + renderBindActions(); + toast( + t( + 'invite.cannotBindSelf', + 'You cannot bind yourself as inviter' + ) + ); + return; + } + inviter.inviteCode = inviteCode; + state.searchedInviter = inviter; + showBindResult(inviter); + }) + .catch(function (error) { + state.searchingInviter = false; + renderBindActions(); + toast( + (error && error.message) || + t('invite.inviterNotFound', 'Inviter not found') + ); + }); + } + + function confirmBindInviter() { + if (state.bindingInviter) return; + var api = window.HyAppAPI && window.HyAppAPI.user; + var inviter = state.searchedInviter; + if (!inviter) { + searchInviter(); + return; + } + if (!api || !api.bindInviteReferrer) { + toast(t('invite.bindFailed', 'Bind failed')); + return; + } + state.bindingInviter = true; + renderBindActions(); + api.bindInviteReferrer(inviter.inviteCode || state.pendingInviteCode) + .then(function (response) { + var invite = response && response.invite; + state.bindingInviter = false; + renderBindActions(); + if (invite && (invite.bound || invite.invite_code)) { + toast(t('invite.bindSuccess', 'Bound successfully')); + closeBindModal(); + loadCurrentUser().catch(function () {}); + loadInviteActivityReward(); + return; + } + toast( + t( + 'invite.bindNoChange', + 'Invite referrer is already bound.' + ) + ); + }) + .catch(function (error) { + state.bindingInviter = false; + renderBindActions(); + toast( + (error && error.message) || + t('invite.bindFailed', 'Bind failed') + ); + }); + } + function bindEvents() { document.querySelectorAll('.task-tab').forEach(function (button) { button.addEventListener('click', function () { @@ -774,6 +1063,20 @@ }); $('#copyLinkButton').addEventListener('click', copyInviteLink); $('#copyCodeButton').addEventListener('click', copyInviteCode); + $('#bindInviteButton').addEventListener('click', openBindModal); + $('#bindCancelButton').addEventListener('click', closeBindModal); + $('#confirmBindButton').addEventListener('click', confirmBindInviter); + $('#bindInviteModal').addEventListener('click', function (event) { + if (event.target === $('#bindInviteModal')) closeBindModal(); + }); + $('#bindInviteInput').addEventListener('keydown', function (event) { + if (event.key === 'Enter') confirmBindInviter(); + }); + $('#bindInviteInput').addEventListener('input', function (event) { + var nextValue = normalizeInviteCodeInput(event.target.value); + if (event.target.value !== nextValue) + event.target.value = nextValue; + }); document .querySelectorAll('.copy-task-button') .forEach(function (button) { @@ -798,6 +1101,7 @@ window.addEventListener('hyapp:i18n-ready', function () { renderRank(); + renderBindActions(); renderInviteActivityReward(state.inviteRewardStatus); }); diff --git a/activity/invite/style.css b/activity/invite/style.css index 94dc5c1..bd20eea 100644 --- a/activity/invite/style.css +++ b/activity/invite/style.css @@ -171,18 +171,8 @@ button { width: calc(1016 * var(--u)); margin: 0 auto; overflow: hidden; - background-image: - url('./assets/panel-top-a.png'), url('./assets/panel-bottom-a.png'), - url('./assets/panel-middle-a.png'); - background-repeat: no-repeat, no-repeat, no-repeat; - background-position: - top center, - bottom center, - center center; - background-size: - 100% auto, - 100% auto, - 100% 100%; + background-position: center; + background-size: 100% 100%; } .ornate-panel > * { @@ -211,9 +201,9 @@ p { } .reward-panel { - height: calc(790 * var(--u)); + height: calc(770 * var(--u)); margin-top: calc(20 * var(--u)); - padding-top: calc(122 * var(--u)); + padding-top: calc(104 * var(--u)); background-image: url('./assets/reward-panel-bg.png'); background-position: center; background-size: 100% 100%; @@ -232,73 +222,75 @@ p { .reward-grid { position: relative; width: calc(1016 * var(--u)); - height: calc(486 * var(--u)); - margin: calc(28 * var(--u)) auto 0; + height: calc(450 * var(--u)); + margin: calc(34 * var(--u)) auto 0; } .reward-item { position: absolute; top: 0; - width: calc(486 * var(--u)); - height: calc(486 * var(--u)); - color: var(--gold); + width: calc(420 * var(--u)); + height: calc(450 * var(--u)); + color: #ffd133; text-align: center; } -.reward-item:first-child { - left: calc(8 * var(--u)); +.reward-item-success { + left: calc(54 * var(--u)); } -.reward-item:last-child { - left: calc(522 * var(--u)); +.reward-item-topup { + left: calc(560 * var(--u)); } -.reward-orb { +.reward-visual { position: absolute; z-index: 0; display: block; - inset: 0; - width: calc(486 * var(--u)); - height: calc(486 * var(--u)); + left: 50%; + top: 0; + width: calc(370 * var(--u)); + max-width: none; + transform: translateX(-50%); + object-fit: contain; } -.reward-gold { - position: absolute; - z-index: 1; - top: 50%; - left: 50%; - width: calc(178 * var(--u)); - transform: translate(-50%, -50%); +.reward-item-topup .reward-visual { + width: calc(382 * var(--u)); } .reward-item strong { position: absolute; - top: calc(98 * var(--u)); - left: calc(290 * var(--u)); - font-size: calc(40 * var(--u)); + top: calc(338 * var(--u)); + left: 50%; + display: block; + width: calc(520 * var(--u)); + transform: translateX(-50%); + font-size: calc(42 * var(--u)); line-height: 1; white-space: nowrap; } .reward-item span { position: absolute; - top: calc(421 * var(--u)); + top: calc(404 * var(--u)); left: 50%; display: block; - width: calc(360 * var(--u)); + width: calc(430 * var(--u)); transform: translateX(-50%); - color: var(--gold); - font-size: calc(30 * var(--u)); + color: var(--white); + font-size: calc(34 * var(--u)); + font-weight: 700; line-height: 1.2; } .plus { position: absolute; - top: calc(207 * var(--u)); + top: calc(142 * var(--u)); left: calc(472 * var(--u)); display: block; - width: calc(71 * var(--u)); - height: calc(71 * var(--u)); + width: calc(78 * var(--u)); + height: calc(78 * var(--u)); } .invite-copy { @@ -319,25 +311,35 @@ p { .invite-fields { display: grid; - grid-template-columns: minmax(0, 1fr) calc(286 * var(--u)); - gap: calc(24 * var(--u)); - width: calc(760 * var(--u)); + gap: calc(34 * var(--u)); + width: calc(1016 * var(--u)); margin: calc(58 * var(--u)) auto 0; } +.invite-field { + position: relative; + height: calc(93 * var(--u)); + background: url('./assets/link-input.svg') center / 100% 100% no-repeat; +} + .link-box { display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; min-width: 0; - height: calc(85 * var(--u)); - padding: 0 calc(28 * var(--u)); + height: 100%; + padding: 0 calc(112 * var(--u)) 0 calc(52 * var(--u)); overflow: hidden; color: var(--gold); font-size: calc(30 * var(--u)); + font-weight: 700; text-overflow: ellipsis; white-space: nowrap; - background: url('./assets/link-input.svg') center / 100% 100% no-repeat; +} + +.invite-field.is-code .link-box { + justify-content: center; + padding-left: calc(112 * var(--u)); } .invite-code { @@ -345,13 +347,20 @@ p { letter-spacing: calc(1 * var(--u)); } -.invite-actions { - display: flex; - width: calc(764 * var(--u)); - align-items: center; - justify-content: center; - gap: calc(24 * var(--u)); - margin: calc(50 * var(--u)) auto 0; +.copy-icon-button { + position: absolute; + top: 50%; + right: calc(42 * var(--u)); + width: calc(52 * var(--u)); + height: calc(52 * var(--u)); + cursor: pointer; + transform: translateY(-50%); + background: url('./assets/generated/copy-icon.png') center / 100% 100% + no-repeat; +} + +.copy-icon-button:active { + transform: translateY(-50%) scale(0.96); } .ornate-button { @@ -374,19 +383,8 @@ p { transform: translateY(calc(2 * var(--u))); } -.invite-actions .ornate-button { - flex: 0 0 calc(520 * var(--u)); - width: calc(520 * var(--u)); - height: calc(100 * var(--u)); - font-size: calc(42 * var(--u)); -} - -.invite-actions .small-copy-button { - flex: 0 0 calc(220 * var(--u)); - width: calc(220 * var(--u)); - height: calc(78 * var(--u)); - font-size: calc(30 * var(--u)); - background-image: url('./assets/claim-active.png'); +.bind-invite-button { + margin-top: calc(68 * var(--u)); } .stats-panel { @@ -397,6 +395,7 @@ p { height: calc(520 * var(--u)); margin-top: calc(62 * var(--u)); padding: calc(108 * var(--u)) calc(92 * var(--u)) calc(92 * var(--u)); + background-image: url('./assets/generated/panel-a-stats-bg.png'); } .stat { @@ -428,10 +427,12 @@ p { margin-top: calc(34 * var(--u)); margin-bottom: calc(32 * var(--u)); padding: calc(126 * var(--u)) calc(78 * var(--u)) calc(70 * var(--u)); + background-image: url('./assets/generated/panel-a-monthly-bg.png'); } .monthly-panel.is-quota { min-height: calc(1375 * var(--u)); + background-image: url('./assets/generated/panel-a-monthly-quota-bg.png'); } .monthly-panel h2, @@ -610,9 +611,7 @@ p { margin-top: calc(42 * var(--u)); margin-bottom: calc(28 * var(--u)); padding: calc(126 * var(--u)) calc(68 * var(--u)) calc(72 * var(--u)); - background-image: - url('./assets/panel-top-b.png'), url('./assets/panel-bottom-b.png'), - url('./assets/panel-middle-b.png'); + background-image: url('./assets/generated/panel-b-rank-bg.png'); } .rank-list { @@ -687,6 +686,156 @@ p { display: none; } +.invite-modal { + position: fixed; + z-index: 1000; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: calc(28 * var(--u)); + background: rgba(0, 0, 0, 0.68); +} + +.bind-card { + position: relative; + width: min(calc(1016 * var(--u)), calc(100vw - 24px)); + min-height: calc(786 * var(--u)); + padding: calc(104 * var(--u)) calc(90 * var(--u)) calc(78 * var(--u)); + overflow: hidden; + background: url('./assets/generated/modal-bg.png') center / 100% 100% + no-repeat; +} + +.bind-card > * { + position: relative; + z-index: 1; +} + +.bind-card h2 { + color: var(--gold); + font-size: calc(60 * var(--u)); + line-height: 1; + font-weight: 800; + text-align: center; + text-shadow: 0 calc(2 * var(--u)) calc(4 * var(--u)) rgba(0, 0, 0, 0.35); +} + +.bind-input-state, +.bind-result-state { + width: calc(827 * var(--u)); + max-width: 100%; + margin: calc(70 * var(--u)) auto 0; +} + +.bind-input-shell { + position: relative; + display: flex; + align-items: center; + height: calc(93 * var(--u)); + background: url('./assets/generated/invite-input-bg.png') center / 100% 100% + no-repeat; +} + +.bind-input-shell input { + display: block; + width: 100%; + height: 100%; + padding: 0 calc(124 * var(--u)) 0 calc(36 * var(--u)); + border: 0; + outline: 0; + color: var(--gold); + font: + 800 calc(30 * var(--u)) / 1 'Source Han Sans SC', + 'Noto Sans', + 'Helvetica Neue', + Arial, + sans-serif; + text-align: left; + background: url('./assets/generated/search-icon.png') right + calc(44 * var(--u)) center / calc(52 * var(--u)) calc(49 * var(--u)) + no-repeat; +} + +.bind-input-shell input::placeholder { + color: rgba(246, 224, 178, 0.55); +} + +.inviter-card { + display: grid; + grid-template-columns: calc(122 * var(--u)) minmax(0, 1fr); + align-items: center; + gap: calc(28 * var(--u)); + height: calc(152 * var(--u)); + padding: 0 calc(42 * var(--u)); + background: url('./assets/generated/inviter-card-bg.png') center / 100% 100% + no-repeat; +} + +.inviter-card img { + width: calc(104 * var(--u)); + height: calc(104 * var(--u)); + border: calc(2 * var(--u)) solid #ffebbb; + border-radius: 999px; + object-fit: cover; +} + +.inviter-card strong, +.inviter-card span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.inviter-card strong { + color: var(--white); + font-size: calc(36 * var(--u)); + line-height: 1.1; + font-weight: 800; +} + +.inviter-card span { + margin-top: calc(14 * var(--u)); + color: var(--gold); + font-size: calc(28 * var(--u)); + line-height: 1; +} + +.bind-actions { + display: grid; + grid-template-columns: repeat(2, calc(441 * var(--u))); + justify-content: center; + gap: calc(38 * var(--u)); + margin-top: calc(152 * var(--u)); +} + +.bind-action-button { + width: 100%; + height: calc(120 * var(--u)); + color: #6f2d12; + font-size: calc(34 * var(--u)); + line-height: 1; + font-weight: 900; + text-align: center; + text-shadow: 0 calc(1 * var(--u)) calc(1 * var(--u)) + rgba(255, 255, 255, 0.45); + background: url('./assets/generated/modal-button-active.png') center / 100% + 100% no-repeat; + cursor: pointer; +} + +.bind-cancel-button { + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 calc(2 * var(--u)) calc(2 * var(--u)) rgba(0, 0, 0, 0.28); + background-image: url('./assets/generated/modal-button-inactive.png'); +} + +.bind-action-button:disabled { + cursor: default; + filter: grayscale(0.25) brightness(0.86); +} + [hidden] { display: none !important; } diff --git a/common/api.js b/common/api.js index 9e57c15..31079d9 100644 --- a/common/api.js +++ b/common/api.js @@ -541,6 +541,12 @@ var default_api = 'https://api.global-interaction.com/'; body: payload || {}, }); }, + completeOnboarding: function (payload) { + return request('/api/v1/users/me/onboarding/complete', { + method: 'POST', + body: payload || {}, + }); + }, resolveDisplayUserID: function (displayUserID) { return request( '/api/v1/users/by-display-user-id/' + @@ -550,6 +556,18 @@ var default_api = 'https://api.global-interaction.com/'; } ); }, + searchInviteReferrer: function (inviteCode) { + return request('/api/v1/users/me/invite-referrer/search', { + method: 'GET', + query: { invite_code: inviteCode || '' }, + }); + }, + bindInviteReferrer: function (inviteCode) { + return request('/api/v1/users/me/invite-referrer/bind', { + method: 'POST', + body: { invite_code: String(inviteCode || '') }, + }); + }, }; var walletAPI = { diff --git a/common/locales/ar.json b/common/locales/ar.json index a3373de..3094215 100644 --- a/common/locales/ar.json +++ b/common/locales/ar.json @@ -133,6 +133,32 @@ "invite.noRankData": "لا توجد بيانات ترتيب", "invite.copied": "تم النسخ", "invite.copyFailed": "فشل النسخ", + "invite.successfulInvite": "دعوة ناجحة", + "invite.friendTopUp": "شحن الصديق", + "invite.copyInviteLink": "نسخ رابط الدعوة", + "invite.copyInviteCode": "نسخ رمز الدعوة", + "invite.linkInviter": "ربط الداعي", + "invite.bindInviteTitle": "ربط الداعي", + "invite.bindInviteHint": "أدخل رمز الدعوة ثم أكد الداعي قبل الربط.", + "invite.bindInvitePlaceholder": "أدخل رمز الدعوة", + "invite.searchInviter": "بحث", + "invite.searchingInviter": "جار البحث...", + "invite.inviterFoundTitle": "تأكيد الداعي", + "invite.bindInviteConfirmHint": "يرجى التحقق من معلومات الداعي قبل الربط.", + "invite.confirmBind": "ربط", + "invite.cancel": "إلغاء", + "invite.confirm": "تأكيد", + "invite.binding": "جار الربط...", + "invite.back": "رجوع", + "invite.close": "إغلاق", + "invite.inviterNotFound": "لم يتم العثور على الداعي", + "invite.loginRequired": "يرجى تسجيل الدخول أولا", + "invite.searchRequired": "أدخل رمز الدعوة", + "invite.cannotBindSelf": "لا يمكنك ربط نفسك كداع", + "invite.bindProfileRequired": "لا يمكن ربط هذا الحساب هنا.", + "invite.bindSuccess": "تم الربط بنجاح", + "invite.bindNoChange": "تم ربط الداعي بهذا الحساب بالفعل.", + "invite.bindFailed": "فشل الربط", "host_center.title": "مركز المضيف", "host_center.back": "رجوع", "host_center.change_language": "تغيير اللغة", diff --git a/common/locales/en.json b/common/locales/en.json index 45a30af..65e9c42 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -133,6 +133,32 @@ "invite.noRankData": "No rank data", "invite.copied": "Copied", "invite.copyFailed": "Copy failed", + "invite.successfulInvite": "Successful Invite", + "invite.friendTopUp": "Friend's Top-Up", + "invite.copyInviteLink": "Copy invitation link", + "invite.copyInviteCode": "Copy invitation code", + "invite.linkInviter": "Link Inviter", + "invite.bindInviteTitle": "Bind Referrer", + "invite.bindInviteHint": "Enter an invitation code and confirm the referrer before binding.", + "invite.bindInvitePlaceholder": "Enter Invitation Code", + "invite.searchInviter": "Search", + "invite.searchingInviter": "Searching...", + "invite.inviterFoundTitle": "Confirm inviter", + "invite.bindInviteConfirmHint": "Please check the inviter information before binding.", + "invite.confirmBind": "Bind", + "invite.cancel": "Cancel", + "invite.confirm": "Confirm", + "invite.binding": "Binding...", + "invite.back": "Back", + "invite.close": "Close", + "invite.inviterNotFound": "Inviter not found", + "invite.loginRequired": "Please log in first", + "invite.searchRequired": "Please enter an invitation code", + "invite.cannotBindSelf": "You cannot bind yourself as inviter", + "invite.bindProfileRequired": "Current account cannot bind here.", + "invite.bindSuccess": "Bound successfully", + "invite.bindNoChange": "Invite referrer is already bound.", + "invite.bindFailed": "Bind failed", "host_center.title": "Host Center", "host_center.back": "Back", "host_center.change_language": "Change language", diff --git a/common/locales/es.json b/common/locales/es.json index 23b0a64..185a16e 100644 --- a/common/locales/es.json +++ b/common/locales/es.json @@ -133,6 +133,32 @@ "invite.noRankData": "No hay datos de ranking", "invite.copied": "Copiado", "invite.copyFailed": "Error al copiar", + "invite.successfulInvite": "Invitación exitosa", + "invite.friendTopUp": "Recarga del amigo", + "invite.copyInviteLink": "Copiar enlace de invitación", + "invite.copyInviteCode": "Copiar código de invitación", + "invite.linkInviter": "Vincular invitador", + "invite.bindInviteTitle": "Vincular invitador", + "invite.bindInviteHint": "Ingresa el código de invitación y confirma el invitador antes de vincular.", + "invite.bindInvitePlaceholder": "Ingresa el código de invitación", + "invite.searchInviter": "Buscar", + "invite.searchingInviter": "Buscando...", + "invite.inviterFoundTitle": "Confirmar invitador", + "invite.bindInviteConfirmHint": "Verifica la información del invitador antes de vincular.", + "invite.confirmBind": "Vincular", + "invite.cancel": "Cancelar", + "invite.confirm": "Confirmar", + "invite.binding": "Vinculando...", + "invite.back": "Volver", + "invite.close": "Cerrar", + "invite.inviterNotFound": "Invitador no encontrado", + "invite.loginRequired": "Inicia sesión primero", + "invite.searchRequired": "Ingresa un código de invitación", + "invite.cannotBindSelf": "No puedes vincularte como invitador", + "invite.bindProfileRequired": "Esta cuenta no puede vincular aquí.", + "invite.bindSuccess": "Vinculado correctamente", + "invite.bindNoChange": "Esta cuenta ya tiene un invitador vinculado.", + "invite.bindFailed": "Error al vincular", "host_center.title": "Centro de anfitrión", "host_center.back": "Volver", "host_center.change_language": "Cambiar idioma", diff --git a/common/locales/id.json b/common/locales/id.json index f575803..ee9b62b 100644 --- a/common/locales/id.json +++ b/common/locales/id.json @@ -100,6 +100,32 @@ "invite.noRankData": "Belum ada data peringkat", "invite.copied": "Disalin", "invite.copyFailed": "Gagal menyalin", + "invite.successfulInvite": "Undangan Berhasil", + "invite.friendTopUp": "Top up teman", + "invite.copyInviteLink": "Salin tautan undangan", + "invite.copyInviteCode": "Salin kode undangan", + "invite.linkInviter": "Tautkan pengundang", + "invite.bindInviteTitle": "Tautkan pengundang", + "invite.bindInviteHint": "Masukkan kode undangan, lalu konfirmasi pengundang sebelum menautkan.", + "invite.bindInvitePlaceholder": "Masukkan kode undangan", + "invite.searchInviter": "Cari", + "invite.searchingInviter": "Mencari...", + "invite.inviterFoundTitle": "Konfirmasi pengundang", + "invite.bindInviteConfirmHint": "Periksa informasi pengundang sebelum menautkan.", + "invite.confirmBind": "Tautkan", + "invite.cancel": "Batal", + "invite.confirm": "Konfirmasi", + "invite.binding": "Menautkan...", + "invite.back": "Kembali", + "invite.close": "Tutup", + "invite.inviterNotFound": "Pengundang tidak ditemukan", + "invite.loginRequired": "Silakan masuk terlebih dahulu", + "invite.searchRequired": "Masukkan kode undangan", + "invite.cannotBindSelf": "Kamu tidak bisa menautkan dirimu sebagai pengundang", + "invite.bindProfileRequired": "Akun ini tidak dapat ditautkan di sini.", + "invite.bindSuccess": "Berhasil ditautkan", + "invite.bindNoChange": "Akun ini sudah memiliki pengundang.", + "invite.bindFailed": "Gagal menautkan", "host_center.title": "Pusat Host", "host_center.back": "Kembali", "host_center.change_language": "Ganti bahasa", diff --git a/common/locales/tr.json b/common/locales/tr.json index e8b9b93..2c1a8d4 100644 --- a/common/locales/tr.json +++ b/common/locales/tr.json @@ -100,6 +100,32 @@ "invite.noRankData": "Sıralama verisi yok", "invite.copied": "Kopyalandı", "invite.copyFailed": "Kopyalama başarısız", + "invite.successfulInvite": "Başarılı Davet", + "invite.friendTopUp": "Arkadaş yüklemesi", + "invite.copyInviteLink": "Davet bağlantısını kopyala", + "invite.copyInviteCode": "Davet kodunu kopyala", + "invite.linkInviter": "Davetçiyi bağla", + "invite.bindInviteTitle": "Davetçiyi bağla", + "invite.bindInviteHint": "Davet kodunu gir ve bağlamadan önce davetçiyi onayla.", + "invite.bindInvitePlaceholder": "Davet kodu gir", + "invite.searchInviter": "Ara", + "invite.searchingInviter": "Aranıyor...", + "invite.inviterFoundTitle": "Davetçiyi onayla", + "invite.bindInviteConfirmHint": "Bağlamadan önce davetçi bilgisini kontrol et.", + "invite.confirmBind": "Bağla", + "invite.cancel": "İptal", + "invite.confirm": "Onayla", + "invite.binding": "Bağlanıyor...", + "invite.back": "Geri", + "invite.close": "Kapat", + "invite.inviterNotFound": "Davetçi bulunamadı", + "invite.loginRequired": "Önce giriş yap", + "invite.searchRequired": "Davet kodu gir", + "invite.cannotBindSelf": "Kendini davetçi olarak bağlayamazsın", + "invite.bindProfileRequired": "Bu hesap burada bağlanamaz.", + "invite.bindSuccess": "Başarıyla bağlandı", + "invite.bindNoChange": "Bu hesap zaten bir davetçiye bağlı.", + "invite.bindFailed": "Bağlama başarısız", "host_center.title": "Host Merkezi", "host_center.back": "Geri", "host_center.change_language": "Dili değiştir", diff --git a/common/locales/zh.json b/common/locales/zh.json index 0176bd0..19cb1d7 100644 --- a/common/locales/zh.json +++ b/common/locales/zh.json @@ -100,6 +100,32 @@ "invite.noRankData": "暂无排行榜数据", "invite.copied": "已复制", "invite.copyFailed": "复制失败", + "invite.successfulInvite": "成功邀请", + "invite.friendTopUp": "好友充值", + "invite.copyInviteLink": "复制邀请链接", + "invite.copyInviteCode": "复制邀请码", + "invite.linkInviter": "绑定邀请人", + "invite.bindInviteTitle": "绑定邀请人", + "invite.bindInviteHint": "输入邀请码,确认邀请人后再绑定。", + "invite.bindInvitePlaceholder": "输入邀请码", + "invite.searchInviter": "搜索", + "invite.searchingInviter": "搜索中...", + "invite.inviterFoundTitle": "确认邀请人", + "invite.bindInviteConfirmHint": "绑定前请确认邀请人信息。", + "invite.confirmBind": "绑定", + "invite.cancel": "取消", + "invite.confirm": "确认", + "invite.binding": "绑定中...", + "invite.back": "返回", + "invite.close": "关闭", + "invite.inviterNotFound": "未找到邀请人", + "invite.loginRequired": "请先登录", + "invite.searchRequired": "请输入邀请码", + "invite.cannotBindSelf": "不能绑定自己为邀请人", + "invite.bindProfileRequired": "当前账号不能在这里绑定。", + "invite.bindSuccess": "绑定成功", + "invite.bindNoChange": "当前账号已绑定邀请人。", + "invite.bindFailed": "绑定失败", "host_center.title": "主播中心", "host_center.back": "返回", "host_center.change_language": "切换语言",