!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