1 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>ShiftLoop Wallet</title> 8 <style> 9 body { 10 background-color: #1a202c; 11 color: white; 12 font-family: system-ui, -apple-system, sans-serif; 13 margin: 0; 14 padding: 0; 15 line-height: 1.5; 16 } 17 .container { 18 max-width: 600px; 19 margin: 0 auto; 20 padding: 20px; 21 } 22 header { 23 text-align: center; 24 margin: 30px 0; 25 } 26 header h1 { 27 margin-bottom: 10px; 28 } 29 header p { 30 color: #a0aec0; 31 margin: 0; 32 } 33 .card { 34 background-color: #2d3748; 35 border-radius: 8px; 36 padding: 20px; 37 margin-bottom: 20px; 38 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 39 } 40 .row { 41 display: flex; 42 justify-content: space-between; 43 align-items: center; 44 margin-bottom: 10px; 45 } 46 .progress-container { 47 height: 20px; 48 background-color: #4a5568; 49 border-radius: 9999px; 50 overflow: hidden; 51 margin-bottom: 20px; 52 position: relative; 53 } 54 .progress-bar { 55 height: 100%; 56 background-color: #6b46c1; 57 width: 0%; 58 position: relative; 59 overflow: hidden; 60 } 61 .progress-bar::after { 62 content: ''; 63 position: absolute; 64 top: 0; 65 left: 0; 66 width: 100%; 67 height: 100%; 68 background-image: linear-gradient( 69 45deg, 70 rgba(255, 255, 255, 0.2) 25%, 71 transparent 25%, 72 transparent 50%, 73 rgba(255, 255, 255, 0.2) 50%, 74 rgba(255, 255, 255, 0.2) 75%, 75 transparent 75%, 76 transparent 77 ); 78 background-size: 30px 30px; 79 animation: progress-animation 1s linear infinite; 80 z-index: 1; 81 } 82 @keyframes progress-animation { 83 0% { background-position: 0 0; } 84 100% { background-position: 30px 0; } 85 } 86 h2, h3 { 87 margin-top: 0; 88 } 89 .mt-4 { 90 margin-top: 20px; 91 } 92 .btn { 93 background-color: #6b46c1; 94 color: white; 95 border: none; 96 padding: 10px 16px; 97 border-radius: 4px; 98 cursor: pointer; 99 display: inline-block; 100 text-decoration: none; 101 } 102 .btn:disabled { 103 opacity: 0.7; 104 cursor: not-allowed; 105 } 106 .btn:hover:not(:disabled) { 107 background-color: #553c9a; 108 } 109 .text-muted { 110 color: #a0aec0; 111 } 112 .footer { 113 text-align: center; 114 margin-top: 20px; 115 } 116 .footer a { 117 color: #a0aec0; 118 text-decoration: none; 119 } 120 .footer a:hover { 121 text-decoration: underline; 122 } 123 .badge-container { 124 background-color: #374151; 125 border-radius: 8px; 126 padding: 20px; 127 margin-top: 20px; 128 text-align: center; 129 } 130 .badge-icon { 131 width: 120px; 132 height: 120px; 133 background-color: #6b46c1; 134 border-radius: 50%; 135 margin: 0 auto 15px; 136 display: flex; 137 align-items: center; 138 justify-content: center; 139 font-size: 40px; 140 } 141 .badge-info { 142 margin-bottom: 15px; 143 } 144 .navbar { 145 background-color: #2d3748; 146 padding: 15px 0; 147 margin-bottom: 20px; 148 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 149 } 150 .navbar-container { 151 display: flex; 152 justify-content: space-between; 153 align-items: center; 154 padding: 0 20px; 155 } 156 .nav-link { 157 color: white; 158 text-decoration: none; 159 } 160 .dev-tools { 161 margin-top: 20px; 162 border: 1px solid #553c9a; 163 background-color: rgba(102, 51, 153, 0.1); 164 } 165 </style> 166 <!-- Add manifest link for PWA --> 167 <link rel="manifest" href="/manifest.json"> 168 <!-- iOS support --> 169 <meta name="apple-mobile-web-app-capable" content="yes"> 170 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 171 <meta name="apple-mobile-web-app-title" content="ShiftLoop Wallet"> 172 <link rel="apple-touch-icon" href="/icon-192x192.png"> 173 <meta name="theme-color" content="#1a202c"> 174 <!-- Alpine.js --> 175
175<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
175 176 </head> 177 <body> 178 <nav class="navbar"> 179 <div class="navbar-container container"> 180 <a href="/" class="nav-link">ShiftLoop</a> 181 <a href="/dashboard" class="nav-link">Dashboard</a> 182 </div> 183 </nav> 184 185 <div class="container" x-data="{ 186 phone: '123456789', 187 xp: 0, 188 maxXp: 50, 189 progress: 0, 190 lessonCount: 79, 191 completedLessons: 0, 192 badgeTx: 'None' || null, 193 init() { 194 // Initialize progress bar 195 setTimeout(() => { 196 this.animateProgress() 197 }, 500) 198 199 // Set up auto-refresh 200 this.setupRefresh() 201 202 // Register service worker 203 this.registerServiceWorker() 204 205 // Load cached data from localStorage 206 this.loadCachedData() 207 }, 208 animateProgress() { 209 document.getElementById('xp-progress').style.transition = 'width 1s ease-in-out' 210 document.getElementById('xp-progress').style.width = this.progress + '%' 211 }, 212 setupRefresh() { 213 // Auto-refresh progress data every 30 seconds 214 setInterval(() => { 215 fetch(`/api/progress?phone=${this.phone}`) 216 .then(res => res.json()) 217 .then(data => { 218 if (data.xp !== undefined) { 219 this.xp = data.xp 220 this.progress = Math.min(100, parseInt((data.xp / this.maxXp) * 100)) 221 document.getElementById('xp-progress').style.width = this.progress + '%' 222 223 // Store in localStorage for offline 224 localStorage.setItem('shiftloop_xp', data.xp) 225 localStorage.setItem('shiftloop_progress', this.progress) 226 localStorage.setItem('shiftloop_phone', this.phone) 227 localStorage.setItem('shiftloop_updated', new Date().toISOString()) 228 } 229 }) 230 .catch(err => { 231 console.log('Could not update progress data. Using cached data.') 232 }) 233 }, 30000) 234 }, 235 registerServiceWorker() { 236 if ('serviceWorker' in navigator) { 237 window.addEventListener('load', () => { 238 navigator.serviceWorker.register('/service-worker.js') 239 .then(reg => console.log('ServiceWorker registration successful')) 240 .catch(err => console.log('ServiceWorker registration failed', err)) 241 }) 242 } 243 }, 244 loadCachedData() { 245 // If we have cached data and are offline, use it 246 if (!navigator.onLine) { 247 const cachedXp = localStorage.getItem('shiftloop_xp') 248 const cachedProgress = localStorage.getItem('shiftloop_progress') 249 if (cachedXp && cachedProgress) { 250 this.xp = parseInt(cachedXp) 251 this.progress = parseInt(cachedProgress) 252 this.animateProgress() 253 } 254 } 255 }, 256 simulateXpChange() { 257 fetch(`/dev/simulate-xp/${this.phone}`) 258 .then(res => res.json()) 259 .then(data => { 260 console.log('XP updated:', data) 261 if (data.new_xp !== undefined) { 262 this.xp = data.new_xp 263 this.progress = Math.min(100, parseInt((data.new_xp / this.maxXp) * 100)) 264 this.animateProgress() 265 } 266 }) 267 .catch(err => console.error('Error simulating XP change:', err)) 268 }, 269 createTestUser() { 270 fetch(`/dev/create-test-user/${this.phone}`) 271 .then(res => res.json()) 272 .then(data => { 273 console.log('Test user created/updated:', data) 274 location.reload() 275 }) 276 .catch(err => console.error('Error creating test user:', err)) 277 } 278 }"> 279 <header> 280 <h1>ShiftLoop Wallet</h1> 281 <p>Your Web3 bootcamp achievements</p> 282 </header> 283 284 <div class="card"> 285 <h2>Your Progress</h2> 286 <div class="row">
287 <span>Experience Points (XP)</span> 288 <span x-text="xp + '/' + maxXp">0/50</span> 289 </div> 290 <div class="progress-container"> 291 <div class="progress-bar" id="xp-progress"></div> 292 </div> 293 294 <div class="row"> 295 <span>Lessons Completed</span> 296 <span x-text="completedLessons + '/' + lessonCount">0/79</span> 297 </div> 298 </div> 299 300 <div class="card"> 301 <h2>NFT Badge</h2> 302 <template x-if="badgeTx"> 303 <div class='badge-container'> 304 <div class='badge-icon'>ð</div> 305 <h3 class='badge-info'>Web3 Bootcamp Graduate</h3> 306 <p>Congratulations! You've earned a blockchain-verified credential.</p> 307 <p class='text-muted'> 308 Transaction: 309 <a :href="'https://polygonscan.com/tx/' + badgeTx" target='_blank' style='color:#a0aec0;' 310 x-text="badgeTx.substring(0,8) + '...' + badgeTx.substring(badgeTx.length-6)"> 311 312 </a> 313 </p> 314 <div class='mt-4'> 315 <a href='#' class='btn'>Share with Friends</a> 316 </div> 317 </div> 318 </template> 319 <template x-if="!badgeTx"> 320 <div> 321 <p>Complete all lessons to earn your blockchain-verified badge!</p> 322 <div class='mt-4'> 323 <button class='btn' disabled>Share with Friends</button> 324 </div> 325 </div> 326 </template> 327 </div> 328 329 <div class="card"> 330 <div class="row"> 331 <div> 332 <h3 style="margin:0;">Offline Mode</h3> 333 <p class="text-muted" style="margin:0;">This wallet works offline after your first visit!</p> 334 </div> 335 <div class="badge-icon" style="width:40px;height:40px;font-size:20px;background-color:#38a169;">â</div> 336 </div> 337 </div> 338 339 <!-- Development tools (only visible in development) --> 340 <div class="card dev-tools"> 341 <h2>Development Tools</h2> 342 <p class="text-muted">These controls are for testing only</p> 343 <div style="display: flex; gap: 10px; margin-top: 15px;"> 344 <button @click="simulateXpChange()" class="btn">Simulate XP Change</button> 345 <button @click="createTestUser()" class="btn" style="background-color: #4299e1;">Create Test User</button> 346 </div> 347 </div> 348 349 <div class="footer"> 350 <a href="/dashboard">â Back to Dashboard</a> 351 </div> 352 </div> 353 </body> 354 </html> 355
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.