1/** 2 * Wolf Card Vending System - Frontend 3 * 4 * Allows users to sell their cards to dummy users for points. 5 * Features dynamic pricing based on edition numbers and daily market modifiers. 6 */ 7 8import { playBingeSound } from 'internal/audioManager'; 9import { apiMyRewards, apiWolfQuote, apiWolfAccept } from 'internal/endpoints'; 10import { contentLoadingGeneric } from 'internal/loading'; 11import { showMiniModal } from 'internal/modals'; 12import { dataApiCall, dataApiPostOrPut } from 'internal/requestHandling'; 13import { showFullscreenOverlay, closeFullscreenOverlay } from 'internal/fullscreenOverlay'; 14import { fetchAndDisplayPoints } from 'internal/points'; 15 16// Store current vending state 17let userCards = []; 18let selectedCard = null; 19let currentQuote = null; 20let quoteTimer = null; 21 22// Badge shape mapping 23const BADGE_SHAPES = { 24 1: 'badge-series-1', // Circle 25 2: 'badge-series-2', // Square 26 3: 'badge-series-3', // Hexagon 27 4: 'badge-series-4', // Shield 28 5: 'badge-series-5' // Star 29}; 30 31function getBadgeClass(seriesNumber) { 32 return BADGE_SHAPES[seriesNumber] || BADGE_SHAPES[1]; 33} 34 35/** 36 * Add parallax effect to card on mouse move 37 */ 38function addCardParallax(cardElement) { 39 cardElement.addEventListener('mousemove', (e) => { 40 const rect = cardElement.getBoundingClientRect(); 41 const x = e.clientX - rect.left; 42 const y = e.clientY - rect.top; 43 const centerX = rect.width / 2; 44 const centerY = rect.height / 2; 45 const rotateX = (y - centerY) / 25; 46 const rotateY = (centerX - x) / 25; 47 48 const frame = cardElement.querySelector('.card-frame'); 49 if (frame) { 50 frame.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; 51 } 52 }); 53 54 cardElement.addEventListener('mouseleave', () => { 55 const frame = cardElement.querySelector('.card-frame'); 56 if (frame) { 57 frame.style.transform = ''; 58 } 59 }); 60} 61 62/** 63 * Main entry point - Show wolf vending modal 64 */ 65export async function showWolfVendingModal() { 66 const loadingContent = ` 67 <div class="wolf-vending-fullscreen"> 68 <h2 class="wolf-vending-title">Sell Cards to Wolf Market</h2> 69 <div class="wolf-vending-content"> 70 ${contentLoadingGeneric} 71 </div> 72 </div> 73 `; 74 75 showFullscreenOverlay(loadingContent, resetVendingState, []); 76 77 try { 78 // Fetch cards and ensure minimum 1 second loading for smooth UX 79 const [cardsData] = await Promise.all([ 80 dataApiCall(apiMyRewards()), 81 new Promise(resolve => setTimeout(resolve, 1000)) 82 ]); 83 84 userCards = cardsData.all_rewards || []; 85 86 // Filter to only cards with copies 87 userCards = userCards.filter(card => card.copies && card.copies.length > 0); 88 89 if (userCards.length === 0) { 90 const noCardsContent = ` 91 <div class="wolf-vending-fullscreen"> 92 <h2 class="wolf-vending-title">Sell Cards to Wolf Market</h2> 93 <div class="wolf-vending-content"> 94 <p class="wolf-vending-message">You don't have any cards to sell yet.</p> 95 </div> 96 </div> 97 `; 98 showFullscreenOverlay(noCardsContent, resetVendingState, []); 99 return; 100 } 101 102 renderCardSelector(); 103 } catch (error) { 104 console.error('Error loading cards:', error); 105 const errorContent = ` 106 <div class="wolf-vending-fullscreen"> 107 <h2 class="wolf-vending-title">Sell Cards to Wolf Market</h2> 108 <div class="wolf-vending-content"> 109 <p class="wolf-vending-error">Unable to load your cards. Please try again later.</p> 110 </div> 111 </div> 112 `; 113 showFullscreenOverlay(errorContent, resetVendingState, []); 114 } 115} 116 117/** 118 * Render card selector with dropdown 119 */ 120function renderCardSelector() { 121 // Build dropdown options - flatten all card editions
122 let cardOptions = '<option value="">-- Select a card to sell --</option>'; 123 124 userCards.forEach(card => { 125 card.copies.forEach(copy => { 126 const optionValue = `${card.urlname}|${copy.edition_number}`; 127 cardOptions += `<option value="${optionValue}">${card.title} (No. ${copy.edition_number})</option>`; 128 }); 129 }); 130 131 const overlayContent = ` 132 <div class="wolf-vending-fullscreen"> 133 <h2 class="wolf-vending-title">Sell Cards to Wolf Market</h2> 134 <div class="wolf-vending-modal-content"> 135 <p class="wolf-vending-instructions">Select a card to get a price quote from Wolf Market.</p> 136 <div class="wolf-vending-footer"> 137 <label class="trade-footer-label">Your card:</label> 138 <select id="wolf-card-select" class="user-card-select" onchange="onSelectCard(this.value)"> 139 ${cardOptions} 140 </select> 141 <button id="get-quote-btn" class="confirm-trade-btn" disabled onclick="requestQuote()">Get Quote</button> 142 <button class="cancel-trade-btn" onclick="closeFullscreenOverlay()">Cancel</button> 143 </div> 144 </div> 145 </div> 146 `; 147 148 showFullscreenOverlay(overlayContent, resetVendingState, ['.wolf-vending-footer', '.user-card-select', '.confirm-trade-btn', '.cancel-trade-btn']); 149} 150 151/** 152 * Handle card dropdown selection 153 */ 154export function onSelectCard(value) { 155 if (!value) { 156 selectedCard = null; 157 } else { 158 const [urlname, editionNumber] = value.split('|'); 159 const cardData = userCards.find(c => c.urlname === urlname); 160 161 if (cardData) { 162 selectedCard = { 163 urlname, 164 edition_number: parseInt(editionNumber), 165 title: cardData.title, 166 tagline: cardData.tagline, 167 secret_image: cardData.secret_image, 168 series_number: cardData.series_number, 169 card_number: cardData.card_number 170 }; 171 } 172 } 173 174 // Update get quote button state 175 const getQuoteBtn = document.getElementById('get-quote-btn'); 176 if (getQuoteBtn) { 177 getQuoteBtn.disabled = !selectedCard; 178 } 179} 180 181/** 182 * Request a quote for the selected card 183 */ 184export async function requestQuote() { 185 if (!selectedCard) { 186 showMiniModal('Error', 'Please select a card'); 187 return; 188 } 189 190 await selectCardForQuote(selectedCard.urlname, selectedCard.edition_number); 191} 192 193/** 194 * User selected a card - get quote 195 */ 196async function selectCardForQuote(urlname, editionNumber) { 197 // Find the card data 198 const cardData = userCards.find(c => c.urlname === urlname); 199 if (!cardData) { 200 showMiniModal('Error', 'Card not found'); 201 return; 202 } 203 204 selectedCard = { 205 urlname, 206 edition_number: editionNumber, 207 title: cardData.title, 208 tagline: cardData.tagline, 209 secret_image: cardData.secret_image, 210 series_number: cardData.series_number, 211 card_number: cardData.card_number 212 }; 213 214 // Show loading state 215 const loadingContent = ` 216 <div class="wolf-vending-fullscreen"> 217 <h2 class="wolf-vending-title">Getting Quote...</h2> 218 <div class="wolf-vending-content"> 219 ${contentLoadingGeneric} 220 </div> 221 </div> 222 `; 223 showFullscreenOverlay(loadingContent, resetVendingState, []); 224 225 try { 226 // Get quote with minimum 1 second loading for smooth UX 227 const [response] = await Promise.all([ 228 dataApiPostOrPut(apiWolfQuote(), { 229 urlname: urlname, 230 edition_number: editionNumber 231 }, 'POST'), 232 new Promise(resolve => setTimeout(resolve, 1000)) 233 ]); 234 235 const result = await response.json(); 236 237 if (result.error) { 238 showMiniModal('Error', result.error); 239 renderCardSelector(); // Go back to card selection 240 return; 241 } 242 243 currentQuote = result; 244 displayQuote(result); 245 246 } catch (error) { 247 console.error('Error getting quote:', error); 248 showMiniModal('Error', 'Unable to get quote. Please try again.'); 249 renderCardSelector(); // Go back to card selection 250 } 251} 252 253/** 254 * Display the quote with countdown timer 255 */ 256function displayQuote(quoteData) { 257 const secretImage = `https://bc-rewards.nyc3.cdn.digitaloceanspaces.com/bc-rewards/${quoteData.secret_image}.png`; 258 const seriesNumber = quoteData.series_number !== undefined && quoteData.series_number !== null ? quoteData.series_number : 1; 259 const badgeClass = getBadgeClass(seriesNumber); 260 const cardNumber = selectedCard && selectedCard.card_number !== undefined && selectedCard.card_number !== null ? selectedCard.card_number : quoteData.edition_number; 261 262 // Determine if modifier is positive or negative 263 // From seller's perspective: > 1.0 is GOOD (more points), < 1.0 is BAD (less points) 264 const modifierValue = parseFloat(quoteData.daily_modifier); 265 const isBonus = modifierValue > 1.0; 266 const modifierClass = isBonus ? 'market-discount' : 'market-premium'; 267 const modifierArrow = isBonus ? 'â' : 'â'; 268 269 const overlayContent = ` 270 <div class="wolf-vending-fullscreen"> 271 <h2 class="wolf-vending-title">Wolf Market Quote</h2> 272 <div class="wolf-vending-modal-content"> 273 <p class="wolf-vending-instructions">Review your quote and accept to complete the sale.</p> 274 <div class="wolf-vending-quote-display"> 275 <div class="wolf-vending-card-container"> 276 <div class="trade-card shelf-card trading-card"> 277 <div class="card-frame"> 278 <div class="card-badges-container"> 279 <div class="card-edition-badge ${badgeClass}">#${cardNumber}</div> 280 <div class="card-series-label">Series ${seriesNumber}</div> 281 </div> 282 <div class="card-art-frame"> 283 <img src="${secretImage}" alt="${quoteData.title}" class="card-art"> 284 </div> 285 <div class="card-info"> 286 <h3 class="card-title">${quoteData.title}</h3> 287 </div> 288 </div> 289 </div> 290 </div> 291 <div class="wolf-vending-quote-details"> 292 <div class="wolf-vending-quote-pricing"> 293 <div class="wolf-vending-quote-row">
294 <span class="wolf-vending-quote-label">Base Price:</span> 295 <span class="wolf-vending-quote-value">${quoteData.base_price} pts</span> 296 </div> 297 <div class="wolf-vending-quote-row"> 298 <span class="wolf-vending-quote-label">Market Today:</span> 299 <span class="wolf-vending-quote-value ${modifierClass}">${quoteData.daily_modifier_text} ${modifierArrow}</span> 300 </div> 301 <div class="wolf-vending-quote-row wolf-vending-quote-final"> 302 <span class="wolf-vending-quote-label">You'll Receive:</span> 303 <span class="wolf-vending-quote-value wolf-vending-final-price">${quoteData.quoted_price} pts</span> 304 </div> 305 </div> 306 <div class="wolf-vending-quote-timer"> 307 <span class="wolf-vending-timer-label">Quote expires in:</span> 308 <span class="wolf-vending-timer-value" id="wolf-quote-countdown">5:00</span> 309 </div> 310 </div> 311 </div> 312 <div class="wolf-vending-footer"> 313 <button class="confirm-trade-btn" onclick="acceptWolfQuote()"> 314 Accept & Sell 315 </button> 316 <button class="cancel-trade-btn" onclick="cancelWolfQuote()"> 317 Cancel 318 </button> 319 </div> 320 </div> 321 </div> 322 `; 323 324 showFullscreenOverlay(overlayContent, resetVendingState, ['.trade-card', '.wolf-vending-footer', '.confirm-trade-btn', '.cancel-trade-btn']); 325 326 // Start countdown timer 327 startQuoteCountdown(quoteData.expires_at); 328 329 // Add parallax to card 330 setTimeout(() => { 331 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 332 if (!prefersReducedMotion) { 333 const card = document.querySelector('.trade-card'); 334 if (card) { 335 addCardParallax(card); 336 } 337 } 338 }, 100); 339} 340 341/** 342 * Start countdown timer for quote expiration 343 */ 344function startQuoteCountdown(expiresAt) { 345 // Clear any existing timer 346 if (quoteTimer) { 347 clearInterval(quoteTimer); 348 } 349 350 const expiryTime = new Date(expiresAt).getTime(); 351 352 quoteTimer = setInterval(() => { 353 const now = new Date().getTime(); 354 const timeLeft = expiryTime - now; 355 356 if (timeLeft <= 0) { 357 clearInterval(quoteTimer); 358 showMiniModal('Quote Expired', 'This quote has expired. Please get a new quote.'); 359 closeFullscreenOverlay(); 360 resetVendingState(); 361 return; 362 } 363 364 // Calculate minutes and seconds 365 const minutes = Math.floor(timeLeft / 60000); 366 const seconds = Math.floor((timeLeft % 60000) / 1000); 367 368 // Update display 369 const timerElement = document.getElementById('wolf-quote-countdown'); 370 if (timerElement) { 371 timerElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; 372 373 // Add warning class if less than 1 minute left 374 if (timeLeft < 60000) { 375 timerElement.classList.add('wolf-vending-timer-warning'); 376 } 377 } 378 }, 1000); 379} 380 381/** 382 * Accept the quote and complete the sale 383 */ 384export async function acceptWolfQuote() { 385 if (!currentQuote) { 386 showMiniModal('Error', 'No active quote'); 387 return; 388 } 389 390 // Disable button 391 const acceptBtn = document.querySelector('.wolf-vending-accept-btn'); 392 if (acceptBtn) { 393 acceptBtn.disabled = true; 394 acceptBtn.textContent = 'Processing...'; 395 } 396 397 try { 398 const response = await dataApiPostOrPut(apiWolfAccept(), { 399 quote_id: currentQuote.quote_id 400 }, 'POST'); 401 402 const result = await response.json(); 403 404 if (result.error) { 405 showMiniModal('Sale Failed', result.error); 406 if (acceptBtn) { 407 acceptBtn.disabled = false;
408 acceptBtn.textContent = 'Accept & Sell'; 409 } 410 return; 411 } 412 413 // Success! 414 clearInterval(quoteTimer); 415 closeFullscreenOverlay(); 416 417 // Update points display 418 fetchAndDisplayPoints(); 419 420 //playBingeSound('sale_made'); 421 422 // Show success message 423 const successHtml = ` 424 <div class="wolf-vending-success-container"> 425 <p class="wolf-vending-success-message"> 426 Successfully sold <strong>${result.title} (No. ${result.edition_number})</strong> for <strong>${result.cost} points</strong>! 427 </p> 428 <p class="wolf-vending-success-balance">New Balance: ${result.remaining_points}</p> 429 </div> 430 `; 431 432 showMiniModal('Card Sold!', successHtml); 433 434 resetVendingState(); 435 436 } catch (error) { 437 console.error('Error accepting quote:', error); 438 showMiniModal('Sale Failed', 'An error occurred. Please try again.'); 439 if (acceptBtn) { 440 acceptBtn.disabled = false; 441 acceptBtn.textContent = 'Accept & Sell'; 442 } 443 } 444} 445 446/** 447 * Cancel the current quote 448 */ 449export function cancelWolfQuote() { 450 clearInterval(quoteTimer); 451 currentQuote = null; 452 renderCardSelector(); // Go back to card selection 453} 454 455/** 456 * Reset vending state 457 */ 458function resetVendingState() { 459 if (quoteTimer) { 460 clearInterval(quoteTimer); 461 quoteTimer = null; 462 } 463 selectedCard = null; 464 currentQuote = null; 465 userCards = []; 466} 467 468// Expose functions to window for onclick handlers 469window.showWolfVendingModal = showWolfVendingModal; 470window.onSelectCard = onSelectCard; 471window.requestQuote = requestQuote; 472window.acceptWolfQuote = acceptWolfQuote; 473window.cancelWolfQuote = cancelWolfQuote; 474
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.