1// Shop Enhancer - Non-blocking individual features 2 3// Translation helper - reads from window.APEX_LANG.shop 4function _shopLang(key, fallback) { 5 var T = (window.APEX_LANG && window.APEX_LANG.shop) || {}; 6 return T[key] !== undefined ? T[key] : fallback; 7} 8 9document.addEventListener("DOMContentLoaded", function () { 10 // Compute RGB CSS variables from hex/rgb theme colors (live update) 11 try { 12 function parseColorToRgb(cssVar) { 13 const val = getComputedStyle(document.documentElement) 14 .getPropertyValue(cssVar) 15 .trim(); 16 if (!val) return null; 17 18 // Try hex 19 const hexMatch = val.match(/^#([0-9a-f]{3,8})$/i); 20 if (hexMatch) { 21 let hex = hexMatch[1]; 22 if (hex.length === 3) 23 hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 24 return [ 25 parseInt(hex.substring(0, 2), 16), 26 parseInt(hex.substring(2, 4), 16), 27 parseInt(hex.substring(4, 6), 16), 28 ]; 29 } 30 31 // Try rgb/rgba 32 const rgbMatch = val.match( 33 /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/, 34 ); 35 if (rgbMatch) 36 return [ 37 parseInt(rgbMatch[1]), 38 parseInt(rgbMatch[2]), 39 parseInt(rgbMatch[3]), 40 ]; 41 42 return null; 43 } 44 45 function updateRgbVariables() { 46 const color5Rgb = parseColorToRgb("--color5"); 47 const color2Rgb = parseColorToRgb("--color2"); 48 if (color5Rgb) 49 document.documentElement.style.setProperty( 50 "--color5-rgb", 51 color5Rgb.join(", "), 52 ); 53 if (color2Rgb) 54 document.documentElement.style.setProperty( 55 "--color2-rgb", 56 color2Rgb.join(", "), 57 ); 58 } 59 60 // Initial computation 61 updateRgbVariables(); 62 63 // Watch for style changes on <html> and all <style> tags for live editor updates 64 const styleObserver = new MutationObserver(updateRgbVariables); 65 styleObserver.observe(document.documentElement, { 66 attributes: true, 67 attributeFilter: ["style"], 68 }); 69 document.querySelectorAll("style").forEach(function (s) { 70 styleObserver.observe(s, { 71 childList: true, 72 characterData: true, 73 subtree: true, 74 }); 75 }); 76 // Also watch for new <style> tags added to <head> 77 const headObserver = new MutationObserver(function (mutations) { 78 mutations.forEach(function (m) { 79 m.addedNodes.forEach(function (node) { 80 if (node.nodeName === "STYLE") { 81 styleObserver.observe(node, { 82 childList: true, 83 characterData: true, 84 subtree: true, 85 }); 86 updateRgbVariables(); 87 } 88 }); 89 }); 90 }); 91 headObserver.observe(document.head, { childList: true }); 92 } catch (error) { 93 console.warn("Shop Enhancer - RGB Variables: ", error); 94 } 95 96 // Add global animations at startup 97 if (!document.getElementById("shine-animation")) { 98 const style = document.createElement("style"); 99 style.id = "shine-animation"; 100 style.textContent = ` 101 @keyframes shine { 102 0% { transform: translateX(-100%); } 103 30% { transform: translateX(100%); } 104 100% { transform: translateX(100%); } 105 } 106 `; 107 document.head.appendChild(style); 108 } 109 110 // Feature 1: Category Description Card Enhancer 111 try { 112 const categoryDescriptionCards = 113 document.querySelectorAll(".card-body.pb-1"); 114 115 categoryDescriptionCards.forEach(function (cardBody) { 116 // Remove pb-1 class 117 cardBody.classList.remove("pb-1"); 118 119 // Add mb-0 to all <p> elements inside this card-body 120 const paragraphs = cardBody.querySelectorAll("p"); 121 paragraphs.forEach(function (p) { 122 p.classList.add("mb-0"); 123 }); 124 }); 125 } catch (error) { 126 console.warn("Shop Enhancer - Category Description: ", error); 127 } 128 129 // Feature 2: Transform d-grid to card shop-profile-card 130 try { 131 const profileGrids = document.querySelectorAll(".d-grid.gap-2.mb-4"); 132 133 profileGrids.forEach(function (grid) { 134 // Check if it contains the profile content (has d-flex justify-content-center) 135 const hasProfileContent = grid.querySelector( 136 ".d-flex.justify-content-center.mb-3", 137 ); 138 139 if (hasProfileContent) { 140 grid.classList.remove("d-grid"); 141 grid.classList.add("card", "shop-profile-card"); 142 } 143 }); 144 } catch (error) { 145 console.warn("Shop Enhancer - Profile Card Transform: ", error); 146 } 147 148 // Feature 2b: Move profile card above categories list 149 try { 150 const profileCard = document.querySelector(".shop-profile-card"); 151 const categoryList = document.querySelector(".list-group.mb-4"); 152 153 if ( 154 profileCard && 155 categoryList && 156 categoryList.parentNode === profileCard.parentNode 157 ) { 158 // Insert profile card before the category list 159 categoryList.parentNode.insertBefore(profileCard, categoryList); 160 } 161 } catch (error) { 162 console.warn("Shop Enhancer - Profile Card Position: ", error); 163 } 164 165 // Feature 3: Enhance shop-profile-card design 166 try { 167 const profileCards = document.querySelectorAll(".shop-profile-card"); 168 169 profileCards.forEach(function (card) { 170 // Remove gap-2 and add proper structure 171 card.classList.remove("gap-2"); 172 card.classList.add("p-0", "overflow-hidden"); 173 174 // Get user info section 175 const userInfoSection = card.querySelector( 176 ".d-flex.justify-content-center.mb-3", 177 ); 178 if (userInfoSection) { 179 // Create card header for user info 180 userInfoSection.classList.remove( 181 "justify-content-center", 182 "mb-3", 183 ); 184 userInfoSection.classList.add("card-header", "p-3"); 185 186 // Style the avatar 187 const avatar = userInfoSection.querySelector("img"); 188 if (avatar) { 189 avatar.classList.add( 190 "rounded-circle", 191 "border", 192 "border-2",
193 "border-light", 194 ); 195 avatar.classList.remove("rounded", "me-3"); 196 avatar.classList.add("me-2"); 197 avatar.style.width = "56px"; 198 avatar.style.height = "56px"; 199 avatar.style.objectFit = "cover"; 200 } 201 202 // Style the username container 203 const userTextContainer = 204 userInfoSection.querySelector(".align-self-center"); 205 if (userTextContainer) { 206 userTextContainer.classList.remove("align-self-center"); 207 userTextContainer.classList.add( 208 "d-flex", 209 "flex-column", 210 "gap-1", 211 "justify-content-center", 212 ); 213 214 // Style the username 215 const username = userTextContainer.querySelector("h3"); 216 if (username) { 217 username.classList.add("fw-bold"); 218 username.style.fontSize = "1.1rem"; 219 } 220 221 // Transform money text to badge with gaming coin style 222 const moneyParagraph = userTextContainer.querySelector("p"); 223 if (moneyParagraph) { 224 const moneyText = moneyParagraph.textContent.trim(); 225 226 // Extract only the amount (remove "Money: " or any "Label: " pattern) 227 const amountOnly = moneyText 228 .replace(/^[^:]+:\s*/, "") 229 .trim(); 230 231 // Create badge element 232 const badge = document.createElement("span"); 233 badge.className = "badge rounded-pill px-2 py-1"; 234 badge.style.cssText = ` 235 background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); 236 color: #000; 237 font-weight: 700; 238 font-size: 0.75rem; 239 text-shadow: 0 1px 2px rgba(255, 255, 255, 0.3); 240 box-shadow: 241 0 2px 8px rgba(255, 215, 0, 0.3), 242 inset 0 1px 0 rgba(255, 255, 255, 0.4), 243 inset 0 -1px 0 rgba(0, 0, 0, 0.2); 244 border: 1.5px solid rgba(255, 255, 255, 0.3); 245 position: relative; 246 overflow: hidden; 247 `; 248 badge.innerHTML = `<i class="bi bi-coin me-1" style="filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3)); font-size: 0.85rem;"></i>${amountOnly}`; 249 250 // Add shine animation 251 const shine = document.createElement("span"); 252 shine.style.cssText = ` 253 position: absolute; 254 top: 0; 255 left: 0; 256 width: 100%; 257 height: 100%; 258 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%); 259 animation: shine 3s infinite; 260 pointer-events: none; 261 `; 262 badge.appendChild(shine); 263 264 // Replace the <p> with the badge 265 moneyParagraph.replaceWith(badge); 266 } 267 } 268 } 269 270 // Create card body for buttons with row grid 271 const cardBody = document.createElement("div"); 272 cardBody.className = "card-body p-3"; 273 274 // Create row for buttons 275 const buttonRow = document.createElement("div"); 276 buttonRow.className = "row g-2"; 277 278 // Move all buttons and form to card body 279 const buttons = card.querySelectorAll(":scope > .btn"); 280 const form = card.querySelector(":scope > form"); 281 282 buttons.forEach(function (btn) { 283 // Create col wrapper 284 const col = document.createElement("div"); 285 col.className = "col"; 286 287 // Keep btn-primary style, just add flex for icon alignment 288 btn.classList.add("w-100"); 289 290 col.appendChild(btn); 291 buttonRow.appendChild(col); 292 }); 293 294 cardBody.appendChild(buttonRow); 295 296 // Style and move logout button to the same row 297 if (form) { 298 const logoutBtn = form.querySelector("button"); 299 if (logoutBtn) { 300 // Create col wrapper for logout button 301 const col = document.createElement("div"); 302 col.className = "col"; 303 304 // Style with red gradient matching other buttons 305 logoutBtn.classList.remove("btn-secondary", "w-100"); 306 logoutBtn.classList.add("w-100"); 307 logoutBtn.style.cssText = ` 308 background: linear-gradient(135deg, #ff6b6b 0%, #dc3545 100%) !important; 309 color: #fff !important; 310 font-weight: 800; 311 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 312 box-shadow: 313 0 4px 15px rgba(220, 53, 69, 0.4), 314 inset 0 2px 0 rgba(255, 255, 255, 0.2), 315 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 316 border: 2px solid rgba(255, 255, 255, 0.2) !important; 317 position: relative; 318 overflow: hidden; 319 `; 320 321 // Style the icon 322 const icon = logoutBtn.querySelector("i"); 323 if (icon) { 324 icon.style.cssText = 325 "filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); position: relative; z-index: 2;"; 326 } 327 328 // Add shine animation 329 const shine = document.createElement("span"); 330 shine.style.cssText = ` 331 position: absolute; 332 top: 0; 333 left: 0; 334 width: 100%; 335 height: 100%; 336 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%); 337 animation: shine 3s infinite; 338 pointer-events: none; 339 z-index: 1; 340 `; 341 logoutBtn.appendChild(shine); 342 343 // Wrap text content to add z-index
344 logoutBtn.childNodes.forEach((node) => { 345 if ( 346 node.nodeType === Node.TEXT_NODE && 347 node.textContent.trim() 348 ) { 349 const span = document.createElement("span"); 350 span.style.cssText = 351 "position: relative; z-index: 2;"; 352 span.textContent = node.textContent; 353 node.replaceWith(span); 354 } 355 }); 356 357 // Move form into the col (keep form for CSRF) 358 form.classList.remove("text-center"); 359 form.style.cssText = "margin: 0; display: contents;"; 360 col.appendChild(form); 361 buttonRow.appendChild(col); 362 } 363 } 364 365 // Append card body to card 366 card.appendChild(cardBody); 367 }); 368 } catch (error) { 369 console.warn("Shop Enhancer - Profile Card Design: ", error); 370 } 371 372 // Feature 4: Style Login Button with gold coin design 373 try { 374 // Find login button by looking for the login icon 375 const loginButton = Array.from(document.querySelectorAll("a.btn")).find( 376 (btn) => btn.querySelector("i.bi-box-arrow-in-right"), 377 ); 378 379 if (loginButton) { 380 loginButton.style.cssText = ` 381 background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%) !important; 382 color: #000 !important; 383 font-weight: 800; 384 text-shadow: 0 1px 2px rgba(255, 255, 255, 0.3); 385 box-shadow: 386 0 4px 15px rgba(255, 215, 0, 0.4), 387 inset 0 2px 0 rgba(255, 255, 255, 0.4), 388 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 389 border: 2px solid rgba(255, 255, 255, 0.3) !important; 390 position: relative; 391 overflow: hidden; 392 `; 393 394 // Style the icon 395 const icon = loginButton.querySelector("i"); 396 if (icon) { 397 icon.style.cssText = 398 "filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); position: relative; z-index: 2;"; 399 } 400 401 // Add shine animation 402 const shine = document.createElement("span"); 403 shine.style.cssText = ` 404 position: absolute; 405 top: 0; 406 left: 0; 407 width: 100%; 408 height: 100%; 409 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%); 410 animation: shine 3s infinite; 411 pointer-events: none; 412 z-index: 1; 413 `; 414 loginButton.appendChild(shine); 415 416 // Wrap text content to add z-index 417 loginButton.childNodes.forEach((node) => { 418 if ( 419 node.nodeType === Node.TEXT_NODE && 420 node.textContent.trim() 421 ) { 422 const span = document.createElement("span"); 423 span.style.cssText = "position: relative; z-index: 2;"; 424 span.textContent = node.textContent; 425 node.replaceWith(span); 426 } 427 }); 428 } 429 } catch (error) { 430 console.warn("Shop Enhancer - Login Button Style: ", error); 431 } 432 433 // Feature 4b: Style Credit Button with gold coin design 434 try { 435 // Find credit button by looking for the credit-card icon 436 const creditButton = Array.from( 437 document.querySelectorAll("a.btn"), 438 ).find((btn) => btn.querySelector("i.bi-credit-card")); 439 440 if (creditButton) { 441 creditButton.style.cssText = ` 442 background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%) !important; 443 color: #000 !important; 444 font-weight: 800; 445 text-shadow: 0 1px 2px rgba(255, 255, 255, 0.3); 446 box-shadow: 447 0 4px 15px rgba(255, 215, 0, 0.4), 448 inset 0 2px 0 rgba(255, 255, 255, 0.4), 449 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 450 border: 2px solid rgba(255, 255, 255, 0.3) !important; 451 position: relative; 452 overflow: hidden; 453 `; 454 455 // Style the icon 456 const icon = creditButton.querySelector("i"); 457 if (icon) { 458 icon.style.cssText = 459 "filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); position: relative; z-index: 2;"; 460 } 461 462 // Add shine animation 463 const shine = document.createElement("span"); 464 shine.style.cssText = ` 465 position: absolute; 466 top: 0; 467 left: 0; 468 width: 100%; 469 height: 100%; 470 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%); 471 animation: shine 3s infinite; 472 pointer-events: none; 473 z-index: 1; 474 `; 475 creditButton.appendChild(shine); 476 477 // Wrap text content to add z-index
478 creditButton.childNodes.forEach((node) => { 479 if ( 480 node.nodeType === Node.TEXT_NODE && 481 node.textContent.trim() 482 ) { 483 const span = document.createElement("span"); 484 span.style.cssText = "position: relative; z-index: 2;"; 485 span.textContent = node.textContent; 486 node.replaceWith(span); 487 } 488 }); 489 } 490 } catch (error) { 491 console.warn("Shop Enhancer - Credit Button Style: ", error); 492 } 493 494 // Feature 4c: Style Cart Button with same effects but primary colors 495 try { 496 // Find cart button by looking for the cart icon 497 const cartButton = Array.from(document.querySelectorAll("a.btn")).find( 498 (btn) => btn.querySelector("i.bi-cart"), 499 ); 500 501 if (cartButton) { 502 cartButton.style.cssText = ` 503 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 504 color: var(--color2) !important; 505 font-weight: 800; 506 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 507 box-shadow: 508 0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.4), 509 inset 0 2px 0 rgba(255, 255, 255, 0.2), 510 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 511 border: 2px solid rgba(255, 255, 255, 0.2) !important; 512 position: relative; 513 overflow: hidden; 514 `; 515 516 // Style the icon 517 const icon = cartButton.querySelector("i"); 518 if (icon) { 519 icon.style.cssText = 520 "filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); position: relative; z-index: 2;"; 521 } 522 523 // Add shine animation 524 const shine = document.createElement("span"); 525 shine.style.cssText = ` 526 position: absolute; 527 top: 0; 528 left: 0; 529 width: 100%; 530 height: 100%; 531 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%); 532 animation: shine 3s infinite; 533 pointer-events: none; 534 z-index: 1; 535 `; 536 cartButton.appendChild(shine); 537 538 // Wrap text content to add z-index 539 cartButton.childNodes.forEach((node) => { 540 if ( 541 node.nodeType === Node.TEXT_NODE && 542 node.textContent.trim() 543 ) { 544 const span = document.createElement("span"); 545 span.style.cssText = "position: relative; z-index: 2;"; 546 span.textContent = node.textContent; 547 node.replaceWith(span); 548 } 549 }); 550 } 551 } catch (error) { 552 console.warn("Shop Enhancer - Cart Button Style: ", error); 553 } 554 555 // Feature 5: Enhance Goal Progress Bar with gaming style 556 try { 557 // Find goal card by looking for bi-graph-up icon in card-header 558 const goalCard = Array.from(document.querySelectorAll(".card")).find( 559 (card) => card.querySelector(".card-header i.bi-graph-up"), 560 ); 561 562 if (goalCard) { 563 const progressContainer = goalCard.querySelector(".progress"); 564 const progressBar = goalCard.querySelector(".progress-bar"); 565 const goalText = goalCard.querySelector(".card-text"); 566 567 if (progressContainer && progressBar) { 568 // Cap the visual progress at 100% so over-completed goals still fill the bar cleanly. 569 const rawProgressValue = 570 parseFloat(progressBar.getAttribute("aria-valuenow")) || 0; 571 const progressValue = Math.max( 572 0, 573 Math.min(rawProgressValue, 100), 574 ); 575 576 // Style progress container - taller for text inside 577 progressContainer.style.cssText = ` 578 height: 32px; 579 background: rgba(0, 0, 0, 0.3); 580 border-radius: 16px; 581 border: 2px solid rgba(255, 255, 255, 0.1); 582 overflow: hidden; 583 position: relative; 584 box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); 585 `; 586 587 // Style progress bar with theme gradient 588 progressBar.classList.remove( 589 "progress-bar-striped", 590 "progress-bar-animated", 591 ); 592 progressBar.style.cssText = `
593 width: ${progressValue}%; 594 background: linear-gradient(90deg, var(--color6) 0%, var(--color5) 100%); 595 border-radius: 14px; 596 position: relative; 597 overflow: hidden; 598 height: 100%; 599 min-width: 50px; 600 display: flex; 601 align-items: center; 602 justify-content: center; 603 box-shadow: 604 0 0 20px var(--color5), 605 0 0 40px rgba(var(--color5-rgb, 138, 43, 226), 0.3); 606 transition: width 1s ease-in-out; 607 `; 608 progressBar.setAttribute("aria-valuenow", progressValue); 609 610 // Add animated stripes using background-size for seamless loop 611 const stripes = document.createElement("div"); 612 stripes.style.cssText = ` 613 position: absolute; 614 top: 0; 615 left: 0; 616 right: 0; 617 bottom: 0; 618 background-image: linear-gradient( 619 -45deg, 620 transparent 25%, 621 rgba(255, 255, 255, 0.1) 25%, 622 rgba(255, 255, 255, 0.1) 50%, 623 transparent 50%, 624 transparent 75%, 625 rgba(255, 255, 255, 0.1) 75%, 626 rgba(255, 255, 255, 0.1) 100% 627 ); 628 background-size: 28px 28px; 629 animation: moveStripes 0.7s linear infinite; 630 `; 631 progressBar.appendChild(stripes); 632 633 // Add shine effect 634 const shine = document.createElement("div"); 635 shine.style.cssText = ` 636 position: absolute; 637 top: 0; 638 left: 0; 639 right: 0; 640 height: 50%; 641 background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%); 642 border-radius: 14px 14px 0 0; 643 pointer-events: none; 644 `; 645 progressBar.appendChild(shine); 646 647 // Remove goal text 648 if (goalText) { 649 goalText.remove(); 650 } 651 652 // Add percentage text inside the progress bar 653 const percentText = document.createElement("span"); 654 percentText.style.cssText = ` 655 position: relative; 656 z-index: 2; 657 color: #fff; 658 font-size: 0.85rem; 659 font-weight: 800; 660 text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); 661 letter-spacing: 0.5px; 662 `; 663 percentText.textContent = `${progressValue}%`; 664 progressBar.appendChild(percentText); 665 666 // Add CSS animations if not already added 667 if (!document.getElementById("goal-animations")) { 668 const style = document.createElement("style"); 669 style.id = "goal-animations"; 670 style.textContent = ` 671 @keyframes moveStripes { 672 from { background-position: 0 0; } 673 to { background-position: 28px 0; } 674 } 675 `; 676 document.head.appendChild(style); 677 } 678 } 679 } 680 } catch (error) { 681 console.warn("Shop Enhancer - Goal Progress Bar: ", error); 682 } 683 684 // Feature 6: Enhance Shop Login Page Layout 685 try { 686 // Check if we're on the shop login page (has #shop and login form) 687 const shopContainer = document.querySelector("#shop"); 688 const loginForm = document.querySelector( 689 'form[action*="shop"][action*="login"]', 690 ); 691 692 if (shopContainer && loginForm) { 693 const loginCard = loginForm.closest(".card"); 694 const contentCol = loginCard?.closest(".col-lg-9"); 695 696 if (loginCard && contentCol) { 697 // Remove text-center from col and add centering 698 contentCol.classList.remove("text-center"); 699 contentCol.classList.add( 700 "d-flex", 701 "align-items-center", 702 "justify-content-center", 703 ); 704 705 // Style the card with max-width for better centering 706 loginCard.style.maxWidth = "450px"; 707 loginCard.style.width = "100%"; 708 709 // Create styled card header with icon 710 const title = shopContainer.querySelector("h1"); 711 if (title) { 712 const cardHeader = document.createElement("div"); 713 cardHeader.className = "card-header text-center py-4"; 714 715 // Add large icon 716 const iconDiv = document.createElement("div"); 717 iconDiv.className = "mb-3"; 718 iconDiv.innerHTML =
719 '<i class="bi bi-person-circle" style="font-size: 3rem; opacity: 0.8;"></i>'; 720 cardHeader.appendChild(iconDiv); 721 722 // Move and style title 723 title.classList.add("mb-0", "h4"); 724 cardHeader.appendChild(title); 725 726 loginCard.insertBefore(cardHeader, loginCard.firstChild); 727 } 728 729 // Style card body 730 const cardBody = loginCard.querySelector(".card-body"); 731 if (cardBody) { 732 cardBody.classList.add("p-4"); 733 } 734 735 // Make form full width 736 loginForm.classList.remove("col-md-6", "mx-auto"); 737 loginForm.style.maxWidth = "none"; 738 739 // Style input group with icon 740 const inputGroup = loginForm.querySelector(".mb-3"); 741 const input = loginForm.querySelector("input.form-control"); 742 const label = loginForm.querySelector(".form-label"); 743 744 if (inputGroup && input) { 745 inputGroup.classList.remove("mb-3"); 746 inputGroup.classList.add("mb-4"); 747 748 // Create input group with icon 749 const inputWrapper = document.createElement("div"); 750 inputWrapper.className = "input-group"; 751 752 const inputIcon = document.createElement("span"); 753 inputIcon.className = "input-group-text"; 754 inputIcon.innerHTML = '<i class="bi bi-person"></i>'; 755 756 // Style input 757 input.classList.add("py-2"); 758 759 // Wrap input 760 input.parentNode.insertBefore(inputWrapper, input); 761 inputWrapper.appendChild(inputIcon); 762 inputWrapper.appendChild(input); 763 764 // Move error message outside of input-group 765 const errorSpan = 766 loginForm.querySelector(".invalid-feedback"); 767 if (errorSpan) { 768 inputWrapper.parentNode.insertBefore( 769 errorSpan, 770 inputWrapper.nextSibling, 771 ); 772 } 773 } 774 775 // Style button 776 const submitBtn = loginForm.querySelector( 777 'button[type="submit"]', 778 ); 779 if (submitBtn) { 780 submitBtn.classList.remove("mx-auto"); 781 submitBtn.classList.add("w-100", "py-2", "mt-2"); 782 } 783 } 784 } 785 } catch (error) { 786 console.warn("Shop Enhancer - Login Page Layout: ", error); 787 } 788 789 // Feature 7: Enhance Shop Modal Footer Design 790 try { 791 // Use MutationObserver to catch modals when they appear 792 const observer = new MutationObserver(function (mutations) { 793 mutations.forEach(function (mutation) { 794 mutation.addedNodes.forEach(function (node) { 795 if (node.nodeType === Node.ELEMENT_NODE) { 796 const modal = 797 node.closest?.(".modal") || 798 (node.classList?.contains("modal") ? node : null); 799 if (modal) { 800 enhanceModalFooter(modal); 801 } 802 } 803 }); 804 }); 805 }); 806 807 observer.observe(document.body, { childList: true, subtree: true }); 808 809 // Also enhance any already existing modals 810 document.querySelectorAll(".modal").forEach(enhanceModalFooter); 811 812 function enhanceModalFooter(modal) { 813 const footer = modal.querySelector(".modal-footer"); 814 if (!footer || footer.dataset.enhanced) return; 815 footer.dataset.enhanced = "true"; 816 817 // Style the footer - wrap-friendly layout 818 footer.style.cssText = ` 819 background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%); 820 border-top: 1px solid rgba(255, 255, 255, 0.08); 821 padding: 1.25rem 1.75rem; 822 display: flex; 823 align-items: center; 824 justify-content: space-between; 825 flex-wrap: wrap; 826 gap: 0.75rem; 827 `; 828 829 // Style the price display 830 const priceSpan = footer.querySelector("span.flex-md-fill"); 831 if (priceSpan) { 832 priceSpan.classList.remove("flex-md-fill", "font-weight-bold"); 833 priceSpan.style.cssText = ` 834 display: inline-flex; 835 align-items: center; 836 gap: 0.5rem; 837 background: linear-gradient(135deg, rgba(var(--color5-rgb, 138, 43, 226), 0.15) 0%, rgba(var(--color5-rgb, 138, 43, 226), 0.05) 100%); 838 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.3); 839 border-radius: 10px; 840 padding: 0.5rem 1rem; 841 font-weight: 800; 842 font-size: 1.15rem; 843 font-family: 'Lexend', sans-serif; 844 color: var(--color2); 845 position: relative; 846 overflow: hidden; 847 `; 848 849 // Add coin icon before price 850 const priceIcon = document.createElement("i"); 851 priceIcon.className = "bi bi-coin"; 852 priceIcon.style.cssText = ` 853 color: var(--color6); 854 font-size: 1.2rem; 855 filter: drop-shadow(0 0 4px rgba(var(--color5-rgb, 138, 43, 226), 0.5)); 856 `; 857 priceSpan.insertBefore(priceIcon, priceSpan.firstChild); 858 859 // Style the discount (del) element if present 860 const discount = priceSpan.querySelector("del"); 861 if (discount) { 862 discount.style.cssText = ` 863 color: rgba(var(--color2), 0.4); 864 opacity: 0.5; 865 font-size: 0.85rem; 866 font-weight: 400; 867 `; 868 } 869 870 // Add subtle shimmer on the price tag 871 const priceShine = document.createElement("span"); 872 priceShine.style.cssText = ` 873 position: absolute; 874 top: 0; 875 left: 0; 876 width: 100%; 877 height: 100%; 878 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.08) 50%, transparent 100%); 879 animation: shine 4s infinite; 880 pointer-events: none; 881 `; 882 priceSpan.appendChild(priceShine); 883 } 884 885 // Style all buttons in the footer 886 const buttons = footer.querySelectorAll(".btn"); 887 buttons.forEach(function (btn) { 888 // Determine button type 889 const isBuy = btn.querySelector("i.bi-cart-plus"); 890 const isRemove = btn.querySelector("i.bi-cart-x"); 891 892 if (isBuy) { 893 // Buy/Add to cart button - themed gradient 894 btn.style.cssText = ` 895 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 896 color: var(--color2) !important; 897 font-weight: 800; 898 font-size: 0.95rem; 899 padding: 0.625rem 1.5rem; 900 border-radius: 10px; 901 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 902 box-shadow: 903 0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.4), 904 inset 0 2px 0 rgba(var(--color2-rgb, 255, 255, 255), 0.2), 905 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 906 border: 2px solid rgba(var(--color2-rgb, 255, 255, 255), 0.15) !important; 907 position: relative; 908 overflow: hidden; 909 transition: all 0.3s ease; 910 text-transform: uppercase; 911 font-family: 'Lexend', sans-serif; 912 display: inline-flex; 913 align-items: center; 914 gap: 0.5rem; 915 `; 916 917 // Add hover 918 btn.addEventListener("mouseenter", function () { 919 this.style.transform = "translateY(-2px) scale(1.03)"; 920 this.style.boxShadow = ` 921 0 6px 25px rgba(var(--color5-rgb, 138, 43, 226), 0.5), 922 inset 0 2px 0 rgba(var(--color2-rgb, 255, 255, 255), 0.2), 923 inset 0 -2px 0 rgba(0, 0, 0, 0.2) 924 `; 925 }); 926 btn.addEventListener("mouseleave", function () { 927 this.style.transform = ""; 928 this.style.boxShadow = ` 929 0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.4), 930 inset 0 2px 0 rgba(var(--color2-rgb, 255, 255, 255), 0.2), 931 inset 0 -2px 0 rgba(0, 0, 0, 0.2) 932 `; 933 }); 934 935 // Add shine 936 const btnShine = document.createElement("span"); 937 btnShine.style.cssText = ` 938 position: absolute; 939 top: 0; 940 left: 0; 941 width: 100%; 942 height: 100%; 943 background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.25) 50%, transparent 100%); 944 animation: shine 3s infinite; 945 pointer-events: none; 946 z-index: 1; 947 `; 948 btn.appendChild(btnShine); 949 950 // Z-index text/icon 951 const icon = btn.querySelector("i"); 952 if (icon) 953 icon.style.cssText = 954 "position: relative; z-index: 2; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3));";
955 btn.childNodes.forEach((n) => { 956 if ( 957 n.nodeType === Node.TEXT_NODE && 958 n.textContent.trim() 959 ) { 960 const s = document.createElement("span"); 961 s.style.cssText = "position: relative; z-index: 2;"; 962 s.textContent = n.textContent; 963 n.replaceWith(s); 964 } 965 }); 966 } else if (isRemove) { 967 // Remove from cart - red style 968 btn.style.cssText = ` 969 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 970 color: var(--color2) !important; 971 font-weight: 800; 972 font-size: 0.95rem; 973 padding: 0.625rem 1.5rem; 974 border-radius: 10px; 975 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 976 box-shadow: 977 0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.4), 978 inset 0 2px 0 rgba(var(--color2-rgb, 255, 255, 255), 0.2), 979 inset 0 -2px 0 rgba(0, 0, 0, 0.2); 980 border: 2px solid rgba(var(--color2-rgb, 255, 255, 255), 0.15) !important; 981 position: relative; 982 overflow: hidden; 983 transition: all 0.3s ease; 984 text-transform: uppercase; 985 font-family: 'Lexend', sans-serif; 986 display: inline-flex; 987 align-items: center; 988 gap: 0.5rem; 989 `; 990 991 btn.addEventListener("mouseenter", function () { 992 this.style.transform = "translateY(-2px) scale(1.03)"; 993 }); 994 btn.addEventListener("mouseleave", function () { 995 this.style.transform = ""; 996 }); 997 } else { 998 // Other buttons (manage, subscribe, login) 999 btn.style.cssText = ` 1000 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 1001 color: var(--color2) !important; 1002 font-weight: 800; 1003 padding: 0.625rem 1.5rem; 1004 border-radius: 10px; 1005 border: 2px solid rgba(var(--color2-rgb, 255, 255, 255), 0.15) !important; 1006 transition: all 0.3s ease; 1007 text-transform: uppercase; 1008 font-family: 'Lexend', sans-serif; 1009 `; 1010 btn.addEventListener("mouseenter", function () { 1011 this.style.transform = "translateY(-2px) scale(1.03)"; 1012 }); 1013 btn.addEventListener("mouseleave", function () { 1014 this.style.transform = ""; 1015 }); 1016 } 1017 }); 1018 1019 // Restyle the buy form layout 1020 const buyForm = footer.querySelector("form.row"); 1021 if (buyForm) { 1022 buyForm.classList.remove( 1023 "row", 1024 "row-cols-lg-auto", 1025 "g-0", 1026 "gy-2", 1027 ); 1028 buyForm.style.cssText = ` 1029 display: flex; 1030 align-items: center; 1031 gap: 0.75rem; 1032 flex-wrap: wrap; 1033 `; 1034 } 1035 1036 // Transform quantity input into styled +/- counter 1037 const quantityInput = footer.querySelector("input#quantity"); 1038 if (quantityInput) { 1039 const min = parseInt(quantityInput.min) || 1; 1040 const max = parseInt(quantityInput.max) || 99; 1041 const parentDiv = 1042 quantityInput.closest(".mx-3") || 1043 quantityInput.parentElement; 1044 const label = footer.querySelector('label[for="quantity"]'); 1045 1046 // Create the counter container 1047 const counter = document.createElement("div"); 1048 counter.style.cssText = ` 1049 display: inline-flex; 1050 align-items: center; 1051 background: rgba(0, 0, 0, 0.25); 1052 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.3); 1053 border-radius: 10px; 1054 overflow: hidden; 1055 height: 40px; 1056 `; 1057 1058 // Minus button 1059 const minusBtn = document.createElement("button"); 1060 minusBtn.type = "button"; 1061 minusBtn.innerHTML = 1062 '<i class="bi bi-dash" style="margin:0;"></i>'; 1063 minusBtn.style.cssText = ` 1064 width: 36px; 1065 height: 100%; 1066 border: none; 1067 background: rgba(var(--color5-rgb, 138, 43, 226), 0.2); 1068 color: var(--color2); 1069 font-size: 1.1rem; 1070 cursor: pointer; 1071 display: flex; 1072 align-items: center; 1073 justify-content: center; 1074 transition: background 0.2s; 1075 padding: 0; 1076 `; 1077 minusBtn.addEventListener("mouseenter", function () { 1078 this.style.background = 1079 "rgba(var(--color5-rgb, 138, 43, 226), 0.4)"; 1080 }); 1081 minusBtn.addEventListener("mouseleave", function () { 1082 this.style.background = 1083 "rgba(var(--color5-rgb, 138, 43, 226), 0.2)"; 1084 }); 1085 1086 // Display value 1087 const display = document.createElement("span"); 1088 display.textContent = quantityInput.value || "1"; 1089 display.style.cssText = ` 1090 min-width: 40px; 1091 text-align: center; 1092 font-weight: 800; 1093 font-size: 1rem; 1094 font-family: 'Lexend', sans-serif; 1095 color: var(--color2); 1096 padding: 0 4px; 1097 user-select: none; 1098 `; 1099 1100 // Plus button 1101 const plusBtn = document.createElement("button"); 1102 plusBtn.type = "button"; 1103 plusBtn.innerHTML = 1104 '<i class="bi bi-plus" style="margin:0;"></i>'; 1105 plusBtn.style.cssText = ` 1106 width: 36px; 1107 height: 100%; 1108 border: none; 1109 background: rgba(var(--color5-rgb, 138, 43, 226), 0.2); 1110 color: var(--color2); 1111 font-size: 1.1rem; 1112 cursor: pointer; 1113 display: flex; 1114 align-items: center; 1115 justify-content: center; 1116 transition: background 0.2s; 1117 padding: 0; 1118 `; 1119 plusBtn.addEventListener("mouseenter", function () { 1120 this.style.background = 1121 "rgba(var(--color5-rgb, 138, 43, 226), 0.4)"; 1122 }); 1123 plusBtn.addEventListener("mouseleave", function () { 1124 this.style.background = 1125 "rgba(var(--color5-rgb, 138, 43, 226), 0.2)"; 1126 }); 1127
1128 // Wire up logic 1129 minusBtn.addEventListener("click", function () { 1130 let val = parseInt(quantityInput.value) || 1; 1131 if (val > min) { 1132 val--; 1133 quantityInput.value = val; 1134 display.textContent = val; 1135 } 1136 }); 1137 plusBtn.addEventListener("click", function () { 1138 let val = parseInt(quantityInput.value) || 1; 1139 if (val < max) { 1140 val++; 1141 quantityInput.value = val; 1142 display.textContent = val; 1143 } 1144 }); 1145 1146 // Hide original input but keep it in DOM for form submission 1147 quantityInput.style.cssText = 1148 "position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;"; 1149 1150 // Build counter 1151 counter.appendChild(minusBtn); 1152 counter.appendChild(display); 1153 counter.appendChild(plusBtn); 1154 1155 // Create wrapper with label 1156 const wrapper = document.createElement("div"); 1157 wrapper.style.cssText = 1158 "display: flex; align-items: center; gap: 0.5rem;"; 1159 if (label) { 1160 label.style.cssText = ` 1161 font-weight: 700; 1162 font-size: 0.75rem; 1163 text-transform: uppercase; 1164 letter-spacing: 0.5px; 1165 opacity: 0.7; 1166 margin: 0; 1167 white-space: nowrap; 1168 `; 1169 wrapper.appendChild(label); 1170 } 1171 wrapper.appendChild(counter); 1172 // Keep the hidden input inside 1173 wrapper.appendChild(quantityInput); 1174 1175 // Replace original parent 1176 if (parentDiv) { 1177 parentDiv.replaceWith(wrapper); 1178 } 1179 } 1180 1181 // Style price input 1182 const priceInput = footer.querySelector("input#price"); 1183 if (priceInput) { 1184 const parentDiv = 1185 priceInput.closest(".mx-3") || priceInput.parentElement; 1186 const label = footer.querySelector('label[for="price"]'); 1187 1188 priceInput.style.cssText = ` 1189 background: rgba(0, 0, 0, 0.25); 1190 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.3); 1191 border-radius: 10px; 1192 color: var(--color2); 1193 font-weight: 700; 1194 font-family: 'Lexend', sans-serif; 1195 text-align: center; 1196 padding: 0.5rem 0.75rem; 1197 width: 100px; 1198 height: 40px; 1199 transition: all 0.3s ease; 1200 `; 1201 priceInput.addEventListener("focus", function () { 1202 this.style.borderColor = "var(--color5)"; 1203 this.style.boxShadow = 1204 "0 0 12px rgba(var(--color5-rgb, 138, 43, 226), 0.3)"; 1205 this.style.outline = "none"; 1206 }); 1207 priceInput.addEventListener("blur", function () { 1208 this.style.borderColor = 1209 "rgba(var(--color5-rgb, 138, 43, 226), 0.3)"; 1210 this.style.boxShadow = "none"; 1211 }); 1212 1213 // Create wrapper with label 1214 const wrapper = document.createElement("div"); 1215 wrapper.style.cssText = 1216 "display: flex; align-items: center; gap: 0.5rem;"; 1217 if (label) { 1218 label.style.cssText = ` 1219 font-weight: 700; 1220 font-size: 0.75rem; 1221 text-transform: uppercase; 1222 letter-spacing: 0.5px; 1223 opacity: 0.7; 1224 margin: 0; 1225 white-space: nowrap; 1226 `; 1227 wrapper.appendChild(label); 1228 } 1229 wrapper.appendChild(priceInput); 1230 1231 if (parentDiv) { 1232 parentDiv.replaceWith(wrapper); 1233 } 1234 } 1235 } 1236 } catch (error) { 1237 console.warn("Shop Enhancer - Modal Footer Design: ", error); 1238 } 1239 1240 const packageImageStorageKey = "apexShopPackageImages"; 1241 const packageImageMemory = {}; 1242 1243 function getPackageIdFromUrl(url) { 1244 if (!url) return null; 1245 1246 try { 1247 const path = new URL(url, window.location.origin).pathname; 1248 const match = path.match( 1249 /\/(?:[^/]+\/)?(?:packages|cart\/remove)\/(\d+)(?:\/|$)/, 1250 ); 1251 1252 return match ? match[1] : null; 1253 } catch (error) { 1254 return null; 1255 } 1256 } 1257 1258 function getPackageImageMap() { 1259 try { 1260 return Object.assign( 1261 {}, 1262 packageImageMemory, 1263 JSON.parse( 1264 localStorage.getItem(packageImageStorageKey) || "{}", 1265 ), 1266 ); 1267 } catch (error) { 1268 return packageImageMemory; 1269 } 1270 } 1271 1272 function rememberPackageImage(id, src, alt) { 1273 if (!id || !src || src.endsWith("#")) return; 1274 1275 const images = getPackageImageMap(); 1276 images[id] = { src, alt: alt || "" }; 1277 packageImageMemory[id] = images[id]; 1278 1279 try { 1280 localStorage.setItem( 1281 packageImageStorageKey, 1282 JSON.stringify(images), 1283 ); 1284 } catch (error) { 1285 // Ignore storage quota/private-mode failures; the cart can still use its fallback icon. 1286 } 1287 } 1288 1289 function renderCartItemImage(media, imageData, name) { 1290 if (!media || !imageData || !imageData.src) return false;
1291 1292 media.innerHTML = ""; 1293 1294 const img = document.createElement("img"); 1295 img.src = imageData.src; 1296 img.alt = imageData.alt || name || ""; 1297 img.loading = "lazy"; 1298 img.style.cssText = ` 1299 width: 100%; 1300 height: 100%; 1301 object-fit: contain; 1302 padding: 0.35rem; 1303 filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.35)); 1304 `; 1305 img.addEventListener("error", function () { 1306 media.innerHTML = 1307 '<i class="bi bi-box-seam" style="font-size: 1.25rem; color: var(--color6);"></i>'; 1308 }); 1309 1310 media.appendChild(img); 1311 return true; 1312 } 1313 1314 function rememberPackageImagesFromDocument(doc) { 1315 doc.querySelectorAll("a[data-package-url]").forEach(function (link) { 1316 const packageId = getPackageIdFromUrl(link.dataset.packageUrl); 1317 const img = link.querySelector("img"); 1318 1319 if (packageId && img) { 1320 rememberPackageImage(packageId, img.src, img.alt); 1321 } 1322 }); 1323 } 1324 1325 function getShopBasePath() { 1326 const path = window.location.pathname.replace(/\/+$/, ""); 1327 const cartMatch = path.match(/^(.*)\/cart(?:\/|$)/); 1328 1329 if (cartMatch) return cartMatch[1] || "/shop"; 1330 1331 return "/shop"; 1332 } 1333 1334 async function hydrateCartImages(packageIds) { 1335 const missingIds = Array.from(new Set(packageIds)).filter(function (id) { 1336 return id && !getPackageImageMap()[id]; 1337 }); 1338 1339 if (missingIds.length === 0) return; 1340 1341 try { 1342 const parser = new DOMParser(); 1343 const shopBase = getShopBasePath(); 1344 const shopResponse = await fetch(shopBase, { 1345 credentials: "same-origin", 1346 }); 1347 1348 if (!shopResponse.ok) return; 1349 1350 const shopDoc = parser.parseFromString( 1351 await shopResponse.text(), 1352 "text/html", 1353 ); 1354 rememberPackageImagesFromDocument(shopDoc); 1355 1356 const categoryUrls = Array.from( 1357 shopDoc.querySelectorAll('a[href*="/categories/"]'), 1358 ) 1359 .map(function (link) { 1360 return new URL(link.href, window.location.origin).href; 1361 }) 1362 .filter(function (href, index, urls) { 1363 return ( 1364 href.startsWith(window.location.origin) && 1365 urls.indexOf(href) === index 1366 ); 1367 }); 1368 1369 for (const url of categoryUrls) { 1370 const currentMap = getPackageImageMap(); 1371 if (missingIds.every((id) => currentMap[id])) break; 1372 1373 const response = await fetch(url, { 1374 credentials: "same-origin", 1375 }); 1376 if (!response.ok) continue; 1377 1378 const doc = parser.parseFromString( 1379 await response.text(), 1380 "text/html", 1381 ); 1382 rememberPackageImagesFromDocument(doc); 1383 } 1384 } catch (error) { 1385 console.warn("Shop Enhancer - Cart Images: ", error); 1386 } 1387 1388 const images = getPackageImageMap(); 1389 document 1390 .querySelectorAll("[data-cart-package-image]") 1391 .forEach(function (media) { 1392 const id = media.dataset.cartPackageImage; 1393 renderCartItemImage(media, images[id], media.dataset.name); 1394 }); 1395 } 1396 1397 function createCartItemMedia(packageId, name) { 1398 const media = document.createElement("div"); 1399 media.dataset.cartPackageImage = packageId || ""; 1400 media.dataset.name = name || ""; 1401 media.style.cssText = ` 1402 width: 58px; 1403 height: 58px; 1404 border-radius: 12px; 1405 background: rgba(var(--color5-rgb, 138, 43, 226), 0.12); 1406 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.18); 1407 display: flex; 1408 align-items: center; 1409 justify-content: center; 1410 flex-shrink: 0; 1411 overflow: hidden; 1412 position: relative; 1413 `; 1414 1415 const imageData = packageId ? getPackageImageMap()[packageId] : null; 1416 1417 if (!renderCartItemImage(media, imageData, name)) { 1418 media.innerHTML = 1419 '<i class="bi bi-box-seam" style="font-size: 1.25rem; color: var(--color6);"></i>'; 1420 } 1421 1422 return media; 1423 } 1424
1425 function extractAmountText(text) { 1426 const clean = (text || "").replace(/\s+/g, " ").trim(); 1427 const colonAmount = clean.match(/:\s*(.+)$/); 1428 1429 if (colonAmount) return colonAmount[1].trim(); 1430 1431 const amountMatch = clean.match(/[\d.,]+\s*(?:[^\s:]+)?$/); 1432 1433 return amountMatch ? amountMatch[0].trim() : clean; 1434 } 1435 1436 function escapeHtml(value) { 1437 return String(value || "").replace(/[&<>"']/g, function (char) { 1438 return { 1439 "&": "&", 1440 "<": "<", 1441 ">": ">", 1442 '"': """, 1443 "'": "'", 1444 }[char]; 1445 }); 1446 } 1447 1448 function restylePromotionTable(table, iconClass) { 1449 const rows = Array.from(table.querySelectorAll("tbody tr")); 1450 const list = document.createElement("div"); 1451 list.style.cssText = ` 1452 display: flex; 1453 flex-direction: column; 1454 gap: 0.5rem; 1455 margin-bottom: 0.6rem; 1456 `; 1457 1458 rows.forEach(function (row) { 1459 const cells = row.querySelectorAll("th, td"); 1460 if (cells.length < 2) return; 1461 1462 const code = cells[0].textContent.trim(); 1463 const discount = cells[1].textContent.trim(); 1464 const actionForm = cells[2] ? cells[2].querySelector("form") : null; 1465 const item = document.createElement("div"); 1466 item.style.cssText = ` 1467 display: grid; 1468 grid-template-columns: 28px minmax(0, 1fr) auto; 1469 gap: 0.65rem; 1470 align-items: center; 1471 padding: 0.55rem 0.65rem; 1472 border-radius: 10px; 1473 background: rgba(0, 0, 0, 0.18); 1474 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.16); 1475 `; 1476 1477 const icon = document.createElement("div"); 1478 icon.style.cssText = ` 1479 width: 28px; 1480 height: 28px; 1481 border-radius: 8px; 1482 display: flex; 1483 align-items: center; 1484 justify-content: center; 1485 background: rgba(var(--color5-rgb, 138, 43, 226), 0.16); 1486 color: var(--color6); 1487 flex-shrink: 0; 1488 `; 1489 icon.innerHTML = `<i class="${iconClass}" style="margin:0; font-size:0.85rem;"></i>`; 1490 1491 const info = document.createElement("div"); 1492 info.style.cssText = "min-width: 0;"; 1493 info.innerHTML = ` 1494 <div style="font-family: 'Lexend', sans-serif; font-weight: 800; font-size: 0.85rem; color: var(--color2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(code)}</div> 1495 <div style="font-size: 0.78rem; color: rgba(var(--color2-rgb, 255, 255, 255), 0.58);
1495 margin-top: 1px;">${escapeHtml(discount)}</div> 1496 `; 1497 1498 item.appendChild(icon); 1499 item.appendChild(info); 1500 1501 if (actionForm) { 1502 actionForm.classList.remove("d-inline-block"); 1503 actionForm.style.cssText = 1504 "margin: 0; display: flex; align-items: center; justify-content: center;"; 1505 const btn = actionForm.querySelector(".btn-danger"); 1506 if (btn) { 1507 btn.classList.remove("btn", "btn-sm", "btn-danger"); 1508 btn.style.cssText = ` 1509 width: 28px; 1510 height: 28px; 1511 padding: 0; 1512 display: grid; 1513 place-items: center; 1514 background: rgba(220, 53, 69, 0.14); 1515 border: 1px solid rgba(220, 53, 69, 0.28); 1516 color: #ff6b6b; 1517 border-radius: 8px; 1518 font-size: 0.72rem; 1519 line-height: 1; 1520 transition: all 0.2s; 1521 `; 1522 const icon = btn.querySelector("i"); 1523 if (icon) { 1524 icon.style.cssText = 1525 "display: block; margin: 0; line-height: 1; font-size: 0.72rem;"; 1526 } 1527 } 1528 item.appendChild(actionForm); 1529 } 1530 1531 list.appendChild(item); 1532 }); 1533 1534 table.replaceWith(list); 1535 } 1536 1537 // Feature 8: Enhance Product Cards with gaming design 1538 try { 1539 const shopContainer = document.querySelector("#shop"); 1540 if (shopContainer) { 1541 const productCards = shopContainer.querySelectorAll( 1542 ".col-md-4 > .card.h-100", 1543 ); 1544 1545 productCards.forEach(function (card) { 1546 // === Card container styling === 1547 card.style.cssText = ` 1548 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.15); 1549 border-radius: 14px; 1550 overflow: hidden; 1551 transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1552 cursor: pointer; 1553 position: relative; 1554 `; 1555 1556 // Hover effects on card 1557 card.addEventListener("mouseenter", function () { 1558 this.style.transform = "translateY(-8px)"; 1559 this.style.boxShadow = ` 1560 0 15px 40px rgba(0, 0, 0, 0.5), 1561 0 0 30px rgba(var(--color5-rgb, 138, 43, 226), 0.2), 1562 0 0 60px rgba(var(--color5-rgb, 138, 43, 226), 0.1) 1563 `; 1564 this.style.borderColor = 1565 "rgba(var(--color5-rgb, 138, 43, 226), 0.5)"; 1566 }); 1567 card.addEventListener("mouseleave", function () { 1568 this.style.transform = ""; 1569 this.style.boxShadow = ""; 1570 this.style.borderColor = 1571 "rgba(var(--color5-rgb, 138, 43, 226), 0.15)"; 1572 }); 1573 1574 // === Image section === 1575 const imgLink = card.querySelector("a[data-package-url]"); 1576 const img = card.querySelector("img.card-img-top"); 1577 const packageId = getPackageIdFromUrl( 1578 imgLink ? imgLink.dataset.packageUrl : "", 1579 ); 1580 1581 if (img) { 1582 rememberPackageImage(packageId, img.src, img.alt); 1583 1584 // Create image wrapper 1585 const imgWrapper = document.createElement("div"); 1586 imgWrapper.style.cssText = ` 1587 position: relative; 1588 overflow: hidden; 1589 background: radial-gradient(circle at center, rgba(var(--color5-rgb, 138, 43, 226), 0.1) 0%, transparent 70%); 1590 display: flex; 1591 align-items: center; 1592 justify-content: center; 1593 padding: 1.5rem; 1594 min-height: 180px; 1595 `; 1596 1597 // Style image 1598 img.style.cssText = ` 1599 max-height: 160px; 1600 width: auto; 1601 max-width: 100%; 1602 object-fit: contain; 1603 filter: drop-shadow(0 5px 15px rgba(0, 0, 0, 0.4)); 1604 transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1605 position: relative; 1606 z-index: 2; 1607 `; 1608 img.classList.remove("card-img-top"); 1609 1610 // Hover zoom on image 1611 card.addEventListener("mouseenter", function () { 1612 img.style.transform = "scale(1.08)"; 1613 img.style.filter = 1614 "drop-shadow(0 8px 25px rgba(var(--color5-rgb, 138, 43, 226), 0.4))"; 1615 }); 1616 card.addEventListener("mouseleave", function () { 1617 img.style.transform = ""; 1618 img.style.filter = 1619 "drop-shadow(0 5px 15px rgba(0, 0, 0, 0.4))"; 1620 }); 1621 1622 // Add subtle glow behind image 1623 const glow = document.createElement("div"); 1624 glow.style.cssText = ` 1625 position: absolute; 1626 top: 50%; 1627 left: 50%; 1628 transform: translate(-50%, -50%); 1629 width: 60%; 1630 height: 60%; 1631 background: radial-gradient(circle, rgba(var(--color5-rgb, 138, 43, 226), 0.25) 0%, transparent 70%); 1632 filter: blur(20px); 1633 z-index: 1; 1634 pointer-events: none; 1635 transition: opacity 0.4s; 1636 `; 1637 imgWrapper.appendChild(glow); 1638 1639 // Bottom gradient fade
1640 const imgFade = document.createElement("div"); 1641 imgFade.style.cssText = ` 1642 position: absolute; 1643 bottom: 0; 1644 left: 0; 1645 right: 0; 1646 height: 40%; 1647 background: linear-gradient(180deg, transparent 0%, var(--color4, #0d0c17) 100%); 1648 z-index: 3; 1649 pointer-events: none; 1650 `; 1651 imgWrapper.appendChild(imgFade); 1652 1653 // Replace original image placement 1654 if (imgLink) { 1655 imgLink.style.cssText = 1656 "text-decoration: none; display: block;"; 1657 imgLink.innerHTML = ""; 1658 imgLink.appendChild(imgWrapper); 1659 imgWrapper.appendChild(img); 1660 } else { 1661 const parent = img.parentElement; 1662 parent.insertBefore(imgWrapper, img); 1663 imgWrapper.appendChild(img); 1664 } 1665 } 1666 1667 // === Card body === 1668 const cardBody = card.querySelector(".card-body"); 1669 if (cardBody) { 1670 cardBody.style.cssText = ` 1671 padding: 1.25rem 1.25rem 1.5rem; 1672 display: flex; 1673 flex-direction: column; 1674 gap: 0.75rem; 1675 position: relative; 1676 z-index: 4; 1677 `; 1678 1679 // Style title 1680 const title = cardBody.querySelector(".card-title"); 1681 if (title) { 1682 title.style.cssText = ` 1683 font-family: 'Lexend', sans-serif; 1684 font-weight: 800; 1685 font-size: 1.05rem; 1686 margin: 0; 1687 color: var(--color2); 1688 text-transform: uppercase; 1689 letter-spacing: 0.5px; 1690 `; 1691 } 1692 1693 // Style price + buy button in a row 1694 const priceEl = cardBody.querySelector(".card-subtitle"); 1695 const buyBtn = cardBody.querySelector(".btn"); 1696 1697 if (priceEl || buyBtn) { 1698 // Create row container for price + buy button 1699 const actionRow = document.createElement("div"); 1700 actionRow.style.cssText = ` 1701 display: flex; 1702 gap: 0.5rem; 1703 margin-top: auto; 1704 align-items: stretch; 1705 `; 1706 1707 if (priceEl) { 1708 priceEl.classList.remove("card-subtitle", "mb-3"); 1709 priceEl.style.cssText = ` 1710 display: inline-flex; 1711 align-items: center; 1712 justify-content: center; 1713 gap: 0.4rem; 1714 background: linear-gradient(135deg, rgba(var(--color5-rgb, 138, 43, 226), 0.2) 0%, rgba(var(--color5-rgb, 138, 43, 226), 0.08) 100%); 1715 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.25); 1716 border-radius: 10px; 1717 padding: 0.6rem 0.85rem; 1718 font-weight: 800; 1719 font-size: 0.85rem; 1720 font-family: 'Lexend', sans-serif; 1721 color: var(--color2); 1722 margin: 0; 1723 white-space: nowrap; 1724 flex: 1 1 0; 1725 `; 1726 1727 // Add coin icon 1728 const coinIcon = document.createElement("i"); 1729 coinIcon.className = "bi bi-coin"; 1730 coinIcon.style.cssText = ` 1731 color: var(--color6); 1732 font-size: 0.85rem; 1733 margin: 0; 1734 filter: drop-shadow(0 0 3px rgba(var(--color5-rgb, 138, 43, 226), 0.4)); 1735 `; 1736 priceEl.insertBefore(coinIcon, priceEl.firstChild); 1737 1738 // Style discount 1739 const discount = priceEl.querySelector("del"); 1740 if (discount) { 1741 discount.style.cssText = ` 1742 opacity: 0.5; 1743 font-size: 0.75rem; 1744 font-weight: 400; 1745 margin-right: 0.25rem; 1746 `; 1747 } 1748 1749 actionRow.appendChild(priceEl); 1750 } 1751 1752 if (buyBtn) { 1753 buyBtn.style.cssText = ` 1754 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 1755 color: var(--color2) !important; 1756 font-weight: 800; 1757 font-size: 0.85rem; 1758 padding: 0.6rem 1.25rem; 1759 border-radius: 10px; 1760 border: 1px solid rgba(var(--color2-rgb, 255, 255, 255), 0.15) !important; 1761 position: relative; 1762 overflow: hidden; 1763 transition: all 0.3s ease; 1764 text-transform: uppercase; 1765 font-family: 'Lexend', sans-serif; 1766 box-shadow: 0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.3); 1767 flex: 1 1 0; 1768 display: flex; 1769 align-items: center; 1770 justify-content: center; 1771 margin: 0; 1772 `; 1773 1774 buyBtn.addEventListener("mouseenter", function () { 1775 this.style.boxShadow = 1776 "0 6px 25px rgba(var(--color5-rgb, 138, 43, 226), 0.5)"; 1777 }); 1778 buyBtn.addEventListener("mouseleave", function () { 1779 this.style.boxShadow = 1780 "0 4px 15px rgba(var(--color5-rgb, 138, 43, 226), 0.3)"; 1781 }); 1782 1783 // Add shine 1784 const btnShine = document.createElement("span"); 1785 btnShine.style.cssText = ` 1786 position: absolute; 1787 top: 0; 1788 left: 0; 1789 width: 100%; 1790 height: 100%; 1791 background: linear-gradient(90deg, transparent 0%, rgba(var(--color2-rgb, 255, 255, 255), 0.2) 50%, transparent 100%); 1792 animation: shine 3s infinite; 1793 pointer-events: none; 1794 z-index: 1; 1795 `; 1796 buyBtn.appendChild(btnShine); 1797 1798 // Z-index on content 1799 const icon = buyBtn.querySelector("i"); 1800 if (icon) 1801 icon.style.cssText = 1802 "position: relative; z-index: 2; margin: 0 0.4rem 0 0;";
1803 buyBtn.childNodes.forEach((n) => { 1804 if ( 1805 n.nodeType === Node.TEXT_NODE && 1806 n.textContent.trim() 1807 ) { 1808 const s = document.createElement("span"); 1809 s.style.cssText = 1810 "position: relative; z-index: 2;"; 1811 s.textContent = n.textContent; 1812 n.replaceWith(s); 1813 } 1814 }); 1815 1816 actionRow.appendChild(buyBtn); 1817 } 1818 1819 cardBody.appendChild(actionRow); 1820 } 1821 } 1822 1823 // === Top-right decorative glow corner === 1824 const cornerGlow = document.createElement("div"); 1825 cornerGlow.style.cssText = ` 1826 position: absolute; 1827 top: -30px; 1828 right: -30px; 1829 width: 80px; 1830 height: 80px; 1831 background: radial-gradient(circle, rgba(var(--color5-rgb, 138, 43, 226), 0.3) 0%, transparent 70%); 1832 filter: blur(15px); 1833 pointer-events: none; 1834 z-index: 0; 1835 `; 1836 card.appendChild(cornerGlow); 1837 }); 1838 } 1839 } catch (error) { 1840 console.warn("Shop Enhancer - Product Cards: ", error); 1841 } 1842 1843 // Feature 9: Redesign Cart Page with gaming layout 1844 try { 1845 // Detect cart page: look for cart-items table OR the alert-warning inside a card on a cart page 1846 const cartTable = document.querySelector("table.cart-items"); 1847 let cartCard = cartTable ? cartTable.closest(".card") : null; 1848 1849 // Fallback: detect empty cart page by URL or alert content 1850 if (!cartCard && window.location.pathname.includes("/cart")) { 1851 // Find the main card on the page (should be the one after h1) 1852 const allCards = document.querySelectorAll(".card"); 1853 allCards.forEach(function (c) { 1854 if ( 1855 !cartCard && 1856 c.querySelector(".alert-warning, .input-group") 1857 ) { 1858 cartCard = c; 1859 } 1860 }); 1861 } 1862 1863 if (cartCard) { 1864 const cardBody = cartCard.querySelector(".card-body"); 1865 if (!cardBody) throw new Error("No card-body found"); 1866 1867 // === Hide original <h1> and restyle === 1868 const pageTitle = 1869 cardBody.parentElement.parentElement.querySelector( 1870 ":scope > h1", 1871 ); 1872 if (pageTitle) { 1873 pageTitle.style.cssText = ` 1874 font-family: 'Lexend', sans-serif; 1875 font-weight: 900; 1876 font-size: 1.8rem; 1877 text-transform: uppercase; 1878 letter-spacing: 1px; 1879 margin-bottom: 1.25rem; 1880 display: flex; 1881 align-items: center; 1882 gap: 0.75rem; 1883 `; 1884 const cartIcon = document.createElement("i"); 1885 cartIcon.className = "bi bi-cart3"; 1886 cartIcon.style.cssText = ` 1887 color: var(--color6); 1888 font-size: 1.6rem; 1889 filter: drop-shadow(0 0 6px rgba(var(--color5-rgb, 138, 43, 226), 0.4)); 1890 `; 1891 pageTitle.insertBefore(cartIcon, pageTitle.firstChild); 1892 } 1893 1894 // === Remove the wrapping card - we'll make our own layout === 1895 cardBody.style.cssText = 1896 "padding: 0; border: none; background: none;"; 1897 cartCard.style.cssText = 1898 "border: none; background: none; box-shadow: none;"; 1899 // Remove top glow line from card via ::before 1900 cartCard.classList.add("cart-redesigned"); 1901 1902 // Create main 2-column layout 1903 const mainRow = document.createElement("div"); 1904 mainRow.style.cssText = 1905 "display: grid; grid-template-columns: 1fr 380px; gap: 1.5rem;";
1906 mainRow.id = "cart-gaming-layout"; 1907 1908 // === LEFT COLUMN: items === 1909 const leftCol = document.createElement("div"); 1910 leftCol.style.cssText = "min-width: 0;"; 1911 1912 // === RIGHT COLUMN: summary sidebar === 1913 const rightCol = document.createElement("div"); 1914 rightCol.style.cssText = "min-width: 0;"; 1915 1916 // =============================== 1917 // LEFT: Transform table rows into item cards 1918 // =============================== 1919 // Find update form as the form containing the cart-items table 1920 const updateForm = cartTable ? cartTable.closest("form") : null; 1921 // Find clear form (form with btn-danger right after the update form) 1922 const clearForm = updateForm 1923 ? updateForm.nextElementSibling && 1924 updateForm.nextElementSibling.tagName === "FORM" 1925 ? updateForm.nextElementSibling 1926 : cardBody.querySelector("form.text-end.mb-4") 1927 : null; 1928 const tbody = cartTable ? cartTable.querySelector("tbody") : null; 1929 const rows = tbody ? tbody.querySelectorAll("tr") : []; 1930 1931 if (updateForm && rows.length > 0) { 1932 // Hide original table 1933 cartTable.style.display = "none"; 1934 const cartPackageIds = []; 1935 1936 // Create items container 1937 const itemsContainer = document.createElement("div"); 1938 itemsContainer.style.cssText = 1939 "display: flex; flex-direction: column; gap: 0.75rem;"; 1940 1941 // Create hidden form for CSRF + quantities 1942 const hiddenFormData = document.createElement("div"); 1943 hiddenFormData.style.display = "none"; 1944 if (!updateForm.id) { 1945 updateForm.id = "apex-cart-update-form"; 1946 } 1947 // Move CSRF token 1948 const csrf = updateForm.querySelector('input[name="_token"]'); 1949 if (csrf) hiddenFormData.appendChild(csrf.cloneNode()); 1950 1951 rows.forEach(function (row) { 1952 const cells = row.querySelectorAll("td, th"); 1953 if (cells.length < 5) return; 1954 1955 const name = cells[0].textContent.trim(); 1956 const price = cells[1].textContent.trim(); 1957 const total = cells[2].textContent.trim(); 1958 const qtyInput = cells[3].querySelector("input"); 1959 const removeLink = cells[4].querySelector("a"); 1960 const packageId = getPackageIdFromUrl( 1961 removeLink ? removeLink.href : "", 1962 ); 1963 if (packageId) cartPackageIds.push(packageId); 1964 1965 // Item card 1966 const itemCard = document.createElement("div"); 1967 itemCard.style.cssText = ` 1968 background: linear-gradient(135deg, var(--color3) 0%, var(--color4) 100%); 1969 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.12); 1970 border-radius: 12px; 1971 padding: 1rem 1.25rem; 1972 display: flex; 1973 align-items: center; 1974 gap: 1rem; 1975 transition: all 0.3s ease; 1976 position: relative; 1977 overflow: hidden; 1978 `; 1979 1980 itemCard.addEventListener("mouseenter", function () { 1981 this.style.borderColor = 1982 "rgba(var(--color5-rgb, 138, 43, 226), 0.35)"; 1983 this.style.boxShadow = 1984 "0 4px 20px rgba(var(--color5-rgb, 138, 43, 226), 0.12)"; 1985 }); 1986 itemCard.addEventListener("mouseleave", function () { 1987 this.style.borderColor = 1988 "rgba(var(--color5-rgb, 138, 43, 226), 0.12)"; 1989 this.style.boxShadow = "none"; 1990 }); 1991 1992 // Item image, using the product image cached from shop cards when available. 1993 const itemIcon = createCartItemMedia(packageId, name); 1994 1995 // Item info (name + unit price) 1996 const itemInfo = document.createElement("div"); 1997 itemInfo.style.cssText = "flex: 1; min-width: 0;"; 1998 itemInfo.innerHTML = ` 1999 <div style="font-family: 'Lexend', sans-serif; font-weight: 700; font-size: 0.95rem; color: var(--color2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> 2000 ${escapeHtml(name)} 2001 </div> 2002 <div style="font-size: 0.8rem; color: rgba(var(--color2-rgb, 255, 255, 255), 0.5); margin-top: 2px;"> 2003 ${escapeHtml(price)} ${escapeHtml(_shopLang("per_unit", "/ unit"))} 2004 </div> 2005 `; 2006 2007 // Quantity controls
2008 const qtyContainer = document.createElement("div"); 2009 qtyContainer.style.cssText = ` 2010 display: flex; 2011 align-items: center; 2012 background: rgba(0, 0, 0, 0.25); 2013 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.2); 2014 border-radius: 8px; 2015 overflow: hidden; 2016 height: 34px; 2017 flex-shrink: 0; 2018 `; 2019 2020 if (qtyInput) { 2021 const min = parseInt(qtyInput.min) || 0; 2022 const max = parseInt(qtyInput.max) || 99; 2023 const isReadonly = qtyInput.hasAttribute("readonly"); 2024 2025 // Clone input for form submission (hidden) 2026 const hiddenQty = qtyInput.cloneNode(); 2027 hiddenQty.style.cssText = 2028 "position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;"; 2029 hiddenFormData.appendChild(hiddenQty); 2030 2031 const minusBtn = document.createElement("button"); 2032 minusBtn.type = "button"; 2033 minusBtn.innerHTML = 2034 '<i class="bi bi-dash" style="margin:0;"></i>'; 2035 minusBtn.style.cssText = ` 2036 width: 30px; height: 100%; border: none; 2037 background: rgba(var(--color5-rgb, 138, 43, 226), 0.15); 2038 color: var(--color2); cursor: pointer; 2039 display: flex; align-items: center; justify-content: center; 2040 transition: background 0.2s; padding: 0; font-size: 1rem; 2041 `; 2042 if (isReadonly) minusBtn.disabled = true; 2043 2044 const display = document.createElement("span"); 2045 display.textContent = qtyInput.value || "1"; 2046 display.style.cssText = ` 2047 min-width: 32px; text-align: center; font-weight: 800; 2048 font-size: 0.85rem; font-family: 'Lexend', sans-serif; 2049 color: var(--color2); user-select: none; 2050 `; 2051 2052 const plusBtn = document.createElement("button"); 2053 plusBtn.type = "button"; 2054 plusBtn.innerHTML = 2055 '<i class="bi bi-plus" style="margin:0;"></i>'; 2056 plusBtn.style.cssText = minusBtn.style.cssText; 2057 if (isReadonly) plusBtn.disabled = true; 2058 2059 minusBtn.addEventListener("mouseenter", function () { 2060 this.style.background = 2061 "rgba(var(--color5-rgb, 138, 43, 226), 0.35)"; 2062 }); 2063 minusBtn.addEventListener("mouseleave", function () { 2064 this.style.background = 2065 "rgba(var(--color5-rgb, 138, 43, 226), 0.15)"; 2066 }); 2067 plusBtn.addEventListener("mouseenter", function () { 2068 this.style.background = 2069 "rgba(var(--color5-rgb, 138, 43, 226), 0.35)"; 2070 }); 2071 plusBtn.addEventListener("mouseleave", function () { 2072 this.style.background = 2073 "rgba(var(--color5-rgb, 138, 43, 226), 0.15)"; 2074 }); 2075 2076 minusBtn.addEventListener("click", function () { 2077 let val = parseInt(hiddenQty.value) || 0; 2078 if (val > min) { 2079 val--; 2080 hiddenQty.value = val; 2081 display.textContent = val; 2082 } 2083 }); 2084 plusBtn.addEventListener("click", function () { 2085 let val = parseInt(hiddenQty.value) || 0; 2086 if (val < max) { 2087 val++; 2088 hiddenQty.value = val; 2089 display.textContent = val; 2090 } 2091 }); 2092 2093 qtyContainer.appendChild(minusBtn); 2094 qtyContainer.appendChild(display); 2095 qtyContainer.appendChild(plusBtn); 2096 } 2097 2098 // Total price 2099 const totalBadge = document.createElement("div"); 2100 totalBadge.style.cssText = ` 2101 font-family: 'Lexend', sans-serif; 2102 font-weight: 800; 2103 font-size: 0.9rem; 2104 color: var(--color2); 2105 white-space: nowrap; 2106 min-width: 70px; 2107 text-align: right; 2108 flex-shrink: 0; 2109 `; 2110 totalBadge.textContent = total; 2111 2112 // Remove button 2113 const removeBtn = document.createElement("a"); 2114 removeBtn.href = removeLink ? removeLink.href : "#"; 2115 removeBtn.style.cssText = ` 2116 width: 32px; height: 32px; border-radius: 8px; 2117 background: rgba(220, 53, 69, 0.15); 2118 border: 1px solid rgba(220, 53, 69, 0.25); 2119 display: flex; align-items: center; justify-content: center; 2120 color: #ff6b6b; transition: all 0.2s; flex-shrink: 0; 2121 text-decoration: none; font-size: 0.8rem; 2122 `; 2123 removeBtn.innerHTML = 2124 '<i class="bi bi-trash3" style="margin:0;"></i>'; 2125 removeBtn.addEventListener("mouseenter", function () { 2126 this.style.background = "rgba(220, 53, 69, 0.35)"; 2127 this.style.borderColor = "rgba(220, 53, 69, 0.5)"; 2128 }); 2129 removeBtn.addEventListener("mouseleave", function () { 2130 this.style.background = "rgba(220, 53, 69, 0.15)"; 2131 this.style.borderColor = "rgba(220, 53, 69, 0.25)"; 2132 }); 2133 2134 itemCard.appendChild(itemIcon); 2135 itemCard.appendChild(itemInfo); 2136 itemCard.appendChild(qtyContainer); 2137 itemCard.appendChild(totalBadge); 2138 itemCard.appendChild(removeBtn); 2139 itemsContainer.appendChild(itemCard); 2140 }); 2141 2142 // Action bar under items (update + clear) 2143 const actionBar = document.createElement("div"); 2144 actionBar.style.cssText = ` 2145 display: flex; 2146 justify-content: space-between; 2147 align-items: center; 2148 margin-top: 0.75rem; 2149 gap: 0.75rem; 2150 `; 2151 2152 // Re-wire update form 2153 updateForm.style.cssText = "display: contents;"; 2154 const updateBtn = updateForm.querySelector( 2155 'button[type="submit"]', 2156 ); 2157 if (updateBtn) { 2158 updateBtn.className = ""; 2159 updateBtn.setAttribute("form", updateForm.id); 2160 updateBtn.style.cssText = `
2161 background: rgba(var(--color5-rgb, 138, 43, 226), 0.15); 2162 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.3); 2163 color: var(--color2); 2164 font-family: 'Lexend', sans-serif; 2165 font-weight: 700; 2166 font-size: 0.8rem; 2167 padding: 0.5rem 1rem; 2168 border-radius: 8px; 2169 cursor: pointer; 2170 transition: all 0.2s; 2171 text-transform: uppercase; 2172 display: flex; 2173 align-items: center; 2174 gap: 0.4rem; 2175 `; 2176 updateBtn.addEventListener("mouseenter", function () { 2177 this.style.background = 2178 "rgba(var(--color5-rgb, 138, 43, 226), 0.3)"; 2179 }); 2180 updateBtn.addEventListener("mouseleave", function () { 2181 this.style.background = 2182 "rgba(var(--color5-rgb, 138, 43, 226), 0.15)"; 2183 }); 2184 } 2185 // Hide the old <p> wrapper 2186 const updateP = updateForm.querySelector("p.text-end"); 2187 if (updateP) updateP.style.display = "none"; 2188 2189 // Clear button 2190 if (clearForm) { 2191 clearForm.style.cssText = "display: contents;"; 2192 const clearBtn = clearForm.querySelector("button"); 2193 if (clearBtn) { 2194 clearBtn.className = ""; 2195 clearBtn.style.cssText = ` 2196 background: rgba(220, 53, 69, 0.12); 2197 border: 1px solid rgba(220, 53, 69, 0.25); 2198 color: #ff6b6b; 2199 font-family: 'Lexend', sans-serif; 2200 font-weight: 700; 2201 font-size: 0.8rem; 2202 padding: 0.5rem 1rem; 2203 border-radius: 8px; 2204 cursor: pointer; 2205 transition: all 0.2s; 2206 text-transform: uppercase; 2207 display: flex; 2208 align-items: center; 2209 gap: 0.4rem; 2210 `; 2211 clearBtn.addEventListener("mouseenter", function () { 2212 this.style.background = "rgba(220, 53, 69, 0.25)"; 2213 }); 2214 clearBtn.addEventListener("mouseleave", function () { 2215 this.style.background = "rgba(220, 53, 69, 0.12)"; 2216 }); 2217 } 2218 actionBar.appendChild(clearForm); 2219 } 2220 2221 if (updateBtn) { 2222 actionBar.appendChild(updateBtn); 2223 } 2224 2225 leftCol.appendChild(itemsContainer); 2226 leftCol.appendChild(actionBar); 2227 2228 // Move hidden form data to update form 2229 updateForm.appendChild(hiddenFormData); 2230 2231 hydrateCartImages(cartPackageIds); 2232 } 2233 2234 // Handle empty cart 2235 const emptyAlert = cardBody.querySelector(".alert-warning"); 2236 if (emptyAlert) { 2237 emptyAlert.style.cssText = ` 2238 background: rgba(var(--color5-rgb, 138, 43, 226), 0.08); 2239 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.2); 2240 border-radius: 12px; 2241 color: var(--color2); 2242 font-family: 'Lexend', sans-serif; 2243 text-align: center; 2244 padding: 2rem; 2245 `; 2246 leftCol.appendChild(emptyAlert); 2247 } 2248 2249 // =============================== 2250 // RIGHT: Summary sidebar 2251 // =============================== 2252 const summaryCard = document.createElement("div"); 2253 summaryCard.style.cssText = ` 2254 background: linear-gradient(135deg, var(--color3) 0%, var(--color4) 100%); 2255 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.15); 2256 border-radius: 14px; 2257 padding: 0; 2258 overflow: hidden; 2259 position: sticky; 2260 top: 100px; 2261 `; 2262 2263 // Summary header 2264 const summaryHeader = document.createElement("div"); 2265 summaryHeader.style.cssText = ` 2266 padding: 1rem 1.25rem; 2267 border-bottom: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.12); 2268 display: flex; 2269 align-items: center; 2270 gap: 0.6rem; 2271 `; 2272 summaryHeader.innerHTML = ` 2273 <i class="bi bi-receipt" style="color: var(--color6); font-size: 1.1rem;"></i>
2274 <span style="font-family: 'Lexend', sans-serif; font-weight: 800; font-size: 1rem; text-transform: uppercase; color: var(--color2);"> 2275 ${_shopLang("summary", "Summary")} 2276 </span> 2277 `; 2278 summaryCard.appendChild(summaryHeader); 2279 2280 const summaryBody = document.createElement("div"); 2281 summaryBody.style.cssText = "padding: 1.25rem;"; 2282 2283 const payableTotalH5 = Array.from( 2284 cardBody.querySelectorAll(":scope > .row h5.text-end"), 2285 ).pop(); 2286 2287 // Move coupons/gift cards section 2288 const couponRows = cardBody.querySelectorAll(":scope > .row"); 2289 couponRows.forEach(function (row) { 2290 const h5 = row.querySelector("h5"); 2291 if (!h5) return; 2292 const text = h5.textContent.trim().toLowerCase(); 2293 const isGiftcardSection = 2294 !!row.querySelector( 2295 'input[name="giftcard"], form[action*="giftcards"]', 2296 ) || 2297 text.includes("gift") || 2298 text.includes("cadeau"); 2299 2300 // Style coupon/giftcard forms 2301 const inputGroups = row.querySelectorAll(".input-group"); 2302 inputGroups.forEach(function (ig) { 2303 ig.style.cssText = "margin-bottom: 0.75rem !important;"; 2304 const input = ig.querySelector("input.form-control"); 2305 if (input) { 2306 input.style.cssText = ` 2307 background: rgba(0, 0, 0, 0.2); 2308 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.2); 2309 color: var(--color2); 2310 border-radius: 8px 0 0 8px; 2311 font-size: 0.85rem; 2312 padding: 0.5rem 0.75rem; 2313 `; 2314 } 2315 const btn = ig.querySelector(".btn"); 2316 if (btn) { 2317 btn.style.cssText = ` 2318 background: rgba(var(--color5-rgb, 138, 43, 226), 0.2); 2319 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.3); 2320 color: var(--color2); 2321 font-size: 0.85rem; 2322 font-weight: 700; 2323 border-radius: 0 8px 8px 0; 2324 padding: 0.5rem 0.75rem; 2325 transition: all 0.2s; 2326 `; 2327 btn.addEventListener("mouseenter", function () { 2328 this.style.background = 2329 "rgba(var(--color5-rgb, 138, 43, 226), 0.35)"; 2330 }); 2331 btn.addEventListener("mouseleave", function () { 2332 this.style.background = 2333 "rgba(var(--color5-rgb, 138, 43, 226), 0.2)"; 2334 }); 2335 } 2336 }); 2337 2338 // Style section titles 2339 row.querySelectorAll("h5").forEach(function (title) { 2340 title.style.cssText = ` 2341 font-family: 'Lexend', sans-serif; 2342 font-weight: 700; 2343 font-size: 0.85rem; 2344 text-transform: uppercase; 2345 color: rgba(var(--color2-rgb, 255, 255, 255), 0.6); 2346 letter-spacing: 0.5px; 2347 margin-bottom: 0.5rem; 2348 `; 2349 }); 2350 2351 // Replace coupon/gift card tables with compact applied-code rows. 2352 const tables = row.querySelectorAll("table"); 2353 tables.forEach(function (table) { 2354 restylePromotionTable( 2355 table, 2356 isGiftcardSection 2357 ? "bi bi-gift" 2358 : "bi bi-ticket-perforated", 2359 ); 2360 }); 2361 2362 if (payableTotalH5 && row.contains(payableTotalH5)) { 2363 payableTotalH5.style.display = "none"; 2364 } 2365 2366 // Fix column layout ââ¬â remove offset, make full width 2367 row.querySelectorAll(".col-md-4, .col-md-6").forEach( 2368 function (col) { 2369 col.className = "mb-3"; 2370 col.style.cssText = ""; 2371 }, 2372 ); 2373 row.querySelectorAll('[class*="offset-"]').forEach( 2374 function (col) { 2375 col.className = "mb-3"; 2376 }, 2377 ); 2378 2379 row.style.cssText = ""; 2380 row.className = ""; 2381 2382 // Create a section wrapper 2383 const section = document.createElement("div"); 2384 section.style.cssText = ` 2385 border-bottom: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.1); 2386 padding-bottom: 0.75rem; 2387 margin-bottom: 0.75rem; 2388 `; 2389 section.appendChild(row);
2390 summaryBody.appendChild(section); 2391 }); 2392 2393 // Total line. When a gift card is applied, the plugin renders the correct 2394 // payable total inside the gift card block; use that value and hide the 2395 // pre-gift-card total to avoid showing two conflicting totals. 2396 const totalH5 = cardBody.querySelector(":scope > h5.text-end"); 2397 const totalSource = payableTotalH5 || totalH5; 2398 if (totalSource) { 2399 const totalSection = document.createElement("div"); 2400 totalSection.style.cssText = ` 2401 display: flex; 2402 justify-content: space-between; 2403 align-items: center; 2404 padding: 0.75rem 0; 2405 border-bottom: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.1); 2406 margin-bottom: 0.75rem; 2407 `; 2408 2409 const totalLabel = document.createElement("span"); 2410 totalLabel.style.cssText = ` 2411 font-family: 'Lexend', sans-serif; 2412 font-weight: 700; 2413 font-size: 0.85rem; 2414 text-transform: uppercase; 2415 color: rgba(var(--color2-rgb, 255, 255, 255), 0.6); 2416 `; 2417 totalLabel.textContent = _shopLang("total", "Total"); 2418 2419 const totalValue = document.createElement("span"); 2420 totalValue.style.cssText = ` 2421 font-family: 'Lexend', sans-serif; 2422 font-weight: 800; 2423 font-size: 1.2rem; 2424 color: var(--color2); 2425 display: flex; 2426 align-items: center; 2427 gap: 0.4rem; 2428 `; 2429 const amount = extractAmountText(totalSource.textContent); 2430 totalValue.innerHTML = `<i class="bi bi-coin" style="color: var(--color6); font-size: 1rem;"></i> ${escapeHtml(amount)}`; 2431 2432 if (totalH5) totalH5.style.display = "none"; 2433 if (payableTotalH5) payableTotalH5.style.display = "none"; 2434 2435 totalSection.appendChild(totalLabel); 2436 totalSection.appendChild(totalValue); 2437 summaryBody.appendChild(totalSection); 2438 } 2439 2440 // Move checkout form ââ¬â find the form with back/checkout buttons 2441 const allForms = cardBody.querySelectorAll(":scope > form"); 2442 let checkoutForm = null; 2443 allForms.forEach(function (f) { 2444 if ( 2445 f.querySelector(".btn-secondary, .btn-primary") && 2446 !f.querySelector("table") && 2447 f !== updateForm && 2448 f !== clearForm 2449 ) { 2450 checkoutForm = f; 2451 } 2452 }); 2453 if (checkoutForm) { 2454 const checkoutSection = document.createElement("div"); 2455 checkoutSection.style.cssText = "margin-top: 0.5rem;"; 2456 2457 // Style back button 2458 const backBtn = checkoutForm.querySelector(".btn-secondary"); 2459 if (backBtn) { 2460 backBtn.style.cssText = ` 2461 background: rgba(var(--color2-rgb, 255, 255, 255), 0.08); 2462 border: 1px solid rgba(var(--color2-rgb, 255, 255, 255), 0.12); 2463 color: var(--color2); 2464 font-family: 'Lexend', sans-serif; 2465 font-weight: 700; 2466 font-size: 0.8rem; 2467 padding: 0.6rem 1rem; 2468 border-radius: 10px; 2469 width: 100%; 2470 text-align: center; 2471 display: block; 2472 transition: all 0.2s; 2473 text-decoration: none; 2474 margin-bottom: 0.5rem; 2475 `; 2476 backBtn.addEventListener("mouseenter", function () { 2477 this.style.background = 2478 "rgba(var(--color2-rgb, 255, 255, 255), 0.15)"; 2479 }); 2480 backBtn.addEventListener("mouseleave", function () { 2481 this.style.background = 2482 "rgba(var(--color2-rgb, 255, 255, 255), 0.08)"; 2483 }); 2484 } 2485 2486 // Style checkout/buy button 2487 const primaryBtn = checkoutForm.querySelector(".btn-primary"); 2488 if (primaryBtn) { 2489 primaryBtn.style.cssText = ` 2490 background: linear-gradient(135deg, var(--color6) 0%, var(--color5) 100%) !important; 2491 border: 1px solid rgba(var(--color2-rgb, 255, 255, 255), 0.15) !important; 2492 color: var(--color2) !important; 2493 font-family: 'Lexend', sans-serif; 2494 font-weight: 800; 2495 font-size: 0.9rem; 2496 padding: 0.75rem 1.25rem; 2497 border-radius: 10px; 2498 width: 100%; 2499 text-align: center; 2500 cursor: pointer; 2501 transition: all 0.3s; 2502 text-transform: uppercase; 2503 position: relative; 2504 overflow: hidden; 2505 box-shadow: 0 4px 20px rgba(var(--color5-rgb, 138, 43, 226), 0.3); 2506 display: block; 2507 margin: 0; 2508 `; 2509 primaryBtn.classList.remove("ms-auto"); 2510 2511 primaryBtn.addEventListener("mouseenter", function () { 2512 this.style.boxShadow = 2513 "0 6px 30px rgba(var(--color5-rgb, 138, 43, 226), 0.5)"; 2514 this.style.transform = "translateY(-2px)"; 2515 }); 2516 primaryBtn.addEventListener("mouseleave", function () { 2517 this.style.boxShadow = 2518 "0 4px 20px rgba(var(--color5-rgb, 138, 43, 226), 0.3)"; 2519 this.style.transform = ""; 2520 }); 2521 2522 // Add shine 2523 const btnShine = document.createElement("span"); 2524 btnShine.style.cssText = ` 2525 position: absolute; top: 0; left: 0; width: 100%; height: 100%; 2526 background: linear-gradient(90deg, transparent 0%, rgba(var(--color2-rgb, 255, 255, 255), 0.2) 50%, transparent 100%); 2527 animation: shine 3s infinite; pointer-events: none; z-index: 1; 2528 `; 2529 primaryBtn.appendChild(btnShine); 2530 } 2531 2532 // Change flex layout to vertical stack 2533 const flexDiv = checkoutForm.querySelector(".d-flex"); 2534 if (flexDiv) { 2535 flexDiv.style.cssText = 2536 "display: flex; flex-direction: column; gap: 0.5rem;"; 2537 } 2538 2539 // Terms 2540 const termsDiv = checkoutForm.querySelector( 2541 ".d-flex.justify-content-end", 2542 ); 2543 if (termsDiv) { 2544 termsDiv.style.cssText = 2545 "display: flex; justify-content: center; margin-bottom: 0.5rem;"; 2546 termsDiv 2547 .querySelectorAll("label, a") 2548 .forEach(function (el) { 2549 el.style.fontSize = "0.8rem"; 2550 }); 2551 } 2552 2553 // Email input if present 2554 const emailInput = checkoutForm.querySelector("#email"); 2555 if (emailInput) { 2556 emailInput.style.cssText = ` 2557 background: rgba(0, 0, 0, 0.2); 2558 border: 1px solid rgba(var(--color5-rgb, 138, 43, 226), 0.2); 2559 color: var(--color2); 2560 border-radius: 8px; 2561 font-size: 0.85rem; 2562 padding: 0.6rem 0.75rem; 2563 width: 100%; 2564 margin-bottom: 0.5rem; 2565 `; 2566 emailInput.classList.remove("form-group"); 2567 } 2568 2569 checkoutSection.appendChild(checkoutForm); 2570 summaryBody.appendChild(checkoutSection); 2571 } 2572 2573 summaryCard.appendChild(summaryBody); 2574 rightCol.appendChild(summaryCard); 2575 2576 // === Assemble layout === 2577 // Insert mainRow before cardBody content 2578 mainRow.appendChild(leftCol); 2579 mainRow.appendChild(rightCol); 2580 cardBody.appendChild(mainRow); 2581 2582 // Hide original table and move remaining original content away 2583 if (cartTable) cartTable.style.display = "none"; 2584 2585 // === Add responsive style === 2586 const responsiveStyle = document.createElement("style"); 2587 responsiveStyle.textContent = ` 2588 .cart-redesigned::before { display: none !important; } 2589 .cart-redesigned { background: none !important; } 2590 @media (max-width: 991px) { 2591 #cart-gaming-layout { 2592 grid-template-columns: 1fr !important; 2593 } 2594 } 2595 `; 2596 document.head.appendChild(responsiveStyle); 2597 } 2598 } catch (error) { 2599 console.warn("Shop Enhancer - Cart Page Redesign: ", error); 2600 } 2601});
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.