!DOCTYPE html
html lang=en
head
meta charset=UTF-8
meta name=viewport content=width=device-width, initial-scale=1.0
titleBadgeQuesttitle
!-- BADGE CONFIGURATION INJECTION POINT - SERVER WILL INJECT SCRIPTS HERE --
!-- Example injection
script
window.BADGE_CONFIG = [
{
title CheckMate,
shortName hasChecking,
iconUrl httpswww.dixiescripts.orgiconschecking.svg,
ctaUrl httpsexample.comchecking,
modalMarkDown ## CheckMatenOpen a checking account...,
points 250,
procedureName hasChecking,
enabled true
}
];
window.ADMIN_CONFIG = {
admin { gameName BadgeQuest, creditUnionName Example CU },
redemption { shareDeposit { requiredPoints 500, depositAmount 25 } },
engagementInfo { dailyRewards true, minimumPoints 10, maxPoints 100 }
};
window.MEMBER_BADGE_DATA = {
MemberInfo { PointsEarned 350 },
Badges [{ BadgeID hasChecking, earned true }]
};
script
--
script defer src=httpscdn.jsdelivr.netnpmalpinejs@3.x.xdistcdn.min.jsscript
script src=httpscdn.jsdelivr.netnpmmarkedmarked.min.jsscript
link href=httpscdn.jsdelivr.netnpmbootstrap-icons@1.11.0fontbootstrap-icons.css rel=stylesheet
link rel=preconnect href=httpsfonts.googleapis.com
link rel=preconnect href=httpsfonts.gstatic.com crossorigin
link href=httpsfonts.googleapis.comcss2family=Interwght@300;400;500;600;700&family=Orbitronwght@400;700;900&display=swap rel=stylesheet
style
[x-cloak] { display none !important; }
Modern Design System
root {
--primary-gradient linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--danger-gradient linear-gradient(135deg, #fa709a 0%, #fee140 100%);
--warning-gradient linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
--gold-gradient linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
--emerald-gradient linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
--purple-gradient linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--pink-gradient linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--glassmorphism rgba(255, 255, 255, 0.1);
--shadow-soft 0 8px 32px rgba(0, 0, 0, 0.3);
--shadow-hard 0 4px 20px rgba(0, 0, 0, 0.4);
--border-radius 16px;
--border-radius-sm 8px;
--transition all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
{
transition var(--transition);
box-sizing border-box;
}
html {
overflow-x hidden;
}
body {
background linear-gradient(135deg, #0b0c10 0%, #1a1a2e 50%, #16213e 100%);
display flex;
justify-content center;
align-items center;
height 100vh;
margin 0;
font-family 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
position relative;
overflow-x hidden;
min-height 100vh;
box-sizing border-box;
}
bodybefore {
content '';
position fixed;
top 0;
left 0;
width 100%;
height 100%;
background
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(0, 255, 136, 0.15) 0%, transparent 50%);
pointer-events none;
z-index -1;
animation float 20s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform translateY(0px) rotate(0deg); }
50% { transform translateY(-20px) rotate(0.5deg); }
}
.wheel-container {
perspective 1000px;
width min(340px, 85vw);
height min(230px, 60vw);
max-width 340px;
max-height 230px;
position relative;
touch-action none;
user-select none;
-webkit-user-select none;
margin-bottom 40px;
filter drop-shadow(0 8px 32px rgba(255, 215, 0, 0.2));
}
.wheel-wrapper {
width 100%;
height 100%;
transform-style preserve-3d;
transform rotateX(-10deg);
position absolute;
}
.wheel {
width 100%;
height 100%;
transform-style preserve-3d;
transition transform 0.1s linear;
position absolute;
}
.badge {
position absolute;
width 110px;
height 110px;
background rgba(34, 34, 34, 0.8);
backdrop-filter blur(20px);
border-radius 50%;
border 2px solid rgba(255, 215, 0, 0.6);
box-shadow
0 0 20px rgba(255, 215, 0, 0.4),
inset 0 2px 10px rgba(255, 255, 255, 0.1);
display flex;
justify-content center;
align-items center;
left 50%;
top 50%;
margin-left -55px;
margin-top -55px;
cursor pointer;
transition all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.badgehover {
transform scale(1.1);
box-shadow
0 0 30px rgba(255, 215, 0, 0.6),
inset 0 2px 15px rgba(255, 255, 255, 0.2);
border-color rgba(255, 215, 0, 0.8);
}
.badge.completed {
border-color #00ff88;
box-shadow
0 0 20px rgba(0, 255, 136, 0.4),
inset 0 2px 10px rgba(0, 255, 136, 0.1);
background rgba(0, 255, 136, 0.05);
}
.badge.completedhover {
box-shadow
0 0 30px rgba(0, 255, 136, 0.6),
inset 0 2px 15px rgba(0, 255, 136, 0.2);
}
.badge img {
width 90%;
filter drop-shadow(0 0 8px rgba(255,215,0,0.5));
border-radius 50%;
transition all 0.3s ease;
}
.badge.completed img {
filter drop-shadow(0 0 8px rgba(0,255,136,0.5));
}
.tooltip {
position absolute;
bottom -10px;
left 50%;
transform translateX(-50%);
background rgba(0, 0, 0, 0.95);
backdrop-filter blur(20px);
color #fff;
padding 12px 20px;
border-radius var(--border-radius-sm);
font-size 16px;
font-weight 600;
border 2px solid #FFD700;
pointer-events none;
text-align center;
z-index 10;
box-shadow var(--shadow-soft);
min-width 120px;
}
.modal-overlay {
position fixed;
top 0;
left 0;
width 100%;
height 100%;
background rgba(0, 0, 0, 0.8);
backdrop-filter blur(10px);
display flex;
justify-content center;
align-items center;
z-index 20000;
}
.modal-content {
background rgba(31, 31, 31, 0.95);
backdrop-filter blur(20px);
border-radius var(--border-radius);
padding 30px;
position relative;
width 90%;
max-width 600px;
box-shadow var(--shadow-soft);
border 1px solid rgba(255, 255, 255, 0.1);
overflow hidden;
}
.modal-contentbefore {
content '';
position absolute;
top 0;
left 0;
right 0;
height 4px;
background var(--gold-gradient);
}
.modal-title {
color #FFD700;
margin 0 0 20px 0;
font-size 28px;
font-weight 700;
text-align center;
text-shadow 0 2px 10px rgba(255, 215, 0, 0.3);
}
.modal-body {
color #fff;
font-size 16px;
line-height 1.6;
max-height 60vh;
overflow-y auto;
}
.modal-footer {
display flex;
justify-content center;
margin-top 20px;
gap 15px;
}
.modal-cta {
background var(--emerald-gradient);
color #fff;
border none;
border-radius var(--border-radius-sm);
padding 12px 24px;
font-size 16px;
font-weight 600;
cursor pointer;
transition var(--transition);
box-shadow var(--shadow-hard);
text-transform uppercase;
letter-spacing 0.5px;
}
.modal-ctahover {
transform translateY(-2px);
box-shadow 0 8px 25px rgba(0, 255, 136, 0.4);
}
.modal-close {
position absolute;
top 15px;
right 15px;
background rgba(255, 255, 255, 0.1);
border none;
color #fff;
font-size 24px;
cursor pointer;
border-radius 50%;
width 40px;
height 40px;
display flex;
align-items center;
justify-content center;
transition var(--transition);
}
.modal-closehover {
background rgba(255, 255, 255, 0.2);
transform scale(1.1);
}
@media (max-width 480px) {
.wheel-container {
width min(240px, 75vw);
height min(170px, 50vw);
max-width 240px;
max-height 170px;
perspective 500px;
margin-bottom 15px;
}
.badge {
width 55px;
height 55px;
margin-left -27.5px;
margin-top -27.5px;
}
h1 {
font-size 1.4rem !important;
}
}
Additional mobile and tablet responsive styles
@media (max-width 768px) {
body {
padding 5px;
height auto;
min-height 100vh;
}
.main-content-container {
min-height auto !important;
padding 15px 5px !important;
gap 10px !important;
}
.wheel-container {
width min(280px, 80vw);
height min(200px, 55vw);
max-width 280px;
max-height 200px;
perspective 600px;
margin-bottom 20px;
}
.badge {
width 65px;
height 65px;
margin-left -32.5px;
margin-top -32.5px;
}
h1 {
font-size 1.8rem !important;
text-align center;
margin 0 0 10px 0 !important;
}
.rewards-bar {
flex-direction column;
gap 8px;
width 100%;
max-width 280px;
}
.rewards-btn {
width 100%;
justify-content space-between;
padding 12px 16px;
font-size 1rem;
}
}
@media (max-width 320px) {
.wheel-container {
width min(200px, 70vw);
height min(140px, 45vw);
max-width 200px;
max-height 140px;
margin-bottom 10px;
}
.badge {
width 45px;
height 45px;
margin-left -22.5px;
margin-top -22.5px;
}
h1 {
font-size 1.2rem !important;
margin 0 0 8px 0 !important;
}
.rewards-btn {
padding 10px 12px;
font-size 0.8rem;
}
}
.rewards-bar {
display flex;
gap 15px;
margin-top 20px;
margin-bottom 10px;
justify-content center;
}
.rewards-btn {
background rgba(42, 42, 42, 0.8);
backdrop-filter blur(20px);
border 2px solid #FFD700;
color #fff;
border-radius var(--border-radius-sm);
padding 12px 20px;
font-weight 600;
cursor pointer;
transition var(--transition);
display flex;
align-items center;
gap 10px;
font-size 1rem;
text-transform uppercase;
letter-spacing 0.5px;
box-shadow var(--shadow-hard);
}
.rewards-btnhover {
background rgba(58, 58, 58, 0.9);
box-shadow 0 8px 25px rgba(255, 215, 0, 0.4);
transform translateY(-2px);
border-color #FFA500;
}
.rewards-btn .points {
background var(--gold-gradient);
color #000;
padding 4px 12px;
border-radius 20px;
font-size 0.9em;
font-weight 700;
box-shadow inset 0 2px 4px rgba(0, 0, 0, 0.2);
}
.badge-list-item {
display flex;
align-items center;
gap 15px;
padding 10px;
border-bottom 1px solid #333;
}
.badge-list-itemlast-child {
border-bottom none;
}
.badge-list-item img {
width 50px;
height 50px;
}
.badge-list-item .completed-check {
color #00ff88;
font-size 1.5em;
margin-left auto;
}
@keyframes spin {
from { transform rotate(0deg); }
to { transform rotate(360deg); }
}
PowerOn Integration Button Styles
.redeem-action-btn {
background rgba(255,255,255,0.2) !important;
color #fff !important;
border none;
transition all 0.3s ease;
}
.redeem-action-btnhover {
background rgba(255,255,255,0.3) !important;
transform translateY(-2px);
box-shadow 0 8px 25px rgba(255,255,255,0.2);
}
.redeem-action-btn-disabled {
background rgba(255,255,255,0.05) !important;
color rgba(255,255,255,0.5) !important;
cursor not-allowed !important;
}
.redeem-merch-btn {
background rgba(255,255,255,0.2) !important;
color #fff !important;
transition all 0.3s ease;
}
.redeem-merch-btnhover {
background rgba(255,255,255,0.3) !important;
transform scale(1.05);
}
.redeem-merch-btn-disabled {
background rgba(255,255,255,0.05) !important;
color rgba(255,255,255,0.3) !important;
cursor not-allowed !important;
}
Enhanced Glow Animation
@keyframes badgeQuestGlow {
0% {
text-shadow 0 0 8px rgba(255, 215, 0, 0.4), 0 0 16px rgba(255, 165, 0, 0.2);
filter drop-shadow(0 4px 8px rgba(255, 215, 0, 0.3));
}
50% {
text-shadow 0 0 12px rgba(255, 215, 0, 0.6), 0 0 24px rgba(255, 165, 0, 0.3), 0 0 4px rgba(0, 255, 136, 0.2);
filter drop-shadow(0 6px 12px rgba(255, 215, 0, 0.4));
}
100% {
text-shadow 0 0 8px rgba(255, 215, 0, 0.4), 0 0 16px rgba(255, 165, 0, 0.2);
filter drop-shadow(0 4px 8px rgba(255, 215, 0, 0.3));
}
}
style
head
body x-data=appState() x-init=initModalListener()
div class=main-content-container style=display flex; flex-direction column; align-items center; justify-content center; min-height 100vh; gap 0; padding 15px 5px; box-sizing border-box; max-width 100vw; overflow-x hidden; width 100%;
h1 style=
font-size 2.4rem;
font-family 'Orbitron', Arial, sans-serif;
font-weight 900;
letter-spacing 0.08em;
background linear-gradient(45deg, #FFD700 0%, #FFA500 30%, #00ff88 60%, #667eea 100%);
-webkit-background-clip text;
-webkit-text-fill-color transparent;
background-clip text;
padding 0 12px;
border-radius var(--border-radius-sm);
z-index 10;
margin 0 0 10px 0;
animation badgeQuestGlow 4s infinite alternate;
display flex;
align-items center;
justify-content center;
gap 0.8ch;
filter drop-shadow(0 4px 8px rgba(255, 215, 0, 0.3));
text-shadow 0 0 20px rgba(255, 215, 0, 0.5);
span style=font-size1.3em; filter drop-shadow(0 0 10px rgba(255, 215, 0, 0.8));πspan
spanBadge Questspan
span style=font-size1.3em; filter drop-shadow(0 0 10px rgba(0, 255, 136, 0.8));πspan
h1
!-- Authentication Status Indicator --
div x-show=authError
style=background rgba(250, 112, 154, 0.1);
border 2px solid #fa709a;
color #fa709a;
padding 12px 16px;
border-radius var(--border-radius-sm);
margin-bottom 15px;
font-size 0.9em;
text-align center;
backdrop-filter blur(20px);
box-shadow var(--shadow-hard);
i class=bi bi-exclamation-triangle-fill me-2iAuthentication Error span x-text=authErrorspan
div
div x-show=isAuthenticated
style=background rgba(0, 255, 136, 0.1);
border 2px solid #00ff88;
color #00ff88;
padding 10px 16px;
border-radius var(--border-radius-sm);
margin-bottom 15px;
font-size 0.9em;
text-align center;
backdrop-filter blur(20px);
box-shadow var(--shadow-hard);
i class=bi bi-shield-check-fill me-2iAuthenticated & Ready
div
div class=wheel-container x-data=badgeWheel() x-init=init()
div class=wheel-wrapper
div class=wheel style=`transform rotateY(${rotation}deg)`
template x-for=(badge, index) in badges key=index
div
class=badge
class={ 'completed' badge.completed }
style=`transform rotateY(${index angle}deg) translateZ(${radius}px);`
@mouseenter=hoveredBadge = badge.name; hoveredIndex = index
@mouseleave=hoveredBadge = null; hoveredIndex = null
@click.stop=$dispatch('open-badge', badge)
@dragstart.prevent
img src=badge.img alt=badge.name draggable=false
div
x-show=hoveredBadge === badge.name
class=tooltip
class=badge.completed 'completed' 'incomplete'
x-transition.opacity.duration.200ms
span x-text=badge.namespan
span x-show=badge.completed style=color #00ff88; β
span
span x-show=!badge.completed style=color #FFD700; display block; margin-top 4px; Start Nowspan
div
div
template
div
div
div
!-- Rewards Section --
div class=rewards-bar
button @click.stop=openRedeemModal() class=rewards-btn
spanπspan
spanRedeemspan
span class=points x-text=pointsspan
button
button @click.stop=showAllBadgesModal = true class=rewards-btn
spanπ
span
spanBadgesspan
span class=points x-text=`${badgesCompleted} ${totalBadges}`span
button
div
div
!-- Badge Modal --
div x-show=showModal x-transition.opacity.duration.200ms class=modal-overlay @click.self=closeModal() style=`displayflex;z-index20000;pointer-events${overlayPointerEvents}`
div class=modal-content style=max-height98vh;overflowauto;
button @click=closeModal() class=modal-close×button
h2 x-text=modalBadge.name class=modal-titleh2
div x-html=modalBadgeContent class=modal-bodydiv
!-- Celebratory modal for completed badge --
template x-if=modalBadge.completed
div style=text-aligncenter; margin-top20px;
div style=font-size2.2em; color#00ff88; margin-bottom10px;π Level Up! πdiv
div style=margin-bottom16px; color#fff;You completed this badge! Share your achievementdiv
div style=displayflex; justify-contentcenter; gap16px; margin-bottom10px;
a href=`httpstwitter.comintenttweettext=${encodeURIComponent('I leveled up! π
' + modalBadge.name + ' #badgequest ' + (modalBadge.img modalBadge.img ''))}` target=_blank style=color#1da1f2; font-size1.5em; title=Share on XTwitterπ¦a
a href=`httpswww.facebook.comsharersharer.phpu=${encodeURIComponent(window.location.href)}"e=${encodeURIComponent('I leveled up! π
' + modalBadge.name + ' #badgequest')}` target=_blank style=color#4267B2; font-size1.5em; title=Share on Facebookπa
button @click=navigator.clipboard.writeText('I leveled up! π
' + modalBadge.name + ' #badgequest ' + (modalBadge.img modalBadge.img '')) style=backgroundnone; bordernone; color#FFD700; font-size1.5em; cursorpointer; title=Copy to clipboardπbutton
div
div
template
!-- Normal CTA for incomplete badge --
div class=modal-footer x-show=!modalBadge.completed
button @click=startNow(modalBadge) class=modal-ctaStart Nowbutton
div
div
div
!-- All Badges Modal --
div x-show=showAllBadgesModal x-transition.opacity.duration.200ms class=modal-overlay @click.self=showAllBadgesModal = false style=z-index20001;
div class=modal-content style=max-width 480px;
button @click=showAllBadgesModal = false class=modal-close×button
h2 class=modal-titleAll Badgesh2
div class=modal-body style=max-height 70vh;
template x-for=badge in allBadges key=badge.name
div class=badge-list-item @click=$dispatch('open-badge', badge); $nextTick(() = { showAllBadgesModal = false; showModal = true }) style=cursorpointer;
img src=badge.img alt=badge.name
span x-text=badge.namespan
template x-if=badge.completed
span class=completed-checkβ
span
template
div
template
div
div
div
!-- Daily Engagement Modal --
div x-show=shouldShowDailyModal x-transition.opacity.duration.300ms class=modal-overlay style=z-index20002;
div class=modal-content style=max-width 420px; background linear-gradient(135deg, #1f1f1f 0%, #2a2a2a 100%); border 2px solid #FFD700;
button @click=closeDailyModal() class=modal-close×button
!-- Header --
div style=text-align center; margin-bottom 20px;
h2 class=modal-title style=margin-bottom 5px;π Daily Quest πh2
div style=color #FFD700; font-size 1.1em; font-weight bold;
Day span x-text=dailyStreakDaysspan Streak!
template x-if=isDoublePointsDay
span style=color #00ff88; font-size 0.9em; display block;π₯ DOUBLE POINTS DAY! π₯span
template
div
div
!-- Next Badge Preview --
div style=text-align center; margin-bottom 25px; padding 15px; background rgba(255, 215, 0, 0.1); border-radius 8px; border 1px solid rgba(255, 215, 0, 0.3);
div style=color #FFD700; font-weight bold; margin-bottom 10px;π― Next Badge Goaldiv
div style=display flex; align-items center; justify-content center; gap 10px;
img src=nextBadge.img alt=nextBadge.name style=width 40px; height 40px; border-radius 50%;
span style=color #fff; font-weight bold; x-text=nextBadge.namespan
div
template x-if=isStreakMilestone
div style=color #00ff88; font-size 0.9em; margin-top 5px;β¨ Milestone Streak! β¨div
template
div
!-- Claim Reward Section --
div style=text-align center;
template x-if=!isClaimingReward && rewardPoints === 0
div
div style=color #fff; margin-bottom 20px; font-size 1.1em;
Ready to earn your daily rewards
div
button @click=claimDailyReward() class=modal-cta style=background linear-gradient(45deg, #FFD700, #FFA500); color #000; font-weight bold; padding 15px 30px; font-size 1.1em; border-radius 25px; border none; cursor pointer; transition all 0.3s ease; box-shadow 0 4px 15px rgba(255, 215, 0, 0.4);
π² Claim Daily Reward!
button
div
template
template x-if=isClaimingReward
div
div style=color #FFD700; font-size 1.2em; margin-bottom 15px;
π° Rolling for rewards...
div
div style=font-size 2em; animation spin 1s linear infinite;π²div
div
template
template x-if=!isClaimingReward && rewardPoints 0
div
div style=font-size 2.5em; color #00ff88; margin-bottom 10px;πdiv
div style=color #FFD700; font-size 1.3em; font-weight bold; margin-bottom 10px;
You earned span x-text=rewardPointsspan points!
div
template x-if=isDoublePointsDay
div style=color #00ff88; font-size 1em; margin-bottom 10px;
π₯ Double points bonus applied! π₯
div
template
div style=color #fff; font-size 0.9em;
Keep your streak alive tomorrow!
div
div
template
div
div
div
script
function renderMarkdown(md) {
if (!md) return '';
return window.marked.parse(md);
}
function appState() {
return {
Badge modal state
showModal false,
modalBadge null,
modalBadgeContent '',
overlayPointerEvents 'none',
Redeem modal state
showRedeem false,
redemptionInfo null,
All Badges Modal state
showAllBadgesModal false,
allBadges [],
points 1240, Example points
Daily Engagement Modal state
dailyModalOpen true,
closeDaily false,
dailyStreakDays 2, Example streak
nextBadge { name Goal Getter, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestbae2147142b6fa5a4bfdd07521d1d695948d1cbcGoal%20Getter.svg },
rewardPoints 0,
isClaimingReward false,
engagementInfo null, Will be loaded from injection or defaults
PowerOn Integration state
isProcessing false,
lastRedemptionStatus null, 'success', 'error', or null
lastRedemptionAction null,
lastRedemptionTime null,
Authentication state
isAuthenticated false,
jwtToken null,
authError null,
Load engagement configuration from injection or defaults
loadEngagementConfig() {
console.log('βοΈ Loading engagement configuration...');
Check for injected engagement config
if (window.ADMIN_CONFIG && window.ADMIN_CONFIG.engagementInfo) {
console.log('β
Found injected engagement config', window.ADMIN_CONFIG.engagementInfo);
this.engagementInfo = window.ADMIN_CONFIG.engagementInfo;
}
Check for standalone engagement config
else if (window.ENGAGEMENT_CONFIG) {
console.log('β
Found standalone engagement config', window.ENGAGEMENT_CONFIG);
this.engagementInfo = window.ENGAGEMENT_CONFIG;
}
Fallback to defaults
else {
console.log('β οΈ No engagement config found, using defaults');
this.engagementInfo = this.getDefaultEngagementConfig();
}
console.log('π― Loaded engagement config', this.engagementInfo);
},
Default engagement configuration
getDefaultEngagementConfig() {
return {
dailyRewards true,
minimumPoints 10,
maxPoints 100,
doubleMultiplierDays 7,
streaks [
{ badgeDays 3, pointsEarned 30, enabled true },
{ badgeDays 7, pointsEarned 100, enabled true },
{ badgeDays 14, pointsEarned 200, enabled true },
{ badgeDays 30, pointsEarned 500, enabled true },
{ badgeDays 60, pointsEarned 1000, enabled true },
{ badgeDays 90, pointsEarned 1500, enabled true },
{ badgeDays 180, pointsEarned 3000, enabled true },
{ badgeDays 365, pointsEarned 5000, enabled true }
]
};
},
Load redemption configuration from injection or defaults
loadRedemptionConfig() {
console.log('π Loading redemption configuration...');
Check for injected redemption config
if (window.ADMIN_CONFIG && window.ADMIN_CONFIG.redemption) {
console.log('β
Found injected redemption config', window.ADMIN_CONFIG.redemption);
this.redemptionInfo = window.ADMIN_CONFIG.redemption;
}
Check for standalone redemption config
else if (window.REDEMPTION_CONFIG) {
console.log('β
Found standalone redemption config', window.REDEMPTION_CONFIG);
this.redemptionInfo = window.REDEMPTION_CONFIG;
}
Fallback to defaults
else {
console.log('β οΈ No redemption config found, using defaults');
this.redemptionInfo = this.getDefaultRedemptionConfig();
}
console.log('π― Loaded redemption config', this.redemptionInfo);
},
Default redemption configuration
getDefaultRedemptionConfig() {
return {
branchNames [
Main Branch,
Downtown Branch,
North Branch
],
shareDeposit {
primaryShareId S01,
requiredPoints 500,
depositAmount 25.00
},
certificateBumps [
{
cdTypeEligibleId CD12,
noteCode NC123,
requiredPoints 1000,
bumpRate 0.25
},
{
cdTypeEligibleId CD24,
noteCode NC456,
requiredPoints 1500,
bumpRate 0.50
}
],
swagStore [
{
swagItemName Credit Union T-Shirt,
requiredPoints 300,
branch Main Branch,
notificationEmail rewards@creditunion.org,
swagImageUrl httpsmedia.istockphoto.comid482948743photoblank-white-t-shirt-front-with-clipping-path.jpgs=612x612&w=0&k=20&c=cJG_B0mOIG42FKtC_rqIeZCClYOj7UCFNNs9WTkYEEE=
},
{
swagItemName Insulated Water Bottle,
requiredPoints 150,
branch Downtown Branch,
notificationEmail rewards@creditunion.org,
swagImageUrl httpstarget.scene7.comisimageTargetGUEST_7aaf2450-42d3-4db5-80a2-6319a01f43f9
},
{
swagItemName Coffee Mug,
requiredPoints 100,
branch North Branch,
notificationEmail rewards@creditunion.org,
swagImageUrl httpsvia.placeholder.com100x1000066ccfffffftext=CU+Mug
}
]
};
},
get badgesCompleted() {
return this.allBadges.filter(b = b.completed).length;
},
get totalBadges() {
return this.allBadges.length;
},
Modal listeners
initModalListener() {
Load configurations on startup
this.loadEngagementConfig();
this.loadRedemptionConfig();
Handle JWTPKCE authentication for iframe context
this.handleAuthentication();
Wait for badge wheel to initialize, then get badge data
setTimeout(() = {
Get the badge wheel component and sync the badge data
const wheelElement = document.querySelector('[x-data=badgeWheel]');
if (wheelElement && wheelElement._x_dataStack) {
const wheelData = wheelElement._x_dataStack[0];
if (wheelData && wheelData.badges) {
this.allBadges = wheelData.badges;
console.log('β
Synced badge data from wheel', this.allBadges);
}
}
Load member badge completion status
this.loadMemberBadgeStatus();
}, 300);
Check if we should show daily modal on startup
setTimeout(() = {
if (this.shouldShowDailyModal) {
console.log('Daily modal auto-showing...');
}
}, 500);
this.$watch('showModal', (val) = {
if (val) {
window.dispatchEvent(new CustomEvent('pause-rotation'));
} else {
window.dispatchEvent(new CustomEvent('resume-rotation'));
}
});
this.$root.addEventListener('open-badge', e = {
this.openModal(e.detail);
});
Watch for showRedeem to fetch rewards from admin.json
this.$watch('showRedeem', (val) = {
if (val) {
this.redemptionInfo = null; Always show loading state when opening
this.fetchRedemptionInfo();
}
});
},
Load member badge completion status from server or injected data
async loadMemberBadgeStatus() {
try {
console.log('π
Loading member badge status...');
Check for injected member data from server
if (window.MEMBER_BADGE_DATA) {
console.log('β
Found injected member badge data', window.MEMBER_BADGE_DATA);
this.updateBadgeCompletionStatus(window.MEMBER_BADGE_DATA);
return;
}
Try to fetch from authenticated API
if (this.isAuthenticated) {
console.log('π Fetching member badge status from API...');
const response = await this.makeAuthenticatedRequest('apimember-badges', {
method 'GET',
cache 'no-store'
});
if (response.ok) {
const memberData = await response.json();
console.log('β
Member badge data from API', memberData);
this.updateBadgeCompletionStatus(memberData);
Update points if provided
if (memberData.MemberInfo && memberData.MemberInfo.PointsEarned) {
this.points = memberData.MemberInfo.PointsEarned - (memberData.MemberInfo.PointsSpent 0);
}
return;
}
}
console.log('β οΈ No member badge data available, using defaults');
} catch (error) {
console.error('Error loading member badge status', error);
}
},
Update badge completion status based on member data
updateBadgeCompletionStatus(memberData) {
if (!memberData !memberData.Badges) {
console.log('No badge completion data provided');
return;
}
console.log('π Updating badge completion status...');
Create a map of completed badges for quick lookup
const completedBadgeIds = new Set();
const badgeMap = new Map();
memberData.Badges.forEach(memberBadge = {
if (memberBadge.earned) {
completedBadgeIds.add(memberBadge.BadgeID);
badgeMap.set(memberBadge.BadgeID, memberBadge);
}
});
Update badge completion status
this.allBadges.forEach((badge, index) = {
Try to match by badge ID, shortName, or procedure name
const badgeId = badge.badgeData.shortName badge.badgeData.procedureName badge.name;
const isCompleted = completedBadgeIds.has(badgeId)
completedBadgeIds.has(badge.badgeData.shortName)
completedBadgeIds.has(badge.badgeData.procedureName);
badge.completed = isCompleted;
if (isCompleted) {
console.log(`β
Badge ${badge.name} marked as completed`);
}
});
Force Alpine.js to update
this.$nextTick(() = {
console.log('π― Badge completion status updated');
});
},
openModal(badge) {
this.modalBadge = badge;
this.modalBadgeContent = renderMarkdown(badge.content);
this.showModal = true;
this.overlayPointerEvents = 'none';
setTimeout(() = { this.overlayPointerEvents = 'auto'; }, 200);
},
closeModal() {
if (this.overlayPointerEvents === 'none') return;
this.showModal = false;
this.modalBadge = null;
this.modalBadgeContent = '';
this.overlayPointerEvents = 'none';
},
openRedeemModal() {
console.log('Opening redeem modal, showRedeem before', this.showRedeem);
this.showRedeem = true;
console.log('Opening redeem modal, showRedeem after', this.showRedeem);
},
closeRedeemModal() {
console.log('Closing redeem modal, showRedeem before', this.showRedeem);
this.showRedeem = false;
console.log('Closing redeem modal, showRedeem after', this.showRedeem);
},
startNow(badge) {
console.log('π Starting quest for badge', badge.name);
Use the badge's CTA URL if available
if (badge.badgeData && badge.badgeData.ctaUrl) {
console.log('π Redirecting to CTA URL', badge.badgeData.ctaUrl);
window.open(badge.badgeData.ctaUrl, '_blank');
} else {
Fallback to generic message
alert(`Starting quest for ${badge.name}`);
}
this.closeModal();
},
Fetch redemption info from authenticated API
async fetchRedemptionInfo() {
try {
console.log('π¦ Fetching redemption info...');
If we already have redemption info loaded, use it
if (this.redemptionInfo) {
console.log('β
Using pre-loaded redemption config');
return;
}
console.log('π Fetching redemption info from authenticated API...');
const res = await this.makeAuthenticatedRequest('apiredemption', {
method 'GET',
cache 'no-store'
});
console.log('π‘ Redemption API response', res);
if (!res.ok) {
Fallback to local admin.json if API fails
console.warn('β οΈ API failed, falling back to admin.json');
const fallbackRes = await fetch('admin.json', {cache 'no-store'});
if (!fallbackRes.ok) {
console.warn('β οΈ admin.json also failed, using defaults');
this.redemptionInfo = this.getDefaultRedemptionConfig();
return;
}
const fallbackData = await fallbackRes.json();
this.redemptionInfo = fallbackData.redemptionInfo fallbackData.redemption;
return;
}
const data = await res.json();
console.log('Parsed redemption data', data);
this.redemptionInfo = data.redemptionInfo data; Handle different response formats
console.log('Set redemptionInfo', this.redemptionInfo);
Force Alpine to re-evaluate the template
this.$nextTick(() = {
console.log('Alpine.js template updated with redemption data');
});
} catch (e) {
console.error('Error fetching redemption info', e);
this.redemptionInfo = null;
this.authError = `Failed to load rewards ${e.message}`;
}
},
Daily Engagement Modal methods
closeDailyModal() {
this.closeDaily = true;
this.dailyModalOpen = false;
},
async claimDailyReward() {
if (this.isClaimingReward) return;
this.isClaimingReward = true;
Calculate base reward points
const basePoints = Math.floor(Math.random() (this.engagementInfo.maxPoints - this.engagementInfo.minimumPoints + 1)) + this.engagementInfo.minimumPoints;
Check if it's a double multiplier day
const isDoubleDay = this.dailyStreakDays % this.engagementInfo.doubleMultiplierDays === 0;
const finalPoints = isDoubleDay basePoints 2 basePoints;
this.rewardPoints = finalPoints;
Add animation delay for excitement
await new Promise(resolve = setTimeout(resolve, 1500));
try {
Call backend to add points using authenticated request
const response = await this.makeAuthenticatedRequest('apiaddpoints', {
method 'POST',
body JSON.stringify({ points finalPoints })
});
if (response.ok) {
const result = await response.json();
console.log('β
Points added successfully', result);
this.points += finalPoints;
Update points from server response if provided
if (result.totalPoints) {
this.points = result.totalPoints;
}
} else {
throw new Error(`Failed to add points ${response.status}`);
}
} catch (error) {
console.error('Error adding points', error);
Still update points locally even if server call fails
this.points += finalPoints;
}
this.isClaimingReward = false;
Auto-close after claiming
setTimeout(() = {
this.closeDailyModal();
}, 2000);
},
get shouldShowDailyModal() {
return this.dailyModalOpen && !this.closeDaily && this.engagementInfo.dailyRewards;
},
get isStreakMilestone() {
return this.engagementInfo.streaks.includes(this.dailyStreakDays);
},
get isDoublePointsDay() {
return this.dailyStreakDays % this.engagementInfo.doubleMultiplierDays === 0;
},
Authentication methods
async handleAuthentication() {
try {
Check if we're in an iframe from dixiescripts.org
const isInIframe = window.self !== window.top;
const referrer = document.referrer;
if (isInIframe && referrer.includes('dixiescripts.org')) {
console.log('Detected iframe context from dixiescripts.org');
Extract JWT and PKCE from URL parameters or postMessage
const urlParams = new URLSearchParams(window.location.search);
let jwtToken = urlParams.get('jwt') urlParams.get('token');
const pkceCode = urlParams.get('code');
const pkceState = urlParams.get('state');
If no JWT in URL, listen for postMessage from parent
if (!jwtToken) {
jwtToken = await this.waitForAuthMessage();
}
if (jwtToken) {
console.log('JWT token received, authenticating...');
await this.authenticateWithServer(jwtToken, pkceCode, pkceState);
} else {
console.warn('No JWT token found in URL or postMessage');
this.authError = 'No authentication token provided';
}
} else {
console.log('Not in iframe context or not from dixiescripts.org');
}
} catch (error) {
console.error('Authentication error', error);
this.authError = error.message;
}
},
async waitForAuthMessage(timeout = 5000) {
return new Promise((resolve) = {
const timer = setTimeout(() = resolve(null), timeout);
const messageHandler = (event) = {
Verify origin for security
if (event.origin !== 'httpswww.dixiescripts.org') {
return;
}
if (event.data && (event.data.jwt event.data.token)) {
clearTimeout(timer);
window.removeEventListener('message', messageHandler);
resolve(event.data.jwt event.data.token);
}
};
window.addEventListener('message', messageHandler);
});
},
async authenticateWithServer(jwtToken, pkceCode, pkceState) {
try {
const authPayload = {
jwt jwtToken
};
Include PKCE parameters if available
if (pkceCode) authPayload.code = pkceCode;
if (pkceState) authPayload.state = pkceState;
console.log('π Sending authentication request to localhostauth');
console.log('π¦ Request payload', authPayload);
console.log('π Authorization header', `Bearer ${jwtToken.substring(0, 20)}...`);
const response = await fetch('httplocalhostauth', {
method 'POST',
headers {
'Authorization' `Bearer ${jwtToken}`,
'Content-Type' 'applicationjson'
},
body JSON.stringify(authPayload)
});
console.log('π‘ Authentication response status', response.status);
console.log('π‘ Authentication response headers', Object.fromEntries(response.headers.entries()));
if (response.ok) {
const authData = await response.json();
this.isAuthenticated = true;
this.jwtToken = jwtToken;
this.authError = null;
console.log('β
Authentication successful', authData);
Update user data if provided
if (authData.points) {
this.points = authData.points;
console.log('π° Updated points from server', authData.points);
}
if (authData.streakDays) {
this.dailyStreakDays = authData.streakDays;
console.log('π₯ Updated streak days from server', authData.streakDays);
}
} else {
const errorText = await response.text();
console.error('β Authentication failed with status', response.status);
console.error('β Error response body', errorText);
throw new Error(`Authentication failed ${response.status} ${response.statusText}`);
}
} catch (error) {
console.error('π¨ Server authentication error', error);
this.authError = error.message;
this.isAuthenticated = false;
}
},
Update API calls to include authentication
async makeAuthenticatedRequest(url, options = {}) {
const headers = {
'Content-Type' 'applicationjson',
...options.headers
};
if (this.jwtToken) {
headers['Authorization'] = `Bearer ${this.jwtToken}`;
console.log(`π Making authenticated request to ${url}`);
console.log(`π Using JWT ${this.jwtToken.substring(0, 20)}...`);
} else {
console.log(`π Making unauthenticated request to ${url}`);
}
return fetch(url, {
...options,
headers
});
},
PowerOn Integration Methods
async processCashback() {
if (this.isProcessing this.points this.redemptionInfo.shareDeposit.requiredPoints) return;
this.isProcessing = true;
this.lastRedemptionStatus = null;
try {
const payload = {
action 'CASHBACK',
shareId this.redemptionInfo.shareDeposit.primaryShareId 'S01',
amount this.redemptionInfo.shareDeposit.depositAmount,
requiredPoints this.redemptionInfo.shareDeposit.requiredPoints,
memberPoints this.points,
timestamp new Date().toISOString()
};
console.log('π¦ Processing cashback', payload);
const response = await this.makeAuthenticatedRequest('apipoweronredemption', {
method 'POST',
body JSON.stringify(payload)
});
const result = await response.json();
if (response.ok) {
this.points -= this.redemptionInfo.shareDeposit.requiredPoints;
this.lastRedemptionStatus = 'success';
this.lastRedemptionAction = `Cashback $${payload.amount} processed successfully`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
Show success notification
this.showNotification(`π Cashback processed! $${payload.amount} deposited to your account.`, 'success');
} else {
throw new Error(result.message 'Cashback processing failed');
}
} catch (error) {
console.error('Cashback error', error);
this.lastRedemptionStatus = 'error';
this.lastRedemptionAction = `Cashback failed ${error.message}`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
this.showNotification(`β Cashback failed ${error.message}`, 'error');
} finally {
this.isProcessing = false;
}
},
async processCDBump() {
if (this.isProcessing !this.redemptionInfo.certificateBumps.length) return;
const bump = this.redemptionInfo.certificateBumps[0];
if (this.points bump.requiredPoints) return;
this.isProcessing = true;
this.lastRedemptionStatus = null;
try {
const payload = {
action 'CDBUMPRATE',
cdTypeId bump.cdTypeEligibleId,
noteCode bump.noteCode,
bumpRate bump.bumpRate,
requiredPoints bump.requiredPoints,
memberPoints this.points,
timestamp new Date().toISOString()
};
console.log('π Processing CD rate bump', payload);
const response = await this.makeAuthenticatedRequest('apipoweronredemption', {
method 'POST',
body JSON.stringify(payload)
});
const result = await response.json();
if (response.ok) {
this.points -= bump.requiredPoints;
this.lastRedemptionStatus = 'success';
this.lastRedemptionAction = `CD rate bump +${bump.bumpRate}% applied successfully`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
this.showNotification(`π CD Rate Bump applied! Your rate increased by ${bump.bumpRate}%.`, 'success');
} else {
throw new Error(result.message 'CD rate bump failed');
}
} catch (error) {
console.error('CD bump error', error);
this.lastRedemptionStatus = 'error';
this.lastRedemptionAction = `CD bump failed ${error.message}`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
this.showNotification(`β CD Rate Bump failed ${error.message}`, 'error');
} finally {
this.isProcessing = false;
}
},
async processMerchandise(item) {
if (this.isProcessing this.points item.requiredPoints) return;
this.isProcessing = true;
this.lastRedemptionStatus = null;
try {
const payload = {
action 'GIVEMERCHANDISE',
itemName item.swagItemName,
branch item.branch,
notificationEmail item.notificationEmail,
requiredPoints item.requiredPoints,
imageUrl item.swagImageUrl,
memberPoints this.points,
timestamp new Date().toISOString()
};
console.log('π Processing merchandise order', payload);
const response = await this.makeAuthenticatedRequest('apipoweronredemption', {
method 'POST',
body JSON.stringify(payload)
});
const result = await response.json();
if (response.ok) {
this.points -= item.requiredPoints;
this.lastRedemptionStatus = 'success';
this.lastRedemptionAction = `${item.swagItemName} ordered for pickup at ${item.branch}`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
this.showNotification(`π ${item.swagItemName} ordered! Pickup available at ${item.branch}.`, 'success');
} else {
throw new Error(result.message 'Merchandise order failed');
}
} catch (error) {
console.error('Merchandise error', error);
this.lastRedemptionStatus = 'error';
this.lastRedemptionAction = `Merchandise order failed ${error.message}`;
this.lastRedemptionTime = new Date().toLocaleTimeString();
this.showNotification(`β Merchandise order failed ${error.message}`, 'error');
} finally {
this.isProcessing = false;
}
},
showNotification(message, type = 'success') {
Create a simple notification system
const notification = document.createElement('div');
notification.style.cssText = `
position fixed;
top 20px;
right 20px;
padding 16px 20px;
border-radius var(--border-radius-sm);
color white;
font-weight 600;
z-index 30000;
backdrop-filter blur(20px);
box-shadow var(--shadow-hard);
max-width 400px;
transform translateX(100%);
transition transform 0.3s ease;
${type === 'success' 'background var(--emerald-gradient);' 'background var(--danger-gradient);'}
`;
notification.textContent = message;
document.body.appendChild(notification);
Animate in
setTimeout(() = {
notification.style.transform = 'translateX(0)';
}, 100);
Animate out and remove
setTimeout(() = {
notification.style.transform = 'translateX(100%)';
setTimeout(() = {
document.body.removeChild(notification);
}, 300);
}, 4000);
}
};
}
function badgeWheel() {
let touchStartX = 0;
let touchMoveX = 0;
return {
badges [],
rotation 0,
speed 0.02,
radius 250,
get angle() {
return 360 this.badges.length;
},
hoveredBadge null,
hoveredIndex null,
_autoRotateId null,
Load badges from injected config or fallback to defaults
loadBadges() {
console.log('π§ Loading badge configuration...');
Check for injected badge config from server
if (window.BADGE_CONFIG && Array.isArray(window.BADGE_CONFIG)) {
console.log('β
Found injected badge config', window.BADGE_CONFIG);
this.badges = this.transformBadgeConfig(window.BADGE_CONFIG);
}
Check for full admin config with badges array
else if (window.ADMIN_CONFIG && window.ADMIN_CONFIG.badges) {
console.log('β
Found admin config with badges', window.ADMIN_CONFIG.badges);
this.badges = this.transformBadgeConfig(window.ADMIN_CONFIG.badges);
}
Fallback to default hardcoded badges
else {
console.log('β οΈ No badge config found, using default badges');
this.badges = this.getDefaultBadges();
}
console.log(`π― Loaded ${this.badges.length} badges`, this.badges);
},
Transform badge config from admin format to wheel format
transformBadgeConfig(badgeConfig) {
return badgeConfig
.filter(badge = badge.enabled !== false) Only show enabled badges
.map(badge = ({
name badge.title badge.name 'Unknown Badge',
img badge.iconUrl badge.img 'httpsvia.placeholder.com100',
completed badge.completed false, Will be updated from member data
content badge.modalMarkDown badge.content `${badge.title}nEarn ${badge.points} points!`,
Store additional badge data for API calls
badgeData {
shortName badge.shortName,
procedureName badge.procedureName,
points badge.points 0,
ctaUrl badge.ctaUrl,
moreInfoUrl badge.moreInfoUrl,
priority badge.priority,
accountType badge.accountType,
serviceCode badge.serviceCode,
startDate badge.startDate,
endDate badge.endDate,
timeFrame badge.timeFrame
}
}));
},
Default badges as fallback
getDefaultBadges() {
return [
{ name Always Aware, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainAlways%20Aware.svg, completed true, content Always AwarenStay vigilant and keep your eyes open for new opportunities.nUnlock this badge by completing your first quest! },
{ name Credit Commander, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainCredit%20Commander.svg, completed false, content ## π‘οΈ Credit CommandernOpen a credit card and earn the Credit Commander badge!n### π³ Why It Mattersn- Build credit with smart usagen- Earn points toward rewardsn- Unlock exclusive perksn### ποΈ How to Get Itn1. Apply for a credit card belown2. Get approved (O.A.C)n3. Badge unlocked!n π‘ Tip Use your card wisely to boost your credit score.nTake charge. Become a Credit Commander today! },
{ name Goal Getter, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestbae2147142b6fa5a4bfdd07521d1d695948d1cbcGoal%20Getter.svg, completed false, content Goal GetternSet your sights high and achieve your goals.nEarn this badge by setting and reaching a milestone! },
{ name Locked & Loaded, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainLocked%20%26%20Loaded.svg, completed true, content Locked & LoadednBe prepared for anything that comes your way.nAwarded for prepping your profile! },
{ name Money Minded, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainMoney%20Minded.svg, completed false, content Money MindednKeep your finances in check and your wallet happy.nGet this badge by saving your first dollar! },
{ name One-Year Wonder, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainOne-Year%20Wonder.svg, completed true, content One-Year WondernCelebrate a year of achievements.nEarned after 1 year of activity! },
{ name Personal PowerUp, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainPersonal%20PowerUp.svg, completed false, content Personal PowerUpnLevel up your personal skills.nComplete a self-improvement quest to unlock! },
{ name Swipe Saavy, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainSwipe%20Saavy.svg, completed true, content Swipe SaavynMaster the art of the swipe.nAwarded for your first successful swipe! },
{ name Wellness Wizard, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainWellness%20Wizard.svg, completed false, content Wellness WizardnPrioritize your health and well-being.nUnlock by completing a wellness challenge! },
{ name Paperless Pioneer, img httpsraw.githubusercontent.comdixiescriptsBadgeQuestmainPaperless%20Pioneer.svg, completed true, content Paperless PioneernGo green and embrace the digital age.nEarned by going paperless! }
];
},
init() {
Load badges first, then initialize wheel
this.loadBadges();
this.updateRadius();
window.addEventListener('resize', () = this.updateRadius());
this.initSwipe();
this.startAutoRotate();
document.addEventListener('pause-rotation', () = this.stopAutoRotate());
document.addEventListener('resume-rotation', () = this.startAutoRotate());
},
updateRadius() {
if (window.innerWidth = 480) {
this.radius = 160;
} else {
this.radius = 250;
}
},
initSwipe() {
const self = this;
this.$el.addEventListener('touchstart', (e) = {
if (e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchMoveX = touchStartX;
}
}, { passive true });
this.$el.addEventListener('touchmove', (e) = {
if (e.touches.length === 1) {
touchMoveX = e.touches[0].clientX;
}
e.preventDefault();
}, { passive false });
this.$el.addEventListener('touchend', () = {
const dx = touchMoveX - touchStartX;
if (Math.abs(dx) 40) {
self.rotation += dx 0 -self.angle self.angle;
}
}, { passive true });
},
startAutoRotate() {
if (this._autoRotateId) return;
const rotate = () = {
this.rotation = (this.rotation + this.speed) % 360;
this._autoRotateId = requestAnimationFrame(rotate);
};
this._autoRotateId = requestAnimationFrame(rotate);
},
stopAutoRotate() {
if (this._autoRotateId) {
cancelAnimationFrame(this._autoRotateId);
this._autoRotateId = null;
}
}
};
}
script
!-- Title Animation Styles --
style
Additional responsive enhancements
@media (max-width 768px) {
.modal-content {
margin 15px;
padding 20px;
max-height 90vh;
max-width 95vw;
}
.modal-title {
font-size 1.4rem;
}
.redeem-action-btn, .redeem-merch-btn {
font-size 0.85rem;
padding 12px 18px;
}
}
@media (max-width 480px) {
.modal-content {
margin 10px;
padding 15px;
max-height 95vh;
max-width 95vw;
}
.modal-title {
font-size 1.3rem;
}
.redeem-action-btn, .redeem-merch-btn {
font-size 0.8rem;
padding 10px 16px;
}
.modal-body {
font-size 0.9rem;
}
.tooltip {
font-size 14px;
padding 8px 12px;
min-width 100px;
}
}
@media (max-width 320px) {
.modal-content {
margin 5px;
padding 12px;
max-height 98vh;
}
.modal-title {
font-size 1.2rem;
}
.redeem-action-btn, .redeem-merch-btn {
font-size 0.75rem;
padding 8px 12px;
}
.tooltip {
font-size 12px;
padding 6px 10px;
min-width 80px;
}
}
style
!-- Redeem Modal --
div x-show=showRedeem class=modal-overlay @click.self=closeRedeemModal() style=z-index20000;
div class=modal-content style=max-width 520px; @click.stop
button @click=closeRedeemModal() class=modal-close
i class=bi bi-x-lgi
button
h2 class=modal-title
i class=bi bi-gift-fill me-3iRedeem Rewards
h2
div class=modal-body
!-- Points Display --
div style=text-align center; margin-bottom 20px; padding 10px; background var(--gold-gradient); border-radius var(--border-radius); color #000;
div style=font-size 1.1em; font-weight 600; margin-bottom 5px;Available Pointsdiv
div style=font-size 2.2em; font-weight 900; x-text=pointsdiv
div
template x-if=!redemptionInfo
div style=text-align center; padding 40px; color #FFD700;
div style=font-size 2em; margin-bottom 15px; animation spin 1s linear infinite;β³div
divLoading rewards...div
div
template
template x-if=redemptionInfo
div
!-- PowerOn Integration Actions --
div style=background rgba(255, 255, 255, 0.05); border-radius var(--border-radius); padding 20px; margin-bottom 25px; border 1px solid rgba(255, 255, 255, 0.1);
div style=display grid; gap 15px;
!-- Cashback Button --
div style=background var(--emerald-gradient); border-radius var(--border-radius-sm); padding 16px; position relative; overflow hidden;
div style=display flex; align-items center; justify-content space-between; margin-bottom 10px;
div style=display flex; align-items center; gap 12px;
i class=bi bi-cash-coin style=font-size 1.5em; color #fff;i
div
div style=font-weight 700; color #fff; font-size 1.1em;Share Depositdiv
div style=color rgba(255,255,255,0.8); font-size 0.9em;
$span x-text=redemptionInfo.shareDeposit.depositAmountspan cashback
div
div
div
div style=text-align right; color #fff;
div style=font-size 1.1em; font-weight 700; x-text=redemptionInfo.shareDeposit.requiredPointsdiv
div style=font-size 0.8em; opacity 0.8;pointsdiv
div
div
button
@click=processCashback()
disabled=points redemptionInfo.shareDeposit.requiredPoints isProcessing
class=points = redemptionInfo.shareDeposit.requiredPoints 'redeem-action-btn' 'redeem-action-btn-disabled'
style=width 100%; padding 12px; border none; border-radius var(--border-radius-sm); font-weight 600; text-transform uppercase; letter-spacing 0.5px; transition var(--transition); cursor pointer; background rgba(255,255,255,0.2); color #fff;
span x-show=!isProcessing
i class=bi bi-arrow-right-circle-fill me-2iProcess Cashback
span
span x-show=isProcessing style=display flex; align-items center; justify-content center; gap 10px;
div style=width 16px; height 16px; border 2px solid rgba(255,255,255,0.3); border-top 2px solid #fff; border-radius 50%; animation spin 1s linear infinite;div
Processing...
span
button
div
!-- CD Rate Bump --
template x-if=redemptionInfo.certificateBumps && redemptionInfo.certificateBumps.length 0
div style=background var(--purple-gradient); border-radius var(--border-radius-sm); padding 16px;
div style=display flex; align-items center; justify-content space-between; margin-bottom 10px;
div style=display flex; align-items center; gap 12px;
i class=bi bi-graph-up-arrow style=font-size 1.5em; color #fff;i
div
div style=font-weight 700; color #fff; font-size 1.1em;CD Rate Bumpdiv
div style=color rgba(255,255,255,0.8); font-size 0.9em;
+span x-text=redemptionInfo.certificateBumps[0].bumpRatespan% rate increase
div
div
div
div style=text-align right; color #fff;
div style=font-size 1.1em; font-weight 700; x-text=redemptionInfo.certificateBumps[0].requiredPointsdiv
div style=font-size 0.8em; opacity 0.8;pointsdiv
div
div
button
@click=processCDBump()
disabled=points redemptionInfo.certificateBumps[0].requiredPoints isProcessing
class=points = redemptionInfo.certificateBumps[0].requiredPoints 'redeem-action-btn' 'redeem-action-btn-disabled'
style=width 100%; padding 12px; border none; border-radius var(--border-radius-sm); font-weight 600; text-transform uppercase; letter-spacing 0.5px; transition var(--transition); cursor pointer; background rgba(255,255,255,0.2); color #fff;
span x-show=!isProcessing
i class=bi bi-arrow-right-circle-fill me-2iApply Rate Bump
span
span x-show=isProcessing style=display flex; align-items center; justify-content center; gap 10px;
div style=width 16px; height 16px; border 2px solid rgba(255,255,255,0.3); border-top 2px solid #fff; border-radius 50%; animation spin 1s linear infinite;div
Processing...
span
button
div
template
!-- Merchandise Items --
template x-if=redemptionInfo.swagStore && redemptionInfo.swagStore.length 0
div style=background var(--pink-gradient); border-radius var(--border-radius-sm); padding 16px;
div style=display flex; align-items center; justify-content space-between; margin-bottom 15px;
div style=display flex; align-items center; gap 12px;
i class=bi bi-bag-heart-fill style=font-size 1.5em; color #fff;i
div
div style=font-weight 700; color #fff; font-size 1.1em;Merchandisediv
div style=color rgba(255,255,255,0.8); font-size 0.9em;Choose your rewarddiv
div
div
div
div style=display grid; gap 10px;
template x-for=(item, index) in redemptionInfo.swagStore key=index
div style=background rgba(255,255,255,0.1); border-radius var(--border-radius-sm); padding 12px; display flex; align-items center; justify-content space-between;
div style=display flex; align-items center; gap 10px;
img src=item.swagImageUrl alt=item.swagItemName
style=width 40px; height 40px; border-radius var(--border-radius-sm); object-fit cover; background #fff;
div
div style=color #fff; font-weight 600; font-size 0.95em; x-text=item.swagItemNamediv
div style=color rgba(255,255,255,0.7); font-size 0.8em; x-text=item.branchdiv
div
div
div style=text-align right;
button
@click=processMerchandise(item)
disabled=points item.requiredPoints isProcessing
class=points = item.requiredPoints 'redeem-merch-btn' 'redeem-merch-btn-disabled'
style=padding 8px 12px; border none; border-radius var(--border-radius-sm); font-weight 600; font-size 0.8em; text-transform uppercase; transition var(--transition); cursor pointer; background rgba(255,255,255,0.2); color #fff;
span x-text=item.requiredPointsspan pts
button
div
div
template
div
div
template
div
!-- Status Indicator --
div style=margin-top 20px; padding 15px; background rgba(255,255,255,0.05); border-radius var(--border-radius-sm); border 1px solid rgba(255,255,255,0.1);
div style=display flex; align-items center; justify-content space-between;
div style=display flex; align-items center; gap 10px;
div x-show=!lastRedemptionStatus style=width 8px; height 8px; background #00ff88; border-radius 50%;div
div x-show=lastRedemptionStatus === 'success' style=width 8px; height 8px; background #00ff88; border-radius 50%;div
div x-show=lastRedemptionStatus === 'error' style=width 8px; height 8px; background #fa709a; border-radius 50%;div
span style=color #fff; font-size 0.9em;
span x-show=!lastRedemptionActionReady for Actionspan
span x-show=lastRedemptionAction x-text=lastRedemptionActionspan
span
div
span x-show=lastRedemptionTime style=color rgba(255,255,255,0.6); font-size 0.8em; x-text=lastRedemptionTimespan
div
div
div
div
template
div
div class=modal-footer
button @click=closeRedeemModal() class=modal-cta style=background rgba(255,255,255,0.1); color #fff;
i class=bi bi-x-circle me-2iClose
button
div
div
div
body
html