1 2<!DOCTYPE html> 3<html lang="id"> 4<head> 5 6 <meta charset="UTF-8"> 7 <title>BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler</title> 8 <meta name="description" content="BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama."> 9 <meta name="robots" content="index,follow"> 10 <link rel="canonical" href="https://mgsco.org/"> 11 <link rel="amphtml" href="https://mgsco.org/boykita/"> 12 <link rel="alternate" href="https://mgsco.org/boykita/"> 13 <link rel="alternate" href="https://mgsco.org/boykita/" hreflang="id"> 14 <link rel="alternate" href="https://mgsco.org/boykita/" hreflang="en"> 15 <link rel="alternate" href="https://mgsco.org/boykita/" hreflang="x-default"> 16 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 67 bytes, lines 16-17
16 17 <script async src="https://www.googletagmanager.com/gtag/js?id=
17G-0LSM1YJSPZ
vendor: 48 bytes, lines 17-18
17" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 18
18<script nonce="se3iyzEHMc9FQEB4wSyd0w==">window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"granted",wait_for_update:500});gtag("js",new Date);gtag("config","G-0LSM1YJSPZ",{send_page_view:!0,linker:{domains:["plazait.co.id","www.plazait.co.id"]}});window.ga4Ecomm={viewItem:function(e){if(!window.gtag)return;try{gtag("event","view_item",{currency:e.currency||"IDR",value:e.value||0,items:[{item_id:e.item_id||"",item_name:e.item_name||"",item_brand:e.item_brand||"",item_category:e.item_category||"",price:e.price||0,quantity:1}]})}catch(t){}},addToCart:function(e){if(!window.gtag)return;try{gtag("event","add_to_cart",{currency:e.currency||"IDR",value:(e.price||0)*(e.quantity||1),items:[{item_id:e.item_id||"",item_name:e.item_name||"",item_brand:e.item_brand||"",item_category:e.item_category||"",price:e.price||0,quantity:e.quantity||1}]})}catch(t){}}};</script>
18 19 <meta property="og:locale" content="id_ID"> 20 <meta property="og:site_name" content="BOYTOTO"> 21 <meta property="og:type" content="product"> 22 <meta property="og:title" content="BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler"> 23 <meta property="og:description" content="BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama."> 24 <meta property="og:url" content="https://mgsco.org/"> 25 <meta property="og:image" content="https://ik.imagekit.io/boytoto/banner235.png"> 26 <meta property="og:image:alt" content="BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler"> 27<meta property="product:price:amount" content="5000"> 28<meta property="product:price:currency" content="IDR"> 29<meta property="product:availability" content="out of stock"> 30<meta name="twitter:card" content="summary_large_image"> 31<meta name="twitter:title" content="BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler"> 32<meta name="twitter:description" content="BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama."> 33<meta name="twitter:image" content="https://ik.imagekit.io/boytoto/banner235.png"> 34<meta name="google-site-verification" content="g4W1PGUKVco2_WmXJTlQv6-SYfpDi8_hcXGanUbSfWs" /> 35<meta name="google-site-verification" content="Dfr9owS-fAjxuqIA9xOdZ7g5MhqB859UKXLp-Ba5Ih0" />
36<script type="application/ld+json"> 37 { 38 "@context": "http://schema.org", 39 "@type": "BreadcrumbList", 40 "itemListElement": [ 41 { 42 "@type": "ListItem", 43 "item": { 44 "@id": "https://mgsco.org/", 45 "name": "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler" 46 }, 47 "position": 1 48 } 49 ] 50 } 51 </script>
51 52
52<script type="application/ld+json"> 53{ 54 "@context": "https://schema.org", 55 "@type": "WebPage", 56 "@id": "https://mgsco.org/#webpage", 57 "url": "https://mgsco.org/", 58 "name": "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler", 59 "description": "BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama.", 60 "inLanguage": "id-ID", 61 "primaryImageOfPage": { 62 "@type": "ImageObject", 63 "url": "https://ik.imagekit.io/boytoto/banner235.png" 64 } 65} 66</script>
66 67
67<script type="application/ld+json"> 68 { 69 "@context": "http://schema.org/", 70 "@type": "WebPage", 71 "speakable": { 72 "@type": "SpeakableSpecification", 73 "cssSelector": [".description"], 74 "xpath": ["/html/head/title"] 75 } 76 } 77 </script>
77 78 <!-- ZS --> 79 <!-- Schema JSON untuk SBO -->
80<script type="application/ld+json"> 81{ 82 "@context": "https://schema.org", 83 "@type": "WebSite", 84 "@id": "https://mgsco.org/#website", 85 "url": "https://mgsco.org/", 86 "name": "BOYTOTO", 87 "description": "BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama.", 88 "inLanguage": "id-ID" 89} 90</script>
90 91
92<script type="application/ld+json"> 93{ 94 "@context": "https://schema.org", 95 "@type": "Article", 96 "@id": "https://mgsco.org/#article", 97 "url": "https://mgsco.org/", 98 "headline": "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler", 99 "description": "BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama.", 100 "image": "https://ik.imagekit.io/boytoto/banner235.png", 101 "inLanguage": "id-ID", 102 "publisher": { 103 "@type": "Organization", 104 "name": "BOYTOTO", 105 "logo": { 106 "@type": "ImageObject", 107 "url": "https://ik.imagekit.io/boytoto/logo.webp" 108 } 109 } 110} 111</script>
111 112
113<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 114{ 115 "@context": "https://schema.org", 116 "@type": "Organization", 117 "@id": "https://mgsco.org/#organization", 118 "name": "BOYTOTO", 119 "url": "https://mgsco.org/", 120 "logo": "https://ik.imagekit.io/boytoto/logo.webp", 121 "description": "BOYTOTO hadir sebagai showroom situs boy toto kita game online yang menyediakan beragam pilihan permainan menarik dari provider ternama." 122} 123</script>
123 124
125<script type="application/ld+json"> 126{ 127"@context": "https://schema.org", 128"@type": "BreadcrumbList", 129"itemListElement": [ 130 { 131 "@type": "ListItem", 132 "position": 1, 133 "item": { 134 "@id": "https://mgsco.org/", 135 "name": "Game Online" 136 } 137 }, 138 { 139 "@type": "ListItem", 140 "position": 2, 141 "item": { 142 "@id": "https://mgsco.org/", 143 "name": "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler" 144 } 145 }, 146 { 147 "@type": "ListItem", 148 "position": 3, 149 "item": { 150 "@id": "https://mgsco.org/", 151 "name": "Hiburan Interaktif" 152 } 153 }, 154 { 155 "@type": "ListItem", 156 "position": 4, 157 "item": { 158 "@id": "https://mgsco.org/", 159 "name": "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler" 160 } 161 } 162] 163} 164</script>
164 165 166<!-- Review rich-result markup intentionally omitted: this page is an informational article, not a reviewable Product. -->
167<script type="application/ld+json"> 168{ 169 "@context": "https://schema.org", 170 "@type": "FAQPage", 171 "mainEntity": [ 172 { 173 "@type": "Question", 174 "name": "Apa yang membuat keseruan BOYTOTO berbeda?", 175 "acceptedAnswer": { 176 "@type": "Answer", 177 "text": "BOYTOTO menghadirkan hiburan di balik langit dengan ragam pilihan game seru kekinian. Platform ini dirancang interaktif agar setiap pengguna bisa menikmati sensasi bermain yang menyegarkan kapan saja." 178 } 179 }, 180 { 181 "@type": "Question", 182 "name": "Bagaimana cara mulai menikmati permainan di BOYTOTO hari ini?", 183 "acceptedAnswer": { 184 "@type": "Answer", 185 "text": "Pilih kategori game favorit Anda dari daftar yang tersedia, lalu sesuaikan mode permainan yang diinginkan. Akses yang fleksibel memudahkan Anda langsung menjelajahi tantangan seru secara instan." 186 } 187 }, 188 { 189 "@type": "Question", 190 "name": "Apakah ada banyak pilihan mode permainan yang bisa dimainkan?", 191 "acceptedAnswer": { 192 "@type": "Answer", 193 "text": "Ya. BOYTOTO menyediakan berbagai variasi permainan mulai dari tingkat santai hingga tantangan seru, sehingga setiap pemain dapat memilih mode yang paling sesuai dengan selera." 194 } 195 }, 196 { 197 "@type": "Question", 198 "name": "Untuk apa variasi game dihadirkan di platform ini?", 199 "acceptedAnswer": { 200 "@type": "Answer", 201 "text": "Pilihan game yang beragam berfungsi memberikan referensi hiburan yang kaya, sehingga pengguna tidak merasa bosan dan dapat terus merasakan pengalaman bermain dengan nuansa kekinian." 202 } 203 }, 204 { 205 "@type": "Question", 206 "name": "Apakah ada jaminan kemenangan dalam setiap game?", 207 "acceptedAnswer": { 208 "@type": "Answer", 209 "text": "Tidak. Setiap permainan bergantung pada keberuntungan serta strategi masing-masing pemain. Pilihan game di sini hadir sebagai media hiburan untuk dinikmati dengan seru dan menyenangkan." 210 } 211 } 212 ] 213} 214</script>
214 215 <!-- User Session Data --> 216 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 217 <meta name="user-logged-in" content="false"> 218 <meta name="user-id" content="0"> 219 220 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 221 222 <link rel="shortcut icon" href="https://ik.imagekit.io/boytoto/favicon1.png" type="image/x-icon"> 223 <style data-inlined="style.css"> 224@font-face { 225 font-family: 'Segoe UI'; 226 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 227 url('/assets/fonts/segoeui.woff') format('woff'); 228 font-weight: 400; 229 font-style: normal; 230} 231 232:root { 233 --brand-50:#FFF5E6; 234 --brand-100:#FFE7CC; 235 --brand-500:#F28C1A; 236 --brand-600:#D97809; 237 --gray-900:#1F1F23; 238 --gray-700:#3D3F45; 239 --gray-500:#6F737B; 240 --gray-300:#C5C9CF; 241 --gray-100:#F3F5F7; 242 --border:#E4E7EB; 243 --radius:8px; 244 --transition: .18s ease; 245 /* font-family: "Inter", system-ui, sans-serif; */ 246 font-family: 'Segoe UI', sans-serif; 247} 248 249* { box-sizing: border-box; } 250body { 251 margin:0; 252 padding-bottom: 0; /* Reset for desktop */ 253 color:var(--gray-900); 254 background:#fff; 255} 256img { max-width:100%; display:block; } 257 258.container { 259 width:100%; 260 max-width:1280px; 261 margin:0 auto; 262 padding:10px 0; 263} 264 265.topbar { 266 background:#111; 267 color:#fff; 268 font-size:13px; 269} 270.topbar-inner { 271 display:flex; 272 justify-content:space-between; 273 align-items:center; 274 min-height:36px; 275} 276.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 277 278/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 279.navbar { 280 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 281 backdrop-filter:blur(12px); 282 -webkit-backdrop-filter:blur(12px); 283 border-bottom:1px solid rgba(228,231,235,0.6); 284 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 285 position:sticky; 286 top:0; 287 z-index:500; 288 transition:all .3s ease; 289} 290 291/* Accent line gradient orange */ 292.navbar::after { 293 content:''; 294 position:absolute; 295 bottom:0; 296 left:0; 297 right:0; 298 height:2px; 299 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 300 opacity:0.5; 301} 302 303.nav-inner { 304 display:flex; 305 align-items:center; 306 gap:24px; 307 min-height:70px; 308} 309.nav-inner .search .m-search-ico { display: none; } 310 311/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 312.login-btn .login-ico { display:none; } 313.navbar-logo { 314 display: none; 315 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 316 backdrop-filter:blur(12px); 317 -webkit-backdrop-filter:blur(12px); 318 border-bottom:1px solid rgba(228,231,235,0.6); 319 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 320 position:sticky; 321 top:0; 322 z-index:500; 323} 324 325/* Hide bottom navigation on desktop - only show on mobile */ 326.bottom-nav { 327 display: none; 328} 329 330/* ============================================================== 331 MOBILE (<=640px) 332 ============================================================== */ 333@media (max-width:640px){ 334 .container { padding:10px 16px; } 335 336 .topbar { display:none !important; } 337 338 /* Add padding bottom for sticky bottom nav */ 339 body { 340 padding-bottom: 68px; /* Height of bottom nav + safe area */ 341 } 342 343 /* Mobile Top Navbar - Clean & Minimal */ 344 .navbar { 345 background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); 346 backdrop-filter:blur(16px); 347 -webkit-backdrop-filter:blur(16px); 348 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 349 border: 0; 350 } 351 352 /* Simple grid layout - Logo, Search, Category */ 353 .nav-inner { 354 display:grid; 355 grid-template-columns:1fr; 356 gap:10px; 357 padding:12px 16px 0 12px; 358 min-height:auto; 359 } 360 361 /* Logo centered at top */ 362 .nav-inner .logo { 363 justify-self:center; 364 margin:0; 365 display:flex; 366 } 367 .nav-inner .logo img { height:36px; } 368 369 /* Search bar - full width */ 370 .nav-inner .search { 371 position:relative; 372 display:flex;
373 align-items:center; 374 gap:0; 375 width:100%; 376 height:44px; 377 background:#fff; 378 border:1.5px solid #e5e7eb; 379 border-radius:12px; 380 padding:0 12px; 381 box-shadow:0 1px 3px rgba(0,0,0,0.04); 382 transition:all .25s; 383 } 384 .nav-inner .search:focus-within { 385 border-color:#fb923c; 386 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 387 } 388 389 .nav-inner .search-icon-btn { 390 color:#9ca3af; 391 padding:4px; 392 margin-right:4px; 393 } 394 395 .nav-inner .search-icon-btn svg { 396 width:18px; 397 height:18px; 398 } 399 400 .nav-inner .search input { 401 flex:1; 402 min-width:0; 403 border:none; 404 outline:none; 405 background:transparent; 406 font-size:14px; 407 padding:0 8px; 408 height:100%; 409 } 410 411 .nav-inner .ai-bot-toggle { 412 padding:6px; 413 margin-left:0; 414 } 415 416 .nav-inner .ai-bot-toggle .bot-icon { 417 width:20px; 418 height:20px; 419 } 420 421 .nav-inner .search-suggest { 422 left:0; 423 right:0; 424 width:calc(100vw - 32px); 425 margin-top:6px; 426 border-radius:12px; 427 max-height:70vh; 428 } 429 430 /* Hide desktop nav actions but keep structure for userMenu */ 431 .nav-inner .nav-actions { 432 display:flex !important; /* Keep flex to allow userMenu positioning */ 433 visibility:hidden; /* Hide visually but keep in DOM */ 434 pointer-events:none; /* Disable interactions with hidden elements */ 435 position:absolute; /* Remove from flow */ 436 right:0; 437 top:0; 438 } 439 440 /* Re-enable userMenu for logged-in users */ 441 .nav-inner .nav-actions #userMenu { 442 visibility:visible !important; 443 pointer-events:auto !important; 444 } 445 446 .nav-inner .logo { display: none; } 447 .navbar-logo { 448 display: block; 449 /* background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); */ 450 backdrop-filter:blur(16px); 451 -webkit-backdrop-filter:blur(16px); 452 position: static; /* Remove sticky on mobile */ 453 box-shadow: none; 454 border-bottom: 0; 455 padding-top: .6rem; 456 } 457 .nav-inner .logo-mobile { display: block; } 458 459 /* Category menu wrapper will handle visibility */ 460 461 /* ============================================================== 462 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 463 ============================================================== */ 464 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 465 466 /* Hide desktop elements that now moved to bottom nav */ 467 #wishlistBtn, 468 #notifBtn, 469 #loginBtn { display:none !important; } 470 471 /* Hide desktop user menu on mobile - use bottom sheet instead */ 472 .navbar #userMenu { 473 display:none !important; 474 } 475 476 .nav-actions .cart-indicator, 477 .nav-actions .compare-indicator { 478 display:none !important; 479 } 480 481 /* Hide category button from bottom nav */ 482 #bottomCategoryBtn { 483 display: none !important; 484 } 485 486 /* User menu button styling for mobile - scoped to navbar only */ 487 .navbar #userMenu .login-btn.logged-in { 488 width:40px; 489 height:40px; 490 padding:0; 491 border-radius:50%; 492 display:flex;
493 align-items:center; 494 justify-content:center; 495 background:rgba(255,255,255,0.95); 496 backdrop-filter:blur(12px); 497 -webkit-backdrop-filter:blur(12px); 498 border:1px solid rgba(228,231,235,0.6); 499 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 500 } 501 502 .navbar #userMenu .login-btn.logged-in .login-text { 503 display:none; 504 } 505 506 .navbar #userMenu .login-btn.logged-in .login-ico { 507 display:inline; 508 font-size:20px; 509 line-height:1; 510 } 511 512 /* User dropdown positioning for mobile */ 513 .navbar #userMenu .user-dropdown { 514 position:fixed !important; 515 top:60px !important; 516 right:16px !important; 517 left:auto !important; 518 min-width:200px; 519 background:rgba(255,255,255,0.98); 520 backdrop-filter:blur(20px); 521 -webkit-backdrop-filter:blur(20px); 522 border:1px solid rgba(228,231,235,0.6); 523 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 524 z-index:1002 !important; 525 border-radius:12px; 526 padding:8px 0; 527 } 528 529 /* Force visibility when not hidden attribute */ 530 .navbar #userMenu .user-dropdown:not([hidden]) { 531 display:block !important; 532 visibility:visible !important; 533 opacity:1 !important; 534 } 535 536 /* Container untuk kategori dan tombol kategori sejajar */ 537 .navbar .container:last-child { 538 display:flex !important; 539 align-items:center !important; 540 padding:4px 16px 6px !important; 541 gap:5px; 542 overflow:visible; 543 } 544 545 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 546 .mega-cats-wrapper { 547 position:relative; 548 flex:1; 549 overflow:hidden; 550 min-height:34px; /* Ensure content is visible */ 551 display:flex; /* Ensure wrapper is flex container */ 552 } 553 554 /* Fade gradient indicator di ujung kanan (mobile only) */ 555 .mega-cats-fade { 556 position:absolute; 557 top:0; 558 right:0; 559 bottom:0; 560 width:80px; /* Increased from 60px for better visibility */ 561 background:linear-gradient(to left, 562 rgba(255,245,230,1) 0%, 563 rgba(255,245,230,0.98) 15%, 564 rgba(255,245,230,0.85) 35%, 565 transparent 100%); 566 pointer-events:none; 567 z-index:5; /* Increased z-index */ 568 transition:opacity 0.3s ease; 569 } 570 571 /* Kategori menu di mobile */ 572 .mega-cats.container { 573 display:flex !important; 574 gap:12px; 575 overflow-x:auto; 576 overflow-y:visible; 577 -webkit-overflow-scrolling:touch; 578 scrollbar-width:none; 579 flex:1; 580 padding:0 !important; 581 margin:0 !important; 582 position:relative; /* For z-index stacking */ 583 z-index:1; /* Below fade gradient */ 584 } 585 .mega-cats.container::-webkit-scrollbar { display:none; } 586 587 /* Styling untuk menu items di mobile */ 588 .mega-cats a { 589 font-size:13px; 590 padding:6px 10px; 591 white-space:nowrap; 592 flex-shrink:0; 593 position:relative; /* Ensure proper stacking */ 594 } 595 .mega-cats a.active { 596 background:var(--brand-600); 597 color:#fff; 598 font-weight:600; 599 border-radius:6px; 600 } 601 602 /* Tombol kategori di mobile */ 603 .mm-wp { 604 flex-shrink:0; 605 } 606 .mm2-trigger { 607 display:flex;
608 align-items:center; 609 gap:6px; 610 padding:6px 12px; 611 background:var(--gray-100); 612 color:var(--gray-700); 613 text-decoration:none; 614 border-radius:6px; 615 font-size:13px; 616 white-space:nowrap; 617 } 618 619 .mm2-trigger .label-cat {display: none; } 620 /* Tombol more kategori */ 621 .mega-more-btn { 622 border:1px solid var(--border); 623 background:#fff; 624 border-radius:10px; 625 padding:6px 14px; 626 font-size:18px; 627 cursor:pointer; 628 } 629 .mega-cats-more.active > .mega-more-btn { 630 background:var(--brand-600); 631 color:#fff; 632 border-color:var(--brand-600); 633 } 634 .mega-more-dropdown { 635 top:calc(100% + 10px); 636 right:0; 637 min-width:85vw; 638 max-width:90vw; 639 border-radius:16px; 640 } 641 .mega-more-grid { 642 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 643 } 644 645 /* ============================================================== 646 MOBILE USER BOTTOM SHEET 647 ============================================================== */ 648 .mobile-user-sheet { 649 position:fixed; 650 top:0; 651 left:0; 652 right:0; 653 bottom:0; 654 z-index:10000; 655 pointer-events:none; 656 } 657 658 .mobile-user-sheet.active { 659 pointer-events:auto; 660 } 661 662 .mobile-user-sheet-backdrop { 663 position:absolute; 664 top:0; 665 left:0; 666 right:0; 667 bottom:0; 668 background:rgba(0,0,0,0.5); 669 opacity:0; 670 transition:opacity .3s ease; 671 } 672 673 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 674 opacity:1; 675 } 676 677 .mobile-user-sheet-content { 678 position:absolute; 679 bottom:0; 680 left:0; 681 right:0; 682 background:#fff; 683 border-radius:20px 20px 0 0; 684 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 685 transform:translateY(100%); 686 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 687 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 688 } 689 690 .mobile-user-sheet.active .mobile-user-sheet-content { 691 transform:translateY(0); 692 } 693 694 .mobile-user-sheet-header { 695 padding:16px 20px 12px; 696 border-bottom:1px solid #e5e7eb; 697 } 698 699 .mobile-user-sheet-handle { 700 width:40px; 701 height:4px; 702 background:#d1d5db; 703 border-radius:2px; 704 margin:0 auto 16px; 705 } 706 707 .mobile-user-info { 708 display:flex;
709 align-items:center; 710 gap:12px; 711 } 712 713 .mobile-user-avatar { 714 width:48px; 715 height:48px; 716 border-radius:50%; 717 background:linear-gradient(135deg, #FFE7CC, #FFD9A8); 718 display:flex; 719 align-items:center; 720 justify-content:center; 721 font-size:24px; 722 flex-shrink:0; 723 } 724 725 .mobile-user-details { 726 flex:1; 727 min-width:0; 728 } 729 730 .mobile-user-name { 731 font-size:16px; 732 font-weight:600; 733 color:#111827; 734 margin-bottom:2px; 735 white-space:nowrap; 736 overflow:hidden; 737 text-overflow:ellipsis; 738 } 739 740 .mobile-user-email { 741 font-size:13px; 742 color:#6b7280; 743 white-space:nowrap; 744 overflow:hidden; 745 text-overflow:ellipsis; 746 } 747 748 .mobile-user-sheet-body { 749 padding:8px 0; 750 } 751 752 .mobile-user-sheet-item { 753 display:flex; 754 align-items:center; 755 gap:16px; 756 padding:14px 20px; 757 text-decoration:none; 758 color:#374151; 759 font-size:15px; 760 transition:background .2s; 761 border:none; 762 background:none; 763 width:100%; 764 text-align:left; 765 cursor:pointer; 766 } 767 768 .mobile-user-sheet-item:active { 769 background:#f3f4f6; 770 } 771 772 .mobile-user-sheet-icon { 773 font-size:20px; 774 width:24px; 775 text-align:center; 776 flex-shrink:0; 777 } 778 779 .mobile-user-sheet-label { 780 flex:1; 781 font-weight:500; 782 } 783 784 .mobile-user-sheet-divider { 785 height:1px; 786 background:#e5e7eb; 787 margin:8px 0; 788 } 789} 790 791/* END NAVBAR */ 792 793.logo img { display:block; } 794.search { 795 flex:1; 796 position:relative; 797 display:flex; 798 align-items:center; 799 gap:0; 800 background:#fff; 801 border:1.5px solid #e5e7eb; 802 border-radius:12px; 803 padding:0 16px; 804 height:38px; 805 transition:all 0.3s ease; 806} 807.search:focus-within { 808 border-color:#fb923c; 809 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 810} 811.search input { 812 flex:1; 813 border:none; 814 outline:none; 815 background:transparent; 816 font-size:15px; 817 padding:0 12px; 818 height:100%; 819 color:#1f2937; 820} 821.search input::placeholder { 822 color:#9ca3af; 823} 824.search-icon-btn { 825 background:transparent; 826 border:none; 827 padding:0; 828 display:flex; 829 align-items:center; 830 justify-content:center; 831 cursor:pointer; 832 color:#6b7280; 833 transition:color 0.2s ease; 834 flex-shrink:0; 835} 836.search-icon-btn:hover { 837 color:#fb923c; 838} 839.search-suggest { 840 position:absolute; 841 left:0; right:0; top:100%; 842 background:#fff; 843 border:1px solid var(--border); 844 border-radius:var(--radius); 845 padding:8px 0; 846 box-shadow:0 8px 20px rgba(0,0,0,.08); 847 margin-top:4px; 848 font-size:14px; 849 max-height:260px; 850 overflow:auto; 851} 852.search-suggest div { 853 padding:6px 12px; 854 cursor:pointer; 855} 856.search-suggest div:hover { background:var(--gray-100); } 857 858.nav-actions { display:flex; gap:10px; align-items:center; } 859.icon-btn, .login-btn { 860 background:linear-gradient(135deg, rgba(255,255,255,0.95), rgba(249,250,251,0.9)); 861 border:1px solid rgba(228,231,235,0.6); 862 padding:8px 12px; 863 border-radius:var(--radius); 864 cursor:pointer; 865 font-size:14px; 866 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 867 height: 38px; 868 position:relative; 869 box-shadow: 870 0 1px 3px rgba(0,0,0,0.06), 871 0 0 0 1px rgba(255,255,255,0.5) inset; 872 backdrop-filter:blur(8px); 873 -webkit-backdrop-filter:blur(8px); 874} 875.icon-btn:hover, .login-btn:hover { 876 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 877 border-color:rgba(209,213,219,0.8); 878 box-shadow: 879 0 2px 8px rgba(0,0,0,0.08), 880 0 0 0 1px rgba(255,255,255,0.6) inset; 881 transform:translateY(-1px); 882} 883.cart-indicator, .compare-indicator { position:relative; } 884.badge { 885 background:linear-gradient(135deg, #EF4444, #DC2626); 886 color:#fff; 887 font-size:10.5px; 888 font-weight:600; 889 padding:2px 6px; 890 border-radius:999px; 891 position:absolute; 892 top:-6px; 893 right:-6px; 894 min-width:18px; 895 height:18px; 896 display:flex; 897 align-items:center; 898 justify-content:center; 899 box-shadow: 900 0 2px 8px rgba(239,68,68,0.4), 901 0 0 0 2px #fff, 902 0 0 12px -2px rgba(239,68,68,0.6); 903 transition:all 0.3s ease; 904} 905.icon-btn:hover .badge { 906 transform:scale(1.08); 907 box-shadow: 908 0 4px 12px rgba(239,68,68,0.5), 909 0 0 0 2px #fff, 910 0 0 16px -2px rgba(239,68,68,0.7); 911} 912.badge.is-empty { display:none; } 913 914/* User Menu Dropdown */ 915.user-menu { 916 position:relative; 917} 918.user-dropdown { 919 position:absolute; 920 top:calc(100% + 8px); 921 right:0; 922 background:#fff; 923 border:1px solid var(--border); 924 border-radius:12px; 925 box-shadow:0 10px 40px rgba(0,0,0,0.12);
926 min-width:200px; 927 padding:8px 0; 928 z-index:9999; 929} 930 931/* Ensure dropdown is visible when not hidden */ 932.user-dropdown:not([hidden]) { 933 display:block !important; 934 visibility:visible !important; 935 opacity:1 !important; 936} 937 938/* Mobile: Force user dropdown to be visible above everything */ 939@media (max-width:640px) { 940 .user-dropdown:not([hidden]) { 941 position:fixed !important; 942 z-index:9999 !important; 943 top:60px !important; 944 right:16px !important; 945 } 946} 947 948.dropdown-item { 949 display:flex; 950 align-items:center; 951 gap:12px; 952 padding:10px 16px; 953 text-decoration:none; 954 color:var(--gray-700); 955 font-size:14px; 956 transition:var(--transition); 957 border:none; 958 background:none; 959 width:100%; 960 text-align:left; 961 cursor:pointer; 962} 963.dropdown-item:hover { 964 background:var(--gray-100); 965 color:var(--gray-900); 966} 967.dropdown-icon { 968 font-size:16px; 969 opacity:0.7; 970} 971.dropdown-separator { 972 height:1px; 973 background:var(--border); 974 margin:8px 0; 975} 976.login-btn.logged-in { 977 background:linear-gradient(135deg, rgba(255,245,230,0.8), rgba(255,231,204,0.6)); 978 border:1px solid rgba(242,140,26,0.2); 979 color:var(--brand-700); 980 max-width:200px; /* Prevent button from getting too wide */ 981 display:inline-flex; 982 align-items:center; 983 gap:10px; 984 padding:4px 12px 4px 4px; 985 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 986 position:relative; 987 overflow:hidden; 988 box-shadow: 989 0 1px 3px rgba(242,140,26,0.08), 990 0 0 0 1px rgba(255,255,255,0.5) inset, 991 0 0 12px -6px rgba(242,140,26,0.1); 992 backdrop-filter:blur(8px); 993 -webkit-backdrop-filter:blur(8px); 994} 995/* Subtle background glow effect */ 996.login-btn.logged-in::before { 997 content:''; 998 position:absolute; 999 top:50%; 1000 left:0; 1001 width:32px; 1002 height:32px; 1003 background:radial-gradient(circle, rgba(242,140,26,0.15), transparent 70%); 1004 transform:translateY(-50%); 1005 opacity:0; 1006 transition:opacity 0.3s ease; 1007} 1008.login-btn.logged-in:hover::before { 1009 opacity:1; 1010} 1011.login-btn.logged-in .user-avatar { 1012 width:32px; 1013 height:32px; 1014 border-radius:50%; 1015 background:linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 1016 color:#D97809; 1017 font-size:11.5px; 1018 font-weight:700; 1019 display:flex; 1020 align-items:center; 1021 justify-content:center; 1022 flex-shrink:0; 1023 letter-spacing:0.3px; 1024 position:relative; 1025 box-shadow: 1026 0 2px 8px rgba(242,140,26,0.25), 1027 0 1px 3px rgba(0,0,0,0.12), 1028 inset 0 1px 0 rgba(255,255,255,0.5); 1029 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1030} 1031/* Ring gradient border */ 1032.login-btn.logged-in .user-avatar::before { 1033 content:''; 1034 position:absolute; 1035 inset:-2px; 1036 border-radius:50%; 1037 padding:2px; 1038 background:linear-gradient(135deg, #F28C1A, #FFD9A8, #F28C1A); 1039 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1040 -webkit-mask-composite:xor; 1041 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1042 mask-composite:exclude; 1043 opacity:0; 1044 transition:opacity 0.3s ease; 1045} 1046.login-btn.logged-in:hover .user-avatar::before { 1047 opacity:1; 1048} 1049.login-btn.logged-in:hover .user-avatar { 1050 transform:scale(1.08) rotate(-3deg); 1051 box-shadow: 1052 0 4px 12px rgba(242,140,26,0.35), 1053 0 2px 6px rgba(0,0,0,0.15), 1054 inset 0 1px 0 rgba(255,255,255,0.6); 1055} 1056.login-btn.logged-in .login-text { 1057 overflow:hidden; 1058 text-overflow:ellipsis; 1059 white-space:nowrap; 1060 flex:1; 1061 min-width:0; 1062 font-weight:500; 1063 font-size:14px; 1064 transition:color 0.2s ease; 1065} 1066.login-btn.logged-in:hover .login-text { 1067 color:var(--brand-600); 1068} 1069.login-btn.logged-in:hover { 1070 background:linear-gradient(135deg, rgba(255,245,230,0.95), rgba(255,231,204,0.85)); 1071 border-color:rgba(242,140,26,0.3); 1072 box-shadow: 1073 0 4px 16px -4px rgba(242,140,26,0.25), 1074 0 0 0 1px rgba(242,140,26,0.1) inset, 1075 0 1px 2px rgba(0,0,0,0.05); 1076 transform:translateY(-1px); 1077} 1078 1079/* Desktop: hide wrapper elements, show menu normally */ 1080@media (min-width: 761px) { 1081 .mega-cats-wrapper { 1082 display:contents; /* Remove wrapper from layout */ 1083 } 1084 .mega-cats-fade { 1085 display:none !important; /* Hide fade on desktop */ 1086 } 1087} 1088 1089.mega-cats { 1090 display:flex; 1091 gap:5px; 1092 /* padding:6px 16px 10px; */ 1093 scrollbar-width:none; 1094 position: relative; 1095 z-index: 510; 1096 overflow-x:auto; 1097 -webkit-overflow-scrolling:touch;
1098 flex:1; 1099} 1100.mega-cats::-webkit-scrollbar { 1101 display:none; 1102} 1103.mega-cats a { 1104 text-decoration:none; 1105 font-size:14px; 1106 color:var(--gray-700); 1107 padding:6px 10px; 1108 border-radius:6px; 1109 white-space:nowrap; 1110 transition:var(--transition); 1111} 1112.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1113/*.mega-cats::after { 1114 content:''; 1115 position:absolute; 1116 right:0; 1117 top:0; 1118 width:40px; 1119 height:100%; 1120 background:linear-gradient(to right,rgba(255,255,255,0),#fff); 1121 pointer-events:none; 1122}*/ 1123 1124/* Tambahan untuk tombol More & dropdown. 1125 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1126 1127.mega-cats-more { 1128 position:relative; 1129 display:flex; 1130 align-items:center; 1131} 1132 1133.mega-cats-more.active > .mega-more-btn { 1134 background:var(--brand-600); 1135 color:#fff; 1136} 1137 1138.mega-more-btn { 1139 cursor:pointer; 1140 border:none; 1141 background:var(--gray-100,#f5f5f5); 1142 color:var(--gray-700,#333); 1143 padding:6px 12px; 1144 font-size:16px; 1145 line-height:1; 1146 border-radius:6px; 1147 display:flex; 1148 align-items:center; 1149 justify-content:center; 1150 transition:var(--transition); 1151} 1152.mega-more-btn:hover { 1153 background:var(--brand-100); 1154 color:var(--brand-600); 1155} 1156 1157.mega-more-dropdown { 1158 position:absolute; 1159 top:100%; 1160 right:0; 1161 margin-top:8px; 1162 background:#fff; 1163 border:1px solid var(--gray-200,#e3e6e9); 1164 border-radius:10px; 1165 padding:12px 14px 14px; 1166 min-width:400px; 1167 max-width:520px; 1168 max-height:420px; 1169 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1170 z-index: 3000; 1171} 1172 1173.mega-more-grid { 1174 display:grid; 1175 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1176 gap:8px 10px; 1177 overflow-x: hidden; 1178 max-height:380px; 1179 overscroll-behavior:contain; 1180 scrollbar-width:thin; 1181} 1182.mega-more-grid::-webkit-scrollbar { 1183 width:6px; 1184} 1185.mega-more-grid::-webkit-scrollbar-thumb { 1186 background:var(--gray-300,#c7ccd1); 1187 border-radius:3px; 1188} 1189 1190/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1191 Kita tambahkan state active agar konsisten. */ 1192.mega-more-grid a.active { 1193 background:var(--brand-600); 1194 color:#fff; 1195 font-weight:600; 1196} 1197.hero { 1198 background:linear-gradient(135deg,var(--brand-50), #fff); 1199} 1200.hero-inner { 1201 display:grid; 1202 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1203 gap:48px; 1204 align-items:center; 1205} 1206.hero-text h1 { 1207 font-size:48px; 1208 line-height:1.1; 1209 margin:0; 1210 font-weight:600; 1211 font-family:"Poppins",sans-serif; 1212} 1213.hero-text .text-brand { color:var(--brand-500); } 1214.lead { font-size:18px; color:var(--gray-700); margin-top:16px; } 1215.hero-cta { margin-top:28px; display:flex; gap:16px; } 1216.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1217.hero-box { 1218 background:#fff; 1219 padding:10px; 1220 border-radius:16px; 1221 box-shadow:0 4px 18px rgba(0,0,0,.08); 1222 /* min-height:320px; */ 1223 display:flex; 1224 align-items:center; 1225 justify-content:center; 1226} 1227.hero-box img {border-radius: 10px; } 1228 1229@media (max-width:680px) { 1230 .hero-inner { gap: 25px; } 1231} 1232 1233.usp-section { 1234 position:relative; 1235 background: 1236 linear-gradient(180deg,#FFFDFB 0%, #FFFFFF 60%) ; 1237 border-top:1px solid var(--border); 1238 border-bottom:1px solid var(--border); 1239 padding:10px 0 10px; 1240} 1241 1242.usp-list { 1243 list-style:none; 1244 margin:0; 1245 padding:0; 1246 display:grid; 1247 gap:26px; 1248} 1249 1250.usp-list.variant-soft { 1251 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1252} 1253 1254.usp-item { 1255 display:flex; 1256 gap:16px; 1257 align-items:flex-start; 1258 background:#fff; 1259 padding:18px 18px 18px 16px; 1260 border:1px solid rgba(226,226,226,0.7); 1261 border-radius:18px; 1262 position:relative; 1263 overflow:hidden; 1264 isolation:isolate; 1265 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1266 transition: .35s cubic-bezier(.16,.8,.32,1); 1267} 1268 1269.usp-item::before { 1270 content:""; 1271 position:absolute; 1272 inset:0; 1273 background:radial-gradient(circle at 18% 22%, rgba(242,140,26,.18), transparent 60%); 1274 opacity:.65; 1275 transition:.5s; 1276 z-index:-1; 1277} 1278 1279.usp-item:hover { 1280 transform:translateY(-6px); 1281 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1282 border-color:rgba(242,140,26,.35); 1283} 1284 1285.usp-item:hover::before { 1286 background:radial-gradient(circle at 70% 40%, rgba(242,140,26,.22), transparent 65%); 1287} 1288 1289.usp-icon { 1290 flex:0 0 56px; 1291 width:56px; 1292 height:56px; 1293 border-radius:16px; 1294 display:flex; 1295 align-items:center; 1296 justify-content:center; 1297 background: 1298 linear-gradient(135deg,#FFE7CC,#FFD9A8); 1299 position:relative; 1300 box-shadow:inset 0 0 0 1px rgba(255,255,255,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1301 transition:.45s; 1302} 1303 1304.usp-item:hover .usp-icon { 1305 transform:rotate(-4deg) scale(1.06); 1306 box-shadow:inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1307} 1308 1309.usp-icon svg { 1310 width:28px; 1311 height:28px; 1312 display:block; 1313 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18));
1314} 1315 1316.usp-text h3 { 1317 font-size:15px; 1318 font-weight:600; 1319 margin:2px 0 6px; 1320 letter-spacing:.2px; 1321} 1322 1323.usp-text p { 1324 font-size:13px; 1325 line-height:1.3; 1326 margin:0; 1327 color:var(--gray-600); 1328} 1329 1330@media (max-width:680px) { 1331 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1332 .usp-item { padding:16px 14px; } 1333 .usp-icon { width:50px;height:50px;border-radius:14px; } 1334 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1335 .usp-text p { font-size:12.5px; } 1336 .usp-list { gap: 10px; } 1337} 1338 1339.flash { padding:50px 0 30px; background:#fff; } 1340.flash-head { 1341 display:flex; align-items:center; 1342 justify-content:space-between; 1343 margin-bottom:20px; 1344} 1345.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1346.flash .countdown { 1347 font-family:monospace; 1348 background:#111; 1349 color:#fff; 1350 padding:8px 14px; 1351 border-radius:8px; 1352 font-size:16px; 1353 letter-spacing:1px; 1354} 1355.flash-scroll { 1356 display:flex; 1357 gap:16px; 1358 overflow-x:auto; 1359 padding-bottom:6px; 1360 scrollbar-width:none; 1361} 1362.flash-scroll::-webkit-scrollbar { display:none; } 1363 1364.products { padding:60px 0; background:#fff; } 1365.products-head { 1366 display:flex; justify-content:space-between; align-items:center; 1367 margin-bottom:24px; 1368 flex-wrap:wrap; 1369 gap:16px; 1370} 1371.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1372.product-filters input { 1373 padding:8px 12px; 1374 border:1px solid var(--border); 1375 border-radius:8px; 1376 font-size:14px; 1377 width:220px; 1378} 1379 1380.product-grid { 1381 display:grid; 1382 gap:24px; 1383 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1384} 1385 1386.prod-card { 1387 background:#fff; 1388 border:1px solid var(--border); 1389 padding:12px; 1390 border-radius:12px; 1391 position:relative; 1392 display:flex; 1393 flex-direction:column; 1394 transition:var(--transition); 1395} 1396.prod-card.small { min-width:160px; } 1397.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1398.prod-card .img-box { 1399 background:var(--gray-100); 1400 border-radius:10px; 1401 aspect-ratio:1/1; 1402 display:flex; 1403 align-items:center; 1404 justify-content:center; 1405 overflow:hidden; 1406} 1407.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1408.prod-card:hover .img-box img { transform:scale(1.04); } 1409.prod-card .name { 1410 font-size:13.5px; 1411 font-weight:500; 1412 margin:10px 0 6px; 1413 min-height:36px; 1414 line-height:1.3; 1415} 1416.price { 1417 display:flex; 1418 gap:6px; 1419 align-items:center; 1420 flex-wrap:wrap; 1421} 1422.price strong { color:var(--brand-600); font-size:14px; } 1423.price .old { 1424 font-size:11px; 1425 color:var(--gray-500); 1426 text-decoration:line-through; 1427} 1428.tag.discount { 1429 position:absolute; 1430 top:8px; left:8px; 1431 background:#E54848; 1432 color:#fff; 1433 font-size:11px; 1434 padding:4px 6px; 1435 border-radius:6px; 1436 font-weight:600; 1437} 1438.btn { 1439 border:none; 1440 cursor:pointer; 1441 font-weight:500; 1442 font-size:14px; 1443 padding:12px 22px; 1444 border-radius:10px; 1445 background:var(--gray-900); 1446 color:#fff; 1447 transition:var(--transition); 1448 text-decoration:none; 1449 text-align:center; 1450 display:inline-flex; 1451 align-items:center; 1452 justify-content:center; 1453 gap:6px; 1454} 1455.btn:hover { background:#000; } 1456.btn-primary { background:var(--brand-500); } 1457.btn-primary:hover { background:var(--brand-600); } 1458.btn-outline { 1459 background:#fff; 1460 color:var(--gray-900); 1461 border:1px solid var(--border); 1462} 1463.btn-outline:hover { background:var(--gray-100); } 1464.btn-sm { padding:8px 14px; font-size:13px; } 1465.btn-mini { padding:6px 10px; font-size:12px; } 1466.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1467.add-cart:hover { background:var(--brand-600); } 1468 1469/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1470 1471/* 1. Pastikan grid aktif */ 1472 1473.site-footer.footer-compact .ft-grid { 1474 display:grid; 1475 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1476 gap:30px 40px; 1477 align-items:start; 1478} 1479 1480/* 2. Spacing atas bawah dipadatkan */ 1481.site-footer.footer-compact .ft-top { 1482 padding:42px 0 26px; 1483} 1484 1485/* 3. Judul & teks */ 1486.site-footer.footer-compact .ft-title { 1487 margin:0 0 14px; 1488 font-size:14px; 1489 letter-spacing:.8px; 1490 text-transform:uppercase; 1491 font-weight:600;
1492 color:#82b1c9; 1493} 1494 1495.site-footer.footer-compact .ft-desc, 1496.site-footer.footer-compact .ft-text { 1497 margin:0 0 16px; 1498 font-size:13.5px; 1499 line-height:1.45; 1500 color:#c6d6e0; 1501 max-width:330px; 1502} 1503.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1504 1505/* 4. Reset ikon SVG agar tidak membesar */ 1506.site-footer.footer-compact svg { 1507 width:20px; 1508 height:20px; 1509 flex:0 0 20px; 1510 display:inline-block; 1511} 1512.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1513.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1514 1515/* Jika ada aturan global svg { width:100%; } â paksa override */ 1516svg { max-width:100%; } 1517.site-footer.footer-compact svg { 1518 width:20px !important; 1519 height:20px !important; 1520} 1521 1522/* 5. Link list */ 1523.site-footer.footer-compact .ft-links { 1524 list-style:none; 1525 margin:0 0 12px; 1526 padding:0; 1527 display:flex; 1528 flex-direction:column; 1529 gap:4px; 1530} 1531.site-footer.footer-compact .ft-links li { margin:0; } 1532.site-footer.footer-compact .ft-links a { 1533 display:flex; 1534 align-items:center; 1535 gap:8px; 1536 font-size:13.5px; 1537 padding:5px 2px; 1538 text-decoration:none; 1539 color:#c6d6e0; 1540 border-radius:8px; 1541 transition:.25s; 1542} 1543.site-footer.footer-compact .ft-links a:hover { 1544 background:rgba(255,255,255,.06); 1545 padding-left:6px; 1546 color:#fff; 1547} 1548 1549.site-footer.footer-compact .ico-s { 1550 width:24px; 1551 height:24px; 1552 background:#103346; 1553 color:#89b8cf; 1554 border-radius:6px; 1555 display:flex; 1556 align-items:center; 1557 justify-content:center; 1558 flex:0 0 24px; 1559 font-size:12px; 1560 transition:.3s; 1561} 1562.site-footer.footer-compact .ft-links a:hover .ico-s { 1563 background:#F28C1A; 1564 color:#fff; 1565 transform:rotate(-4deg); 1566} 1567 1568/* 6. WhatsApp button */ 1569.site-footer.footer-compact .ft-wa-btn { 1570 display:inline-flex; 1571 align-items:center; 1572 gap:8px; 1573 background:#fff; 1574 color:#062437; 1575 padding:8px 18px; 1576 font-size:13.5px; 1577 border-radius:22px;
1578 font-weight:500; 1579 text-decoration:none; 1580 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1581 transition:.25s; 1582} 1583.site-footer.footer-compact .ft-wa-btn:hover { 1584 transform:translateY(-2px); 1585 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1586} 1587 1588/* 7. Sosial */ 1589.site-footer.footer-compact .ft-social { 1590 display:flex; 1591 gap:12px; 1592 margin-top:4px; 1593} 1594.site-footer.footer-compact .ft-social a { 1595 width:34px; 1596 height:34px; 1597 background:rgba(255,255,255,.08); 1598 display:flex; 1599 align-items:center; 1600 justify-content:center; 1601 border-radius:10px; 1602 color:#c6d6e0; 1603 transition:.25s; 1604} 1605.site-footer.footer-compact .ft-social a:hover { 1606 background:#F28C1A; 1607 color:#fff; 1608} 1609 1610/* 8. Alamat */ 1611.site-footer.footer-compact .ft-address { 1612 font-size:12.5px; 1613 line-height:1.44; 1614 color:#8fa4b1; 1615 max-width:330px; 1616 margin-top:12px; 1617} 1618.site-footer.footer-compact .ft-address .company { 1619 margin:0 0 4px; 1620 font-weight:600; 1621 color:#e5f2f7; 1622 font-size:12.8px; 1623} 1624 1625/* 9. Payments kecil & rapi */ 1626.site-footer.footer-compact .ft-payments { 1627 margin-top:14px; 1628 display:flex; 1629 flex-direction:column; 1630 gap:8px; 1631 max-width:350px; 1632} 1633.site-footer.footer-compact .ft-payments .pay-row { 1634 display:flex; 1635 gap:8px; 1636 flex-wrap:wrap; 1637} 1638.site-footer.footer-compact .ft-payments img { 1639 width:46px; 1640 height:32px; 1641 padding:3px 5px; 1642 background:#fff; 1643 border-radius:7px; 1644 object-fit:contain; 1645 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1646 transition:.25s; 1647} 1648.site-footer.footer-compact .ft-payments img:hover { 1649 transform:translateY(-3px); 1650 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1651} 1652 1653/* 10. Bottom bar */ 1654.site-footer.footer-compact .ft-bottom { 1655 border-top:1px solid rgba(255,255,255,.08); 1656 background:#041f31; 1657 padding:12px 0 16px; 1658 margin-top:4px; 1659} 1660.site-footer.footer-compact .ft-copy { 1661 font-size:11.5px; 1662 color:#8fa7b5; 1663 display:flex; 1664 flex-wrap:wrap; 1665 gap:12px; 1666 justify-content:center; 1667 line-height:1.3; 1668 margin:0; 1669} 1670.site-footer.footer-compact .ft-copy span { 1671 position:relative; 1672 padding:0 10px; 1673} 1674.site-footer.footer-compact .ft-copy span+span:before { 1675 content:""; 1676 position:absolute; 1677 left:0; 1678 top:50%; 1679 width:1px; 1680 height:12px; 1681 background:rgba(255,255,255,.14); 1682 transform:translateY(-50%); 1683} 1684@media (max-width:580px){ 1685 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1686 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1687 .site-footer.footer-compact .ft-copy span { padding:0; } 1688 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1689} 1690.copy { 1691 margin-top:40px; 1692 border-top:1px solid #222; 1693 text-align:center; 1694 padding:18px 0; 1695 font-size:12px; 1696 color:#666; 1697} 1698 1699@media (max-width:760px) { 1700 .hero-text h1 { font-size:38px; } 1701 .hero { padding:0px 0 30px; } 1702 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1703 .nav-inner { flex-wrap:wrap; } 1704 .mega-cats { gap:10px; } 1705 .product-grid { gap:16px; } 1706 .prod-card { padding:10px; } 1707} 1708 1709/* === PLAZA IT DARK ORANGE FOOTER THEME === */ 1710.site-footer.footer-compact { 1711 --ft-bg-0:#051722; 1712 --ft-bg-1:#072534; 1713 --ft-bg-2:#0B2F40; 1714 --ft-bg-3:#103A4E; 1715 --ft-orange:#F28C1A; 1716 --ft-orange-dark:#D97809; 1717 --ft-text:#E9F2F6; 1718 --ft-text-sec:#B8CAD3; 1719 --ft-text-muted:#8FA4B1; 1720 --ft-divider:rgba(255,255,255,0.08); 1721 --ft-glow:rgba(242,140,26,0.18); 1722 --ft-glow-fade:rgba(242,140,26,0); 1723 color:var(--ft-text); 1724} 1725 1726/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1727.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1728.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1729.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1730.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1731.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1732 1733/* ICON BACKGROUND (kubu gelap) */ 1734.site-footer.footer-compact .ico-s { 1735 background:#0F3647; 1736 color:#9FC7DA; 1737} 1738.site-footer.footer-compact .ft-links a:hover .ico-s { 1739 background:var(--ft-orange); 1740 color:#fff; 1741} 1742 1743/* WhatsApp button biarkan putih agar kontras */ 1744.site-footer.footer-compact .ft-wa-btn { 1745 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(255,255,255,0.05); 1746} 1747.site-footer.footer-compact .ft-wa-btn:hover { 1748 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(242,140,26,0.25); 1749} 1750 1751/* Payment cards border halus */ 1752.site-footer.footer-compact .ft-payments img { 1753 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,0.05); 1754} 1755.site-footer.footer-compact .ft-payments img:hover { 1756 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(242,140,26,0.4); 1757} 1758 1759/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1760.site-footer.footer-compact.variant-ember { 1761 background: 1762 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1763 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1764} 1765.site-footer.footer-compact.variant-ember .ft-bottom { 1766 background:rgba(0,0,0,0.25); 1767 backdrop-filter:blur(2px); 1768} 1769 1770/* ========== VARIANT B: TOP BAR ORANGE ========== */ 1771.site-footer.footer-compact.variant-bar { 1772 background:var(--ft-bg-1); 1773 position:relative; 1774} 1775.site-footer.footer-compact.variant-bar::before { 1776 content:""; 1777 position:absolute; 1778 left:0;top:0; 1779 width:100%;height:3px; 1780 background:linear-gradient(90deg,var(--ft-orange-dark),var(--ft-orange)); 1781 box-shadow:0 0 12px -2px var(--ft-orange); 1782} 1783.site-footer.footer-compact.variant-bar .ft-bottom { 1784 background:#041c28; 1785} 1786 1787/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1788.site-footer.footer-compact.variant-radial { 1789 background: 1790 radial-gradient(circle at 70% 90%, rgba(242,140,26,0.22) 0%, rgba(242,140,26,0.04) 38%, rgba(242,140,26,0) 65%), 1791 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1792} 1793.site-footer.footer-compact.variant-radial .ft-bottom { 1794 background:#041c29; 1795} 1796
1797/* ========== VARIANT D: LAYERED STRIPES ========== */ 1798.site-footer.footer-compact.variant-stripes { 1799 background: 1800 repeating-linear-gradient(180deg, rgba(255,255,255,0.015) 0 38px, rgba(255,255,255,0) 38px 76px), 1801 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1802 position:relative; 1803} 1804.site-footer.footer-compact.variant-stripes::after { 1805 content:""; 1806 position:absolute; 1807 left:0;bottom:0; 1808 width:100%;height:110px; 1809 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1810 pointer-events:none; 1811} 1812.site-footer.footer-compact.variant-stripes .ft-bottom { 1813 background:#031821; 1814} 1815 1816/* Garis aksen oranye tipis di atas bottom */ 1817.site-footer.footer-compact.variant-stripes .ft-bottom { 1818 position:relative; 1819} 1820.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1821 content:""; 1822 position:absolute; 1823 left:0;top:0; 1824 width:100%;height:2px; 1825 background:linear-gradient(90deg,rgba(242,140,26,0),rgba(242,140,26,.65),rgba(242,140,26,0)); 1826} 1827 1828/* ========== VARIANT E: GLASS PANEL ========== */ 1829.site-footer.footer-compact.variant-glass { 1830 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 100%); 1831} 1832.site-footer.footer-compact.variant-glass .ft-grid { 1833 position:relative; 1834 padding:32px clamp(10px,2vw,24px); 1835 border-radius:22px;
1836 background:linear-gradient(140deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02)); 1837 border:1px solid rgba(255,255,255,0.08); 1838 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04) inset; 1839 backdrop-filter:blur(8px); 1840 -webkit-backdrop-filter:blur(8px); 1841} 1842.site-footer.footer-compact.variant-glass .ft-bottom { 1843 background:#02141d; 1844} 1845 1846/* HOVER ACCENTS */ 1847.site-footer.footer-compact a { 1848 transition:color .25s, background .25s, box-shadow .25s; 1849} 1850.site-footer.footer-compact .ft-links a:hover { 1851 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1852 0 0 0 1px rgba(242,140,26,0.55); 1853} 1854.site-footer.footer-compact .ft-social a { 1855 background:#0E3445; 1856 color:#bcd4dd; 1857} 1858.site-footer.footer-compact .ft-social a:hover { 1859 background:var(--ft-orange); 1860 color:#fff; 1861 box-shadow:0 0 0 3px rgba(242,140,26,0.28); 1862} 1863 1864/* Divider di atas footer (opsional) */ 1865.footer-divider-top { 1866 height:1px; 1867 background:linear-gradient(90deg,transparent,rgba(255,255,255,0.15),transparent); 1868 width:100%; 1869 margin:0 0 4px; 1870} 1871 1872/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1873 1874/* Desktop: wrapper tidak mengganggu grid layout */ 1875.ft-collapsible-content { 1876 display: contents; 1877} 1878 1879@media (max-width: 768px) { 1880 /* Wrapper untuk konten yang bisa collapse */ 1881 .ft-top.ft-collapsible .ft-collapsible-content { 1882 display: block; 1883 max-height: 80px; 1884 overflow: hidden; 1885 position: relative; 1886 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1887 } 1888 1889 /* Fade gradient overlay */ 1890 .ft-top.ft-collapsible .ft-collapsible-content::after { 1891 content: ''; 1892 position: absolute; 1893 bottom: 0; 1894 left: 0; 1895 right: 0; 1896 height: 70px; 1897 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1898 pointer-events: none; 1899 opacity: 1; 1900 transition: opacity 0.3s ease; 1901 z-index: 1; 1902 } 1903 1904 /* State: Expanded */ 1905 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1906 max-height: 1500px; 1907 } 1908 1909 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1910 opacity: 0; 1911 } 1912 1913 /* Expand Button - Di luar collapsible, selalu visible */ 1914 .ft-expand-btn { 1915 display: flex;
1916 align-items: center; 1917 justify-content: center; 1918 gap: 8px; 1919 width: 100%; 1920 margin: -8px 0 0; 1921 padding: 12px 20px; 1922 background: rgba(255,255,255,0.12); 1923 border: 1px solid rgba(255,255,255,0.25); 1924 border-radius: 10px; 1925 color: #e5f2f7; 1926 font-size: 13.5px; 1927 font-weight: 500; 1928 cursor: pointer; 1929 transition: all 0.3s ease; 1930 outline: none; 1931 position: relative; 1932 z-index: 10; 1933 } 1934 1935 .ft-expand-btn:hover { 1936 background: rgba(255,255,255,0.18); 1937 border-color: rgba(255,255,255,0.35); 1938 color: #fff; 1939 transform: translateY(-2px); 1940 } 1941 1942 .ft-expand-btn:active { 1943 transform: scale(0.98); 1944 } 1945 1946 .ft-expand-btn svg { 1947 width: 18px; 1948 height: 18px; 1949 transition: transform 0.3s ease; 1950 fill: currentColor; 1951 } 1952 1953 .ft-top.ft-collapsible.expanded .ft-expand-btn { 1954 margin-top: 15px; 1955 } 1956 1957 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 1958 transform: rotate(180deg); 1959 } 1960} 1961 1962/* Desktop: hide expand button */ 1963@media (min-width: 769px) { 1964 .ft-expand-btn { 1965 display: none !important; 1966 } 1967} 1968 1969/* RESPONSIVE (jika background panel butuh penyesuaian) */ 1970@media (max-width:640px){ 1971 .site-footer.footer-compact.variant-glass .ft-grid { 1972 padding:26px 18px; 1973 } 1974} 1975 1976</style> 1977 <style data-inlined="bottom-nav.css"> 1978/* Mobile Bottom Navigation */ 1979.bottom-nav { 1980 position: fixed; 1981 bottom: 0; 1982 left: 0; 1983 right: 0; 1984 background: #ffffff; 1985 border-top: 1px solid #e5e7eb; 1986 z-index: 9998; /* Below chat widget */ 1987 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 1988 display: none; /* Hidden on desktop */ 1989} 1990 1991.bottom-nav-inner { 1992 display: flex; 1993 justify-content: space-around; 1994 align-items: center; 1995 padding: 2px 0; 1996 max-width: 640px; 1997 margin: 0 auto; 1998} 1999 2000.bottom-nav-item { 2001 flex: 1; 2002 display: flex; 2003 flex-direction: column; 2004 align-items: center; 2005 justify-content: center; 2006 gap: 4px; 2007 padding: 8px 8px; 2008 background: none; 2009 border: none; 2010 cursor: pointer; 2011 text-decoration: none; 2012 color: #6b7280; 2013 transition: all 0.3s ease; 2014 position: relative; 2015} 2016 2017.bottom-nav-item:hover { 2018 color: #F28C1A; 2019} 2020 2021.bottom-nav-item.active { 2022 flex-direction: row; 2023 gap: 8px; 2024 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2025 color: white; 2026 border-radius: 20px; 2027 padding: 8px 16px; 2028 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 2029} 2030 2031.bottom-nav-item-icon { 2032 width: 24px; 2033 height: 24px; 2034 position: relative; 2035} 2036 2037.bottom-nav-item-icon svg { 2038 width: 100%; 2039 height: 100%; 2040} 2041 2042/* Icon variant switching */ 2043.bottom-nav-item .icon-outlined { 2044 display: block; 2045} 2046 2047.bottom-nav-item .icon-filled { 2048 display: none; 2049} 2050 2051.bottom-nav-item.active .icon-outlined { 2052 display: none; 2053} 2054 2055.bottom-nav-item.active .icon-filled { 2056 display: block; 2057} 2058 2059.bottom-nav-item-label { 2060 font-size: 12px; 2061 font-weight: 600; 2062 line-height: 1; 2063 display: none; 2064 white-space: nowrap; 2065} 2066 2067.bottom-nav-item.active .bottom-nav-item-label { 2068 display: block; 2069} 2070 2071/* Avatar styles for bottom-nav */ 2072.bottom-nav-avatar-img, 2073.bottom-nav-avatar-initials { 2074 width: 24px; 2075 height: 24px; 2076 border-radius: 50%; 2077 display: block; 2078} 2079 2080.bottom-nav-avatar-img { 2081 object-fit: cover; 2082} 2083 2084.bottom-nav-avatar-initials { 2085 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2086 color: white; 2087 font-size: 10px; 2088 font-weight: 700; 2089 display: flex; 2090 align-items: center; 2091 justify-content: center; 2092 line-height: 1; 2093 letter-spacing: 0.5px; 2094} 2095 2096/* Hide SVG when avatar is shown */ 2097.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2098.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2099 display: none; 2100} 2101 2102/* Active state adjustments for avatar */ 2103.bottom-nav-item.active .bottom-nav-avatar-initials { 2104 background: white; 2105 color: #F28C1A; 2106 border: 2px solid rgba(255, 255, 255, 0.8); 2107 box-sizing: border-box; 2108} 2109 2110.bottom-nav-item.active .bottom-nav-avatar-img { 2111 border: 2px solid rgba(255, 255, 255, 0.9); 2112 box-sizing: border-box; 2113} 2114 2115.bottom-nav-item-badge { 2116 position: absolute; 2117 top: -4px; 2118 right: -4px; 2119 background: #ef4444; 2120 color: white; 2121 border-radius: 10px; 2122 padding: 2px 5px; 2123 font-size: 10px; 2124 font-weight: 600; 2125 min-width: 18px;
2126 height: 18px; 2127 display: flex; 2128 align-items: center; 2129 justify-content: center; 2130 line-height: 1; 2131} 2132 2133/* Show on mobile only */ 2134@media (max-width: 640px) { 2135 .bottom-nav { 2136 display: block; 2137 } 2138 2139 /* Add bottom padding to body to prevent content being hidden */ 2140 body { 2141 padding-bottom: 60px; 2142 } 2143} 2144 2145/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2146.mobile-user-sheet { 2147 position: fixed; 2148 bottom: 0; 2149 left: 0; 2150 right: 0; 2151 z-index: 10000; 2152 pointer-events: none; 2153} 2154 2155.mobile-user-sheet:not([hidden]) { 2156 pointer-events: auto; 2157} 2158 2159.mobile-user-sheet-backdrop { 2160 position: fixed; 2161 inset: 0; 2162 background: rgba(0, 0, 0, 0.5); 2163 opacity: 0; 2164 transition: opacity 0.2s ease-out; 2165} 2166 2167.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2168 opacity: 1; 2169} 2170 2171.mobile-user-sheet-content { 2172 position: absolute; 2173 bottom: 0; 2174 left: 0; 2175 right: 0; 2176 background: white; 2177 border-radius: 16px 16px 0 0; 2178 max-height: 80vh; 2179 overflow-y: auto; 2180 transform: translateY(100%); 2181 transition: transform 0.3s ease-out; 2182} 2183 2184.mobile-user-sheet.active .mobile-user-sheet-content { 2185 transform: translateY(0); 2186} 2187 2188.mobile-user-sheet-handle { 2189 width: 40px; 2190 height: 4px; 2191 background: #d1d5db; 2192 border-radius: 2px; 2193 margin: 12px auto 16px; 2194} 2195 2196.mobile-user-sheet-header { 2197 padding: 0 20px 16px; 2198 border-bottom: 1px solid #e5e7eb; 2199} 2200 2201.mobile-user-info { 2202 display: flex; 2203 align-items: center; 2204 gap: 12px; 2205} 2206 2207.mobile-user-avatar { 2208 width: 48px; 2209 height: 48px; 2210 background: linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 2211 border-radius: 50%; 2212 display: flex; 2213 align-items: center; 2214 justify-content: center; 2215 font-size: 24px; 2216 color: #D97809; 2217 font-weight: 600; 2218} 2219 2220.mobile-user-details { 2221 flex: 1; 2222} 2223 2224.mobile-user-name { 2225 font-weight: 600; 2226 font-size: 16px; 2227 color: #1f2937; 2228 margin-bottom: 4px; 2229} 2230 2231.mobile-user-email { 2232 font-size: 13px; 2233 color: #6b7280; 2234} 2235 2236.mobile-user-sheet-body { 2237 padding: 8px 0; 2238} 2239 2240.mobile-user-sheet-item { 2241 display: flex; 2242 align-items: center; 2243 gap: 12px; 2244 padding: 14px 20px; 2245 background: none; 2246 border: none; 2247 width: 100%; 2248 text-align: left; 2249 cursor: pointer; 2250 text-decoration: none; 2251 color: #374151; 2252 transition: background 0.2s; 2253} 2254 2255.mobile-user-sheet-item:hover { 2256 background: #f9fafb; 2257} 2258 2259.mobile-user-sheet-icon { 2260 font-size: 20px; 2261 width: 24px; 2262 text-align: center; 2263} 2264 2265.mobile-user-sheet-label { 2266 font-size: 15px; 2267 font-weight: 500; 2268} 2269 2270.mobile-user-sheet-divider { 2271 height: 1px; 2272 background: #e5e7eb; 2273 margin: 8px 0; 2274} 2275 2276@keyframes fadeIn { 2277 from { 2278 opacity: 0; 2279 } 2280 to { 2281 opacity: 1; 2282 } 2283} 2284 2285@keyframes slideUp { 2286 from { 2287 transform: translateY(100%); 2288 } 2289 to { 2290 transform: translateY(0); 2291 } 2292} 2293 2294/* Ensure chat widget works well with bottom nav */ 2295@media (max-width: 640px) { 2296 .chat-widget { 2297 bottom: 0 !important; /* Chat opens from bottom */ 2298 } 2299 2300 .chat-popup { 2301 /* Chat popup should be above bottom nav */ 2302 z-index: 9999; 2303 } 2304} 2305 2306</style> 2307 <style data-inlined="search-suggest.css"> 2308.search { 2309 position: relative; 2310} 2311 2312/* AI Bot Toggle Button */ 2313.ai-bot-toggle { 2314 display: flex; 2315 align-items: center; 2316 justify-content: center; 2317 background: transparent; 2318 border: none; 2319 padding: 8px; 2320 cursor: pointer; 2321 border-radius: 8px; 2322 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2323 margin-left: 4px; 2324 flex-shrink: 0; 2325 display: none; 2326} 2327 2328.ai-bot-toggle .bot-icon { 2329 color: #9ca3af; 2330 transition: all 0.3s ease; 2331} 2332 2333.ai-bot-toggle:hover .bot-icon { 2334 color: #6b7280; 2335 transform: scale(1.1); 2336} 2337 2338.ai-bot-toggle.active .bot-icon { 2339 color: #fb923c; 2340 animation: botPulse 2s ease-in-out infinite; 2341} 2342 2343.ai-bot-toggle.active { 2344 background: rgba(251, 146, 60, 0.1); 2345} 2346 2347.ai-bot-toggle.active:hover .bot-icon { 2348 color: #f97316; 2349} 2350 2351@keyframes botPulse { 2352 0%, 100% { transform: scale(1); } 2353 50% { transform: scale(1.12); } 2354} 2355 2356/* Search Suggest Box */ 2357.search-suggest { 2358 position: absolute; 2359 top: 100%; left: 0; right: 0; 2360 background: #fff; 2361 border: 1px solid #e1e4e8; 2362 border-top: none; 2363 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2364 max-height: 400px; 2365 overflow-y: auto; 2366 z-index: 100; 2367 padding: 0; 2368 font-size: 14px; 2369 border-radius: 0 0 8px 8px; 2370} 2371 2372.suggest-item { 2373 display: flex; 2374 align-items: center; 2375 gap: 8px; 2376 padding: 10px 16px; 2377 cursor: pointer; 2378 transition: all 0.2s ease; 2379 border-bottom: 1px solid #f1f5f9; 2380} 2381 2382.suggest-item:last-child { 2383 border-bottom: none; 2384} 2385 2386.suggest-item:hover, 2387.suggest-item.active { 2388 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2389 padding-left: 20px; 2390} 2391 2392.suggest-thumb { 2393 width: 42px; 2394 height: 42px; 2395 object-fit: cover; 2396 border: 1px solid #ddd; 2397 border-radius: 4px; 2398 background: #fafafa; 2399} 2400 2401.suggest-meta { flex: 1; min-width: 0; } 2402 2403.suggest-name { 2404 font-weight: 500;
2405 color: #222; 2406 line-height: 1.2; 2407 white-space: nowrap; 2408 overflow: hidden; 2409 text-overflow: ellipsis; 2410} 2411 2412.suggest-price { 2413 color: #d32f2f; 2414 font-size: 12px; 2415 margin-top: 2px; 2416} 2417 2418/* AI Insights */ 2419.suggest-ai-insight { 2420 display: flex; 2421 align-items: center; 2422 gap: 8px; 2423 padding: 8px 12px; 2424 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 2425 border-bottom: 1px solid #bae6fd; 2426 color: #0369a1; 2427 font-size: 12px; 2428 font-weight: 500; 2429 line-height: 1.4; 2430} 2431 2432.suggest-ai-insight svg { 2433 flex-shrink: 0; 2434 opacity: 0.8; 2435} 2436 2437.suggest-ai-insight span { 2438 flex: 1; 2439} 2440 2441/* AI Progress States */ 2442.ai-progress-container { 2443 padding: 20px 16px; 2444 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2445} 2446 2447.ai-progress-header { 2448 display: flex; 2449 align-items: center; 2450 gap: 10px; 2451 margin-bottom: 16px; 2452} 2453 2454.ai-progress-icon { 2455 width: 36px; 2456 height: 36px; 2457 display: flex; 2458 align-items: center; 2459 justify-content: center; 2460 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 2461 border-radius: 50%; 2462 color: #fff; 2463 font-size: 18px; 2464 animation: aiIconSpin 3s linear infinite; 2465} 2466 2467@keyframes aiIconSpin { 2468 0% { transform: rotate(0deg); } 2469 100% { transform: rotate(360deg); } 2470} 2471 2472.ai-progress-text { 2473 flex: 1; 2474} 2475 2476.ai-progress-title { 2477 font-weight: 600; 2478 color: #1e293b; 2479 font-size: 14px; 2480 margin-bottom: 2px; 2481} 2482 2483.ai-progress-subtitle { 2484 font-size: 12px; 2485 color: #64748b; 2486} 2487 2488/* Progress Steps */ 2489.ai-progress-steps { 2490 display: flex; 2491 flex-direction: column; 2492 gap: 10px; 2493 margin-top: 16px; 2494} 2495 2496.ai-progress-step { 2497 display: flex; 2498 align-items: center; 2499 gap: 10px; 2500 padding: 8px 12px; 2501 background: #fff; 2502 border-radius: 8px; 2503 border: 1px solid #e2e8f0; 2504 transition: all 0.3s ease; 2505} 2506 2507.ai-progress-step.active { 2508 border-color: #667eea; 2509 background: #f8f9ff; 2510 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2511} 2512 2513.ai-progress-step.completed { 2514 border-color: #10b981; 2515 background: #f0fdf4; 2516} 2517 2518.ai-step-icon { 2519 width: 20px; 2520 height: 20px; 2521 display: flex; 2522 align-items: center; 2523 justify-content: center; 2524 border-radius: 50%; 2525 font-size: 11px; 2526 flex-shrink: 0; 2527} 2528 2529.ai-progress-step.active .ai-step-icon { 2530 background: #667eea; 2531 color: #fff; 2532 animation: pulse 1.5s ease-in-out infinite; 2533} 2534 2535.ai-progress-step.completed .ai-step-icon { 2536 background: #10b981; 2537 color: #fff; 2538} 2539 2540.ai-progress-step.pending .ai-step-icon { 2541 background: #e2e8f0; 2542 color: #94a3b8; 2543} 2544 2545.ai-step-text { 2546 flex: 1; 2547 font-size: 13px; 2548 color: #475569; 2549 font-weight: 500; 2550} 2551 2552.ai-progress-step.completed .ai-step-text { 2553 color: #059669; 2554} 2555 2556/* AI Analysis Result Banner */ 2557.ai-result-banner { 2558 padding: 12px 16px; 2559 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2560 border-bottom: 1px solid #86efac; 2561 display: flex; 2562 align-items: center; 2563 gap: 10px; 2564} 2565 2566.ai-result-icon { 2567 font-size: 20px; 2568 animation: bounceIn 0.5s ease; 2569} 2570 2571@keyframes bounceIn { 2572 0% { transform: scale(0); opacity: 0; } 2573 50% { transform: scale(1.1); } 2574 100% { transform: scale(1); opacity: 1; } 2575} 2576 2577.ai-result-text { 2578 flex: 1; 2579} 2580 2581.ai-result-title { 2582 font-weight: 600; 2583 color: #065f46; 2584 font-size: 13px; 2585 margin-bottom: 2px; 2586} 2587 2588.ai-result-description { 2589 font-size: 12px; 2590 color: #047857; 2591 opacity: 0.9; 2592} 2593 2594/* Product Items Section Header */ 2595.suggest-section-header { 2596 padding: 10px 16px; 2597 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2598 border-bottom: 1px solid #e2e8f0; 2599} 2600 2601.suggest-header-main { 2602 display: flex; 2603 align-items: center; 2604 gap: 6px; 2605 margin-bottom: 4px; 2606} 2607 2608.suggest-header-icon { 2609 font-size: 14px; 2610} 2611 2612.suggest-header-text { 2613 font-size: 12px; 2614 font-weight: 600; 2615 color: #64748b; 2616 text-transform: uppercase; 2617 letter-spacing: 0.5px; 2618} 2619 2620.suggest-header-hint { 2621 font-size: 11px; 2622 color: #94a3b8; 2623 font-weight: 400; 2624 text-transform: none; 2625 letter-spacing: 0; 2626 display: flex; 2627 align-items: center; 2628 gap: 4px; 2629} 2630 2631.suggest-header-hint kbd { 2632 display: inline-block; 2633 padding: 2px 6px; 2634 font-size: 10px; 2635 font-weight: 600; 2636 line-height: 1; 2637 color: #475569; 2638 background: #fff; 2639 border: 1px solid #cbd5e1; 2640 border-radius: 4px; 2641 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2642 font-family: 'Segoe UI', monospace; 2643} 2644 2645/* Footer Hint */ 2646.suggest-footer-hint { 2647 display: flex; 2648 align-items: center; 2649 justify-content: center; 2650 padding: 12px 16px; 2651 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 2652 border-top: 1px solid #fbbf24; 2653 color: #92400e; 2654 font-size: 12px; 2655 font-weight: 500; 2656 text-align: center; 2657 cursor: default; 2658} 2659 2660.suggest-footer-hint svg { 2661 color: #f59e0b; 2662 flex-shrink: 0; 2663} 2664 2665.suggest-footer-hint kbd { 2666 display: inline-block; 2667 padding: 3px 7px; 2668 margin: 0 3px; 2669 font-size: 11px; 2670 font-weight: 600; 2671 line-height: 1; 2672 color: #78350f; 2673 background: #fff; 2674 border: 1px solid #f59e0b; 2675 border-radius: 4px; 2676 box-shadow: 0 1px 3px rgba(245, 158, 11, 0.2); 2677 font-family: 'Segoe UI', monospace; 2678} 2679 2680.suggest-footer-link { 2681 color: #ea580c; 2682 font-weight: 600; 2683 text-decoration: underline; 2684 text-underline-offset: 2px; 2685 text-decoration-thickness: 1px; 2686 cursor: pointer; 2687 transition: all 0.2s ease; 2688} 2689 2690.suggest-footer-link:hover { 2691 color: #c2410c; 2692 text-decoration-thickness: 2px; 2693 text-shadow: 0 0 8px rgba(234, 88, 12, 0.3); 2694} 2695 2696/* Loading state */ 2697.suggest-loading { 2698 display: flex; 2699 align-items: center; 2700 justify-content: center; 2701 gap: 8px; 2702 padding: 16px 12px; 2703 color: #64748b; 2704 font-size: 13px; 2705} 2706 2707@keyframes pulse { 2708 0%, 100% { opacity: 1; } 2709 50% { opacity: 0.5; } 2710} 2711 2712.suggest-loading { 2713 animation: pulse 1.5s ease-in-out infinite; 2714} 2715 2716/* Responsive Mobile Styles */ 2717@media (max-width: 768px) { 2718 .ai-bot-toggle { 2719 padding: 6px; 2720 margin-left: 0; 2721 } 2722 2723 .ai-bot-toggle .bot-icon {
2724 width: 20px; 2725 height: 20px; 2726 } 2727 2728 .search-icon-btn { 2729 margin-right: 4px; 2730 } 2731 2732 .search-icon-btn svg { 2733 width: 18px; 2734 height: 18px; 2735 } 2736 2737 .search-suggest { 2738 max-height: 350px; 2739 } 2740 2741 /* Mobile hint adjustments */ 2742 .suggest-header-hint { 2743 font-size: 10px; 2744 } 2745 2746 .suggest-header-hint kbd { 2747 padding: 1px 4px; 2748 font-size: 9px; 2749 } 2750 2751 .suggest-footer-hint { 2752 padding: 10px 12px; 2753 font-size: 11px; 2754 } 2755 2756 .suggest-footer-hint svg { 2757 width: 14px; 2758 height: 14px; 2759 } 2760 2761 .suggest-footer-hint kbd { 2762 padding: 2px 5px; 2763 font-size: 10px; 2764 } 2765 2766 .ai-progress-container { 2767 padding: 16px 12px; 2768 } 2769 2770 .ai-progress-icon { 2771 width: 32px; 2772 height: 32px; 2773 font-size: 16px; 2774 } 2775 2776 .ai-progress-title { 2777 font-size: 13px; 2778 } 2779 2780 .ai-progress-subtitle { 2781 font-size: 11px; 2782 } 2783 2784 .ai-progress-step { 2785 padding: 6px 10px; 2786 } 2787 2788 .ai-step-text { 2789 font-size: 12px; 2790 } 2791 2792 .suggest-item { 2793 padding: 8px 12px; 2794 } 2795 2796 .suggest-thumb { 2797 width: 36px; 2798 height: 36px; 2799 } 2800 2801 .suggest-name { 2802 font-size: 13px; 2803 } 2804 2805 .suggest-price { 2806 font-size: 11px; 2807 } 2808} 2809 2810@media (max-width: 480px) { 2811 .ai-bot-toggle { 2812 padding: 4px; 2813 } 2814 2815 .ai-bot-toggle .bot-icon { 2816 width: 18px; 2817 height: 18px; 2818 } 2819 2820 .search-icon-btn svg { 2821 width: 16px; 2822 height: 16px; 2823 } 2824 2825 /* Ultra-compact hints for small screens */ 2826 .suggest-header-hint { 2827 display: none; /* Hide header hint on very small screens */ 2828 } 2829 2830 .suggest-footer-hint { 2831 padding: 8px 10px; 2832 font-size: 10px; 2833 flex-direction: column; 2834 gap: 4px; 2835 } 2836 2837 .suggest-footer-hint svg { 2838 display: none; /* Hide icon to save space */ 2839 } 2840 2841 .suggest-footer-hint kbd { 2842 padding: 2px 4px; 2843 font-size: 9px; 2844 } 2845 2846 .ai-progress-steps { 2847 gap: 8px; 2848 } 2849 2850 .ai-step-text { 2851 font-size: 11px; 2852 } 2853} 2854 2855/* AI Search Prompt Box */ 2856.ai-search-prompt { 2857 padding: 24px; 2858 background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); 2859} 2860 2861.ai-prompt-header { 2862 display: flex; 2863 align-items: center; 2864 gap: 12px; 2865 margin-bottom: 16px; 2866} 2867 2868.ai-prompt-icon { 2869 width: 48px; 2870 height: 48px; 2871 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 2872 border-radius: 12px; 2873 display: flex; 2874 align-items: center; 2875 justify-content: center; 2876 font-size: 24px; 2877 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.3); 2878} 2879 2880.ai-prompt-text { 2881 flex: 1; 2882} 2883 2884.ai-prompt-title { 2885 font-size: 16px; 2886 font-weight: 700; 2887 color: #1f2937; 2888 margin-bottom: 4px; 2889} 2890 2891.ai-prompt-subtitle { 2892 font-size: 13px; 2893 color: #6b7280; 2894} 2895 2896.ai-prompt-query { 2897 background: white; 2898 border: 2px solid #fb923c; 2899 border-radius: 8px; 2900 padding: 12px 16px; 2901 margin-bottom: 16px; 2902} 2903 2904.ai-query-label { 2905 font-size: 11px; 2906 font-weight: 600; 2907 color: #fb923c; 2908 text-transform: uppercase; 2909 letter-spacing: 0.5px; 2910 margin-bottom: 4px; 2911} 2912 2913.ai-query-text { 2914 font-size: 15px; 2915 font-weight: 600; 2916 color: #1f2937; 2917} 2918 2919.ai-search-trigger-btn { 2920 width: 100%; 2921 padding: 14px 20px; 2922 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 2923 color: white; 2924 border: none; 2925 border-radius: 10px; 2926 font-size: 15px; 2927 font-weight: 700; 2928 cursor: pointer; 2929 display: flex; 2930 align-items: center; 2931 justify-content: center; 2932 gap: 8px; 2933 transition: all 0.3s ease; 2934 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.4); 2935} 2936 2937.ai-search-trigger-btn:hover { 2938 transform: translateY(-2px); 2939 box-shadow: 0 6px 20px rgba(251, 146, 60, 0.5); 2940} 2941 2942.ai-search-trigger-btn:active { 2943 transform: translateY(0); 2944} 2945 2946.ai-btn-icon { 2947 font-size: 18px; 2948 animation: sparkle 2s ease-in-out infinite; 2949} 2950 2951@keyframes sparkle { 2952 0%, 100% { opacity: 1; transform: scale(1); } 2953 50% { opacity: 0.7; transform: scale(1.2); } 2954} 2955 2956.ai-btn-text { 2957 font-size: 15px; 2958} 2959 2960.ai-prompt-footer { 2961 margin-top: 12px; 2962 text-align: center; 2963 font-size: 12px; 2964 color: #9ca3af; 2965 font-style: italic; 2966} 2967 2968/* ==================== TRENDING SEARCH STYLES ==================== */ 2969 2970.search-suggest.trending-mode { 2971 border-top: 3px solid #fb923c; 2972 max-width: 900px; 2973 min-width: 600px; 2974} 2975 2976/* Two-column container */ 2977.trending-container { 2978 display: flex; 2979 gap: 0; 2980 padding: 0 !important; 2981} 2982 2983.trending-column { 2984 flex: 1; 2985 min-width: 0; 2986 padding: 0 !important; 2987} 2988 2989.trending-column.trending-searches { 2990 border-right: 2px solid #e2e8f0; padding: 0 !important; 2991} 2992 2993/* Mobile: stack columns vertically */ 2994@media (max-width: 768px) { 2995 .search-suggest.trending-mode { 2996 min-width: auto; 2997 max-width: 100%; 2998 } 2999 3000 .trending-container { 3001 flex-direction: column; 3002 } 3003 3004 .trending-column.trending-searches {
3005 border-right: none; 3006 border-bottom: 2px solid #e2e8f0; 3007 } 3008} 3009 3010.trending-header { 3011 display: flex; 3012 align-items: center; 3013 gap: 8px; 3014 padding: 14px 16px; 3015 border-bottom: 1px solid #e2e8f0; 3016 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 3017 color: white; 3018 position: sticky; 3019 top: 0; 3020 z-index: 10; 3021} 3022 3023.trending-icon { 3024 font-size: 20px; 3025 animation: trendingPulse 2s infinite; 3026} 3027 3028@keyframes trendingPulse { 3029 0%, 100% { 3030 transform: scale(1); 3031 filter: drop-shadow(0 0 0 rgba(251, 146, 60, 0)); 3032 } 3033 50% { 3034 transform: scale(1.15); 3035 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3036 } 3037} 3038 3039.trending-title { 3040 font-weight: 700; 3041 font-size: 14px; 3042 letter-spacing: 0.3px; 3043} 3044 3045.trending-subtitle { 3046 font-size: 11px; 3047 opacity: 0.9; 3048 margin-left: auto; 3049 font-style: italic; 3050} 3051 3052/* Trending searches item */ 3053.trending-item { 3054 display: flex; 3055 align-items: center; 3056 gap: 12px; 3057 padding: 12px 16px; 3058 cursor: pointer; 3059 transition: all 0.2s ease; 3060 border-bottom: 1px solid #f1f5f9; 3061 position: relative; 3062} 3063 3064.trending-item:hover { 3065 background: linear-gradient(90deg, #fff7ed 0%, #ffedd5 100%); 3066 padding-left: 20px; 3067} 3068 3069.trending-item:hover::before { 3070 content: ''; 3071 position: absolute; 3072 left: 0; 3073 top: 0; 3074 bottom: 0; 3075 width: 4px; 3076 background: linear-gradient(180deg, #fb923c 0%, #f97316 100%); 3077} 3078 3079.trending-item:last-child { 3080 border-bottom: none; 3081} 3082 3083.trending-rank { 3084 font-size: 20px; 3085 flex-shrink: 0; 3086 width: 32px; 3087 text-align: center; 3088} 3089 3090.trending-query { 3091 flex: 1; 3092 font-size: 14px; 3093 color: #1f2937; 3094 font-weight: 500; 3095 line-height: 1.4; 3096} 3097 3098.trending-category { 3099 font-size: 11px; 3100 color: #f97316; 3101 background: #fff7ed; 3102 padding: 3px 10px; 3103 border-radius: 12px; 3104 white-space: nowrap; 3105 font-weight: 500; 3106 border: 1px solid #fed7aa; 3107} 3108 3109/* Trending products item */ 3110.trending-product-item { 3111 display: flex; 3112 align-items: center; 3113 gap: 12px; 3114 padding: 12px 16px; 3115 cursor: pointer; 3116 transition: all 0.2s ease; 3117 border-bottom: 1px solid #f1f5f9; 3118 position: relative; 3119} 3120 3121.trending-product-item:hover { 3122 background: linear-gradient(90deg, #fef3c7 0%, #fde68a 100%); 3123 padding-left: 20px; 3124} 3125 3126.trending-product-item:hover::before { 3127 content: ''; 3128 position: absolute; 3129 left: 0; 3130 top: 0; 3131 bottom: 0; 3132 width: 4px; 3133 background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%); 3134} 3135 3136.trending-product-item:last-child { 3137 border-bottom: none; 3138} 3139 3140.trending-product-thumb { 3141 width: 48px; 3142 height: 48px; 3143 object-fit: cover; 3144 border-radius: 8px; 3145 border: 1px solid #e5e7eb; 3146 flex-shrink: 0; 3147 background: white; 3148} 3149 3150.trending-product-info { 3151 flex: 1; 3152 min-width: 0; 3153} 3154 3155.trending-product-name { 3156 font-size: 13px; 3157 font-weight: 400; 3158 color: #1f2937; 3159 line-height: 1.4; 3160 margin-bottom: 0; 3161 padding: 0 !important; 3162 overflow: hidden; 3163 text-overflow: ellipsis; 3164 display: -webkit-box; 3165 -webkit-line-clamp: 2; 3166 -webkit-box-orient: vertical; 3167} 3168 3169.trending-product-price { 3170 font-size: 12px; 3171 font-weight: 700; 3172 color: #f59e0b; 3173 padding: 4px 0 !important; 3174} 3175.trending-product { padding: 0;} 3176/* ==================== END TRENDING STYLES ==================== */ 3177 3178</style> 3179 <style data-inlined="product-detail.css"> 3180/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3181 3182/* Root / Container */ 3183.product-single { 3184 --pd-gap: 2rem; 3185 --pd-border: #e2e8f0; 3186 --pd-bg: #ffffff; 3187 --pd-muted: #64748b; 3188 --pd-dark: #0f172a; 3189 --pd-accent: #0d6efd; /* tema utama situs */ 3190 --pd-radius: 12px; 3191 --pd-soft: #f8fafc; 3192 --pd-soft-border: #e5e7eb; 3193 --pd-danger: #dc2626; 3194 --pd-success-bg: #dcfce7; 3195 --pd-success-text: #166534; 3196 --pd-danger-bg: #fee2e2; 3197 --pd-danger-text: #b91c1c; 3198 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3199 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3200 max-width:1280px; 3201 margin-inline:auto; 3202} 3203 3204/* === Sticky Product Header Bar (muncul saat scroll) === */ 3205.pd-sticky-header { 3206 position: fixed; 3207 top: 0; 3208 left: 0; 3209 right: 0; 3210 z-index: 998; 3211 background: rgba(255, 255, 255, 0.98); 3212 backdrop-filter: blur(12px); 3213 -webkit-backdrop-filter: blur(12px); 3214 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3215 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3216 transform: translateY(-100%); 3217 opacity: 0; 3218 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3219 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3220 pointer-events: none; 3221} 3222 3223.pd-sticky-header.visible { 3224 transform: translateY(0); 3225 opacity: 1;
3226 pointer-events: auto; 3227} 3228 3229.pd-sticky-header-container { 3230 max-width: 1280px; 3231 margin: 0 auto; 3232 padding: 12px 20px; 3233 display: flex; 3234 align-items: center; 3235 gap: 16px; 3236} 3237 3238.pd-sticky-thumb { 3239 width: 48px; 3240 height: 48px; 3241 flex-shrink: 0; 3242 border-radius: 8px; 3243 border: 1px solid #e2e8f0; 3244 overflow: hidden; 3245 background: #f8fafc; 3246} 3247 3248.pd-sticky-thumb img { 3249 width: 100%; 3250 height: 100%; 3251 object-fit: cover; 3252 display: block; 3253} 3254 3255.pd-sticky-info { 3256 flex: 1; 3257 min-width: 0; 3258 display: flex; 3259 flex-direction: column; 3260 gap: 4px; 3261} 3262 3263.pd-sticky-title { 3264 font-size: 0.9rem; 3265 font-weight: 600; 3266 color: #0f172a; 3267 line-height: 1.3; 3268 white-space: nowrap; 3269 overflow: hidden; 3270 text-overflow: ellipsis; 3271 margin: 0; 3272} 3273 3274.pd-sticky-price-row { 3275 display: flex; 3276 align-items: center; 3277 gap: 8px; 3278 flex-wrap: wrap; 3279} 3280 3281.pd-sticky-price { 3282 font-size: 1.1rem; 3283 font-weight: 700; 3284 color: #0d6efd; 3285 line-height: 1; 3286} 3287 3288.pd-sticky-price-original { 3289 font-size: 0.75rem; 3290 color: #94a3b8; 3291 text-decoration: line-through; 3292 line-height: 1; 3293} 3294 3295.pd-sticky-discount { 3296 background: #dc2626; 3297 color: #fff; 3298 font-size: 0.65rem; 3299 font-weight: 600; 3300 padding: 2px 6px; 3301 border-radius: 4px; 3302 letter-spacing: 0.3px; 3303 line-height: 1; 3304} 3305 3306.pd-sticky-actions { 3307 display: flex; 3308 gap: 8px; 3309 flex-shrink: 0; 3310 align-items: center; 3311} 3312 3313.pd-sticky-share-btn { 3314 padding: 8px 16px; 3315 border-radius: 8px; 3316 border: 1px solid #e2e8f0; 3317 background: #fff; 3318 color: #64748b; 3319 cursor: pointer; 3320 transition: all 0.2s ease; 3321 display: inline-flex; 3322 align-items: center; 3323 justify-content: center; 3324 flex-shrink: 0; 3325} 3326 3327.pd-sticky-share-btn:hover { 3328 background: #f8fafc; 3329 border-color: #cbd5e1; 3330 color: #0f172a; 3331 transform: translateY(-1px); 3332} 3333 3334.pd-sticky-share-btn:active { 3335 transform: translateY(0); 3336} 3337 3338.pd-sticky-share-btn svg { 3339 display: block; 3340 width: 16px; 3341 height: 16px; 3342} 3343 3344.pd-sticky-btn-chat svg { 3345 width: 16px !important; 3346 height: 16px !important; 3347 max-width: none !important; 3348 margin-right: 4px; 3349 flex-shrink: 0; 3350} 3351 3352.pd-sticky-btn { 3353 padding: 8px 16px; 3354 border-radius: 8px; 3355 font-size: 0.85rem; 3356 font-weight: 600; 3357 cursor: pointer; 3358 transition: all 0.2s ease; 3359 border: 0; 3360 display: inline-flex; 3361 align-items: center; 3362 justify-content: center; 3363 white-space: nowrap; 3364} 3365 3366.pd-sticky-btn-cart { 3367 background: #fff; 3368 color: #0d6efd; 3369 border: 1px solid #0d6efd; 3370} 3371 3372.pd-sticky-btn-cart:hover { 3373 background: rgba(13, 110, 253, 0.06); 3374} 3375 3376.pd-sticky-btn-chat { 3377 background: #fff; 3378 color: #64748b; 3379 border: 1px solid #cbd5e1; 3380 flex: 0.8; 3381} 3382 3383.pd-sticky-btn-chat:hover { 3384 background: #f8fafc; 3385 border-color: #94a3b8; 3386 color: #475569; 3387} 3388 3389.pd-sticky-btn-buy { 3390 background: #0d6efd; 3391 color: #fff; 3392 border: 1px solid #0d6efd; 3393 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3394} 3395 3396.pd-sticky-btn-buy:hover { 3397 filter: brightness(0.95); 3398} 3399 3400.pd-sticky-btn:disabled { 3401 opacity: 0.5; 3402 cursor: not-allowed; 3403} 3404 3405/* Hide on mobile (use existing FAB instead) */ 3406@media (max-width: 880px) { 3407 .pd-sticky-header { 3408 display: none; 3409 } 3410} 3411 3412/* === PRINT STYLES (for PDF Export) === */ 3413@media print { 3414 /* Reset & Base */ 3415 * { 3416 -webkit-print-color-adjust: exact !important; 3417 print-color-adjust: exact !important; 3418 } 3419 3420 body { 3421 background: white !important; 3422 margin: 0; 3423 padding: 0; 3424 } 3425 3426 /* Hide all navigation, buttons, and interactive elements */ 3427 nav, header, footer, 3428 nav *, header *, footer *, 3429 .navbar, .navbar *, 3430 .nav-item, .nav-link, 3431 .pd-sticky-header, 3432 .pd-sticky-header.visible, 3433 .pd-breadcrumb, 3434 .pd-sidebar, 3435 .pd-mobile-fab, 3436 .pd-share-btn, 3437 .pd-sticky-share-btn, 3438 #pd-share-btn, 3439 .share-button, 3440 [data-share-btn], 3441 button, 3442 .btn-add-cart, 3443 .btn-buy, 3444 .pd-actions, 3445 .pd-compare-section, 3446 .pd-compare-btn, 3447 .pd-compare-info, 3448 #pd-compare-btn, 3449 .compare-note, 3450 .compare-container, 3451 .compare-dropdown, 3452 .compare-panel, 3453 .compare-wrapper, 3454 [data-compare], 3455 [id*="compare"], 3456 [class*="compare"], 3457 .pd-thumb-gallery, 3458 .pd-installment-card, 3459 .pd-installment-card2, 3460 .inst-alt-banner--compact, 3461 .qoala-banner-card, 3462 .tradein-compact-card, 3463 .pwp-deals-section, 3464 #pwp-deals-section, 3465 .pwp-trigger-badge, 3466 .pd-related-full, 3467 #pd-similar, 3468 .pd-similar-strip, 3469 .pd-scroll-top, 3470 .stock-info-btn, 3471 .stock-popover, 3472 .pd-countdown-modern, 3473 .pd-share-modal-overlay, 3474 [id*="promo-"], 3475 .bottom-nav, 3476 .pd-buy-form, 3477 .pd-buy-card, 3478 .pd-extra-meta, 3479 .pd-reviews-section, 3480 #reviews, 3481 hr, 3482 .divider, 3483 .separator { 3484 display: none !important; 3485 visibility: hidden !important; 3486 opacity: 0 !important; 3487 height: 0 !important; 3488 max-height: 0 !important; 3489 overflow: hidden !important; 3490 margin: 0 !important; 3491 padding: 0 !important; 3492 border: none !important; 3493 } 3494 3495 /* Extra aggressive reset for any remaining borders */ 3496 .pd-info-card *, 3497 .pd-meta-inline *, 3498 .pd-variants * { 3499 border: none !important; 3500 border-top: none !important; 3501 border-bottom: none !important; 3502 } 3503 3504 /* Layout restructure */ 3505 .product-single { 3506 max-width: 100% !important; 3507 margin: 0 !important; 3508 padding: 0 !important; 3509 box-shadow: none !important; 3510 } 3511 3512 /* Reset all shadows and borders for print */ 3513 * { 3514 box-shadow: none !important; 3515 } 3516 3517 .pd-top-layout { 3518 display: grid !important; 3519 grid-template-columns: 280px 1fr !important; 3520 gap: 24px !important; 3521 margin: 0 0 24px 0 !important; 3522 padding: 0 !important; 3523 page-break-inside: avoid;
3524 align-items: start !important; 3525 } 3526 3527 /* Print header with logo - centered and larger */ 3528 .product-single::before { 3529 content: "" !important; 3530 display: block !important; 3531 width: 150px !important; 3532 height: 50px !important; 3533 background: url('/assets/https://ik.imagekit.io/boytoto/logo.webp') no-repeat center !important; 3534 background-size: contain !important; 3535 margin: 0 auto 20px !important; 3536 visibility: visible !important; 3537 opacity: 1 !important; 3538 } 3539 3540 /* Media card - left column */ 3541 .pd-media-card { 3542 border: none !important; 3543 padding: 0 !important; 3544 margin: 0 !important; 3545 text-align: center; 3546 page-break-inside: avoid; 3547 } 3548 3549 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3550 .pd-main-image { 3551 display: none !important; 3552 } 3553 3554 /* Mobile slider - show first image only in print */ 3555 .pd-mobile-slider { 3556 display: block !important; 3557 overflow: visible !important; 3558 } 3559 3560 .pd-mslide { 3561 display: none !important; 3562 } 3563 3564 .pd-mslide:first-child { 3565 display: block !important; 3566 border: 1px solid #e2e8f0 !important; 3567 max-width: 280px !important; 3568 margin: 0 auto !important; 3569 border-radius: 8px !important; 3570 } 3571 3572 .pd-mslide:first-child img { 3573 max-height: 280px !important; 3574 width: 100% !important; 3575 height: auto !important; 3576 object-fit: contain !important; 3577 } 3578 3579 /* Info card - right column */ 3580 .pd-info-card { 3581 border: none !important; 3582 border-top: none !important; 3583 border-bottom: none !important; 3584 padding: 0 !important; 3585 margin: 0 !important; 3586 background: white !important; 3587 box-shadow: none !important; 3588 } 3589 3590 /* Title */ 3591 .pd-title-row { 3592 display: block !important; 3593 margin-bottom: 10px !important; 3594 border: none !important; 3595 border-top: none !important; 3596 border-bottom: none !important; 3597 padding: 0 !important; 3598 box-shadow: none !important; 3599 text-decoration: none !important; 3600 } 3601 3602 /* Hide share button in print */ 3603 .pd-title-row .pd-share-btn, 3604 .pd-title-row #pd-share-btn { 3605 display: none !important; 3606 } 3607 3608 .pd-title { 3609 font-size: 13pt !important; 3610 color: #000 !important; 3611 margin: 0 0 10px 0 !important; 3612 page-break-after: avoid; 3613 font-weight: 700 !important; 3614 line-height: 1.3 !important; 3615 border: none !important; 3616 border-top: none !important; 3617 border-bottom: none !important; 3618 padding-bottom: 0 !important; 3619 box-shadow: none !important; 3620 text-decoration: none !important; 3621 } 3622 3623 /* Price and QR container - side by side */ 3624 .pd-price-qr-container { 3625 display: flex !important; 3626 gap: 16px !important; 3627 align-items: flex-start !important; 3628 margin-bottom: 14px !important; 3629 box-shadow: none !important; 3630 border: none !important; 3631 text-decoration: none !important; 3632 } 3633 3634 /* Price block - left side */ 3635 .pd-price-block { 3636 flex: 1 !important; 3637 margin: 0 !important; 3638 padding-bottom: 0 !important; 3639 border-bottom: none !important; 3640 box-shadow: none !important; 3641 } 3642 3643 .pd-price-row { 3644 margin-bottom: 8px !important; 3645 border: none !important; 3646 border-top: none !important; 3647 border-bottom: none !important; 3648 padding: 0 !important; 3649 } 3650 3651 /* Hide all empty or unnecessary elements in price row */ 3652 .pd-price-row > span:empty, 3653 .pd-price-row > .sold-inline, 3654 #pd-sold-inline { 3655 display: none !important; 3656 } 3657 3658 .pd-price-current { 3659 font-size: 18pt !important; 3660 color: #0d6efd !important; 3661 } 3662 3663 /* Show price range for variant products in print */ 3664 .pd-price-current[data-price-range]::after { 3665 content: attr(data-price-range) !important; 3666 display: block !important; 3667 font-size: 11pt !important; 3668 color: #64748b !important; 3669 margin-top: 4px !important; 3670 font-weight: normal !important; 3671 } 3672 3673 /* Hide 'Range:' prefix text */ 3674 .pd-price-current[data-price-range]::after { 3675 content: attr(data-price-range) !important; 3676 } 3677 3678 .pd-price-original { 3679 font-size: 12pt !important; 3680 color: #64748b !important; 3681 } 3682 3683 .pd-discount-badge { 3684 font-size: 10pt !important; 3685 padding: 2px 6px !important; 3686 } 3687 3688 /* QR Code section - right side beside price */ 3689 .pd-qr-print { 3690 display: block !important; 3691 flex: 0 0 100px !important; 3692 margin: 0 !important; 3693 padding: 8px !important; 3694 background: #f8fafc !important; 3695 border-radius: 8px !important; 3696 text-align: center !important; 3697 page-break-inside: avoid !important; 3698 border: 1px solid #cbd5e1 !important; 3699 } 3700 3701 .pd-qr-label { 3702 font-size: 6.5pt !important; 3703 color: #64748b !important; 3704 margin-bottom: 5px !important; 3705 line-height: 1.2 !important; 3706 font-weight: 600 !important; 3707 } 3708 3709 .pd-qr-image { 3710 width: 80px !important; 3711 height: 80px !important; 3712 display: block !important; 3713 margin: 0 auto !important; 3714 } 3715 3716 /* Meta info */ 3717 .pd-meta-inline { 3718 display: flex !important; 3719 gap: 12px !important; 3720 margin-bottom: 0 !important; 3721 padding-bottom: 14px !important; 3722 font-size: 10pt !important; 3723 border: none !important; 3724 border-top: none !important; 3725 border-bottom: none !important; 3726 padding-top: 0 !important; 3727 box-shadow: none !important; 3728 text-decoration: none !important; 3729 background: transparent !important; 3730 } 3731 3732 /* Hide stock badge in print */ 3733 .stock-badge, 3734 .stock-info-btn { 3735 display: none !important; 3736 } 3737 3738 .sku { 3739 color: #64748b !important; 3740 } 3741 3742 /* Hide sold badge if empty/zero */ 3743 .sold-inline { 3744 display: none !important; 3745 } 3746 3747 /* Hide variant summary section in print */ 3748 .pd-variant-combined, 3749 #pd-variant-combined, 3750 .pd-variant-summary, 3751 [data-role="sold-chip"] { 3752 display: none !important; 3753 } 3754 3755 /* Hide compare section completely in print */ 3756 .pd-compare-section, 3757 .pd-compare-btn, 3758 #pd-compare-btn, 3759 .pd-compare-info, 3760 .compare-note, 3761 .compare-text, 3762 .compare-remove { 3763 display: none !important; 3764 visibility: hidden !important; 3765 height: 0 !important; 3766 margin: 0 !important; 3767 padding: 0 !important; 3768 overflow: hidden !important; 3769 } 3770 3771 /* Variants - show all options with selected highlighted */ 3772 .pd-variants { 3773 margin: 0 !important; 3774 padding: 0 !important; 3775 page-break-inside: avoid !important; 3776 border: none !important; 3777 border-top: none !important; 3778 border-bottom: none !important; 3779 box-shadow: none !important; 3780 background: transparent !important; 3781 } 3782 3783 /* Add 'Variant:' label before variant groups */ 3784 .pd-variants::before { 3785 content: "Variant:" !important; 3786 display: block !important; 3787 font-size: 11pt !important; 3788 font-weight: 700 !important; 3789 color: #0f172a !important; 3790 margin-bottom: 10px !important; 3791 border: none !important; 3792 padding: 0 !important; 3793 border-top: none !important; 3794 border-bottom: none !important; 3795 } 3796 3797 .v-group { 3798 display: flex !important;
3799 align-items: center !important; 3800 gap: 8px !important; 3801 margin-bottom: 8px !important; 3802 line-height: 1 !important; 3803 page-break-inside: avoid !important; 3804 border: none !important; 3805 padding: 0 !important; 3806 } 3807 3808 .v-group:last-child { 3809 margin-bottom: 0 !important; 3810 } 3811 3812 .v-label { 3813 font-weight: 700 !important; 3814 margin: 0 !important; 3815 font-size: 9pt !important; 3816 color: #475569 !important; 3817 min-width: 60px !important; 3818 flex-shrink: 0 !important; 3819 border: none !important; 3820 padding: 0 !important; 3821 } 3822 3823 .v-options { 3824 display: flex !important; 3825 gap: 6px !important; 3826 flex-wrap: wrap !important; 3827 border: none !important; 3828 padding: 0 !important; 3829 margin: 0 !important; 3830 } 3831 3832 .v-chip { 3833 border: 1px solid #cbd5e1 !important; 3834 padding: 3px 10px !important; 3835 font-size: 9pt !important; 3836 background: #f8fafc !important; 3837 border-radius: 6px !important; 3838 display: inline-flex !important; 3839 align-items: center !important; 3840 page-break-inside: avoid !important; 3841 } 3842 3843 /* Highlight selected variant with blue background */ 3844 .v-chip.selected { 3845 background: #eff6ff !important; 3846 border-color: #2563eb !important; 3847 font-weight: 600 !important; 3848 } 3849 3850 /* Hide radio inputs */ 3851 .v-chip input[type="radio"] { 3852 display: none !important; 3853 } 3854 3855 /* Hide "Habis" badge in print */ 3856 .v-chip .chip-badge { 3857 display: none !important; 3858 } 3859 3860 /* Short description - HIDDEN in print */ 3861 .pd-short-desc { 3862 display: none !important; 3863 } 3864 3865 /* QR Code section - right side beside price */ 3866 .pd-qr-print { 3867 display: block !important; 3868 flex: 0 0 100px !important; 3869 margin: 0 !important; 3870 padding: 8px !important; 3871 background: #f8fafc !important; 3872 border-radius: 8px !important; 3873 text-align: center !important; 3874 page-break-inside: avoid !important; 3875 border: 1px solid #cbd5e1 !important; 3876 } 3877 3878 .pd-qr-label { 3879 font-size: 6.5pt !important; 3880 color: #64748b !important; 3881 margin-bottom: 5px !important; 3882 line-height: 1.2 !important; 3883 font-weight: 600 !important; 3884 } 3885 3886 .pd-qr-image { 3887 width: 80px !important; 3888 height: 80px !important; 3889 display: block !important; 3890 margin: 0 auto !important; 3891 } 3892 3893 /* Full description - below 2 column layout */ 3894 .pd-full-description { 3895 border: none !important; 3896 padding: 0 !important; 3897 margin: 24px 0 0 0 !important; 3898 page-break-before: avoid; 3899 grid-column: 1 / -1 !important; 3900 } 3901 3902 .pd-full-desc-title { 3903 font-size: 13pt !important; 3904 font-weight: 700 !important; 3905 margin: 0 0 12px 0 !important; 3906 color: #000 !important; 3907 border-top: none !important; 3908 border-bottom: none !important; 3909 padding-top: 16px !important; 3910 } 3911 3912 .pd-full-desc-body { 3913 font-size: 10pt !important; 3914 line-height: 1.6 !important; 3915 color: #334155 !important; 3916 } 3917 3918 .pd-full-desc-body img { 3919 max-height: 200px !important; 3920 width: auto !important; 3921 page-break-inside: avoid; 3922 display: none !important; /* Hide images in print to save space */ 3923 } 3924 3925 /* Also target #deskripsi img if exists */ 3926 #deskripsi img { 3927 max-width: 350px !important; 3928 height: auto !important; 3929 } 3930 3931 .pd-full-desc-body table { 3932 border-collapse: collapse !important; 3933 width: 100% !important; 3934 margin: 12px 0 !important; 3935 font-size: 9pt !important; 3936 } 3937 3938 .pd-full-desc-body th, 3939 .pd-full-desc-body td { 3940 border: 1px solid #cbd5e1 !important; 3941 padding: 6px 8px !important; 3942 } 3943 3944 /* Print footer */ 3945 .product-single::after { 3946 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 3947 display: block; 3948 margin-top: 30px; 3949 padding-top: 16px; 3950 border-top: 1px solid #e2e8f0; 3951 font-size: 9pt; 3952 color: #64748b; 3953 text-align: center; 3954 } 3955 3956 /* Page breaks */ 3957 .pd-media-card, 3958 .pd-price-block, 3959 .pd-full-desc-title { 3960 page-break-after: avoid; 3961 } 3962 3963 .pd-full-description { 3964 page-break-before: auto; 3965 } 3966} 3967 3968/* === Breadcrumb (truncate last item) === */ 3969.pd-breadcrumb{ 3970 display:flex; 3971 flex-wrap:nowrap; 3972 align-items:center; 3973 gap:.5rem; 3974 font-size:.72rem; 3975 margin:.7rem 0 1.2rem .5rem; 3976 color:var(--pd-muted); 3977 overflow:hidden; 3978} 3979.pd-breadcrumb a{color:var(--pd-accent);text-decoration:none} 3980.pd-breadcrumb a:hover{text-decoration:underline} 3981.pd-breadcrumb a, 3982.pd-breadcrumb span{flex:0 0 auto} 3983.pd-breadcrumb .current{ 3984 flex:1 1 auto; 3985 min-width:0; 3986 white-space:nowrap; 3987 overflow:hidden; 3988 text-overflow:ellipsis; 3989 font-weight:600;
3990 color:var(--pd-dark); 3991 padding-right:2px; 3992} 3993 3994/* === Top layout === */ 3995.pd-top-layout{ 3996 display:grid; 3997 gap:var(--pd-gap); 3998 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 3999 align-items:start; 4000 margin-bottom:2.4rem; 4001} 4002 4003/* === Media column === */ 4004.pd-media-card{ 4005 background:#fff; 4006 border:1px solid var(--pd-border); 4007 border-radius:var(--pd-radius); 4008 padding:1rem 1rem 1.2rem; 4009} 4010.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4011.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4012 4013/* Gallery Thumbnails */ 4014.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4015.pd-thumb-btn{ 4016 border:1px solid var(--pd-soft-border); 4017 background:#fff; 4018 cursor:pointer; 4019 width:64px;height:64px;padding:2px; 4020 display:flex;align-items:center;justify-content:center; 4021 overflow:hidden;border-radius:8px; 4022 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4023 position:relative; 4024} 4025.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4026.pd-thumb-btn:hover{border-color:var(--pd-accent)} 4027.pd-thumb-btn.active{border-color:var(--pd-accent);box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4028.pd-thumb-btn:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4029.pd-thumb-btn:active{transform:scale(.96)} 4030 4031/* === Info column === */ 4032.pd-info-card{ 4033 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4034 padding:1.4rem 1.6rem 2rem; 4035} 4036.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4037.pd-price-block{margin:0 0 1rem} 4038.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4039.pd-price-current{font-size:2rem;font-weight:700;color:var(--pd-accent)} 4040.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4041.pd-discount-badge{background:var(--pd-danger);color:#fff;font-size:.68rem;padding:4px 8px 3px;font-weight:600;border-radius:6px;letter-spacing:.5px} 4042.pd-meta-inline{display:flex;flex-wrap:wrap;gap:.9rem;align-items:center;font-size:.63rem;letter-spacing:.6px;text-transform:uppercase;font-weight:600} 4043.stock-badge{padding:4px 10px;border-radius:24px;background:var(--pd-soft);color:var(--pd-muted);display:inline-block;line-height:1;vertical-align:middle} 4044.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4045.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4046.digital-badge{padding:4px 10px;border-radius:24px;background:linear-gradient(135deg,#FF5722 0%,#764ba2 100%);color:#fff;font-size:.63rem;font-weight:600;letter-spacing:.6px;text-transform:uppercase;display:inline-block;line-height:1;vertical-align:middle;border:none} 4047.sku{color:var(--pd-muted)} 4048.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4049.pd-short-desc p{margin:0 0 .75rem} 4050.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4051 4052/* Meta row for sold and rating badges */ 4053.pd-meta-row { 4054 display: flex; 4055 align-items: center; 4056 flex-wrap: wrap; 4057 gap: 5px; 4058 margin-top: 8px; 4059 margin-bottom: 4px; 4060} 4061 4062/* Sales (Terjual) badge inline */ 4063.sold-inline{ 4064 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4065 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4066} 4067 4068/* Price row divider */ 4069.price-divider { 4070 display: inline-block; 4071 margin: 0 .4rem; 4072 color: #cbd5e1; 4073 font-weight: 400; 4074} 4075 4076/* Rating badge inline */ 4077.rating-inline { 4078 display: inline-flex; 4079 align-items: center; 4080 gap: 4px; 4081 padding: 3px 8px 4px; 4082 background: #fff7ed; 4083 border-radius: 6px; 4084 font-size: .65rem; 4085 font-weight: 600; 4086 color: #ea580c; 4087 text-decoration: none; 4088 transition: all .2s ease; 4089 cursor: pointer; 4090 margin-left: .5rem; 4091 white-space: nowrap; 4092} 4093 4094.rating-inline:hover { 4095 background: #ffedd5; 4096 transform: translateY(-1px); 4097 box-shadow: 0 2px 4px rgba(234, 88, 12, 0.1); 4098} 4099 4100.rating-inline .star-icon { 4101 font-size: .75rem; 4102 color: #f59e0b; 4103 line-height: 1; 4104} 4105 4106.rating-inline .rating-value { 4107 color: #ea580c; 4108} 4109 4110.rating-inline .rating-badge-count { 4111 color: #9a3412; 4112 opacity: 0.85; 4113} 4114 4115@media (max-width: 640px) { 4116 .rating-inline { 4117 font-size: .65rem; 4118 padding: 3px 8px 4px; 4119 } 4120 .rating-inline .star-icon { 4121 font-size: .7rem; 4122 } 4123} 4124 4125/* Highlight animation for review section */ 4126@keyframes highlightFlash { 4127 0%, 100% { background-color: transparent; } 4128 50% { background-color: rgba(251, 146, 60, 0.1); } 4129} 4130 4131.highlight-flash { 4132 animation: highlightFlash 1.5s ease; 4133} 4134 4135/* === Variant chips === */ 4136.pd-variants{margin-top:14px} 4137.v-group{margin-bottom:12px} 4138.v-label{font-weight:600;margin-bottom:6px} 4139.v-options{display:flex;flex-wrap:wrap;gap:8px} 4140.v-chip{ 4141 display:inline-flex;align-items:center;gap:8px; 4142 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4143 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4144} 4145.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4146.v-chip.selected{border-color:#2563eb;background:#eff6ff} 4147.v-chip.disabled{opacity:.45;cursor:not-allowed} 4148.v-chip.out{border-style:dashed} 4149.v-chip input{display:none} 4150.v-chip .dot{width:14px;
4150height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4151.v-chip .dot--neutral{background:#9aa3af} 4152.v-chip .txt{font-size:.95rem;white-space:nowrap} 4153.v-chip .chip-badge{margin-left:2px;font-size:.72rem;line-height:1;padding:3px 6px;border-radius:6px;background:#fee2e2;color:#991b1b;border:1px solid #fecaca} 4154.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4155.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4156 4157/* === Buy form (di sidebar kanan untuk desktop) === */ 4158.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4159.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4160.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4161.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4162 4163/* Tombol senada tema */ 4164.btn-add-cart,.btn-buy,.btn-chat-icon{ 4165 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4166 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4167 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4168} 4169.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4170.btn-add-cart:hover{filter:brightness(.95)} 4171.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4172.btn-buy:hover{background:rgba(13,110,253,.06)} 4173.btn-chat-icon{background:#fff;color:#fb923c;border:2px solid #fb923c;padding:0;width:44px;min-width:44px} 4174.btn-chat-icon:hover{background:rgba(251, 146, 60, 0.06);border-color:#f97316} 4175.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4176.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4177 4178/* Buy with Voucher Button - Cleaner, more compact design */ 4179.btn-buy-voucher{ 4180 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4181 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4182 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4183 background:linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); 4184 color:#fff; 4185 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4186 position:relative; 4187 overflow:hidden; 4188 flex:1 1 auto 4189} 4190.btn-buy-voucher:hover{ 4191 transform:translateY(-2px); 4192 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4193} 4194.btn-buy-voucher:active{ 4195 transform:translateY(0); 4196 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4197} 4198.btn-buy-voucher .bv-icon{ 4199 font-size:20px; 4200 line-height:1; 4201 flex-shrink:0 4202} 4203.btn-buy-voucher .bv-text{ 4204 display:flex; 4205 flex-direction:column; 4206 align-items:flex-start; 4207 gap:2px; 4208 flex:1; 4209 min-width:0; 4210 font-size:14px; 4211 line-height:1.2 4212} 4213.btn-buy-voucher .bv-text small{ 4214 font-size:10px; 4215 opacity:0.85; 4216 font-weight:500; 4217 letter-spacing:0.3px; 4218 font-family:monospace 4219} 4220.btn-buy-voucher .bv-save{ 4221 font-size:12px; 4222 font-weight:700; 4223 background:rgba(255,255,255,0.25); 4224 padding:4px 8px; 4225 border-radius:6px; 4226 white-space:nowrap; 4227 flex-shrink:0 4228} 4229 4230/* Extra meta (opsional) */ 4231.pd-extra-meta{background:var(--pd-soft);border:1px solid var(--pd-border);border-radius:10px;padding:.85rem 1rem;display:grid;gap:.45rem;font-size:.7rem;line-height:1.3} 4232.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4233.pd-extra-meta a{color:var(--pd-accent);text-decoration:none} 4234.pd-extra-meta a:hover{text-decoration:underline} 4235 4236/* === Sidebar === */ 4237.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4238.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4239.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4240.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4241.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4242.pd-side-card li:before{content:'';width:6px;height:6px;background:var(--pd-accent);border-radius:50%;position:absolute;
4242left:0;top:.45rem} 4243.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4244.pay-logos span{font-size:.55rem;background:var(--pd-soft);border:1px solid var(--pd-soft-border);padding:.35rem .55rem;border-radius:6px;font-weight:600;letter-spacing:.5px} 4245 4246/* Buy card hanya desktop (mobile pakai FAB) */ 4247.pd-buy-card{display:block} 4248@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4249 4250/* === Full Description === */ 4251.pd-full-description{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1.6rem 1.9rem .6rem;margin:0 0 .8rem} 4252.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4253.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4254.pd-full-desc-body p{margin:0 0 1rem} 4255.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4256.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4257.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4258.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4259.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4260 4261/* === Related (container) === */ 4262.pd-related-full{ 4263 background:#fff;border:1px solid var(--pd-border); 4264 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4265 margin:0 0 2rem; 4266} 4267.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4268 4269/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4270.pd-related-grid, 4271.pd-related-grid--full{ 4272 display:grid !important; 4273 gap:16px !important; 4274 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4275 align-items:stretch; 4276} 4277 4278/* Fallback jika grid tidak didukung */ 4279@supports not (display:grid){ 4280 .pd-related-grid, 4281 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4282 .pd-related-grid .pd-related-card, 4283 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4284 @media (max-width:1100px){ 4285 .pd-related-grid .pd-related-card, 4286 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4287 } 4288 @media (max-width:820px){ 4289 .pd-related-grid .pd-related-card, 4290 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4291 } 4292 @media (max-width:580px){ 4293 .pd-related-grid .pd-related-card, 4294 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4295 } 4296} 4297 4298/* Related Card */ 4299.pd-related-card{ 4300 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4301 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4302 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4303} 4304.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4305.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4306.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4307.mini-disc{position:absolute;top:6px;left:6px;background:#dc2626;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4308.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4309.pd-related-card h3{font-size:.68rem;line-height:1.25;margin:0;height:2.5em;overflow:hidden;font-weight:600;color:#334155;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} 4310.pd-related-card .price{font-size:.7rem;font-weight:600;color:var(--pd-accent)} 4311.pd-related-card .sold{display:block;font-size:.6rem;font-weight:600;color:#475569;margin-top:2px;letter-spacing:.3px;background:#f1f5f9;padding:2px 6px 3px;border-radius:5px; width: max-content;} 4312.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4313.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4314 4315/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4316.pd-mobile-slider{display:none} 4317.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4318 4319/* Gallery counter badge */ 4320.pd-gallery-counter{ 4321 position:absolute; 4322 bottom:12px; 4323 left:12px; 4324 background:rgba(255,255,255,0.25); 4325 color:#9E9E9E; 4326 padding:4px 10px; 4327 border-radius:10px; 4328 font-size:12px; 4329 font-weight:600; 4330 z-index:100; 4331 backdrop-filter:blur(6px); 4332 -webkit-backdrop-filter:blur(6px); 4333 font-variant-numeric:tabular-nums; 4334 pointer-events:none; 4335 display:none; 4336 box-shadow:0 1px 4px rgba(0,0,0,.15); 4337 line-height:1; 4338 border:1px solid rgba(0,0,0,0.08); 4339} 4340 4341@media (max-width:640px){ 4342 .pd-media-card{border: 0;padding: 0 1rem 0;} 4343 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4344 .pd-side-card {flex: 1 1 !important;} 4345 .pd-top-layout {gap: 0; margin-bottom: 0;} 4346 .pd-variants {margin-bottom: 1.5rem;} 4347 .pd-main-image{display:none} 4348 .pd-gallery-counter{display:block} 4349 .pd-mobile-slider-wrapper{ 4350 position:relative; 4351 margin-bottom:.6rem; 4352 } 4353 .pd-mobile-slider{ 4354 display:flex;gap:8px;overflow-x:auto; 4355 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
4356 } 4357 .pd-mslide{ 4358 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4359 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4360 } 4361 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4362} 4363 4364/* === Mobile Floating Action Bar === */ 4365.pd-mobile-fab{ 4366 position:fixed; 4367 left:0; 4368 right:0; 4369 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4370 display:none; 4371 align-items:center; 4372 gap:.75rem; 4373 padding:12px 16px; 4374 background:rgba(255,255,255,0.95); 4375 backdrop-filter:blur(20px); 4376 -webkit-backdrop-filter:blur(20px); 4377 border-top:1px solid rgba(228,231,235,0.6); 4378 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4379 z-index:999; /* Below bottom-nav (1000) */ 4380 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4381} 4382 4383/* When bottom-nav is hidden, move FAB to bottom */ 4384body:has(.bottom-nav.hidden) .pd-mobile-fab { 4385 bottom: 0; 4386} 4387.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4388.mf-btn{ 4389 border-radius:10px; 4390 padding:.6rem 1rem; 4391 font-weight:600; 4392 font-size:14px; 4393 cursor:pointer; 4394 transition:all .2s; 4395 -webkit-tap-highlight-color:transparent; 4396 display:inline-flex; 4397 align-items:center; 4398 justify-content:center; 4399 gap:4px; 4400} 4401.mf-buy{ 4402 background:#fb923c; 4403 color:#fff; 4404 border:0; 4405 box-shadow:0 2px 8px rgba(251, 146, 60, 0.3); 4406 flex:1; 4407 flex-direction:column; 4408 padding:.55rem 1rem; 4409 line-height:1; 4410 min-height:44px; 4411 font-family:inherit; 4412} 4413.mf-buy .mf-buy-text{ 4414 font-size:11px; 4415 font-weight:600; 4416 opacity:0.95; 4417 font-family:inherit; 4418} 4419.mf-buy .mf-buy-price{ 4420 font-size:13px; 4421 font-weight:800; 4422 letter-spacing:-0.3px; 4423 font-family:inherit; 4424 line-height:1; 4425} 4426.mf-buy:active{transform:scale(0.97)} 4427.mf-cart{ 4428 background:#fff; 4429 color:#fb923c; 4430 border:2px solid #fb923c; 4431 width:44px; 4432 height:44px; 4433 min-height:44px; 4434 padding:0; 4435 flex-shrink:0; 4436 font-family:inherit; 4437} 4438.mf-cart:active{transform:scale(0.95)} 4439.mf-chat{background:#fff;color:#fb923c;border:2px solid #fb923c;width:44px;height:44px;min-height:44px;padding:0;flex-shrink:0;font-family:inherit} 4440.mf-chat:active{transform:scale(0.95)} 4441.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4442.mf-share{ 4443 background:#fff; 4444 color:#fb923c; 4445 border:2px solid #fb923c; 4446 width:44px; 4447 height:44px; 4448 padding:0; 4449 flex-shrink:0; 4450} 4451.mf-share:active{transform:scale(0.95)} 4452.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4453@media (max-width:640px){ 4454 .pd-mobile-fab{display:flex} 4455 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4456} 4457 4458/* === Installment cards === */ 4459.pd-installment-card{border:1px solid var(--pd-border);background:#fff;border-radius:var(--pd-radius);padding:1rem 1.1rem 1.25rem;display:flex;flex-direction:column;gap:.85rem} 4460.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4461 4462.inst-tabs{display:flex;gap:.4rem} 4463.inst-tab{flex:1 1 0;background:var(--pd-soft);border:1px solid var(--pd-soft-border);padding:.45rem .5rem;font-size:.63rem;font-weight:600;border-radius:6px;cursor:pointer;letter-spacing:.4px;transition:var(--pd-transition)} 4464.inst-tab.active,.inst-tab:hover{background:var(--pd-accent);color:#fff;border-color:var(--pd-accent)} 4465.inst-tab:focus{outline:2px solid var(--pd-accent);outline-offset:2px} 4466 4467.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4468.tenor-btn{flex:1 0 auto;background:#fff;border:1px solid var(--pd-soft-border);padding:.42rem .55rem;font-size:.6rem;font-weight:600;border-radius:20px;cursor:pointer;transition:.18s;min-width:56px} 4469.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4470 4471.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4472.inst-line{display:flex;justify-content:space-between;align-items:center;padding:.35rem .55rem;background:var(--pd-soft);border:1px solid var(--pd-soft-border);border-radius:6px} 4473.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4474.inst-line .lbl{opacity:.8} 4475.inst-line .val{font-weight:600;letter-spacing:.3px} 4476.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4477@media (max-width: 860px){ 4478 .pd-installment-card{order:3} 4479 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4480 4481 /* Voucher button responsive - make it simpler */ 4482 .btn-buy-voucher{ 4483 padding:0 12px; 4484 gap:8px 4485 } 4486 .btn-buy-voucher .bv-icon{ 4487 font-size:18px 4488 } 4489 .btn-buy-voucher .bv-text{ 4490 font-size:13px 4491 } 4492 .btn-buy-voucher .bv-text small{ 4493 font-size:9px 4494 } 4495 .btn-buy-voucher .bv-save{ 4496 font-size:11px; 4497 padding:3px 6px 4498 } 4499} 4500 4501/* Installment List v2 (eye-catching) */ 4502.pd-installment-card2{ 4503 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4504 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4505 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4506 position:relative;overflow:hidden 4507} 4508.pd-installment-card2:before{ 4509 content:'';position:absolute;width:140px;height:140px; 4510 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4511 top:-40px;right:-40px;pointer-events:none 4512} 4513.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4514.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4515.inst-head-badge{background:#1d7df0;color:#fff;font-size:.55rem;font-weight:600;padding:.25rem .5rem;border-radius:20px;letter-spacing:.4px;box-shadow:0 2px 6px -2px rgba(29,125,240,.6)} 4516 4517.inst-bank-tabs{display:flex;gap:.45rem} 4518.bank-tab{flex:1 1 0;background:#fff;border:1px solid var(--pd-soft-border);padding:.5rem .55rem;font-size:.63rem;font-weight:600;border-radius:7px;cursor:pointer;transition:.18s;letter-spacing:.3px} 4519.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4520.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4521 4522.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4523.inst-month-item{display:flex;align-items:center;gap:.65rem;background:#fff;border:1px solid var(--pd-soft-border);border-radius:14px;padding:.55rem .7rem;position:relative;overflow:hidden;transition:.25s} 4524.inst-month-item:before{content:'';position:absolute;inset:0;opacity:0;background:linear-gradient(90deg,rgba(29,125,240,.08),rgba(29,125,240,0));transition:.25s} 4525.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4526.inst-month-item:hover:before{opacity:1} 4527.tenor-badge{background:#1d7df0;color:#fff;font-weight:700;min-width:42px;text-align:center;padding:.38rem .4rem;font-size:.8rem;border-radius:10px;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4528.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4529.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4530.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4531 4532.inst-note{font-size:.6rem;font-weight:600;color:#0f2f54;background:#e0f7ed;border:1px solid #b2ebd2;padding:.4rem .6rem;border-radius:8px;letter-spacing:.3px} 4533.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4534 4535@media (max-width:860px){ 4536 .pd-installment-card2{order:3} 4537 .tenor-badge{min-width:38px;font-size:.75rem} 4538 .amount{font-size:.78rem} 4539} 4540 4541/* Alt Fintech Banner (compact) */ 4542.inst-alt-banner--compact{ 4543 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4544 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4545 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4546} 4547.inst-alt-banner--compact:before, 4548.inst-alt-banner--compact:after{ 4549 content:'';position:absolute;border-radius:50%;background:rgba(255,255,255,.15);filter:blur(2px);pointer-events:none; 4550} 4551.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4552.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4553.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4554.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4555.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4556 4557.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4558.iab-logos-compact .logo-fin{width:30%} 4559.iab-logos-compact .logo{ 4560 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4561 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4562 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(255,255,255,.15); 4563 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4564} 4565.iab-logos-compact .logo:before{ 4566 content:'';position:absolute;inset:0;border-radius:inherit; 4567 background:linear-gradient(140deg,rgba(255,255,255,.35),rgba(255,255,255,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4568} 4569.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4570 4571.iab-cta--compact{ 4572 display:inline-flex;padding:.45rem .85rem;font-size:.58rem;font-weight:700;background:#fff;color:#0f3c6d;border-radius:30px;text-decoration:none;letter-spacing:.55px;box-shadow:0 4px 12px -5px rgba(0,0,0,.35);transition:.25s; 4573} 4574.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4575.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4576@media (max-width:860px){ 4577 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4578 .iab-logos-compact .logo{width:46px;height:46px} 4579} 4580 4581/* === Scroll To Top (baru) === */ 4582.pd-scroll-top{ 4583 position:fixed; 4584 right:20px; 4585 bottom:90px; 4586 width:48px; 4587 height:48px; 4588 border-radius:50%; 4589 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4590 color:#0f2f54; 4591 border:1px solid rgba(226, 232, 240, 0.8); 4592 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4593 display:flex; 4594 align-items:center; 4595 justify-content:center; 4596 cursor:pointer; 4597 z-index:1105; 4598 opacity:0; 4599 transform:translateY(12px) scale(.92); 4600 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4601 backdrop-filter:blur(8px); 4602 -webkit-backdrop-filter:blur(8px); 4603} 4604.pd-scroll-top:hover{ 4605 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4606 transform:translateY(0) scale(1); 4607 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4608 border-color:rgba(226, 232, 240, 1); 4609} 4610.pd-scroll-top:active{ 4611 transform:translateY(0) scale(.96); 4612} 4613.pd-scroll-top:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4614.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4615@media (max-width:640px){ 4616 /* Dynamic positioning via JavaScript */ 4617 .pd-scroll-top{ 4618 right:14px; 4619 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4620 } 4621} 4622 4623/* === Responsive layout tweaks === */ 4624@media (max-width:1250px){ 4625 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4626 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4627 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4628 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4629 .pd-buy-card{flex:0 0 100%} 4630} 4631@media (max-width:880px){ 4632 .pd-top-layout{grid-template-columns:1fr} 4633 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4634 .pd-title{font-size:1.2rem} 4635 .pd-price-current{font-size:1.5rem} 4636} 4637@media (max-width:640px){ 4638 /* Related: 2 kolom di mobile */ 4639 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4640 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4641} 4642@media (max-width:560px){ 4643 .pd-actions{flex-direction:column} 4644 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4645 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4646} 4647@media (max-width:380px){ 4648 .pd-related-full{padding:.9rem .7rem 1.1rem} 4649 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4650 .pd-related-card .info{padding:.4rem .48rem .55rem} 4651} 4652 4653/* === Modern Countdown (Product Detail) === */ 4654.pd-countdown-modern{ 4655 --cd-bg1:#0ea5e9; /* sky-500 */ 4656 --cd-bg2:#6366f1; /* indigo-500 */ 4657 --cd-text:#0b1220; 4658 --cd-contrast:#ffffff; 4659 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4660 --cd-border: rgba(255,255,255,.25); 4661 --cd-box: rgba(255,255,255,.65); 4662 --cd-box-text:#0f172a; 4663 --cd-danger:#ef4444; 4664 4665 display:flex; gap:.75rem; align-items:center; 4666 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4667 border:1px solid var(--cd-border); 4668 color:var(--cd-contrast); 4669 border-radius:14px; 4670 padding:.6rem .8rem; 4671 box-shadow:var(--cd-shadow); 4672 position:relative; 4673 overflow:hidden; 4674 margin-bottom: 1rem; 4675} 4676 4677.pd-countdown-modern .pd-cd-icon{ 4678 width:28px;height:28px;flex:0 0 28px; 4679 display:grid;place-items:center; 4680 background:rgba(255,255,255,.22); 4681 border:1px solid var(--cd-border); 4682 border-radius:10px; 4683 box-shadow: inset 0 1px 0 rgba(255,255,255,.35); 4684} 4685.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4686
4687.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4688 4689.pd-countdown-modern .pd-cd-label{ 4690 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4691 text-shadow:0 1px 0 rgba(0,0,0,.08); 4692} 4693 4694.pd-countdown-modern .pd-cd-time{ 4695 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4696 line-height:1; 4697} 4698 4699.pd-countdown-modern .unit{ 4700 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4701 min-width:58px; 4702} 4703.pd-countdown-modern .num{ 4704 display:inline-flex; align-items:center; justify-content:center; 4705 min-width:58px; height:40px; padding:0 .4rem; 4706 background:var(--cd-box); 4707 color:var(--cd-box-text); 4708 border:1px solid rgba(15,23,42,.08); 4709 border-radius:10px; 4710 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4711 letter-spacing:.5px; 4712 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.55); 4713} 4714.pd-countdown-modern .unit-label{ 4715 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(255,255,255,.15); 4716} 4717 4718.pd-countdown-modern .colon{ 4719 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4720 animation: cdBlink 1.2s infinite steps(2, jump-none); 4721} 4722@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4723 4724.pd-countdown-modern .pd-cd-progress{ 4725 display:none; 4726 position:relative; 4727 height:6px; border-radius:999px; overflow:hidden; 4728 background:rgba(255,255,255,.25); 4729 border:1px solid rgba(255,255,255,.28); 4730} 4731.pd-countdown-modern .pd-cd-progress .bar{ 4732 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4733 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4734 transition:width .35s ease; 4735} 4736 4737/* State saat hampir habis */ 4738.pd-countdown-modern.danger{ 4739 --cd-bg1:#f97316; /* orange-500 */ 4740 --cd-bg2:#ef4444; /* red-500 */ 4741} 4742 4743/* Responsive */ 4744@media (max-width:640px){ 4745 .pd-countdown-modern{ 4746 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4747 } 4748 .pd-countdown-modern .unit{min-width:52px} 4749 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4750} 4751 4752/* === Modern Countdown polish (shine + compact) === */ 4753.pd-countdown-modern{ 4754 position:relative; 4755} 4756.pd-countdown-modern::after{ 4757 content:""; 4758 position:absolute; inset:-1px; 4759 background: linear-gradient(120deg, rgba(255,255,255,.12), rgba(255,255,255,0) 35%), 4760 radial-gradient(120% 120% at 0% 0%, rgba(255,255,255,.08), rgba(255,255,255,0) 60%); 4761 border-radius: inherit; 4762 pointer-events:none; 4763 mix-blend-mode: screen; 4764 animation: sheen 4.5s ease-in-out infinite; 4765} 4766@keyframes sheen { 4767 0% { opacity:.35; transform: translateX(-10%) } 4768 50% { opacity:.15; transform: translateX(4%) } 4769 100% { opacity:.35; transform: translateX(-10%) } 4770} 4771 4772/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4773.pd-countdown-modern[data-mode="compact"]{ 4774 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4775} 4776.pd-countdown-modern[data-mode="compact"] .unit{ 4777 min-width:46px; 4778} 4779.pd-countdown-modern[data-mode="compact"] .num{ 4780 min-width:46px; height:34px; font-size:.92rem; 4781} 4782.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4783 font-size:.66rem; 4784} 4785.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4786 width:24px; height:24px; border-radius:8px; 4787} 4788.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4789 height:5px; 4790} 4791 4792/* ============================================================ 4793 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4794 Buy card = form & paylater on the same row. 4795 Alt-fintech logos compact (no over-stretching). 4796 ============================================================ */ 4797@media (min-width:641px) and (max-width:1250px) { 4798 /* Buy card: put qty/buy buttons and paylater promo inline */ 4799 .pd-buy-card { 4800 display: flex !important; 4801 flex-direction: row; 4802 flex-wrap: wrap; 4803 align-items: center; 4804 gap: .6rem .75rem; 4805 } 4806 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4807 .pd-buy-card .paylater-promo { 4808 flex: 0 0 auto;
4809 align-self: center; 4810 margin: 0; 4811 max-width: 44%; 4812 } 4813 4814 /* Alt-fintech banner: tighter logos */ 4815 .iab-logos-compact { 4816 justify-content: center; 4817 gap: 1.25rem; 4818 } 4819 .iab-logos-compact .logo-fin { 4820 width: auto; 4821 max-height: 28px; 4822 object-fit: contain; 4823 } 4824} 4825 4826/* ============================================================ 4827 Tablet layout (641px â 880px) 4828 Restores 2-column grid + horizontal sidebar for mid-size screens. 4829 Overrides the generic â¤880px mobile-collapse rules above. 4830 ============================================================ */ 4831@media (min-width:641px) and (max-width:880px) { 4832 /* 2-column grid: image-card | info-card */ 4833 .pd-top-layout { 4834 grid-template-columns: minmax(260px, 40%) 1fr; 4835 gap: 1rem; 4836 } 4837 4838 /* Sidebar: full-width horizontal flex row below the two main columns */ 4839 .pd-sidebar { 4840 grid-column: 1 / -1; 4841 flex-direction: row; 4842 flex-wrap: wrap; 4843 gap: .75rem; 4844 padding: 0; 4845 } 4846 /* Banner cards fill 2-per-row */ 4847 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4848 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4849 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4850 4851 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4852 .pd-installment-card2, 4853 .inst-alt-banner--compact { display: none !important; } 4854 4855 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4856 .pd-sticky-header { display: flex !important; } 4857 4858 /* Typography â tighten slightly to fit the narrower info column */ 4859 .pd-price-current { font-size: 1.65rem; } 4860 .pd-title { font-size: 1.1rem; } 4861 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4862} 4863</style> 4864 <style data-inlined="product-grid.css"> 4865/* Product Grid (Firebird) */ 4866 4867.product-archive { 4868 max-width: 1320px; 4869 margin-inline: auto; 4870 padding: 0 0 3rem; 4871 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4872} 4873 4874.pa-toolbar { 4875 display: flex; 4876 flex-wrap: wrap; 4877 gap: 1rem; 4878 justify-content: space-between; 4879 align-items: flex-start; 4880} 4881 4882.pa-title { 4883 font-size: 1.4rem; 4884 margin: 0; 4885 font-weight: 600; 4886 line-height: 1.2; 4887 color: #0f172a; 4888} 4889 4890.pa-left { 4891 display: flex; 4892 flex-wrap: wrap; 4893 gap: .75rem; 4894 align-items: center; 4895} 4896 4897.pa-filter-badge { 4898 background: #f1f5f9; 4899 border: 1px solid #e2e8f0; 4900 padding: .45rem .6rem; 4901 border-radius: 8px; 4902 font-size: .7rem; 4903 display: flex; 4904 align-items: center; 4905 gap: .4rem; 4906 line-height: 1; 4907 color: #475569; 4908} 4909.pa-filter-badge strong { color: #0f172a; } 4910.pa-filter-badge .clear { 4911 display:inline-block; 4912 text-decoration:none; 4913 background:#e2e8f0; 4914 color:#475569; 4915 font-weight:600; 4916 padding:0 .35rem .05rem; 4917 border-radius:4px; 4918 transition:.2s; 4919 font-size:.75rem; 4920 line-height:1; 4921} 4922.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 4923 4924.pa-right { 4925 display:flex; 4926 gap:1rem; 4927 flex-wrap:wrap; 4928 align-items:flex-start; 4929 margin-bottom: 0.5rem; 4930} 4931 4932.pa-search { 4933 display:flex; 4934 gap:.5rem; 4935 align-items:center; 4936 background:#fff; 4937 border:1px solid #e2e8f0; 4938 padding:.4rem .5rem; 4939 border-radius:8px; 4940} 4941.pa-search input[type=text] { 4942 border:0; 4943 outline:none; 4944 font-size:.8rem; 4945 padding:.35rem .4rem;
4946 min-width:200px; 4947} 4948.pa-search button { 4949 background:#ec4899; 4950 border:0; 4951 color:#fff; 4952 font-size:.7rem; 4953 padding:.45rem .75rem; 4954 border-radius:6px; 4955 cursor:pointer; 4956 font-weight:600; 4957 letter-spacing:.3px; 4958 transition:.25s; 4959} 4960.pa-search button:hover { background:#db2777; } 4961 4962.pa-sort { 4963 display:flex; 4964 gap:.4rem; 4965 align-items:center; 4966 font-size:.7rem; 4967 background:#fff; 4968 border:1px solid #e2e8f0; 4969 padding:.4rem .55rem; 4970 border-radius:8px; 4971} 4972.pa-sort select { 4973 font-size:.75rem; 4974 padding:.35rem .5rem; 4975 border:1px solid #cbd5e1; 4976 border-radius:6px; 4977 background:#fff; 4978 outline:none; 4979} 4980 4981.pa-empty { 4982 border:1px dashed #cbd5e1; 4983 padding:3rem 1rem; 4984 text-align:center; 4985 border-radius:12px; 4986 background:#fff; 4987} 4988.pa-empty p { 4989 margin:0 0 1rem; 4990 font-size:.9rem; 4991 color:#475569; 4992} 4993.btn-reset { 4994 display:inline-block; 4995 background:#ec4899; 4996 color:#fff; 4997 font-size:.7rem; 4998 padding:.55rem 1rem; 4999 border-radius:8px; 5000 text-decoration:none; 5001 font-weight:600; 5002 letter-spacing:.4px; 5003 transition:.25s; 5004} 5005.btn-reset:hover { background:#db2777; } 5006 5007/* Grid */ 5008.pa-grid { 5009 display:grid; 5010 gap:1rem; 5011 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5012 align-items:stretch; 5013} 5014 5015.pa-card { 5016 background:#fff; 5017 border:1px solid #e2e8f0; 5018 border-radius:12px; 5019 display:flex; 5020 flex-direction:column; 5021 text-decoration:none; 5022 color:inherit; 5023 position:relative; 5024 overflow:hidden; 5025 transition:.28s cubic-bezier(.4,0,.2,1); 5026 min-width:0; 5027 padding: 0; 5028} 5029.pa-card:hover { 5030 transform:translateY(-4px); 5031 box-shadow:0 8px 22px -8px rgba(236,72,153,.2); 5032 border-color:#fbcfe8; 5033} 5034 5035.pa-card .thumb { 5036 aspect-ratio:1/1; 5037 background:#f1f5f9; 5038 display:flex; 5039 align-items:center; 5040 justify-content:center; 5041 overflow:hidden; 5042 position:relative; 5043} 5044.pa-card img { 5045 width:100%; 5046 height:100%; 5047 object-fit:cover; 5048 display:block; 5049} 5050.badge-disc { 5051 position:absolute; 5052 top:8px; 5053 left:8px; 5054 background:#dc2626; 5055 color:#fff; 5056 font-size:.6rem; 5057 padding:4px 6px 3px; 5058 border-radius:6px; 5059 font-weight:700; 5060 letter-spacing:.4px; 5061} 5062 5063.pa-card .body { 5064 padding:.65rem .7rem .8rem; 5065 display:flex; 5066 flex-direction:column; 5067 gap:.45rem; 5068 min-height:90px; 5069} 5070 5071.pa-card .title { 5072 font-size:.75rem; 5073 line-height:1.25; 5074 margin:0; 5075 font-weight:600;
5076 color:#1e293b; 5077 height:2.4em; 5078 overflow:hidden; 5079} 5080 5081.pa-card .prices { 5082 display:flex; 5083 flex-wrap:wrap; 5084 align-items:baseline; 5085 gap:.4rem; 5086} 5087.pa-card .prices .final { 5088 font-size:.83rem; 5089 font-weight:700; 5090 color:#ec4899; 5091} 5092.pa-card .prices .ori { 5093 font-size:.63rem; 5094 text-decoration:line-through; 5095 color:#94a3b8; 5096 font-weight:500; 5097} 5098 5099.pa-card .cat { 5100 margin-top:auto; 5101 font-size:.55rem; 5102 letter-spacing:.6px; 5103 text-transform:uppercase; 5104 background:#fdf2f8; 5105 border:1px solid #fbcfe8; 5106 padding:.28rem .45rem .25rem; 5107 border-radius:20px; 5108 font-weight:600; 5109 color:#db2777; 5110 align-self:flex-start; 5111} 5112.pa-card .meta-line { 5113 margin-top:4px; 5114 font-size:.65rem; 5115 color:#475569; 5116} 5117.pa-card .meta-line .sold { 5118 display:inline-block; 5119 background:#f1f5f9; 5120 padding:2px 6px 3px; 5121 border-radius:4px; 5122 font-weight:600; 5123 letter-spacing:.3px; 5124 color:#334155; 5125} 5126 5127/* Pagination */ 5128.pa-pagination { 5129 margin-top:2rem; 5130 display:flex; 5131 flex-wrap:wrap; 5132 gap:.4rem; 5133 justify-content:center; 5134 font-size:.7rem; 5135 align-items:center; 5136} 5137.pa-pagination a, 5138.pa-pagination span { 5139 min-width:34px; 5140 height:34px; 5141 display:flex; 5142 align-items:center; 5143 justify-content:center; 5144 border-radius:8px; 5145 text-decoration:none; 5146 font-weight:600; 5147 padding:0 .6rem; 5148 box-sizing:border-box; 5149 border:1px solid #e2e8f0; 5150 background:#fff; 5151 color:#334155; 5152 transition:.25s; 5153} 5154.pa-pagination a:hover { 5155 background:#fdf2f8; 5156 color:#db2777; 5157 border-color:#fbcfe8; 5158} 5159.pa-pagination .current { 5160 background:#ec4899; 5161 color:#fff; 5162 border-color:#ec4899; 5163} 5164.pa-pagination .gap { 5165 border:none; 5166 background:transparent; 5167 font-weight:400; 5168 color:#94a3b8; 5169} 5170 5171/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5172.pa-mobile-filterbar{display:none} 5173@media (max-width: 640px){ 5174 /* sembunyikan tombol lama */ 5175 .pa-btn-filter{display:none} 5176 5177 .pa-mobile-filterbar{ 5178 display:block; 5179 position:sticky; 5180 top:0; /* sesuaikan jika ada header fixed di atas */ 5181 z-index:5; 5182 background:#fff; 5183 border-bottom:1px solid #e2e8f0; 5184 padding:.4rem .5rem; 5185 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5186 } 5187 .pa-mobile-filterbar .mf-scroll{ 5188 display:flex;align-items:center;gap:.5rem; 5189 overflow-x:auto; 5190 -webkit-overflow-scrolling:touch; 5191 scrollbar-width:none; 5192 } 5193 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5194 5195 .mf-chip{ 5196 flex:0 0 auto; 5197 background:#fff; 5198 color:#334155; 5199 text-decoration:none; 5200 border:1px solid #e2e8f0; 5201 border-radius:999px; 5202 padding:.44rem .7rem; 5203 font-size:.78rem; 5204 font-weight:600; 5205 letter-spacing:.2px; 5206 white-space:nowrap; 5207 } 5208 .mf-chip:active{transform:scale(.98)} 5209 .mf-chip.active{ 5210 background:#fdf2f8; 5211 border-color:#ec4899; 5212 color:#ec4899; 5213 } 5214 5215 .mf-filter-btn{ 5216 position:relative; 5217 flex:0 0 auto; 5218 width:38px;height:38px; 5219 border-radius:50%; 5220 border:1.5px solid #ec4899; 5221 background:#fff; 5222 display:inline-flex;align-items:center;justify-content:center; 5223 color:#ec4899; 5224 cursor:pointer; 5225 } 5226 .mf-filter-btn:active{transform:scale(.96)} 5227 .mf-filter-btn svg{width:18px;height:18px;display:block} 5228 .mf-filter-btn .mf-badge{ 5229 position:absolute;top:0;right:-4px; 5230 min-width:18px;height:18px;border-radius:9px; 5231 background:#ef4444;color:#fff; 5232 font-size:.65rem;font-weight:700; 5233 display:flex;align-items:center;justify-content:center; 5234 padding:0 4px;line-height:1; 5235 box-shadow:0 2px 6px -2px rgba(239,68,68,.6); 5236 } 5237} 5238 5239/* Responsive */ 5240@media (max-width: 640px) { 5241 .product-archive { padding: 0 1rem 3rem; } 5242 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5243 .pa-search input[type=text]{ min-width:140px; } 5244 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5245 .pa-right { display: none; } 5246 .pa-sort { width: 100%; justify-content:center; } 5247 .pa-mobile-filterbar { display: flex !important; } 5248} 5249 5250.pa-mobile-filterbar { 5251 display: none; 5252 align-items: center; 5253 gap: 8px; 5254 padding: 8px 0 8px 12px; 5255 background: #fff; 5256 width: 100vw; 5257} 5258.pa-mobile-filterbar .mf-scroll { 5259 display: flex; 5260 gap: 8px; 5261 overflow-x: auto; 5262 -webkit-overflow-scrolling: touch; 5263 scrollbar-width: none; /* Firefox */ 5264 flex: 1 1 auto; /* takes remaining width */ 5265} 5266.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5267.pa-mobile-filterbar .mf-chip { 5268 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5269 white-space: nowrap; 5270} 5271.pa-mobile-filterbar .mf-filter-btn { 5272 flex: 0 0 auto; /* do not shrink */ 5273 position: relative; /* keep in place, not inside scroller */ 5274 display: inline-flex; 5275 align-items: center; 5276 gap: 6px; 5277} 5278 5279/* Optional: ensure on small screens the button stays visible with min width */ 5280@media (max-width: 768px) { 5281 .pa-mobile-filterbar .mf-filter-btn { 5282 padding: 6px 10px; 5283 } 5284} 5285 5286/* Product Specification Chips */ 5287.pa-card .spec-chips { 5288 font-size: .6rem; 5289 color: #64748b; 5290 font-weight: 500; 5291 letter-spacing: .2px; 5292 margin-top: 2px; 5293 margin-bottom: 4px;
5294 line-height: 1.2; 5295 opacity: 0.9; 5296 max-width: 100%; 5297 overflow: hidden; 5298 text-overflow: ellipsis; 5299 white-space: nowrap; 5300} 5301 5302.pa-card .spec-chips .spec-divider { 5303 color: #94a3b8; 5304 font-weight: 400; 5305 margin: 0 1px; 5306} 5307</style> 5308 <style data-inlined="site-notice.css"> 5309/** 5310 * Site Notice Banner - Reusable Notification System 5311 * Dismissible top bar for announcements, holidays, maintenance, etc. 5312 */ 5313 5314.holiday-notice { 5315 position: fixed; 5316 top: 0; 5317 left: 0; 5318 right: 0; 5319 z-index: 9999; 5320 background: linear-gradient(135deg, #db2777 0%, #f472b6 100%); 5321 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5322 box-shadow: 0 2px 8px rgba(219, 39, 119, 0.25); 5323 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5324 transform-origin: top; 5325} 5326 5327.holiday-notice.hidden { 5328 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5329} 5330 5331@keyframes slideDown { 5332 from { 5333 transform: translateY(-100%); 5334 opacity: 0; 5335 } 5336 to { 5337 transform: translateY(0); 5338 opacity: 1; 5339 } 5340} 5341 5342@keyframes slideUp { 5343 from { 5344 transform: translateY(0); 5345 opacity: 1; 5346 } 5347 to { 5348 transform: translateY(-100%); 5349 opacity: 0; 5350 } 5351} 5352 5353.holiday-notice-container { 5354 max-width: 1400px; 5355 margin: 0 auto; 5356 padding: 12px 20px; 5357 display: flex;
5358 align-items: center; 5359 justify-content: space-between; 5360 gap: 16px; 5361} 5362 5363.holiday-notice-content { 5364 display: flex; 5365 align-items: center; 5366 gap: 12px; 5367 flex: 1; 5368} 5369 5370.holiday-notice-icon { 5371 font-size: 20px; 5372 line-height: 1; 5373 flex-shrink: 0; 5374} 5375 5376.holiday-notice-text { 5377 color: #ffffff; 5378 font-size: 14px; 5379 font-weight: 500; 5380 line-height: 1.5; 5381 margin: 0; 5382 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); 5383} 5384 5385.holiday-notice-emoji { 5386 display: inline-block; 5387 animation: bounce 2s ease-in-out infinite; 5388} 5389 5390@keyframes bounce { 5391 0%, 100% { 5392 transform: translateY(0); 5393 } 5394 50% { 5395 transform: translateY(-4px); 5396 } 5397} 5398 5399.holiday-notice-close { 5400 background: rgba(255, 255, 255, 0.2); 5401 border: 1px solid rgba(255, 255, 255, 0.3); 5402 border-radius: 6px; 5403 padding: 6px; 5404 cursor: pointer; 5405 color: #ffffff; 5406 transition: all 0.2s ease; 5407 flex-shrink: 0; 5408 display: flex; 5409 align-items: center; 5410 justify-content: center; 5411 width: 32px; 5412 height: 32px; 5413} 5414 5415.holiday-notice-close:hover { 5416 background: rgba(255, 255, 255, 0.35); 5417 transform: scale(1.05); 5418} 5419 5420.holiday-notice-close:active { 5421 transform: scale(0.95); 5422} 5423 5424.holiday-notice-close:focus { 5425 outline: 2px solid rgba(255, 255, 255, 0.6); 5426 outline-offset: 2px; 5427} 5428 5429/* Body padding adjustment when banner is visible */ 5430body.has-holiday-notice { 5431 padding-top: 52px; 5432} 5433 5434/* Tablet Styles */ 5435@media (max-width: 1024px) { 5436 .holiday-notice-container { 5437 padding: 10px 16px; 5438 } 5439 5440 .holiday-notice-text { 5441 font-size: 13px; 5442 } 5443 5444 .holiday-notice-icon { 5445 font-size: 18px; 5446 } 5447 5448 body.has-holiday-notice { 5449 padding-top: 48px; 5450 } 5451} 5452 5453/* Mobile Styles */ 5454@media (max-width: 640px) { 5455 .holiday-notice-container { 5456 padding: 10px 12px; 5457 gap: 8px; 5458 } 5459 5460 .holiday-notice-content { 5461 gap: 8px; 5462 } 5463 5464 .holiday-notice-text { 5465 font-size: 12px; 5466 line-height: 1.4; 5467 } 5468 5469 .holiday-notice-icon { 5470 font-size: 16px; 5471 } 5472 5473 .holiday-notice-close { 5474 width: 28px; 5475 height: 28px; 5476 } 5477 5478 .holiday-notice-close svg { 5479 width: 16px; 5480 height: 16px; 5481 } 5482 5483 body.has-holiday-notice { 5484 padding-top: 44px; 5485 } 5486} 5487 5488/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5489/* .header, 5490.navbar, 5491header { 5492 position: relative; 5493 z-index: 100; 5494} */ 5495 5496</style> 5497 <style data-inlined="benchmark.css"> 5498/** 5499 * Benchmark Section Styles 5500 * For product detail page benchmark display 5501 */ 5502 5503.pd-benchmark-section { 5504 background: #fff; 5505 border-radius: 12px; 5506 padding: 24px; 5507 margin: 32px 0; 5508 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5509 border: 1px solid #e5e7eb; 5510} 5511 5512.benchmark-header { 5513 margin-bottom: 20px; 5514} 5515 5516.benchmark-title { 5517 font-size: 22px; 5518 font-weight: 700; 5519 color: #1a1a1a; 5520 margin: 0 0 12px 0; 5521 display: flex;
5522 align-items: center; 5523 gap: 8px; 5524} 5525 5526.benchmark-hint { 5527 font-size: 14px; 5528 color: #6b7280; 5529 margin: 0; 5530} 5531 5532/* Preview Badges */ 5533.benchmark-preview-badges { 5534 display: flex; 5535 flex-wrap: wrap; 5536 gap: 10px; 5537 margin-top: 12px; 5538} 5539 5540.benchmark-badge { 5541 display: inline-flex; 5542 align-items: center; 5543 gap: 6px; 5544 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5545 color: #fff; 5546 padding: 8px 16px; 5547 border-radius: 20px; 5548 font-size: 13px; 5549 font-weight: 500; 5550 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5551 transition: all 0.2s ease; 5552} 5553 5554.benchmark-badge:hover { 5555 transform: translateY(-2px); 5556 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5557} 5558 5559.benchmark-badge strong { 5560 font-weight: 600; 5561 opacity: 0.9; 5562} 5563 5564/* Load Button */ 5565.btn-load-benchmark { 5566 display: flex; 5567 align-items: center; 5568 justify-content: center; 5569 gap: 10px; 5570 width: 100%; 5571 max-width: 420px; 5572 padding: 14px 24px; 5573 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5574 color: #fff; 5575 border: none; 5576 border-radius: 8px; 5577 font-size: 15px; 5578 font-weight: 600; 5579 cursor: pointer; 5580 transition: all 0.3s ease; 5581 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5582 margin: 0 auto; 5583} 5584 5585.btn-load-benchmark:hover:not(:disabled) { 5586 transform: translateY(-2px); 5587 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5588} 5589 5590.btn-load-benchmark:disabled { 5591 opacity: 0.6; 5592 cursor: not-allowed; 5593} 5594 5595.btn-load-benchmark .btn-icon { 5596 font-size: 20px; 5597} 5598 5599.btn-load-benchmark.loading .btn-icon { 5600 animation: spin 1s linear infinite; 5601} 5602 5603@keyframes spin { 5604 from { transform: rotate(0deg); } 5605 to { transform: rotate(360deg); } 5606} 5607 5608/* Benchmark Content */ 5609.benchmark-content { 5610 margin-top: 24px; 5611 padding-top: 24px; 5612 border-top: 2px solid #f3f4f6; 5613} 5614 5615.benchmark-loading { 5616 text-align: center; 5617 padding: 40px 20px; 5618 color: #6b7280; 5619} 5620 5621.benchmark-loading-spinner { 5622 width: 48px; 5623 height: 48px; 5624 margin: 0 auto 16px; 5625 border: 4px solid #f3f4f6; 5626 border-top-color: #667eea; 5627 border-radius: 50%; 5628 animation: spin 1s linear infinite; 5629} 5630 5631.benchmark-error { 5632 text-align: center; 5633 padding: 32px 20px; 5634 color: #ef4444; 5635 background: #fee2e2; 5636 border-radius: 8px; 5637} 5638 5639/* Benchmark Data Display */ 5640.benchmark-data-grid { 5641 display: grid; 5642 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5643 gap: 16px; 5644 margin-bottom: 24px; 5645} 5646 5647.benchmark-card { 5648 background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); 5649 border: 1px solid #e5e7eb; 5650 border-radius: 10px; 5651 padding: 18px; 5652 text-align: center; 5653 transition: all 0.2s ease; 5654} 5655 5656.benchmark-card:hover { 5657 transform: translateY(-3px); 5658 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5659} 5660 5661.benchmark-card-label { 5662 font-size: 13px; 5663 color: #6b7280; 5664 font-weight: 500; 5665 margin-bottom: 8px; 5666 text-transform: uppercase; 5667 letter-spacing: 0.5px; 5668} 5669 5670.benchmark-card-value { 5671 font-size: 24px; 5672 font-weight: 700; 5673 color: #1a1a1a; 5674 line-height: 1.2; 5675} 5676 5677.benchmark-card-value.large { 5678 font-size: 28px; 5679} 5680 5681.benchmark-card-subtitle { 5682 font-size: 12px; 5683 color: #9ca3af; 5684 margin-top: 4px; 5685} 5686 5687/* Spec Details */ 5688.benchmark-specs { 5689 background: #f9fafb; 5690 border-radius: 10px; 5691 padding: 20px; 5692 margin-top: 20px; 5693} 5694 5695.benchmark-specs-title { 5696 font-size: 16px; 5697 font-weight: 600; 5698 color: #1a1a1a; 5699 margin: 0 0 16px 0; 5700} 5701 5702.benchmark-spec-row { 5703 display: flex; 5704 justify-content: space-between; 5705 padding: 10px 0; 5706 border-bottom: 1px solid #e5e7eb; 5707} 5708 5709.benchmark-spec-row:last-child { 5710 border-bottom: none; 5711} 5712 5713.benchmark-spec-label { 5714 font-size: 14px; 5715 color: #6b7280; 5716 font-weight: 500; 5717} 5718 5719.benchmark-spec-value { 5720 font-size: 14px; 5721 color: #1a1a1a; 5722 font-weight: 600; 5723 text-align: right; 5724} 5725 5726/* Score Badges */ 5727.benchmark-score-badge { 5728 display: inline-block; 5729 padding: 6px 14px; 5730 border-radius: 20px; 5731 font-size: 13px; 5732 font-weight: 600; 5733 margin-top: 8px; 5734} 5735 5736.benchmark-score-badge.excellent { 5737 background: #10b981; 5738 color: #fff; 5739} 5740 5741.benchmark-score-badge.good { 5742 background: #3b82f6; 5743 color: #fff; 5744} 5745 5746.benchmark-score-badge.average { 5747 background: #f59e0b; 5748 color: #fff; 5749} 5750 5751.benchmark-score-badge.poor { 5752 background: #ef4444; 5753 color: #fff; 5754} 5755 5756/* Attribution */ 5757.benchmark-attribution { 5758 text-align: center; 5759 margin-top: 24px; 5760 padding-top: 20px; 5761 border-top: 1px solid #e5e7eb; 5762 font-size: 13px; 5763 color: #9ca3af; 5764} 5765 5766.benchmark-attribution a { 5767 color: #667eea; 5768 text-decoration: none; 5769 font-weight: 500; 5770} 5771 5772.benchmark-attribution a:hover { 5773 text-decoration: underline; 5774} 5775 5776/* Responsive */ 5777@media (max-width: 768px) { 5778 .pd-benchmark-section { 5779 padding: 16px; 5780 margin: 20px 0; 5781 } 5782 5783 .benchmark-title { 5784 font-size: 18px; 5785 } 5786 5787 .benchmark-preview-badges { 5788 flex-direction: column; 5789 gap: 8px; 5790 } 5791 5792 .benchmark-badge { 5793 justify-content: center; 5794 } 5795 5796 .btn-load-benchmark { 5797 font-size: 14px; 5798 padding: 12px 20px; 5799 } 5800 5801 .benchmark-data-grid { 5802 grid-template-columns: 1fr 1fr; 5803 gap: 12px; 5804 } 5805 5806 .benchmark-card { 5807 padding: 14px; 5808 } 5809 5810 .benchmark-card-value { 5811 font-size: 20px; 5812 } 5813 5814 .benchmark-spec-row { 5815 flex-direction: column; 5816 gap: 4px; 5817 } 5818 5819 .benchmark-spec-value { 5820 text-align: left; 5821 } 5822} 5823 5824@media (max-width: 480px) { 5825 .benchmark-data-grid { 5826 grid-template-columns: 1fr; 5827 } 5828} 5829 5830</style> 5831 <style data-inlined="product-filters.css"> 5832/* ========================================================= 5833 Product Filters (Desktop + Mobile Drawer) 5834 Light Only + FIX: 5835 - Desktop: f-actions selalu tampil (static bottom) 5836 - Mobile: konten filter dapat discroll 5837 ========================================================= */ 5838 5839/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5840.product-archive.with-filters .pa-layout { 5841 display:flex; 5842 gap:24px; 5843 align-items:flex-start; 5844} 5845.pa-main { flex:1; min-width:0; } 5846 5847/* ========================================================= 5848 DESKTOP PANEL 5849 ========================================================= */ 5850.pa-filters { 5851 width:300px; 5852 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5853 border:1px solid #e2e8f0; 5854 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5855 border-radius:18px; 5856 position:sticky; 5857 top:80px; 5858 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5859 font-size:14px; 5860 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5861 backdrop-filter:blur(3px); 5862 -webkit-backdrop-filter:blur(3px);
5863 transition:box-shadow .3s, background .4s; 5864 display:flex; 5865 flex-direction:column; 5866 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5867} 5868.pa-filters::before { 5869 content:""; 5870 position:absolute; 5871 inset:0 auto 0 0; 5872 width:7px; 5873 border-radius:18px 0 0 18px; 5874 background:linear-gradient(180deg,#f59e0b 0%,#ffb53a 35%,#1d6ce0 100%); 5875} 5876.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5877 5878/* Scroll area (desktop/mode shared) */ 5879.filters-scroll { 5880 flex:1; 5881 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5882 overflow:auto; 5883 padding-right:6px; 5884 margin-right:-6px; /* kompensasi scrollbar */ 5885 scrollbar-width:thin; 5886 overscroll-behavior:contain; 5887} 5888.filters-scroll::-webkit-scrollbar { width:8px; } 5889.filters-scroll::-webkit-scrollbar-thumb { 5890 background:#cbd5e1; 5891 border-radius:20px; 5892 border:2px solid #f1f5f9; 5893} 5894.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5895 5896/* Heading */ 5897.pa-filters h2 { 5898 margin:0 0 14px; 5899 font-size:20px; 5900 font-weight:700; 5901 letter-spacing:.4px; 5902 line-height:1.15; 5903 background:linear-gradient(90deg,#0f172a,#1d6ce0 60%,#0f172a); 5904 -webkit-background-clip:text; 5905 color:transparent; 5906 background-size:200% 100%; 5907 animation:titleSheen 8s linear infinite; 5908} 5909@keyframes titleSheen { 5910 0%{background-position:0 0} 5911 50%{background-position:100% 0} 5912 100%{background-position:0 0} 5913} 5914 5915/* Form groups */ 5916.pa-filters .f-group { margin-bottom:18px; position:relative; } 5917.pa-filters label { 5918 display:block; 5919 font-weight:600; 5920 margin:0 0 6px; 5921 font-size:13px; 5922 color:#0f172a; 5923 letter-spacing:.3px; 5924} 5925 5926/* Inputs / selects */ 5927.pa-filters input[type=text], 5928.pa-filters input[type=number], 5929.pa-filters select { 5930 width:100%; 5931 padding:7px 10px; 5932 border:1px solid #cbd5e1; 5933 border-radius:8px; 5934 font-size:13px; 5935 min-height:34px; 5936 background:#ffffff; 5937 box-sizing:border-box; 5938 color:#0f172a; 5939 transition:border-color .25s, box-shadow .25s, background .25s; 5940 -webkit-appearance:none; 5941 -moz-appearance:none; 5942 appearance:none; 5943} 5944.pa-filters select { 5945 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%230f172a' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); 5946 background-repeat:no-repeat; 5947 background-position:right 10px center; 5948 background-size:12px; 5949 padding-right:32px; 5950} 5951.pa-filters input[type=text]:hover, 5952.pa-filters input[type=number]:hover, 5953.pa-filters select:hover { border-color:#94b5ec; } 5954.pa-filters input[type=text]:focus, 5955.pa-filters input[type=number]:focus, 5956.pa-filters select:focus { 5957 outline:none; 5958 border-color:#1d6ce0; 5959 box-shadow:0 0 0 3px rgba(29,108,224,.25); 5960 background:#f8fbff; 5961} 5962.pa-filters .range { display:flex; gap:10px; } 5963.pa-filters .range input { flex:1; } 5964.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 5965 5966/* Spec blocks */ 5967.f-specs { margin-top:4px; } 5968.spec-attr { 5969 margin:0 0 14px; 5970 padding:12px 12px 10px; 5971 border:1px solid #e2e8f0; 5972 border-radius:14px; 5973 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 5974 position:relative; 5975 transition:border-color .3s, box-shadow .3s, background .4s; 5976} 5977.spec-attr.collapsed { padding-bottom:8px; } 5978.spec-attr.has-selected { 5979 border-color:#1d6ce0; 5980 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.35); 5981 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 5982} 5983.spec-attr-title { 5984 font-weight:600; 5985 margin:0 0 8px; 5986 font-size:13px; 5987 display:flex; 5988 justify-content:space-between; 5989 align-items:center; 5990 cursor:pointer; 5991 color:#0f172a; 5992 user-select:none; 5993} 5994.spec-attr-title .chevron { 5995 font-size:10px; 5996 transition:transform .25s, opacity .25s; 5997 opacity:.65; 5998} 5999.spec-attr.collapsed .spec-attr-title .chevron { 6000 transform:rotate(-90deg); 6001 opacity:.5; 6002} 6003 6004/* Spec options */ 6005.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6006.spec-attr-options label { 6007 display:inline-flex; 6008 align-items:center; 6009 gap:4px; 6010 font-weight:500; 6011 border:1px solid #d4dde7; 6012 padding:6px 9px; 6013 border-radius:30px; 6014 cursor:pointer; 6015 background:#f3f6fa; 6016 font-size:12px; 6017 line-height:1.1; 6018 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6019 position:relative; 6020} 6021.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6022.spec-attr-options input { 6023 margin:0; 6024 position:absolute; 6025 inset:0; 6026 opacity:0;
6027 cursor:pointer; 6028} 6029.spec-attr-options label:has(input:checked) { 6030 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6031 border-color:#1d6ce0; 6032 color:#ffffff; 6033 box-shadow:0 2px 6px -1px rgba(29,108,224,.55); 6034} 6035.spec-attr-options label:has(input:checked)::after { 6036 content:""; 6037 position:absolute; 6038 right:6px; 6039 top:6px; 6040 width:6px; 6041 height:6px; 6042 background:#fff; 6043 border-radius:50%; 6044 box-shadow:0 0 0 4px rgba(255,255,255,.25),0 0 0 7px rgba(29,108,224,.5); 6045 animation:pulseDot 1.9s ease-out infinite; 6046} 6047@keyframes pulseDot { 6048 0%{transform:scale(.8);opacity:.9} 6049 60%{transform:scale(1.4);opacity:.15} 6050 100%{transform:scale(.8);opacity:.9} 6051} 6052 6053/* Badges */ 6054.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6055.pa-filter-badge { 6056 display:inline-flex; 6057 gap:6px; 6058 align-items:center; 6059 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6060 border:1px solid #b4cff3; 6061 padding:5px 10px 5px 12px; 6062 border-radius:22px; 6063 margin:2px 4px 0 0; 6064 font-size:11px; 6065 font-weight:500; 6066 line-height:1; 6067 color:#0f172a; 6068 position:relative; 6069 overflow:hidden; 6070} 6071.pa-filter-badge::before { 6072 content:""; 6073 position:absolute; 6074 inset:0; 6075 background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.35),rgba(255,255,255,0)); 6076 transform:translateX(-100%); 6077 animation:badgeSheen 6s linear infinite; 6078} 6079@keyframes badgeSheen { 6080 0%{transform:translateX(-100%)} 6081 55%{transform:translateX(120%)} 6082 100%{transform:translateX(120%)} 6083} 6084.pa-filter-badge a.clear { 6085 text-decoration:none; 6086 color:#c02828; 6087 font-weight:700; 6088 padding:0 3px; 6089 display:inline-flex; 6090 align-items:center; 6091 justify-content:center; 6092 border-radius:50%; 6093 line-height:1; 6094 transition:background .25s, color .25s; 6095} 6096.pa-filter-badge a.clear:hover { background:#ffeded; color:#a40000; } 6097.pa-btn-filter { width: 100%;} 6098 6099/* ========================================================= 6100 ACTIONS (DESKTOP) - STATIC BOTTOM 6101 ========================================================= */ 6102.f-actions { 6103 margin-top:10px; 6104 display:flex; 6105 gap:12px; 6106 flex-wrap:wrap; 6107 padding:16px 0 4px; 6108 border-top:1px solid #e2e8f0; 6109 background:linear-gradient(180deg,rgba(255,255,255,0) 0%,#f1f5f9 70%); 6110 flex-shrink:0; 6111} 6112.f-actions button { 6113 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6114 color:#fff; 6115 border:none; 6116 padding:10px 20px; 6117 border-radius:12px; 6118 cursor:pointer; 6119 font-size:14px; 6120 font-weight:600; 6121 letter-spacing:.3px; 6122 display:inline-flex; 6123 align-items:center; 6124 gap:6px; 6125 box-shadow:0 4px 14px -2px rgba(29,108,224,.45); 6126 transition:transform .25s, box-shadow .3s; 6127} 6128.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6129.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6130.f-actions .reset { 6131 font-size:12px; 6132 text-decoration:none; 6133 color:#d92d2d; 6134 align-self:center; 6135 font-weight:600; 6136 padding:4px 6px; 6137 border-radius:8px; 6138 transition:background .25s, color .25s; 6139} 6140.f-actions .reset:hover { background:#ffe6e6; color:#b80000; text-decoration:underline; } 6141.pa-filters .close-drawer { display: none; } 6142 6143/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6144.filter-form { 6145 flex: 1; 6146 min-height: 0; 6147 display: flex; 6148 flex-direction: column; 6149 overflow: hidden; 6150} 6151 6152/* ========================================================= 6153 FILTER TOGGLE (MOBILE TRIGGER) 6154 ========================================================= */ 6155.filter-toggle { 6156 display:none; 6157 margin:0 0 14px; 6158 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6159 color:#fff; 6160 border:none; 6161 padding:11px 18px; 6162 font-size:14px; 6163 font-weight:600; 6164 border-radius:14px; 6165 cursor:pointer; 6166 letter-spacing:.4px; 6167 box-shadow:0 4px 14px -2px rgba(29,108,224,.55); 6168 position:relative; 6169 z-index:100; 6170 transition:box-shadow .3s, transform .25s; 6171} 6172.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(29,108,224,.6); transform:translateY(-2px); } 6173.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6174.filter-toggle .count-badge { 6175 display:inline-block; 6176 background:linear-gradient(135deg,#f59e0b 0%,#ffb43d 55%,#f59e0b 100%); 6177 color:#1d2b44; 6178 font-size:11px; 6179 font-weight:700; 6180 padding:3px 7px 4px;
6181 border-radius:30px; 6182 margin-left:8px; 6183 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #1d6ce0; 6184} 6185 6186/* Overlay (mobile) */ 6187.pa-filters-overlay { display:none; } 6188 6189/* ========================================================= 6190 MOBILE DRAWER 6191 ========================================================= */ 6192@media (max-width:768px){ 6193 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6194 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6195 6196 .pa-filters-overlay { 6197 position:fixed; 6198 inset:0; 6199 background:rgba(0,20,45,.55); 6200 z-index:998; 6201 opacity:0; 6202 transition:opacity .35s; 6203 display:block; 6204 pointer-events:none; 6205 backdrop-filter:blur(2px); 6206 } 6207 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6208 6209 .pa-filters { 6210 position:fixed; 6211 top:auto; 6212 left:0; right:0; bottom:0; 6213 max-height:92vh; 6214 height:auto; 6215 width:100%; 6216 border-radius:30px 30px 0 0; 6217 border:1px solid #dbe4ef; 6218 padding:26px 22px 0; 6219 z-index:10000; /* di atas bottom-nav (9998) */ 6220 transform:translateY(102%); 6221 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6222 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6223 overflow:hidden; /* outer hidden, inner scroll */ 6224 } 6225 .pa-filters.active { transform:translateY(0); } 6226 6227 /* Drag handle indicator */ 6228 .pa-filters::after { 6229 content:""; 6230 display:block; 6231 position:absolute; 6232 top:10px; 6233 left:50%; 6234 transform:translateX(-50%); 6235 width:36px; 6236 height:4px; 6237 background:#cbd5e1; 6238 border-radius:4px; 6239 } 6240 body.filter-open { overflow:hidden; touch-action:none; } 6241 6242 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6243 .pa-filters input[type=text], 6244 .pa-filters input[type=number], 6245 .pa-filters select { 6246 font-size:16px; 6247 min-height:44px; 6248 padding:10px 12px; 6249 border-radius:10px; 6250 } 6251 .pa-filters select { padding-right:32px; } 6252 6253 .filters-scroll { 6254 flex:1; 6255 overflow:auto; 6256 padding:0 0 120px; /* ruang di atas bar actions */ 6257 margin:0; 6258 -webkit-overflow-scrolling:touch; 6259 } 6260 6261 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6262 6263 .pa-filters .close-drawer { 6264 display: block; 6265 position:absolute; 6266 right:14px; top:12px; 6267 background:linear-gradient(135deg,#dc2626 0%,#b91c1c 100%); 6268 color:#fff; 6269 padding:7px 14px; 6270 border:none; 6271 border-radius:10px; 6272 font-size:12px; 6273 font-weight:600; 6274 cursor:pointer; 6275 box-shadow:0 3px 10px -2px rgba(220,38,38,.55); 6276 transition:transform .25s, box-shadow .3s; 6277 z-index: 99; 6278 } 6279 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(220,38,38,.6); } 6280 6281 .spec-attr { border-radius:18px; } 6282 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6283 6284 /* Actions fixed di viewport bawah */ 6285 .f-actions { 6286 position:fixed; 6287 left:0; right:0; bottom:0; 6288 margin:0; 6289 border-top:1px solid #d6dee8; 6290 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6291 background:#ffffffea; 6292 backdrop-filter:blur(6px); 6293 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6294 justify-content:space-between; 6295 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6296 } 6297 6298 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6299 body.filter-open .bottom-nav { 6300 display: none !important; 6301 } 6302 .f-actions button { 6303 flex:1 1 auto; 6304 justify-content:center; 6305 padding:14px 14px; 6306 font-size:15px; 6307 border-radius:14px; 6308 } 6309 .f-actions .reset { 6310 order:-1; 6311 font-size:12px; 6312 padding:2px 8px; 6313 } 6314} 6315 6316/* Scrollbar (outer panel desktop only kept for compatibility) */ 6317.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6318 6319/* Utilities */ 6320.hidden { display:none !important; } 6321.gradient-text { 6322 background:linear-gradient(90deg,#1d6ce0,#155ab8,#1d6ce0); 6323 -webkit-background-clip:text; 6324 color:transparent; 6325 animation:titleSheen 10s linear infinite; 6326} 6327.glow-focus:focus {
6328 outline:none !important; 6329 box-shadow:0 0 0 3px rgba(29,108,224,.4) !important; 6330 border-color:#1d6ce0 !important; 6331} 6332 6333/* Simple fade (opsional) */ 6334@keyframes fadeIn { 6335 from { opacity:0; transform:translateY(8px); } 6336 to { opacity:1; transform:translateY(0); } 6337} 6338</style> 6339 <style data-inlined="mega-menu-tiles.css"> 6340/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6341 6342/* Design Tokens */ 6343:root { 6344 --mm2-bg: #ffffff; 6345 --mm2-surface: #ffffff; 6346 --mm2-surface-elevated: rgba(255,255,255,0.98); 6347 --mm2-muted: #f8fafc; 6348 --mm2-border: #e2e8f0; 6349 --mm2-border-strong: #cbd5e1; 6350 --mm2-accent: #6366f1; 6351 --mm2-accent-hover: #4f46e5; 6352 --mm2-accent-10: rgba(99,102,241,.10); 6353 --mm2-accent-20: rgba(99,102,241,.20); 6354 --mm2-fg: #0f172a; 6355 --mm2-fg-muted: #64748b; 6356 --mm2-fg-light: #94a3b8; 6357 6358 /* Enhanced Shadows - Multi-layer elevation */ 6359 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6360 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6361 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6362 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6363 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6364 6365 /* Border Radius */ 6366 --mm2-radius-xl: 20px; 6367 --mm2-radius-lg: 16px; 6368 --mm2-radius-md: 12px; 6369 --mm2-radius-sm: 10px; 6370 6371 --mm2-z: 60; 6372 6373 /* Animation Timing */ 6374 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6375 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6376 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6377} 6378 6379.navbar { position: relative; z-index: var(--mm2-z); } 6380 6381/* Trigger Button - Enhanced */ 6382.mm2-trigger { 6383 display: inline-flex; align-items: center; gap: 8px; 6384 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6385 border: 1.5px solid var(--mm2-border); 6386 background: var(--mm2-surface); color: var(--mm2-fg); 6387 cursor: pointer; user-select: none; 6388 transition: all var(--mm2-transition-fast); 6389 text-decoration: none; 6390 font-weight: 600; 6391 box-shadow: var(--mm2-shadow-sm); 6392} 6393.mm2-trigger:hover { 6394 border-color: var(--mm2-accent); 6395 background: var(--mm2-muted); 6396 box-shadow: var(--mm2-shadow-md); 6397 transform: translateY(-1px); 6398} 6399.mm2-trigger:active { transform: translateY(0); } 6400.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6401.mm2-trigger:hover svg { transform: scale(1.05); } 6402 6403/* Backdrop - Enhanced Glassmorphism */ 6404.mm2-backdrop { 6405 position: fixed; 6406 inset: 0; 6407 top: 0; 6408 left: 0; 6409 right: 0; 6410 bottom: 0; 6411 background: rgba(15,23,42,.40); 6412 backdrop-filter: blur(8px) saturate(1.2); 6413 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6414 opacity: 0; 6415 pointer-events: none; 6416 z-index: 499; 6417 transition: opacity var(--mm2-transition-base); 6418} 6419.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6420 6421/* Panel - Glassmorphism Enhanced */ 6422.mm2-panel { 6423 position: fixed; 6424 left: 50%; 6425 top: 120px; 6426 transform: translateX(-50%) translateY(-16px); 6427 width: min(1200px, 95vw); 6428 background: var(--mm2-surface-elevated); 6429 backdrop-filter: blur(20px) saturate(1.8); 6430 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6431 border: 1.5px solid var(--mm2-border-strong); 6432 border-radius: var(--mm2-radius-xl); 6433 box-shadow: var(--mm2-shadow-xl); 6434 display: none; 6435 overflow: hidden; 6436 opacity: 0; 6437 z-index: 500; 6438 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6439} 6440.mm2-open .mm2-panel { 6441 display: block; 6442 opacity: 1; 6443 transform: translateX(-50%) translateY(0); 6444} 6445 6446/* View switcher */ 6447.mm2-view { display: none; } 6448.mm2-open .mm2-view.is-active { display: block; } 6449 6450/* Top grid (level-1 tiles) - Enhanced */ 6451.mm2-top { 6452 padding: 24px; 6453 max-height: 75vh; overflow: auto; 6454} 6455.mm2-grid { 6456 display: grid; 6457 grid-template-columns: repeat(4, minmax(0,1fr)); 6458 gap: 14px; 6459} 6460@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6461@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6462 6463/* Stagger animation for cards */ 6464.mm2-open .mm2-card { 6465 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6466} 6467.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6468.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6469.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6470.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6471.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6472.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6473.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6474.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6475.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6476.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6477.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6478.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6479.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6480 6481@keyframes slideInUp { 6482 from { 6483 opacity: 0; 6484 transform: translateY(12px); 6485 } 6486 to { 6487 opacity: 1;
6488 transform: translateY(0); 6489 } 6490} 6491 6492.mm2-card { 6493 position: relative; 6494 display: flex; gap: 14px; align-items: center; 6495 padding: 16px; border-radius: var(--mm2-radius-md); 6496 background: var(--mm2-surface); 6497 border: 1.5px solid var(--mm2-border); 6498 text-decoration: none; color: var(--mm2-fg); 6499 transition: all var(--mm2-transition-fast); 6500 min-height: 72px; 6501 box-shadow: var(--mm2-shadow-sm); 6502} 6503.mm2-card:hover { 6504 border-color: var(--mm2-accent); 6505 background: linear-gradient(135deg, rgba(255,255,255,1), rgba(248,250,252,1)); 6506 box-shadow: var(--mm2-shadow-hover); 6507 transform: translateY(-3px); 6508} 6509.mm2-card:active { transform: translateY(-1px); } 6510 6511/* Icon with category-specific gradients */ 6512.mm2-card .mm2-ico { 6513 width: 48px; height: 48px; border-radius: 12px; 6514 display: inline-flex; align-items: center; justify-content: center; 6515 background: linear-gradient(135deg, #e0e7ff, #fff); 6516 border: 1.5px solid #c7d2fe; 6517 color: #4f46e5; 6518 flex-shrink: 0; 6519 transition: all var(--mm2-transition-fast); 6520 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6521} 6522.mm2-card:hover .mm2-ico { 6523 transform: scale(1.08) rotate(2deg); 6524 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6525} 6526.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6527.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6528 6529.mm2-card .mm2-title { 6530 font-weight: 700; 6531 font-size: 15px; 6532 letter-spacing: -0.01em; 6533 line-height: 1.3; 6534} 6535.mm2-card .mm2-sub { 6536 color: var(--mm2-fg-muted); 6537 font-size: 12.5px; 6538 margin-top: 3px; 6539 display: flex; 6540 align-items: center; 6541 gap: 4px; 6542 white-space: nowrap; 6543 overflow: hidden; 6544 text-overflow: ellipsis; 6545} 6546.mm2-card .mm2-count { 6547 display: inline-flex; 6548 padding: 2px 8px; 6549 border-radius: 6px; 6550 background: var(--mm2-accent-10); 6551 color: var(--mm2-accent); 6552 font-weight: 600; 6553 font-size: 11px; 6554 margin-left: 4px; 6555} 6556 6557/* Detail view (level-2 + level-3) - Enhanced */ 6558.mm2-detail { 6559 position: relative; 6560 padding: 16px 24px 24px; 6561 max-height: 70vh; overflow: auto; 6562} 6563.mm2-detail-header { 6564 display: flex; align-items: center; gap: 14px; 6565 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6566} 6567.mm2-back { 6568 display: inline-flex; align-items: center; justify-content: center; 6569 width: 40px; height: 40px; border-radius: 12px; 6570 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6571 cursor: pointer; 6572 transition: all var(--mm2-transition-fast); 6573 box-shadow: var(--mm2-shadow-sm); 6574} 6575.mm2-back:hover { 6576 border-color: var(--mm2-accent); 6577 background: var(--mm2-muted); 6578 transform: translateX(-3px); 6579 box-shadow: var(--mm2-shadow-md); 6580} 6581.mm2-detail-title { 6582 font-weight: 800; 6583 font-size: 18px; 6584 letter-spacing: -0.02em; 6585} 6586 6587/* Two-pane layout (desktop) - Enhanced */ 6588.mm2-detail-layout { 6589 display: grid; 6590 grid-template-columns: 280px 1fr; 6591 gap: 20px; 6592} 6593 6594/* Left: subnav (level-2 list) - Enhanced */ 6595.mm2-subnav { 6596 position: sticky; top: 8px; 6597 align-self: start; 6598 max-height: calc(70vh - 56px); 6599 overflow: auto; 6600 padding-right: 6px; 6601} 6602.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6603.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6604.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6605.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6606 6607.mm2-subitem { 6608 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6609 width: 100%; 6610 padding: 12px 14px; 6611 border-radius: 12px; 6612 border: 1.5px solid transparent; 6613 background: transparent; 6614 color: var(--mm2-fg); 6615 cursor: pointer; 6616 text-align: left; 6617 transition: all var(--mm2-transition-fast); 6618 margin-bottom: 6px; 6619 font-weight: 600; 6620 font-size: 14px; 6621 position: relative; 6622} 6623.mm2-subitem::before { 6624 content: ''; 6625 position: absolute; 6626 left: 0; 6627 top: 50%; 6628 transform: translateY(-50%); 6629 width: 3px; 6630 height: 0; 6631 background: var(--mm2-accent); 6632 border-radius: 0 2px 2px 0; 6633 transition: height var(--mm2-transition-fast); 6634} 6635.mm2-subitem:hover { 6636 background: var(--mm2-muted); 6637 color: var(--mm2-accent-hover); 6638} 6639.mm2-subitem.active { 6640 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6641 border-color: var(--mm2-accent-20); 6642 color: var(--mm2-accent-hover); 6643 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6644} 6645.mm2-subitem.active::before { 6646 height: 24px; 6647} 6648 6649/* Right: subcontent (level-3 grid) - Enhanced */ 6650.mm2-subcontent { min-height: 240px; } 6651.mm2-links { 6652 display: grid; gap: 12px 14px; 6653 grid-template-columns: repeat(3, minmax(0,1fr)); 6654} 6655@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6656 6657.mm2-link { 6658 display: flex; align-items: center; gap: 12px; 6659 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6660 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6661 min-height: 52px; 6662 font-weight: 500; 6663 font-size: 14px; 6664 transition: all var(--mm2-transition-fast); 6665 box-shadow: var(--mm2-shadow-sm); 6666 position: relative; 6667 justify-content: space-between; 6668} 6669.mm2-link:hover { 6670 background: var(--mm2-muted); 6671 border-color: var(--mm2-accent); 6672 transform: translateX(4px); 6673 box-shadow: var(--mm2-shadow-md); 6674} 6675.mm2-link-text { 6676 flex: 1; 6677} 6678 6679/* Product Thumbnails */ 6680.mm2-thumbs { 6681 display: flex; 6682 gap: 6px; 6683 align-items: center; 6684 margin-left: auto; 6685 flex-shrink: 0; 6686} 6687.mm2-thumb { 6688 width: 36px; 6689 height: 36px; 6690 border-radius: 8px; 6691 object-fit: cover; 6692 border: 1.5px solid var(--mm2-border); 6693 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6694 transition: all var(--mm2-transition-fast); 6695 background: var(--mm2-muted); 6696} 6697.mm2-link:hover .mm2-thumb { 6698 border-color: var(--mm2-accent); 6699 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6700 transform: scale(1.08); 6701} 6702
6703/* Thumbnail Skeleton Loader */ 6704.mm2-thumb-skeleton { 6705 width: 36px; 6706 height: 36px; 6707 border-radius: 8px; 6708 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6709 background-size: 200% 100%; 6710 animation: shimmer 1.5s infinite; 6711 border: 1.5px solid var(--mm2-border); 6712} 6713 6714.mm2-badge { 6715 font-size: 11px; padding: 3px 10px; margin-left: auto; 6716 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6717 font-weight: 700; 6718 border: 1px solid var(--mm2-accent-20); 6719} 6720 6721/* "See all" highlight card - Enhanced */ 6722.mm2-seeall { 6723 border: 2px dashed var(--mm2-accent); 6724 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6725 font-weight: 700; 6726} 6727.mm2-seeall:hover { 6728 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6729 border-style: solid; 6730 transform: translateX(6px) scale(1.02); 6731} 6732 6733/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6734@media (max-width: 960px) { 6735 .mm2-panel { 6736 position: fixed; 6737 left: 0; 6738 right: 0; 6739 bottom: 0; 6740 top: auto; 6741 width: 100%; 6742 max-height: 88vh; 6743 transform: translateX(0) translateY(100%); 6744 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6745 border-bottom: none; 6746 } 6747 .mm2-open .mm2-panel { 6748 transform: translateX(0) translateY(0); z-index: 9999; 6749 } 6750 6751 /* Enhanced backdrop for mobile */ 6752 .mm2-backdrop { 6753 backdrop-filter: blur(6px) saturate(1.2); 6754 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6755 } 6756 6757 /* Mobile close button - Soft glassmorphism */ 6758 .mm2-mobile-close { 6759 position: absolute; 6760 top: 4px; 6761 right: 8px; 6762 width: 28px; 6763 height: 28px; 6764 border-radius: 50%; 6765 border: 1px solid rgba(226, 232, 240, 0.6); 6766 background: rgba(255, 255, 255, 0.85); 6767 backdrop-filter: blur(12px) saturate(1.8); 6768 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6769 color: var(--mm2-fg-muted); 6770 display: inline-flex;
6771 align-items: center; 6772 justify-content: center; 6773 cursor: pointer; 6774 transition: all var(--mm2-transition-fast); 6775 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6776 z-index: 20; 6777 } 6778 .mm2-mobile-close:hover { 6779 background: rgba(254, 242, 242, 0.95); 6780 border-color: rgba(252, 165, 165, 0.8); 6781 color: #dc2626; 6782 box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); 6783 } 6784 .mm2-mobile-close:active { 6785 transform: scale(0.92); 6786 } 6787 6788 /* Drag handle indicator */ 6789 .mm2-top::before, 6790 .mm2-detail::before { 6791 content: ''; 6792 position: sticky; 6793 top: 0; 6794 left: 50%; 6795 transform: translateX(-50%); 6796 width: 40px; 6797 height: 4px; 6798 background: var(--mm2-border-strong); 6799 border-radius: 999px; 6800 margin: 12px auto 20px; 6801 display: block; 6802 z-index: 10; 6803 transition: background var(--mm2-transition-fast); 6804 } 6805 .mm2-panel:active .mm2-top::before, 6806 .mm2-panel:active .mm2-detail::before { 6807 background: #94a3b8; 6808 } 6809 6810 .mm2-top { 6811 padding: 10px; 6812 } 6813 6814 .mm2-top, .mm2-detail { 6815 max-height: min(82vh, 720px); 6816 padding-top: 8px; 6817 } 6818 6819 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6820 .mm2-detail-layout { display: block; } 6821 .mm2-subnav { 6822 position: static; max-height: none; overflow: visible; 6823 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6824 grid-template-columns: repeat(2, minmax(0,1fr)); 6825 border-bottom: 2px solid var(--mm2-muted); 6826 } 6827 .mm2-subitem { 6828 margin: 0; width: 100%; text-align: center; 6829 border-radius: 14px; 6830 background: #fff; border: 1.5px solid var(--mm2-border); 6831 white-space: normal; line-height: 1.3; min-height: 48px; 6832 justify-content: center; 6833 padding: 12px 10px; 6834 font-size: 13.5px; 6835 box-shadow: var(--mm2-shadow-sm); 6836 } 6837 .mm2-subitem::before { display: none; } 6838 .mm2-subitem:hover { 6839 transform: scale(1.02); 6840 } 6841 .mm2-subitem.active { 6842 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6843 border-color: var(--mm2-accent-hover); 6844 color: white; 6845 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6846 font-weight: 700; 6847 } 6848 6849 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6850 .mm2-links { 6851 grid-template-columns: repeat(2, minmax(0,1fr)); 6852 gap: 10px; 6853 } 6854 6855 /* Mobile: hide thumbnails untuk save space */ 6856 .mm2-thumbs { 6857 display: none; 6858 } 6859 6860 /* Larger touch targets for mobile */ 6861 .mm2-card { 6862 min-height: 80px; 6863 padding: 18px 14px; 6864 } 6865 .mm2-card .mm2-ico { 6866 width: 50px; 6867 height: 50px; 6868 } 6869 .mm2-card .mm2-title { 6870 font-size: 14px; 6871 } 6872 .mm2-card .mm2-sub { 6873 font-size: 11.5px; 6874 } 6875 .mm2-link { 6876 min-height: 56px; 6877 padding: 16px; 6878 } 6879 .mm2-back { 6880 width: 44px; 6881 height: 44px; 6882 } 6883 6884 /* Grid spacing */ 6885 .mm2-grid { 6886 gap: 10px; 6887 } 6888} 6889</style> 6890 <style data-inlined="product-compare.css"> 6891/* Product Compare Feature Styles */ 6892 6893/* Compare Button in Product Detail - Modern Chip Style */ 6894.pd-compare-section { 6895 margin: 0.5rem 0; 6896 display: flex; 6897 align-items: center; 6898 gap: 0.5rem; 6899} 6900 6901.pd-compare-btn { 6902 display: inline-flex; 6903 align-items: center; 6904 gap: 0.4rem; 6905 padding: 0.5rem 0.85rem; 6906 border: 1px solid #cbd5e1; 6907 border-radius: 20px; 6908 background: #fff; 6909 color: #64748b; 6910 font-weight: 600; 6911 font-size: 0.8rem; 6912 cursor: pointer; 6913 transition: all 0.2s ease; 6914 position: relative; 6915 white-space: nowrap; 6916} 6917 6918.pd-compare-btn:hover { 6919 border-color: #3b82f6; 6920 color: #3b82f6; 6921 background: #f0f9ff; 6922 transform: translateY(-1px); 6923 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); 6924} 6925 6926.pd-compare-btn.added { 6927 border-color: #16a34a; 6928 color: #16a34a; 6929 background: #f0fdf4; 6930} 6931 6932.pd-compare-btn .compare-remove { 6933 display: none; 6934} 6935 6936.pd-compare-btn.added .compare-text { 6937 display: none; 6938} 6939 6940.pd-compare-btn.added .compare-remove { 6941 display: flex !important; 6942 align-items: center; 6943 gap: 0.25rem; 6944 font-weight: 600; 6945 font-size: 0.9rem; 6946 color: #dc2626 !important; 6947 opacity: 1 !important; 6948 visibility: visible !important; 6949 z-index: 1; 6950 position: relative; 6951} 6952 6953.pd-compare-btn.added .compare-remove[hidden] { 6954 display: flex !important; 6955} 6956 6957/* Force visibility for added state */ 6958.pd-compare-btn.added .compare-remove { 6959 display: flex !important; 6960} 6961 6962[hidden].compare-remove { 6963 display: none !important; 6964} 6965 6966.pd-compare-btn.added [hidden].compare-remove { 6967 display: flex !important; 6968} 6969 6970/* Ensure the added state text is always visible */ 6971.pd-compare-btn.added { 6972 border-color: #dc2626 !important; 6973 color: #dc2626 !important; 6974 background: #fef2f2 !important; 6975} 6976 6977.pd-compare-btn.added .compare-remove svg { 6978 color: #dc2626 !important; 6979} 6980 6981.pd-compare-btn.added:hover { 6982 border-color: #15803d; 6983 color: #15803d; 6984 background: #dcfce7; 6985 transform: translateY(-1px); 6986 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 6987} 6988 6989.pd-compare-btn .compare-icon { 6990 width: 16px; 6991 height: 16px; 6992 flex-shrink: 0; 6993} 6994 6995.pd-compare-info { 6996 display: inline-flex; 6997 align-items: center; 6998} 6999 7000.compare-note { 7001 color: #94a3b8; 7002 font-size: 0.7rem; 7003 font-weight: 500; 7004 transition: color 0.2s ease; 7005} 7006 7007.pd-compare-btn.added + .pd-compare-info .compare-note { 7008 color: #16a34a; 7009 font-weight: 600; 7010} 7011 7012/* Compare Badge in Navigation */ 7013.compare-indicator { 7014 position: relative; 7015} 7016 7017.compare-indicator .badge { 7018 position: absolute; 7019 top: -8px; 7020 right: -8px; 7021 min-width: 18px;
7022 height: 18px; 7023 padding: 0 5px; 7024 background: #ef4444; 7025 color: white; 7026 border-radius: 9px; 7027 font-size: 11px; 7028 7029 7030 7031/* Floating Compare Badge (Sticky) */ 7032.compare-floating-badge { 7033 position: fixed; 7034 right: 16px; 7035 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7036 width: auto; 7037 min-width: 56px; 7038 height: 56px; 7039 padding: 0 16px; 7040 border-radius: 28px; 7041 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 7042 color: #fff; 7043 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.5); 7044 cursor: pointer; 7045 z-index: 1104; 7046 opacity: 0; 7047 transform: translateY(8px) scale(0.9); 7048 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7049 display: flex; 7050 align-items: center; 7051 justify-content: center; 7052 gap: 8px; 7053 font-weight: 600; 7054 font-size: 0.85rem; 7055 pointer-events: none; 7056} 7057 7058.compare-floating-badge.visible { 7059 opacity: 1; 7060 transform: translateY(0) scale(1); 7061 pointer-events: auto; 7062} 7063 7064.compare-floating-badge:hover { 7065 box-shadow: 0 12px 32px -10px rgba(59, 130, 246, 0.6); 7066 transform: translateY(-2px) scale(1.02); 7067} 7068 7069.compare-floating-badge:active { 7070 transform: translateY(0) scale(0.98); 7071} 7072 7073.compare-floating-badge .cfb-icon { 7074 width: 20px; 7075 height: 20px; 7076 flex-shrink: 0; 7077} 7078 7079.compare-floating-badge .cfb-count { 7080 font-size: 0.9rem; 7081 font-weight: 700; 7082 min-width: 20px; 7083 text-align: center; 7084 background: rgba(255, 255, 255, 0.25); 7085 padding: 2px 8px; 7086 border-radius: 12px; 7087 line-height: 1; 7088 display: flex; 7089 align-items: center; 7090 justify-content: center; 7091 height: 24px; 7092} 7093 7094.compare-floating-badge .cfb-text { 7095 display: none; 7096} 7097 7098@media (min-width: 641px) { 7099 .compare-floating-badge .cfb-text { 7100 display: block; 7101 white-space: nowrap; 7102 } 7103} 7104 7105@media (max-width: 640px) { 7106 .compare-floating-badge { 7107 right: 14px; 7108 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7109 min-width: 48px; 7110 height: 48px; 7111 padding: 0 12px; 7112 border-radius: 24px; 7113 } 7114 7115 .compare-floating-badge .cfb-icon { 7116 width: 18px; 7117 height: 18px; 7118 } 7119 7120 .compare-floating-badge .cfb-count { 7121 font-size: 0.8rem; 7122 padding: 2px 6px; 7123 height: 20px; 7124 } 7125} font-weight: 600; 7126 display: flex; 7127 align-items: center; 7128 justify-content: center; 7129 transform: scale(0); 7130 transition: transform 0.2s ease; 7131} 7132 7133.compare-indicator .badge.active { 7134 transform: scale(1); 7135} 7136 7137/* Compare Sidebar */ 7138.compare-sidebar { 7139 position: fixed; 7140 top: 0; 7141 right: -400px; 7142 width: 400px; 7143 height: 100vh; 7144 background: #ffffff; 7145 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7146 z-index: 9999; 7147 display: flex; 7148 flex-direction: column; 7149 transition: right 0.3s ease; 7150} 7151 7152.compare-sidebar.open { 7153 right: 0; 7154} 7155 7156.compare-overlay { 7157 position: fixed; 7158 top: 0; 7159 left: 0; 7160 width: 100%; 7161 height: 100%; 7162 background: rgba(0, 0, 0, 0.5); 7163 z-index: 9998; 7164 opacity: 0; 7165 visibility: hidden; 7166 transition: all 0.3s ease; 7167} 7168 7169.compare-overlay.active { 7170 opacity: 1; 7171 visibility: visible; 7172} 7173 7174.compare-header { 7175 display: flex; 7176 align-items: center; 7177 justify-content: space-between; 7178 padding: 1.25rem; 7179 border-bottom: 1px solid #e2e8f0; 7180 background: #f8fafc; 7181} 7182 7183.compare-title { 7184 display: flex; 7185 align-items: center; 7186 gap: 0.5rem; 7187 font-size: 1rem; 7188 font-weight: 600; 7189 color: #1e293b; 7190 margin: 0; 7191} 7192 7193.compare-count { 7194 color: #64748b; 7195 font-weight: 400; 7196} 7197 7198.compare-close { 7199 display: flex; 7200 align-items: center; 7201 justify-content: center; 7202 width: 32px; 7203 height: 32px; 7204 border: none; 7205 border-radius: 6px; 7206 background: transparent; 7207 color: #64748b; 7208 cursor: pointer; 7209 transition: all 0.2s ease; 7210} 7211 7212.compare-close:hover { 7213 background: #e2e8f0; 7214 color: #374151; 7215} 7216 7217.compare-content { 7218 flex: 1; 7219 overflow-y: auto; 7220 padding: 1rem; 7221} 7222 7223.compare-empty { 7224 display: flex; 7225 flex-direction: column; 7226 align-items: center; 7227 justify-content: center; 7228 height: 100%; 7229 text-align: center; 7230 padding: 2rem 1rem; 7231} 7232 7233.compare-empty h5 { 7234 margin: 1rem 0 0.5rem; 7235 color: #374151; 7236} 7237 7238.compare-empty p { 7239 color: #64748b; 7240 font-size: 0.875rem; 7241 line-height: 1.5; 7242} 7243 7244.compare-list { 7245 display: flex; 7246 flex-direction: column; 7247 gap: 0.75rem; 7248} 7249 7250.compare-item { 7251 display: flex; 7252 align-items: center; 7253 gap: 0.75rem; 7254 padding: 0.75rem; 7255 border: 1px solid #e2e8f0; 7256 border-radius: 8px; 7257 background: #ffffff; 7258 transition: all 0.2s ease; 7259} 7260 7261.compare-item:hover { 7262 border-color: #cbd5e1; 7263 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7264} 7265 7266.compare-item-image { 7267 flex-shrink: 0; 7268 width: 48px; 7269 height: 48px; 7270 border-radius: 6px; 7271 overflow: hidden; 7272 border: 1px solid #e2e8f0; 7273} 7274 7275.compare-item-image img { 7276 width: 100%; 7277 height: 100%; 7278 object-fit: cover; 7279} 7280 7281.compare-item-info { 7282 flex: 1; 7283 min-width: 0; 7284} 7285 7286.compare-item-name { 7287 font-weight: 500; 7288 font-size: 0.875rem; 7289 color: #1e293b; 7290 margin-bottom: 0.25rem; 7291 line-height: 1.3; 7292 display: -webkit-box; 7293 -webkit-line-clamp: 2; 7294 -webkit-box-orient: vertical; 7295 overflow: hidden; 7296} 7297 7298.compare-item-price { 7299 font-weight: 600; 7300 font-size: 0.8rem; 7301 color: #059669; 7302} 7303 7304.compare-item-remove { 7305 flex-shrink: 0; 7306 display: flex; 7307 align-items: center; 7308 justify-content: center; 7309 width: 28px; 7310 height: 28px; 7311 border: none; 7312 border-radius: 4px; 7313 background: transparent; 7314 color: #dc2626; 7315 cursor: pointer; 7316 transition: all 0.2s ease; 7317} 7318 7319.compare-item-remove:hover { 7320 background: #fef2f2; 7321 color: #991b1b; 7322} 7323 7324.compare-actions { 7325 padding: 1rem; 7326 border-top: 1px solid #e2e8f0; 7327 display: flex; 7328 gap: 0.75rem; 7329} 7330 7331.compare-actions .btn { 7332 flex: 1; 7333 padding: 0.75rem 1rem; 7334 border-radius: 6px; 7335 font-weight: 600; 7336 font-size: 0.875rem; 7337 display: flex; 7338 align-items: center; 7339 justify-content: center; 7340 gap: 0.5rem; 7341 cursor: pointer; 7342 transition: all 0.2s ease; 7343} 7344 7345.btn-outline-danger { 7346 border: 1px solid #dc2626; 7347 background: transparent; 7348 color: #dc2626; 7349} 7350 7351.btn-outline-danger:hover { 7352 background: #dc2626; 7353 color: white; 7354} 7355 7356.btn-primary { 7357 border: 1px solid #3b82f6; 7358 background: #3b82f6; 7359 color: white; 7360} 7361 7362.btn-primary:hover { 7363 background: #2563eb; 7364 border-color: #2563eb; 7365} 7366 7367.btn-primary:disabled { 7368 opacity: 0.5;
7369 cursor: not-allowed; 7370} 7371 7372.btn-primary:disabled:hover { 7373 background: #3b82f6; 7374 border-color: #3b82f6; 7375} 7376 7377/* AI Comparison Modal */ 7378.ai-compare-modal { 7379 position: fixed; 7380 top: 0; 7381 left: 0; 7382 width: 100%; 7383 height: 100%; 7384 z-index: 10000; 7385 display: none; 7386 align-items: center; 7387 justify-content: center; 7388} 7389 7390.ai-compare-modal.open { 7391 display: flex; 7392} 7393 7394.ai-compare-modal .modal-backdrop { 7395 position: absolute; 7396 top: 0; 7397 left: 0; 7398 width: 100%; 7399 height: 100%; 7400 background: rgba(0, 0, 0, 0.6); 7401 backdrop-filter: blur(4px); 7402} 7403 7404.modal-container { 7405 position: relative; 7406 width: 90%; 7407 max-width: 1000px; 7408 max-height: 90vh; 7409 background: #ffffff; 7410 border-radius: 12px; 7411 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7412 display: flex; 7413 flex-direction: column; 7414 overflow: hidden; 7415} 7416 7417.modal-header { 7418 display: flex; 7419 align-items: center; 7420 justify-content: space-between; 7421 padding: 1.5rem 2rem; 7422 border-bottom: 1px solid #e2e8f0; 7423 background: #f8fafc; 7424} 7425 7426.modal-title { 7427 display: flex; 7428 align-items: center; 7429 gap: 0.75rem; 7430 font-size: 1.25rem; 7431 font-weight: 600; 7432 color: #1e293b; 7433 margin: 0; 7434} 7435 7436.modal-close { 7437 display: flex; 7438 align-items: center; 7439 justify-content: center; 7440 width: 36px; 7441 height: 36px; 7442 border: none; 7443 border-radius: 8px; 7444 background: transparent; 7445 color: #64748b; 7446 cursor: pointer; 7447 transition: all 0.2s ease; 7448} 7449 7450.modal-close:hover { 7451 background: #e2e8f0; 7452 color: #374151; 7453} 7454 7455.modal-content { 7456 flex: 1; 7457 overflow-y: auto; 7458 padding: 2rem; 7459} 7460 7461/* AI Loading State */ 7462.ai-loading { 7463 display: flex; 7464 flex-direction: column; 7465 align-items: center; 7466 justify-content: center; 7467 padding: 3rem 2rem; 7468 text-align: center; 7469} 7470 7471.loading-animation { 7472 margin-bottom: 1rem; 7473} 7474 7475.typing-indicator { 7476 display: flex; 7477 align-items: center; 7478 justify-content: center; 7479 gap: 4px; 7480 margin-bottom: 1rem; 7481} 7482 7483.typing-indicator span { 7484 width: 8px; 7485 height: 8px; 7486 background: #3b82f6; 7487 border-radius: 50%; 7488 animation: typing 1.4s infinite ease-in-out; 7489} 7490 7491.typing-indicator span:nth-child(2) { 7492 animation-delay: 0.2s; 7493} 7494 7495.typing-indicator span:nth-child(3) { 7496 animation-delay: 0.4s; 7497} 7498 7499@keyframes typing { 7500 0%, 80%, 100% { 7501 opacity: 0.3; 7502 transform: scale(0.8); 7503 } 7504 40% { 7505 opacity: 1; 7506 transform: scale(1); 7507 } 7508} 7509 7510.ai-loading p { 7511 font-weight: 500; 7512 color: #1e293b; 7513 margin-bottom: 0.5rem; 7514} 7515 7516.ai-loading small { 7517 color: #64748b; 7518} 7519 7520/* AI Error State */ 7521.ai-error { 7522 display: flex; 7523 flex-direction: column; 7524 align-items: center; 7525 justify-content: center; 7526 padding: 3rem 2rem; 7527 text-align: center; 7528} 7529 7530.ai-error h4 { 7531 margin: 1rem 0 0.5rem; 7532 color: #dc2626; 7533} 7534 7535.ai-error p { 7536 color: #64748b; 7537 margin-bottom: 1.5rem; 7538} 7539 7540/* AI Results */ 7541.ai-results > div { 7542 margin-bottom: 2rem; 7543} 7544 7545.ai-summary-card, 7546.ai-comparison-table, 7547.ai-detailed-analysis, 7548.ai-recommendations, 7549.ai-tips { 7550 background: #f8fafc; 7551 border: 1px solid #e2e8f0; 7552 border-radius: 12px; 7553 padding: 1.5rem; 7554} 7555 7556.ai-results h4 { 7557 display: flex; 7558 align-items: center; 7559 gap: 0.5rem; 7560 font-size: 1.1rem; 7561 font-weight: 600; 7562 color: #1e293b; 7563 margin-bottom: 1rem; 7564} 7565 7566.summary-text { 7567 font-size: 1rem; 7568 line-height: 1.6; 7569 color: #374151; 7570 margin-bottom: 1rem; 7571} 7572 7573.winner-badge { 7574 padding: 1rem; 7575 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7576 color: white; 7577 border-radius: 8px; 7578} 7579 7580.winner-badge strong { 7581 display: block; 7582 margin-bottom: 0.25rem; 7583} 7584 7585.winner-badge small { 7586 opacity: 0.9; 7587 font-size: 0.875rem; 7588} 7589 7590/* Comparison Table */ 7591.comparison-grid { 7592 display: grid; 7593 gap: 1rem; 7594 margin-top: 1rem; 7595} 7596 7597.comparison-category { 7598 background: white; 7599 border: 1px solid #e2e8f0; 7600 border-radius: 8px; 7601 overflow: hidden; 7602} 7603 7604.category-header { 7605 background: #f1f5f9; 7606 padding: 0.75rem 1rem; 7607 font-weight: 600; 7608 color: #1e293b; 7609 border-bottom: 1px solid #e2e8f0; 7610} 7611 7612.category-products { 7613 display: grid; 7614 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7615 gap: 1px; 7616 background: #e2e8f0; 7617} 7618 7619.product-score { 7620 background: white; 7621 padding: 1rem; 7622 text-align: center; 7623} 7624 7625.score-value { 7626 display: inline-flex; 7627 align-items: center; 7628 gap: 0.25rem; 7629 margin-bottom: 0.5rem; 7630} 7631 7632.score-stars { 7633 color: #fbbf24; 7634} 7635 7636.score-note { 7637 font-size: 0.8rem; 7638 color: #64748b; 7639 line-height: 1.3; 7640} 7641 7642/* Product name header in comparison table */ 7643.product-name-header { 7644 margin-bottom: 0.5rem; 7645} 7646 7647.product-name-header strong { 7648 font-size: .8rem; /* h5 size (18px) */ 7649 font-weight: 600; 7650 color: #1e293b; 7651 line-height: 1.3; 7652 display: block; 7653 word-break: break-word; 7654} 7655 7656.product-price { 7657 font-size: 0.8rem; 7658 font-weight: 600; 7659 color: #059669; 7660} 7661 7662/* Detailed Analysis */ 7663.products-analysis { 7664 display: grid; 7665 gap: 1rem; 7666 margin-top: 1rem; 7667} 7668 7669.product-analysis { 7670 background: white; 7671 border: 1px solid #e2e8f0; 7672 border-radius: 8px; 7673 padding: 1rem; 7674} 7675 7676.product-analysis h5 { 7677 color: #1e293b; 7678 margin-bottom: 0.75rem; 7679 font-weight: 600; 7680} 7681 7682.analysis-section { 7683 margin-bottom: 0.75rem; 7684} 7685 7686.analysis-section:last-child { 7687 margin-bottom: 0; 7688} 7689 7690.analysis-section h6 { 7691 font-size: 0.875rem; 7692 font-weight: 600; 7693 margin-bottom: 0.5rem; 7694} 7695 7696.analysis-section h6.strengths { 7697 color: #059669; 7698} 7699 7700.analysis-section h6.weaknesses { 7701 color: #dc2626; 7702} 7703 7704.analysis-section h6.best-for { 7705 color: #7c3aed; 7706} 7707 7708.analysis-section ul { 7709 list-style: none; 7710 padding: 0; 7711 margin: 0; 7712} 7713 7714.analysis-section li { 7715 display: flex; 7716 align-items: flex-start; 7717 gap: 0.5rem; 7718 margin-bottom: 0.25rem; 7719 font-size: 0.875rem; 7720 line-height: 1.4; 7721} 7722 7723.analysis-section li:before { 7724 content: "â¢"; 7725 color: #64748b; 7726 font-weight: bold; 7727 margin-top: 0.1rem; 7728} 7729 7730.best-for-text { 7731 font-size: 0.875rem; 7732 color: #374151; 7733 font-style: italic; 7734} 7735 7736/* Recommendations */ 7737.recommendations-grid { 7738 display: grid; 7739 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7740 gap: 1rem; 7741 margin-top: 1rem; 7742} 7743 7744.recommendation-card { 7745 background: white; 7746 border: 1px solid #e2e8f0; 7747 border-radius: 8px; 7748 padding: 1rem; 7749 text-align: center; 7750} 7751 7752.recommendation-card h5 { 7753 font-size: 1rem; 7754 margin-bottom: 0.75rem; 7755 color: #1e293b; 7756} 7757 7758.recommendation-card p { 7759 font-size: 0.875rem; 7760 color: #374151; 7761 line-height: 1.5; 7762} 7763 7764/* Tips */ 7765.tips-list { 7766 list-style: none; 7767 padding: 0; 7768 margin: 1rem 0 0 0; 7769} 7770 7771.tips-list li { 7772 display: flex; 7773 align-items: flex-start; 7774 gap: 0.75rem; 7775 margin-bottom: 0.75rem; 7776 font-size: 0.9rem; 7777 line-height: 1.5; 7778 color: #374151; 7779} 7780 7781.tips-list li:before { 7782 content: "ð¡"; 7783 font-size: 1rem; 7784 margin-top: 0.1rem; 7785} 7786 7787/* Modal Footer */ 7788.modal-footer { 7789 display: flex; 7790 justify-content: space-between; 7791 align-items: center; 7792 padding: 1.5rem 2rem; 7793 border-top: 1px solid #e2e8f0; 7794 background: #f8fafc; 7795} 7796 7797.btn-outline-secondary { 7798 border: 1px solid #64748b; 7799 background: transparent; 7800 color: #64748b; 7801 padding: 0.75rem 1.5rem; 7802 border-radius: 6px; 7803 font-weight: 600; 7804 display: flex; 7805 align-items: center; 7806 gap: 0.5rem; 7807 cursor: pointer; 7808 transition: all 0.2s ease; 7809} 7810 7811.btn-outline-secondary:hover { 7812 background: #64748b; 7813 color: white; 7814} 7815 7816/* Responsive for chip button */ 7817@media (max-width: 880px) { 7818 .pd-compare-section { 7819 margin: 1rem 0; 7820 display: block; 7821 } 7822 7823 .pd-compare-btn { 7824 width: 100%; 7825 font-size: 0.85rem; 7826 padding: 0.65rem 1rem; 7827 border-radius: 10px; 7828 justify-content: center; 7829 min-height: 44px;
7830 } 7831 7832 .pd-compare-btn .compare-icon { 7833 width: 16px; 7834 height: 16px; 7835 } 7836 7837 .pd-compare-info { 7838 margin-top: 0.5rem; 7839 display: block; 7840 text-align: center; 7841 } 7842 7843 .compare-note { 7844 font-size: 0.7rem; 7845 display: block; 7846 } 7847} 7848 7849/* Responsive Design */ 7850@media (max-width: 768px) { 7851 .compare-sidebar { 7852 width: 100%; 7853 right: -100%; 7854 /* Fix for mobile viewport height changes */ 7855 height: 100vh; 7856 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7857 } 7858 7859 .compare-actions { 7860 /* Ensure actions are always visible */ 7861 position: sticky; 7862 bottom: 0; 7863 background: white; 7864 border-top: 2px solid #e2e8f0; 7865 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7866 z-index: 10; 7867 } 7868 7869 .compare-actions .btn { 7870 /* Larger touch targets for mobile */ 7871 min-height: 48px; 7872 font-size: 0.9rem; 7873 font-weight: 700; 7874 } 7875 7876 .modal-container { 7877 width: 95%; 7878 max-height: 95vh; 7879 max-height: 95dvh; /* Dynamic viewport height */ 7880 } 7881 7882 .modal-header, 7883 .modal-content, 7884 .modal-footer { 7885 padding: 1rem; 7886 } 7887 7888 .recommendations-grid { 7889 grid-template-columns: 1fr; 7890 } 7891 7892 .category-products { 7893 grid-template-columns: 1fr; 7894 } 7895 7896 .modal-footer { 7897 flex-direction: column; 7898 gap: 0.75rem; 7899 } 7900 7901 .modal-footer .btn { 7902 width: 100%; 7903 } 7904 7905 /* Ensure compare button is visible on mobile */ 7906 .pd-compare-section { 7907 display: block !important; 7908 visibility: visible !important; 7909 opacity: 1 !important; 7910 margin: 1rem 0; 7911 padding: 0; 7912 position: relative !important; 7913 z-index: 1 !important; 7914 max-width: 100%; 7915 overflow: hidden; 7916 } 7917 7918 .pd-compare-btn { 7919 display: flex !important; 7920 visibility: visible !important; 7921 opacity: 1 !important; 7922 width: 100%; 7923 padding: 0.75rem 1rem; 7924 font-size: 0.9rem; 7925 min-height: 48px; /* Minimum touch target for mobile */ 7926 border: 2px solid #e2e8f0 !important; 7927 background: #fff !important; 7928 position: relative !important; 7929 z-index: 2 !important; 7930 box-sizing: border-box; 7931 white-space: normal !important; /* Allow text wrapping on mobile */ 7932 flex-wrap: wrap; 7933 gap: 0.5rem; 7934 } 7935 7936 .pd-compare-btn .compare-text { 7937 display: inline !important; 7938 visibility: visible !important; 7939 font-weight: 600; 7940 color: #475569 !important; 7941 } 7942 7943 .pd-compare-btn .compare-remove { 7944 display: none !important; /* Hidden by default */ 7945 } 7946 7947 .pd-compare-btn.added .compare-remove { 7948 display: flex !important; /* Only show when added */ 7949 flex-wrap: wrap; 7950 align-items: center; 7951 gap: 0.4rem; 7952 white-space: normal !important; 7953 word-break: break-word; 7954 } 7955 7956 .pd-compare-btn .compare-icon { 7957 display: inline-block !important; 7958 width: 18px; 7959 height: 18px; 7960 flex-shrink: 0; 7961 } 7962 7963 .pd-compare-info { 7964 margin-top: 0.5rem; 7965 } 7966 7967 .compare-note { 7968 font-size: 0.85rem !important; 7969 display: block !important; 7970 visibility: visible !important; 7971 color: #64748b !important; 7972 } 7973} 7974 7975/* iOS Safari specific fixes */ 7976@media (max-width: 480px) { 7977 #compareBtn { 7978 width:46px; 7979 height:46px; 7980 } 7981 7982 .pd-compare-section { 7983 -webkit-transform: translateZ(0); 7984 transform: translateZ(0); 7985 } 7986 7987 .pd-compare-btn { 7988 -webkit-appearance: none !important; 7989 appearance: none !important; 7990 -webkit-tap-highlight-color: transparent; 7991 touch-action: manipulation; 7992 } 7993 7994 /* Fix sidebar height for iOS address bar */ 7995 .compare-sidebar { 7996 /* Use fixed positioning that accounts for iOS address bar */ 7997 height: 100vh; 7998 height: -webkit-fill-available; /* iOS Safari compatibility */ 7999 min-height: 100vh; 8000 min-height: -webkit-fill-available; 8001 } 8002 8003 /* Ensure compare actions stay at bottom */ 8004 .compare-actions { 8005 position: sticky; 8006 bottom: 0; 8007 background: white; 8008 border-top: 2px solid #e2e8f0; 8009 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8010 padding: 1.25rem 1rem; 8011 z-index: 20; 8012 } 8013 8014 .compare-actions .btn { 8015 min-height: 52px; /* Larger for easier tapping */ 8016 font-size: 1rem; 8017 font-weight: 700; 8018 border-radius: 8px; 8019 } 8020 8021 /* Adjust content padding to account for sticky actions */ 8022 .compare-content { 8023 padding-bottom: 6rem; /* Extra space for sticky actions */ 8024 } 8025} 8026 8027/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8028@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8029 .pd-compare-section { 8030 display: block !important; 8031 } 8032 8033 .pd-compare-btn { 8034 display: flex !important; 8035 min-height: 44px;
8036 font-size: 16px; /* Prevent zoom on iOS */ 8037 } 8038 8039 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8040 .compare-sidebar { 8041 /* Ensure full height coverage */ 8042 height: 100vh; 8043 height: 100dvh; /* Dynamic viewport height */ 8044 height: -webkit-fill-available; /* iOS fallback */ 8045 max-height: 100vh; 8046 max-height: 100dvh; 8047 max-height: -webkit-fill-available; 8048 overflow: hidden; /* Prevent scroll issues */ 8049 } 8050 8051 .compare-content { 8052 /* Flexible content area */ 8053 flex: 1; 8054 overflow-y: auto; 8055 -webkit-overflow-scrolling: touch; 8056 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8057 } 8058 8059 .compare-actions { 8060 /* Force actions to stay visible */ 8061 position: fixed; 8062 bottom: 0; 8063 left: 0; 8064 right: 0; 8065 width: 100%; 8066 background: white; 8067 border-top: 2px solid #e2e8f0; 8068 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8069 padding: 1rem; 8070 z-index: 9999; 8071 display: flex !important; 8072 gap: 0.75rem; 8073 } 8074 8075 .compare-actions .btn { 8076 flex: 1; 8077 min-height: 30px; /* Extra large for high-DPI screens */ 8078 font-size: .8rem; 8079 font-weight: 600; 8080 border-radius: 12px; 8081 display: flex !important; 8082 align-items: center; 8083 justify-content: center; 8084 gap: 0.5rem; 8085 } 8086 8087 /* Ensure AI analyze button is always visible and prominent */ 8088 #compareAnalyze { 8089 background: #3b82f6 !important; 8090 border-color: #3b82f6 !important; 8091 color: white !important; 8092 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8093 } 8094 8095 #compareAnalyze:not(:disabled) { 8096 background: #2563eb !important; 8097 border-color: #2563eb !important; 8098 transform: scale(1.02); 8099 } 8100 8101 #compareAnalyze:disabled { 8102 opacity: 0.6 !important; 8103 background: #9ca3af !important; 8104 border-color: #9ca3af !important; 8105 } 8106} 8107 8108/* AI Disclaimer Styles */ 8109.ai-disclaimer { 8110 display: flex; 8111 align-items: flex-start; 8112 gap: 0.75rem; 8113 padding: 1rem; 8114 background: #fef3c7; 8115 border: 1px solid #fbbf24; 8116 border-radius: 8px; 8117 margin: 0; 8118} 8119 8120.disclaimer-icon { 8121 font-size: 1.25rem; 8122 flex-shrink: 0; 8123 margin-top: 0.125rem; 8124} 8125 8126.disclaimer-text { 8127 font-size: 0.85rem; 8128 line-height: 1.4; 8129 color: #92400e; 8130} 8131 8132.disclaimer-text strong { 8133 color: #78350f; 8134 font-weight: 600; 8135} 8136 8137/* Compare Disclaimer in Sidebar */ 8138.compare-disclaimer { 8139 margin-top: 1rem; 8140 padding: 1rem; 8141 background: #f0f9ff; 8142 border: 1px solid #0ea5e9; 8143 border-radius: 8px; 8144} 8145 8146.compare-disclaimer h6 { 8147 display: flex; 8148 align-items: center; 8149 gap: 0.5rem; 8150 font-size: 0.875rem; 8151 font-weight: 600; 8152 color: #0c4a6e; 8153 margin-bottom: 0.5rem; 8154} 8155 8156.compare-disclaimer p { 8157 font-size: 0.8rem; 8158 line-height: 1.4; 8159 color: #075985; 8160 margin-bottom: 0.5rem; 8161} 8162 8163.compare-disclaimer p:last-child { 8164 margin-bottom: 0; 8165} 8166 8167.compare-disclaimer ul { 8168 list-style: none; 8169 padding: 0; 8170 margin: 0.5rem 0 0 0; 8171} 8172 8173.compare-disclaimer li { 8174 display: flex; 8175 align-items: flex-start; 8176 gap: 0.5rem; 8177 margin-bottom: 0.25rem; 8178 font-size: 0.75rem; 8179 line-height: 1.3; 8180 color: #075985; 8181} 8182 8183.compare-disclaimer li:before { 8184 content: "â¢"; 8185 color: #0ea5e9; 8186 font-weight: bold; 8187 margin-top: 0.1rem; 8188 flex-shrink: 0; 8189} 8190 8191/* Mobile Browser Address Bar Fixes */ 8192@supports (height: 100dvh) { 8193 /* Modern browsers with dynamic viewport support */ 8194 @media (max-width: 768px) { 8195 .compare-sidebar { 8196 height: 100dvh !important; 8197 max-height: 100dvh !important; 8198 } 8199 8200 .modal-container { 8201 max-height: 95dvh !important; 8202 } 8203 } 8204} 8205 8206@supports (height: -webkit-fill-available) { 8207 /* iOS Safari specific support */ 8208 @media (max-width: 768px) { 8209 .compare-sidebar { 8210 height: -webkit-fill-available !important; 8211 max-height: -webkit-fill-available !important; 8212 } 8213 } 8214} 8215 8216/* Fallback for older browsers */ 8217@media (max-width: 768px) and (orientation: portrait) { 8218 .compare-sidebar { 8219 /* Fixed height that works across different mobile browsers */ 8220 min-height: 600px; 8221 height: calc(100vh - env(keyboard-inset-height, 0px)); 8222 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8223 } 8224 8225 /* Ensure actions are always accessible */ 8226 .compare-actions { 8227 position: sticky !important; 8228 bottom: env(safe-area-inset-bottom, 0px) !important; 8229 margin-bottom: 0 !important; 8230 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8231 } 8232} 8233 8234/* PWA and fullscreen app support */ 8235@media (display-mode: standalone) { 8236 .compare-sidebar { 8237 height: 100vh !important; 8238 height: 100dvh !important; 8239 } 8240} 8241 8242/* Utilities */
8243.btn-sm { 8244 padding: 0.5rem 0.75rem; 8245 font-size: 0.8rem; 8246} 8247 8248[hidden] { 8249 display: none !important; 8250} 8251 8252/* ===== UX ENHANCEMENTS ===== */ 8253 8254/* Compare Button Highlight Animation */ 8255.pulse-highlight { 8256 animation: pulse-glow 2s ease-in-out 3; 8257 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8258} 8259 8260@keyframes pulse-glow { 8261 0% { 8262 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8263 } 8264 50% { 8265 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8266 } 8267 100% { 8268 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); 8269 } 8270} 8271 8272/* Flying Product Animation */ 8273.flying-product { 8274 position: fixed; 8275 z-index: 9999; 8276 pointer-events: none; 8277} 8278 8279.flying-product img { 8280 border-radius: 8px; 8281 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8282} 8283 8284/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8285 8286/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8287</style> 8288 <style data-inlined="product-card-actions.css"> 8289/** 8290 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8291 * Tokopedia-inspired design 8292 */ 8293 8294/* ===== 3 Dot Button on Product Card ===== */ 8295.prod-card { 8296 position: relative; 8297} 8298 8299.prod-card-actions-btn { 8300 position: absolute; 8301 top: 8px; 8302 right: 8px; 8303 width: 32px; 8304 height: 32px; 8305 border-radius: 50%; 8306 background: rgba(255, 255, 255, 0.95); 8307 border: 1px solid rgba(0, 0, 0, 0.08); 8308 display: inline-flex;
8309 align-items: center; 8310 justify-content: center; 8311 cursor: pointer; 8312 z-index: 3; 8313 transition: all 0.2s ease; 8314 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8315 -webkit-tap-highlight-color: transparent; 8316} 8317 8318.prod-card-actions-btn:hover { 8319 background: #fff; 8320 border-color: rgba(0, 0, 0, 0.15); 8321 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8322 transform: scale(1.05); 8323} 8324 8325.prod-card-actions-btn:active { 8326 transform: scale(0.95); 8327} 8328 8329.prod-card-actions-btn svg { 8330 width: 20px; 8331 height: 20px; 8332 color: #64748b; 8333 display: block; 8334} 8335 8336/* Desktop: Show on hover */ 8337@media (min-width: 641px) { 8338 .prod-card-actions-btn { 8339 opacity: 0; 8340 pointer-events: none; 8341 } 8342 8343 .prod-card:hover .prod-card-actions-btn { 8344 opacity: 1; 8345 pointer-events: auto; 8346 } 8347} 8348 8349/* Mobile: Always visible but smaller */ 8350@media (max-width: 640px) { 8351 .prod-card-actions-btn { 8352 width: 28px; 8353 height: 28px; 8354 top: 6px; 8355 right: 6px; 8356 opacity: 95%; 8357 } 8358 8359 .prod-card-actions-btn svg { 8360 width: 18px; 8361 height: 18px; 8362 } 8363} 8364 8365/* ===== Bottom Sheet Overlay ===== */ 8366.pca-overlay { 8367 position: fixed; 8368 inset: 0; 8369 background: rgba(15, 23, 42, 0.6); 8370 backdrop-filter: blur(4px); 8371 z-index: 99998; 8372 opacity: 0; 8373 visibility: hidden; 8374 transition: all 0.3s ease; 8375} 8376 8377.pca-overlay.active { 8378 opacity: 1; 8379 visibility: visible; 8380} 8381 8382/* ===== Bottom Sheet Container ===== */ 8383.pca-bottom-sheet { 8384 position: fixed; 8385 bottom: 0; 8386 left: 0; 8387 right: 0; 8388 background: #fff; 8389 border-radius: 20px 20px 0 0; 8390 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8391 z-index: 99999; 8392 max-width: 600px; 8393 margin: 0 auto; 8394 transform: translateY(100%); 8395 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8396} 8397 8398.pca-bottom-sheet.active { 8399 transform: translateY(0); 8400} 8401 8402/* Drag Handle */ 8403.pca-sheet-handle { 8404 width: 40px; 8405 height: 4px; 8406 background: #cbd5e1; 8407 border-radius: 999px; 8408 margin: 12px auto 8px; 8409} 8410 8411/* Product Preview */ 8412.pca-sheet-product { 8413 display: flex; 8414 gap: 12px; 8415 padding: 12px 20px; 8416 border-bottom: 1px solid #f1f5f9; 8417} 8418 8419.pca-product-image { 8420 width: 60px; 8421 height: 60px; 8422 border-radius: 8px; 8423 object-fit: cover; 8424 flex-shrink: 0; 8425 border: 1px solid #e2e8f0; 8426} 8427 8428.pca-product-info { 8429 flex: 1; 8430 min-width: 0; 8431} 8432 8433.pca-product-name { 8434 font-size: 14px; 8435 font-weight: 600; 8436 color: #0f172a; 8437 margin: 0 0 4px 0; 8438 display: -webkit-box; 8439 -webkit-line-clamp: 2; 8440 -webkit-box-orient: vertical; 8441 overflow: hidden; 8442 line-height: 1.3; 8443} 8444 8445.pca-product-price { 8446 font-size: 15px; 8447 font-weight: 700; 8448 color: #0ea5e9; 8449 margin: 0; 8450} 8451 8452.pca-product-stock { 8453 font-size: 12px; 8454 color: #64748b; 8455 margin: 2px 0 0 0; 8456} 8457 8458.pca-product-stock.in { 8459 color: #16a34a; 8460} 8461 8462.pca-product-stock.low { 8463 color: #ea580c; 8464} 8465 8466.pca-product-stock.out { 8467 color: #dc2626; 8468} 8469 8470/* Action Menu */ 8471.pca-sheet-actions { 8472 padding: 8px 0 20px; 8473} 8474 8475.pca-action-item { 8476 display: flex;
8477 align-items: center; 8478 gap: 16px; 8479 padding: 16px 20px; 8480 background: transparent; 8481 border: 0; 8482 width: 100%; 8483 cursor: pointer; 8484 transition: background-color 0.15s ease; 8485 -webkit-tap-highlight-color: transparent; 8486} 8487 8488.pca-action-item:hover { 8489 background: #f8fafc; 8490} 8491 8492.pca-action-item:active { 8493 background: #f1f5f9; 8494} 8495 8496.pca-action-item:disabled { 8497 opacity: 0.4; 8498 cursor: not-allowed; 8499} 8500 8501.pca-action-item:disabled:hover { 8502 background: transparent; 8503} 8504 8505.pca-action-icon { 8506 width: 24px; 8507 height: 24px; 8508 color: #475569; 8509 flex-shrink: 0; 8510} 8511 8512.pca-action-content { 8513 flex: 1; 8514 text-align: left; 8515} 8516 8517.pca-action-label { 8518 font-size: 15px; 8519 font-weight: 600; 8520 color: #0f172a; 8521 margin: 0 0 2px 0; 8522} 8523 8524.pca-action-desc { 8525 font-size: 12px; 8526 color: #64748b; 8527 margin: 0; 8528} 8529 8530.pca-action-badge { 8531 display: inline-flex; 8532 align-items: center; 8533 justify-content: center; 8534 min-width: 20px; 8535 height: 20px; 8536 padding: 0 6px; 8537 background: #0ea5e9; 8538 color: #fff; 8539 font-size: 11px; 8540 font-weight: 700; 8541 border-radius: 999px; 8542 margin-left: auto; 8543} 8544 8545.pca-action-badge.compare { 8546 background: #8b5cf6; 8547} 8548 8549/* Action Item Colors */ 8550.pca-action-item.cart .pca-action-icon { 8551 color: #0ea5e9; 8552} 8553 8554.pca-action-item.compare .pca-action-icon { 8555 color: #8b5cf6; 8556} 8557 8558.pca-action-item.compare.active { 8559 background: rgba(139, 92, 246, 0.05); 8560} 8561 8562.pca-action-item.compare.active .pca-action-label { 8563 color: #8b5cf6; 8564} 8565 8566.pca-action-item.compare.active .pca-action-icon { 8567 color: #8b5cf6; 8568} 8569 8570.pca-action-item.share .pca-action-icon { 8571 color: #64748b; 8572} 8573 8574/* Loading State */ 8575.pca-action-item.loading { 8576 pointer-events: none; 8577} 8578 8579.pca-action-item.loading .pca-action-icon { 8580 animation: spin 1s linear infinite; 8581} 8582 8583@keyframes spin { 8584 from { transform: rotate(0deg); } 8585 to { transform: rotate(360deg); } 8586} 8587 8588/* Success State Animation */ 8589.pca-action-item.success .pca-action-icon { 8590 animation: checkmark 0.5s ease; 8591} 8592 8593@keyframes checkmark { 8594 0% { transform: scale(0); } 8595 50% { transform: scale(1.2); } 8596 100% { transform: scale(1); } 8597} 8598 8599/* ===== Safe Area for Mobile Devices ===== */ 8600@supports (padding-bottom: env(safe-area-inset-bottom)) { 8601 .pca-sheet-actions { 8602 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8603 } 8604} 8605 8606/* ===== Responsive Tweaks ===== */ 8607@media (max-width: 640px) { 8608 .pca-bottom-sheet { 8609 border-radius: 16px 16px 0 0; 8610 } 8611 8612 .pca-sheet-product { 8613 padding: 10px 16px; 8614 } 8615 8616 .pca-product-image { 8617 width: 50px; 8618 height: 50px; 8619 } 8620 8621 .pca-product-name { 8622 font-size: 13px; 8623 } 8624 8625 .pca-product-price { 8626 font-size: 14px; 8627 } 8628 8629 .pca-action-item { 8630 padding: 14px 16px; 8631 gap: 12px; 8632 } 8633 8634 .pca-action-icon { 8635 width: 22px; 8636 height: 22px; 8637 } 8638 8639 .pca-action-label { 8640 font-size: 14px; 8641 } 8642} 8643 8644/* ===== Animation for Flying Product ===== */ 8645.flying-product { 8646 position: fixed; 8647 pointer-events: none; 8648 z-index: 99999; 8649 animation: fadeOut 0.8s ease forwards; 8650} 8651 8652@keyframes fadeOut { 8653 to { 8654 opacity: 0; 8655 transform: scale(0.5); 8656 } 8657} 8658 8659/* ===== Desktop Dropdown (Alternative) ===== */ 8660@media (min-width: 1024px) { 8661 /* Optional: Could add desktop dropdown menu here */ 8662 /* For now, using bottom sheet for consistency */ 8663} 8664 8665/* ===== Print Styles ===== */ 8666@media print { 8667 .prod-card-actions-btn, 8668 .pca-overlay, 8669 .pca-bottom-sheet { 8670 display: none !important; 8671 } 8672} 8673 8674</style> 8675 <style data-inlined="modal.css"> 8676/* Modern Modal Dialog for Cart Operations */ 8677.modal-overlay { 8678 position: fixed; 8679 top: 0; 8680 left: 0; 8681 right: 0; 8682 bottom: 0; 8683 background: rgba(0, 0, 0, 0.5); 8684 backdrop-filter: blur(4px); 8685 display: flex; 8686 align-items: center; 8687 justify-content: center; 8688 z-index: 9999; 8689 opacity: 0; 8690 visibility: hidden; 8691 transition: all 0.2s ease; 8692} 8693 8694.modal-overlay.show { 8695 opacity: 1;
8696 visibility: visible; 8697} 8698 8699.modal-dialog { 8700 background: white; 8701 border-radius: 16px; 8702 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8703 max-width: 400px; 8704 width: 90%; 8705 max-height: 90vh; 8706 overflow-y: auto; 8707 transform: scale(0.95); 8708 transition: transform 0.2s ease; 8709} 8710 8711.modal-overlay.show .modal-dialog { 8712 transform: scale(1); 8713} 8714 8715.modal-header { 8716 padding: 24px 24px 16px; 8717 border-bottom: 1px solid #f1f5f9; 8718} 8719 8720.modal-title { 8721 font-size: 20px; 8722 font-weight: 700; 8723 color: #0f172a; 8724 margin: 0; 8725 display: flex; 8726 align-items: center; 8727 gap: 12px; 8728} 8729 8730.modal-body { 8731 padding: 16px 24px 24px; 8732} 8733 8734.modal-message { 8735 font-size: 16px; 8736 line-height: 1.5; 8737 color: #475569; 8738 margin: 0; 8739} 8740 8741.modal-actions { 8742 display: flex; 8743 gap: 12px; 8744 margin-top: 24px; 8745} 8746 8747.modal-btn { 8748 flex: 1; 8749 padding: 12px 16px; 8750 border: none; 8751 border-radius: 8px; 8752 font-size: 14px; 8753 font-weight: 600; 8754 cursor: pointer; 8755 transition: all 0.2s ease; 8756 text-align: center; 8757} 8758 8759.modal-btn-secondary { 8760 background: #f8fafc; 8761 color: #64748b; 8762 border: 1px solid #e2e8f0; 8763} 8764 8765.modal-btn-secondary:hover { 8766 background: #f1f5f9; 8767 border-color: #cbd5e1; 8768} 8769 8770.modal-btn-danger { 8771 background: #ef4444; 8772 color: white; 8773} 8774 8775.modal-btn-danger:hover { 8776 background: #dc2626; 8777} 8778 8779.modal-btn-primary { 8780 background: #3b82f6; 8781 color: white; 8782} 8783 8784.modal-btn-primary:hover { 8785 background: #2563eb; 8786} 8787 8788.modal-icon { 8789 font-size: 24px; 8790 width: 32px; 8791 height: 32px; 8792 display: flex; 8793 align-items: center; 8794 justify-content: center; 8795 border-radius: 8px; 8796} 8797 8798.modal-icon.danger { 8799 background: #fef2f2; 8800 color: #ef4444; 8801} 8802 8803.modal-icon.warning { 8804 background: #fffbeb; 8805 color: #f59e0b; 8806} 8807 8808.modal-icon.success { 8809 background: #f0fdf4; 8810 color: #22c55e; 8811} 8812 8813.modal-icon.info { 8814 background: #eff6ff; 8815 color: #3b82f6; 8816} 8817 8818/* Mobile responsive */ 8819@media (max-width: 640px) { 8820 .modal-dialog { 8821 margin: 16px; 8822 max-width: none; 8823 width: auto; 8824 } 8825 8826 .modal-header, 8827 .modal-body { 8828 padding-left: 20px; 8829 padding-right: 20px; 8830 } 8831 8832 .modal-actions { 8833 flex-direction: column; 8834 } 8835 8836 .modal-btn { 8837 flex: none; 8838 } 8839} 8840</style> 8841 8842 <style data-inlined="reviews-points.css"> 8843/** 8844 * Reviews & Points System - Styles 8845 * Product reviews, rating display, and point redemption UI 8846 */ 8847 8848/* =================================== 8849 PRODUCT REVIEWS 8850 =================================== */ 8851 8852.product-tabs { 8853 display: flex; 8854 border-bottom: 2px solid #e5e7eb; 8855 margin: 24px 0 16px; 8856 gap: 8px; 8857} 8858 8859.tab-btn { 8860 padding: 12px 20px; 8861 border: none; 8862 background: none; 8863 cursor: pointer; 8864 font-size: 15px; 8865 font-weight: 500; 8866 color: #6b7280; 8867 border-bottom: 3px solid transparent; 8868 transition: all 0.2s ease; 8869 position: relative; 8870 top: 2px; 8871} 8872 8873.tab-btn:hover { 8874 color: #111827; 8875} 8876 8877.tab-btn.active { 8878 color: #2563eb; 8879 border-bottom-color: #2563eb; 8880} 8881 8882.tab-content { 8883 padding: 20px 0; 8884} 8885 8886/* Review Statistics */ 8887.review-stats { 8888 display: grid; 8889 grid-template-columns: 200px 1fr; 8890 gap: 32px; 8891 padding: 24px; 8892 background: #f9fafb; 8893 border-radius: 12px; 8894 margin-bottom: 24px; 8895} 8896 8897.rating-summary { 8898 text-align: center; 8899} 8900 8901.avg-rating { 8902 display: block; 8903 font-size: 48px; 8904 font-weight: 700; 8905 color: #111827; 8906 line-height: 1; 8907} 8908 8909.rating-summary .stars { 8910 font-size: 24px; 8911 color: #fbbf24; 8912 margin: 8px 0; 8913} 8914 8915.total-reviews { 8916 display: block; 8917 font-size: 14px; 8918 color: #6b7280; 8919 margin-top: 4px; 8920} 8921 8922.rating-breakdown { 8923 display: flex; 8924 flex-direction: column; 8925 gap: 8px; 8926} 8927 8928.rating-bar { 8929 display: flex; 8930 align-items: center; 8931 gap: 12px; 8932} 8933 8934.rating-label { 8935 font-size: 14px; 8936 color: #374151; 8937 width: 32px; 8938 text-align: right; 8939} 8940 8941.bar-container { 8942 flex: 1; 8943 height: 8px; 8944 background: #e5e7eb; 8945 border-radius: 4px; 8946 overflow: hidden; 8947} 8948 8949.bar-fill { 8950 height: 100%; 8951 background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%); 8952 transition: width 0.3s ease; 8953} 8954 8955.rating-count { 8956 font-size: 14px; 8957 color: #6b7280; 8958 width: 40px; 8959 text-align: right; 8960} 8961 8962/* Review Filters */ 8963.review-filters { 8964 display: flex; 8965 justify-content: space-between; 8966 align-items: center; 8967 margin-bottom: 20px; 8968} 8969 8970.review-sort { 8971 padding: 8px 12px; 8972 border: 1px solid #d1d5db; 8973 border-radius: 8px; 8974 font-size: 14px; 8975 background: white; 8976} 8977 8978.write-review-btn { 8979 padding: 10px 20px; 8980 background: #2563eb; 8981 color: white; 8982 border: none; 8983 border-radius: 8px; 8984 font-weight: 500;
8985 cursor: pointer; 8986 transition: background 0.2s; 8987} 8988 8989.write-review-btn:hover { 8990 background: #1d4ed8; 8991} 8992 8993/* Review List */ 8994.reviews-list { 8995 display: flex; 8996 flex-direction: column; 8997 gap: 16px; 8998} 8999 9000.review-item { 9001 padding: 20px; 9002 background: white; 9003 border: 1px solid #e5e7eb; 9004 border-radius: 12px; 9005 transition: box-shadow 0.2s; 9006} 9007 9008.review-item:hover { 9009 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9010} 9011 9012.review-header { 9013 display: flex; 9014 justify-content: space-between; 9015 align-items: flex-start; 9016 margin-bottom: 8px; 9017} 9018 9019.review-author { 9020 display: flex; 9021 align-items: center; 9022 gap: 8px; 9023} 9024 9025.review-author strong { 9026 font-size: 15px; 9027 color: #111827; 9028} 9029 9030.verified-badge { 9031 display: inline-flex; 9032 align-items: center; 9033 gap: 4px; 9034 font-size: 12px; 9035 color: #059669; 9036 background: #d1fae5; 9037 padding: 2px 8px; 9038 border-radius: 4px; 9039 font-weight: 500; 9040} 9041 9042.anonymous-badge { 9043 display: inline-flex; 9044 align-items: center; 9045 font-size: 12px; 9046 color: #6b7280; 9047 background: #f3f4f6; 9048 padding: 2px 6px; 9049 border-radius: 4px; 9050 margin-left: 4px; 9051 cursor: help; 9052} 9053 9054.review-rating .stars { 9055 font-size: 16px; 9056 color: #fbbf24; 9057} 9058 9059.review-date { 9060 font-size: 13px; 9061 color: #9ca3af; 9062 margin-bottom: 12px; 9063} 9064 9065.review-text { 9066 font-size: 15px; 9067 line-height: 1.6; 9068 color: #374151; 9069 margin-bottom: 12px; 9070} 9071 9072.review-images { 9073 display: flex; 9074 gap: 8px; 9075 margin-bottom: 12px; 9076 flex-wrap: wrap; 9077} 9078 9079.review-img { 9080 width: 80px; 9081 height: 80px; 9082 object-fit: cover; 9083 border-radius: 8px; 9084 cursor: pointer; 9085 transition: transform 0.2s; 9086} 9087 9088.review-img:hover { 9089 transform: scale(1.05); 9090} 9091 9092.review-actions { 9093 display: flex; 9094 gap: 12px; 9095 padding-top: 12px; 9096 border-top: 1px solid #f3f4f6; 9097} 9098 9099.btn-helpful { 9100 display: flex; 9101 align-items: center; 9102 gap: 6px; 9103 padding: 6px 12px; 9104 background: #f9fafb; 9105 border: 1px solid #e5e7eb; 9106 border-radius: 6px; 9107 font-size: 13px; 9108 color: #6b7280; 9109 cursor: pointer; 9110 transition: all 0.2s; 9111} 9112 9113.btn-helpful:hover { 9114 background: #f3f4f6; 9115 color: #111827; 9116 border-color: #d1d5db; 9117} 9118 9119.btn-helpful.voted { 9120 background: #dbeafe; 9121 color: #1d4ed8; 9122 border-color: #93c5fd; 9123} 9124 9125.helpful-icon { 9126 font-size: 14px; 9127} 9128 9129.no-reviews { 9130 text-align: center; 9131 padding: 60px 20px; 9132 color: #9ca3af; 9133 font-size: 15px; 9134} 9135 9136.load-more-btn { 9137 display: block; 9138 margin: 24px auto 0; 9139 padding: 12px 32px; 9140 background: white; 9141 border: 1px solid #d1d5db; 9142 border-radius: 8px; 9143 font-size: 14px; 9144 font-weight: 500; 9145 color: #374151; 9146 cursor: pointer; 9147 transition: all 0.2s; 9148} 9149 9150.load-more-btn:hover { 9151 background: #f9fafb; 9152 border-color: #9ca3af; 9153} 9154 9155/* =================================== 9156 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9157 =================================== */ 9158 9159.point-balance-card { 9160 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 9161 color: white; 9162 padding: 24px; 9163 border-radius: 16px; 9164 margin-bottom: 24px; 9165 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9166} 9167 9168.point-header { 9169 display: flex; 9170 justify-content: space-between; 9171 align-items: center; 9172 margin-bottom: 12px; 9173} 9174 9175.point-header h3 { 9176 font-size: 18px; 9177 font-weight: 600; 9178 margin: 0; 9179 display: flex; 9180 align-items: center; 9181 gap: 8px; 9182} 9183 9184.point-header .balance { 9185 font-size: 32px; 9186 font-weight: 700; 9187} 9188 9189.point-value { 9190 font-size: 14px; 9191 opacity: 0.9; 9192 margin: 8px 0 16px; 9193} 9194 9195.point-stats { 9196 display: flex; 9197 gap: 20px; 9198 font-size: 13px; 9199 opacity: 0.85; 9200 margin-bottom: 16px; 9201} 9202 9203.btn-view-history { 9204 display: inline-block; 9205 padding: 10px 20px; 9206 background: rgba(255, 255, 255, 0.2); 9207 color: white; 9208 text-decoration: none; 9209 border-radius: 8px; 9210 font-size: 14px; 9211 font-weight: 500; 9212 transition: background 0.2s; 9213} 9214 9215.btn-view-history:hover { 9216 background: rgba(255, 255, 255, 0.3); 9217} 9218 9219/* =================================== 9220 CHECKOUT POINT REDEMPTION 9221 =================================== */ 9222 9223.checkout-points { 9224 padding: 16px; 9225 background: #fef3c7; 9226 border: 2px solid #fbbf24; 9227 border-radius: 12px; 9228 margin-bottom: 16px; 9229} 9230 9231.points-header { 9232 display: flex; 9233 align-items: center; 9234 gap: 10px; 9235 margin-bottom: 12px; 9236} 9237 9238.points-header input[type="checkbox"] { 9239 width: 18px;
9240 height: 18px; 9241 cursor: pointer; 9242} 9243 9244.points-header label { 9245 font-size: 15px; 9246 color: #78350f; 9247 cursor: pointer; 9248 margin: 0; 9249} 9250 9251#points-slider { 9252 padding: 16px; 9253 background: white; 9254 border-radius: 8px; 9255 margin-top: 12px; 9256} 9257 9258#points-slider label { 9259 display: block; 9260 font-size: 14px; 9261 color: #374151; 9262 margin-bottom: 8px; 9263 font-weight: 500; 9264} 9265 9266#points-amount { 9267 width: 100%; 9268 margin-bottom: 8px; 9269} 9270 9271#points-display { 9272 display: flex; 9273 justify-content: space-between; 9274 font-size: 14px; 9275 color: #111827; 9276 margin-top: 8px; 9277} 9278 9279#points-display .discount-amount { 9280 color: #059669; 9281 font-weight: 600; 9282} 9283 9284.price-row.point-discount { 9285 color: #059669; 9286 font-weight: 500; 9287} 9288 9289/* =================================== 9290 REVIEWABLE ORDERS 9291 =================================== */ 9292 9293.reviewable-orders { 9294 background: white; 9295 border: 1px solid #e5e7eb; 9296 border-radius: 12px; 9297 padding: 20px; 9298 margin-bottom: 24px; 9299} 9300 9301.reviewable-orders h3 { 9302 font-size: 18px; 9303 margin-bottom: 16px; 9304 color: #111827; 9305} 9306 9307.order-card { 9308 display: flex; 9309 gap: 16px; 9310 padding: 16px; 9311 background: #f9fafb; 9312 border-radius: 8px; 9313 margin-bottom: 12px; 9314} 9315 9316.order-card:last-child { 9317 margin-bottom: 0; 9318} 9319 9320.order-card img { 9321 width: 80px; 9322 height: 80px; 9323 object-fit: cover; 9324 border-radius: 8px; 9325} 9326 9327.order-info { 9328 flex: 1; 9329} 9330 9331.order-info h4 { 9332 font-size: 15px; 9333 margin: 0 0 4px; 9334 color: #111827; 9335} 9336 9337.order-info p { 9338 font-size: 13px; 9339 color: #6b7280; 9340 margin: 0 0 12px; 9341} 9342 9343.btn-review { 9344 padding: 8px 16px; 9345 background: #059669; 9346 color: white; 9347 border: none; 9348 border-radius: 6px; 9349 font-size: 14px; 9350 font-weight: 500; 9351 cursor: pointer; 9352 transition: background 0.2s; 9353} 9354 9355.btn-review:hover { 9356 background: #047857; 9357} 9358 9359/* =================================== 9360 TOAST NOTIFICATIONS 9361 =================================== */ 9362 9363.review-toast { 9364 position: fixed; 9365 bottom: 24px; 9366 right: 24px; 9367 padding: 14px 20px; 9368 background: #111827; 9369 color: white; 9370 border-radius: 8px; 9371 font-size: 14px; 9372 line-height: 1.4; 9373 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9374 transform: translateY(100px); 9375 opacity: 0; 9376 transition: all 0.3s ease; 9377 z-index: 10000; 9378 max-width: 400px; 9379 word-wrap: break-word; 9380} 9381 9382.review-toast.show { 9383 transform: translateY(0); 9384 opacity: 1; 9385} 9386 9387/* Mobile: Position above bottom-nav */ 9388@media (max-width: 768px) { 9389 .review-toast { 9390 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9391 right: 16px; 9392 left: 16px; 9393 max-width: calc(100% - 32px); 9394 } 9395} 9396 9397.review-toast.toast-success { 9398 background: #059669; 9399} 9400 9401.review-toast.toast-error { 9402 background: #dc2626; 9403} 9404 9405.review-toast.toast-info { 9406 background: #2563eb; 9407} 9408 9409/* =================================== 9410 RESPONSIVE 9411 =================================== */ 9412 9413@media (max-width: 768px) { 9414 .point-balance-card { 9415 border-radius: 0; 9416 } 9417 9418 .review-stats { 9419 grid-template-columns: 70px 1fr; 9420 gap: 20px; 9421 padding: 16px; 9422 } 9423 9424 .rating-summary { 9425 text-align: center; 9426 } 9427 9428 .rating-summary .avg-rating { 9429 font-size: 32px; 9430 } 9431 9432 .rating-summary .stars { 9433 font-size: 18px; 9434 margin: 4px 0; 9435 } 9436 9437 .total-reviews { 9438 font-size: 11px; 9439 } 9440 9441 /* Rating breakdown - more compact */ 9442 .rating-breakdown { 9443 gap: 2px; 9444 } 9445 9446 .rating-breakdown .rating-bar { 9447 gap: 4px; 9448 } 9449 9450 .rating-breakdown .rating-label { 9451 font-size: 11px; 9452 width: 16px; 9453 min-width: 16px; 9454 text-align: left; 9455 } 9456 9457 .rating-breakdown .bar-container { 9458 height: 6px; 9459 } 9460 9461 .rating-breakdown .rating-count { 9462 font-size: 11px; 9463 width: 16px; 9464 min-width: 16px; 9465 } 9466 9467 .review-header { 9468 flex-direction: column; 9469 gap: 8px; 9470 align-items: flex-start; 9471 } 9472 9473 .review-rating { 9474 order: -1; 9475 margin-bottom: 4px; 9476 } 9477 9478 .review-rating .stars { 9479 font-size: 14px; 9480 } 9481 9482 .review-filters { 9483 flex-direction: column; 9484 gap: 12px;
9485 align-items: stretch; 9486 } 9487 9488 .review-sort { 9489 width: 100%; 9490 } 9491 9492 .point-header .balance { 9493 font-size: 24px; 9494 } 9495 9496 .point-stats { 9497 flex-direction: column; 9498 gap: 8px; 9499 } 9500 9501 .order-card { 9502 flex-direction: column; 9503 } 9504 9505 .order-card img { 9506 width: 100%; 9507 height: 200px; 9508 } 9509} 9510 9511/* =================================== 9512 LOADING & ERROR STATES 9513 =================================== */ 9514 9515.review-loading { 9516 text-align: center; 9517 padding: 40px; 9518 color: #9ca3af; 9519 font-size: 14px; 9520} 9521 9522.review-error { 9523 text-align: center; 9524 padding: 40px; 9525 color: #dc2626; 9526 font-size: 14px; 9527 background: #fee2e2; 9528 border-radius: 8px; 9529} 9530 9531.review-empty { 9532 text-align: center; 9533 padding: 10px; 9534 color: #6b7280; 9535 font-size: 14px; 9536} 9537 9538/* =================================== 9539 REVIEWABLE ORDERS LIST 9540 =================================== */ 9541 9542.reviewable-orders-list { 9543 display: flex; 9544 flex-direction: column; 9545 gap: 16px; 9546} 9547 9548.reviewable-order-item { 9549 display: grid; 9550 grid-template-columns: 80px 1fr auto; 9551 gap: 16px; 9552 padding: 16px; 9553 background: #fff; 9554 border: 1px solid #e5e7eb; 9555 border-radius: 12px; 9556 align-items: center; 9557 transition: box-shadow 0.2s; 9558} 9559 9560.reviewable-order-item:hover { 9561 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9562} 9563 9564.reviewable-order-item .roi-image { 9565 width: 80px; 9566 height: 80px; 9567 object-fit: cover; 9568 border-radius: 8px; 9569 border: 1px solid #e5e7eb; 9570} 9571 9572.reviewable-order-item .roi-info { 9573 display: flex; 9574 flex-direction: column; 9575 gap: 4px; 9576} 9577 9578.reviewable-order-item .roi-info h4 { 9579 margin: 0; 9580 font-size: 15px; 9581 font-weight: 600; 9582 color: #111827; 9583 line-height: 1.4; 9584} 9585 9586.reviewable-order-item .roi-meta { 9587 margin: 0; 9588 font-size: 13px; 9589 color: #6b7280; 9590} 9591 9592.reviewable-order-item .btn-write-review { 9593 white-space: nowrap; 9594 padding: 10px 20px; 9595 font-size: 14px; 9596} 9597 9598@media (max-width: 640px) { 9599 .reviewable-order-item { 9600 grid-template-columns: 60px 1fr; 9601 gap: 12px; 9602 } 9603 9604 .reviewable-order-item .roi-image { 9605 width: 60px; 9606 height: 60px; 9607 } 9608 9609 .reviewable-order-item .btn-write-review { 9610 grid-column: 1 / -1; 9611 width: 100%; 9612 } 9613} 9614 9615/* =================================== 9616 REVIEW SUBMISSION MODAL 9617 =================================== */ 9618 9619/* Modal Overlay Background */ 9620#review-modal-overlay { 9621 position: fixed; 9622 top: 0; 9623 left: 0; 9624 right: 0; 9625 bottom: 0; 9626 background: rgba(0, 0, 0, 0.6) !important; 9627 backdrop-filter: blur(8px); 9628 display: flex; 9629 align-items: center; 9630 justify-content: center; 9631 z-index: 99999 !important; 9632 padding: 20px; 9633} 9634 9635.review-modal { 9636 max-width: 600px !important; 9637 width: 90% !important; 9638 background: white; 9639 border-radius: 16px !important; 9640 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9641 max-height: 90vh; 9642 display: flex; 9643 flex-direction: column; 9644 overflow: hidden !important; 9645} 9646 9647#review-modal-overlay .modal-content, 9648.review-modal .modal-content { 9649 padding: 0 !important; 9650 flex: initial !important; 9651 overflow-y: visible !important; 9652} 9653 9654.review-modal .modal-header { 9655 display: flex; 9656 justify-content: space-between; 9657 align-items: center; 9658 padding: 20px 24px; 9659 border-bottom: 1px solid #e5e7eb; 9660} 9661 9662.review-modal .modal-title { 9663 margin: 0; 9664 font-size: 20px; 9665 font-weight: 700; 9666 color: #111827; 9667} 9668 9669.review-modal .modal-close { 9670 background: none; 9671 border: none; 9672 cursor: pointer; 9673 padding: 4px; 9674 color: #6b7280; 9675 transition: color 0.2s; 9676 display: flex; 9677 align-items: center; 9678 justify-content: center; 9679} 9680
9681.review-modal .modal-close:hover { 9682 color: #111827; 9683} 9684 9685.review-modal .modal-body { 9686 padding: 24px; 9687 max-height: calc(90vh - 140px); 9688 overflow-y: auto; 9689} 9690 9691/* Custom Scrollbar for Modal */ 9692.review-modal .modal-body::-webkit-scrollbar { 9693 width: 8px; 9694} 9695 9696.review-modal .modal-body::-webkit-scrollbar-track { 9697 background: #f1f5f9; 9698 border-radius: 4px; 9699} 9700 9701.review-modal .modal-body::-webkit-scrollbar-thumb { 9702 background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); 9703 border-radius: 4px; 9704} 9705 9706.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9707 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9708} 9709 9710/* Firefox Scrollbar */ 9711.review-modal .modal-body { 9712 scrollbar-width: thin; 9713 scrollbar-color: #3b82f6 #f1f5f9; 9714} 9715 9716.review-modal .modal-footer { 9717 display: flex; 9718 gap: 12px; 9719 margin-top: 24px; 9720 padding-top: 20px; 9721 border-top: 1px solid #f1f5f9; 9722} 9723 9724.review-product-info { 9725 display: flex; 9726 gap: 16px; 9727 padding: 16px; 9728 background: #f9fafb; 9729 border-radius: 8px; 9730 margin-bottom: 24px; 9731} 9732 9733.review-product-thumb { 9734 width: 80px; 9735 height: 80px; 9736 object-fit: cover; 9737 border-radius: 6px; 9738 border: 1px solid #e5e7eb; 9739} 9740 9741.review-product-details h4 { 9742 margin: 0 0 8px 0; 9743 font-size: 16px; 9744 font-weight: 600; 9745 color: #111827; 9746} 9747 9748.review-product-details p { 9749 margin: 0; 9750 font-size: 13px; 9751 color: #6b7280; 9752} 9753 9754/* Star Rating Input */ 9755.star-rating-input { 9756 display: inline-flex; 9757 gap: 8px; 9758 margin-right: 12px; 9759 vertical-align: middle; 9760} 9761 9762.star-btn { 9763 background: none; 9764 border: none; 9765 font-size: 32px; 9766 color: #d1d5db; 9767 cursor: pointer; 9768 transition: all 0.2s; 9769 padding: 0; 9770 line-height: 1; 9771} 9772 9773.star-btn:hover, 9774.star-btn.active { 9775 color: #fbbf24; 9776 transform: scale(1.1); 9777} 9778 9779.rating-label { 9780 display: inline-block; 9781 font-size: 15px; 9782 color: #6b7280; 9783 margin: 0; 9784 vertical-align: middle; 9785 min-width: 150px; 9786} 9787 9788/* Form Elements */ 9789.review-form .form-group { 9790 margin-bottom: 20px; 9791} 9792 9793.review-form .form-label { 9794 display: block; 9795 font-size: 14px; 9796 font-weight: 600; 9797 color: #374151; 9798 margin-bottom: 8px; 9799} 9800 9801.review-form .form-control { 9802 width: 100%; 9803 padding: 12px; 9804 border: 1px solid #d1d5db; 9805 border-radius: 8px; 9806 font-size: 14px; 9807 font-family: inherit; 9808 transition: border-color 0.2s; 9809} 9810 9811.review-form .form-control:focus { 9812 outline: none; 9813 border-color: #2563eb; 9814 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 9815} 9816 9817.review-form .form-hint { 9818 font-size: 13px; 9819 color: #6b7280; 9820 margin-top: 8px; 9821} 9822 9823/* Image Upload */ 9824.btn-upload-trigger { 9825 display: inline-flex; 9826 align-items: center; 9827 gap: 8px; 9828 padding: 10px 16px; 9829 background: #f9fafb; 9830 border: 2px dashed #d1d5db; 9831 border-radius: 8px; 9832 color: #374151; 9833 cursor: pointer; 9834 font-size: 14px; 9835 font-weight: 500; 9836 transition: all 0.2s; 9837} 9838 9839.btn-upload-trigger:hover { 9840 background: #f3f4f6; 9841 border-color: #2563eb; 9842 color: #2563eb; 9843} 9844 9845.image-preview-grid { 9846 display: grid; 9847 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9848 gap: 12px; 9849 margin-top: 16px; 9850} 9851 9852.image-preview-item { 9853 position: relative; 9854 aspect-ratio: 1; 9855 border-radius: 8px; 9856 overflow: hidden; 9857 border: 1px solid #e5e7eb; 9858} 9859 9860.image-preview-item img { 9861 width: 100%; 9862 height: 100%; 9863 object-fit: cover; 9864} 9865 9866.remove-image { 9867 position: absolute; 9868 top: 4px; 9869 right: 4px; 9870 background: rgba(0, 0, 0, 0.7); 9871 border: none; 9872 color: white; 9873 width: 24px; 9874 height: 24px; 9875 border-radius: 50%; 9876 cursor: pointer; 9877 display: flex; 9878 align-items: center; 9879 justify-content: center; 9880 transition: background 0.2s; 9881} 9882 9883.remove-image:hover { 9884 background: rgba(0, 0, 0, 0.9); 9885} 9886 9887/* Point Reward Info */ 9888.point-reward-info { 9889 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 9890 border: 2px solid #fbbf24; 9891 border-radius: 12px; 9892 padding: 16px; 9893 margin: 20px 0; 9894} 9895 9896.pri-header { 9897 display: flex; 9898 align-items: center; 9899 gap: 8px; 9900 font-size: 16px; 9901 font-weight: 700;
9902 color: #92400e; 9903 margin-bottom: 12px; 9904} 9905 9906.pri-list { 9907 list-style: none; 9908 padding: 0; 9909 margin: 0 0 12px 0; 9910} 9911 9912.pri-list li { 9913 padding: 6px 0; 9914 font-size: 14px; 9915 color: #78350f; 9916 transition: opacity 0.2s; 9917} 9918 9919.pri-total { 9920 padding-top: 12px; 9921 border-top: 2px solid rgba(251, 191, 36, 0.3); 9922 font-size: 16px; 9923 color: #92400e; 9924 font-weight: 600; 9925} 9926 9927.form-error { 9928 background: #fee2e2; 9929 border: 1px solid #f87171; 9930 color: #991b1b; 9931 padding: 12px; 9932 border-radius: 8px; 9933 font-size: 14px; 9934 margin-top: 16px; 9935} 9936 9937/* Buttons */ 9938.review-form .btn { 9939 padding: 12px 24px; 9940 border: none; 9941 border-radius: 8px; 9942 font-size: 15px; 9943 font-weight: 600; 9944 cursor: pointer; 9945 transition: all 0.2s; 9946} 9947 9948.review-form .btn-primary { 9949 background: #2563eb; 9950 color: white; 9951} 9952 9953.review-form .btn-primary:hover { 9954 background: #1d4ed8; 9955} 9956 9957.review-form .btn-primary:disabled { 9958 background: #94a3b8; 9959 cursor: not-allowed; 9960} 9961 9962.review-form .btn-secondary { 9963 background: #f1f5f9; 9964 color: #64748b; 9965} 9966 9967.review-form .btn-secondary:hover { 9968 background: #e2e8f0; 9969 color: #475569; 9970} 9971 9972@media (max-width: 768px) { 9973 #review-modal-overlay { padding: 0; } 9974 .review-modal { max-width: 100%; width: 100% !important; } 9975} 9976 9977/* =================================== 9978 POINT SUMMARY GRID (Profile Page) 9979 =================================== */ 9980 9981.point-summary-grid { 9982 display: grid; 9983 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 9984 gap: 12px; 9985 margin-bottom: 32px; 9986} 9987 9988.point-summary-card { 9989 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 9990 border: 1px solid #e5e7eb; 9991 border-radius: 12px; 9992 padding: 16px 20px; 9993 display: flex; 9994 align-items: center; 9995 justify-content: space-between; 9996 gap: 12px; 9997 transition: all 0.2s; 9998} 9999 10000.point-summary-card:hover { 10001 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10002 transform: translateY(-2px); 10003} 10004 10005.point-summary-card .psc-left { 10006 display: flex; 10007 flex-direction: column; 10008 gap: 2px; 10009} 10010 10011.point-summary-card .psc-icon { 10012 font-size: 24px; 10013 margin-bottom: 4px; 10014} 10015 10016.point-summary-card .psc-label { 10017 font-size: 13px; 10018 color: #6b7280; 10019 font-weight: 500; 10020 text-transform: uppercase; 10021 letter-spacing: 0.5px; 10022} 10023 10024.point-summary-card .psc-value { 10025 font-size: 26px; 10026 font-weight: 700; 10027 color: #111827; 10028 flex-shrink: 0; 10029} 10030 10031/* Point History Section */ 10032.point-history-section { 10033 margin-top: 32px; 10034} 10035 10036.point-history-section h3 { 10037 font-size: 18px; 10038 font-weight: 600; 10039 color: #111827; 10040 margin-bottom: 16px; 10041} 10042 10043.point-history-list { 10044 display: flex; 10045 flex-direction: column; 10046 gap: 12px; 10047} 10048 10049.point-history-item { 10050 display: grid; 10051 grid-template-columns: 40px 1fr auto; 10052 gap: 16px; 10053 padding: 16px; 10054 background: white; 10055 border: 1px solid #e5e7eb; 10056 border-radius: 10px; 10057 align-items: center; 10058 transition: all 0.2s; 10059} 10060 10061.point-history-item:hover { 10062 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10063} 10064 10065.point-history-item.credit { 10066 border-left: 3px solid #10b981; 10067} 10068 10069.point-history-item.debit { 10070 border-left: 3px solid #f59e0b; 10071} 10072 10073.point-history-item .phi-icon { 10074 width: 40px; 10075 height: 40px; 10076 display: flex; 10077 align-items: center; 10078 justify-content: center; 10079 font-size: 20px; 10080 border-radius: 50%; 10081 background: #f9fafb; 10082} 10083 10084.point-history-item.credit .phi-icon { 10085 background: #d1fae5; 10086} 10087 10088.point-history-item.debit .phi-icon { 10089 background: #fef3c7; 10090} 10091
10092.point-history-item .phi-details { 10093 display: flex; 10094 flex-direction: column; 10095 gap: 4px; 10096} 10097 10098.point-history-item .phi-desc { 10099 font-size: 15px; 10100 font-weight: 500; 10101 color: #111827; 10102} 10103 10104.point-history-item .phi-date { 10105 font-size: 13px; 10106 color: #9ca3af; 10107} 10108 10109.point-history-item .phi-amount { 10110 font-size: 18px; 10111 font-weight: 700; 10112} 10113 10114.point-history-item .phi-amount.credit { 10115 color: #10b981; 10116} 10117 10118.point-history-item .phi-amount.debit { 10119 color: #f59e0b; 10120} 10121 10122@media (max-width: 640px) { 10123 .point-summary-grid { 10124 grid-template-columns: 1fr; 10125 } 10126 10127 .point-summary-card .psc-value { 10128 font-size: 22px; 10129 } 10130 10131 .point-history-item { 10132 grid-template-columns: 36px 1fr; 10133 gap: 12px; 10134 } 10135 10136 .point-history-item .phi-amount { 10137 grid-column: 2; 10138 text-align: right; 10139 margin-top: 8px; 10140 } 10141} 10142 10143/* ââ Pending points: compact widget indicator ââ */ 10144.pbc-pending { 10145 font-size: 12px; 10146 opacity: 0.85; 10147 margin-top: 4px; 10148 color: #fde68a; 10149} 10150 10151/* ââ Pending points: summary card ââ */ 10152.point-summary-card.psc-pending-card { 10153 background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); 10154 border-color: #fcd34d; 10155} 10156 10157.point-summary-card.psc-pending-card .psc-value { 10158 color: #b45309; 10159} 10160 10161.point-summary-card .psc-sub { 10162 font-size: 11px; 10163 color: #9ca3af; 10164 margin-top: 4px; 10165} 10166 10167/* ââ Pending points: history item ââ */ 10168.point-history-item.phi-pending { 10169 opacity: 0.75; 10170 border-left-color: #fcd34d; 10171 background: #fffbeb; 10172} 10173 10174.point-history-item.phi-pending .phi-icon { 10175 background: #fef3c7; 10176} 10177 10178.phi-pending-badge { 10179 display: inline-block; 10180 font-size: 11px; 10181 font-weight: 600; 10182 padding: 2px 7px; 10183 border-radius: 10px; 10184 background: #fef3c7; 10185 color: #92400e; 10186 border: 1px solid #fcd34d; 10187 margin-left: 8px; 10188 vertical-align: middle; 10189} 10190 10191/* ââ Reversed points: history item ââ */ 10192.point-history-item.phi-reversed { 10193 opacity: 0.55; 10194 border-left-color: #d1d5db; 10195 background: #f9fafb; 10196} 10197 10198.point-history-item.phi-reversed .phi-icon { 10199 background: #f3f4f6; 10200} 10201 10202.point-history-item.phi-reversed .phi-amount { 10203 text-decoration: line-through; 10204 color: #9ca3af !important; 10205} 10206 10207.phi-reversed-badge { 10208 display: inline-block; 10209 font-size: 11px; 10210 font-weight: 600; 10211 padding: 2px 7px; 10212 border-radius: 10px; 10213 background: #fee2e2; 10214 color: #991b1b; 10215 border: 1px solid #fca5a5; 10216 margin-left: 8px; 10217 vertical-align: middle; 10218} 10219 10220</style> 10221 10222</head> 10223<body> 10224<header class="topbar"> 10225 <div class="container topbar-inner"> 10226 <div class="topbar-left"> 10227 BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler 10228 </div> 10229 10230 <div class="topbar-right"> 10231 <a href="https://mgsco.org/">Utama</a> 10232 <a href="https://mgsco.org/">Koleksi Game</a> 10233 <a href="https://mgsco.org/">Game Terpopuler</a> 10234 <a href="https://mgsco.org/boykita/">Daftar BOYTOTO</a> 10235 <a href="https://mgsco.org/boykita/">Login BOYTOTO</a> 10236 </div> 10237 </div> 10238</header> 10239<nav class="navbar"> 10240 <nav class="navbar-logo" style="width: 100%;"> 10241 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10242 <a href="https://mgsco.org/" class="logo-mobile" title="BOYTOTO"> 10243 <img src="https://ik.imagekit.io/boytoto/logo.webp" alt="BOYTOTO" height="40"> 10244 </a> 10245 </div> 10246</nav> 10247 <div class="container nav-inner"> 10248 <a href="https://mgsco.org/" class="logo" title="BOYTOTO"> 10249 <img src="https://ik.imagekit.io/boytoto/logo.webp" alt="BOYTOTO" height="40"> 10250 </a> 10251 10252 10253 10254 <form class="search" method="get" action="#"> 10255 <input type="hidden" name="page" value="produk"> 10256 10257 <!-- Search Icon (Desktop) --> 10258 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10259 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10260 <circle cx="11" cy="11" r="8"/> 10261 <path d="m21 21-4.35-4.35"/> 10262 </svg> 10263 </button> 10264 10265 <input type="text" name="q" id="searchInput" placeholder="Cari BOYTOTO Di Google" 10266 value="" autocomplete="off"> 10267 10268 <div class="search-suggest" id="searchSuggest" hidden></div> 10269 </form> 10270 10271 <div class="nav-actions"> 10272 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10273 <button class="icon-btn cart-indicator" id="cartBtn" title="Catatan Shio"> 10274 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10275 </button> 10276 10277 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10278 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10279 </button> 10280 10281 <!-- Dynamic login/logout button --> 10282 <button class="login-btn" id="loginBtn" aria-label="Masuk">
10283 <span class="login-text">Masuk</span> 10284 <span class="login-ico" aria-hidden="true">ð</span> 10285 </button> 10286 </div> 10287 </div> 10288 10289 <div class="container" style="display: flex; align-items: center;"> 10290 10291 10292 10293 <div class="mega-cats-wrapper"> 10294 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10295 <a class="" href="https://mgsco.org/"> 10296 BOYTOTO </a> 10297 <a class="" href="https://mgsco.org/"> 10298 Link BOYTOTO </a> 10299 <a class="" href="https://mgsco.org/"> 10300 Game Online </a> 10301 <a class="" href="https://mgsco.org/"> 10302 Pelopor Terpercaya </a> 10303 <a class="" href="https://mgsco.org/"> 10304 Game Resmi </a> 10305 <a class="" href="https://mgsco.org/"> 10306 Pelopor Gacor </a> 10307 </div> 10308 <div class="mega-cats-fade" id="megaCatsFade"></div> 10309 </div> 10310 </div> 10311</nav> 10312 10313<!-- Panel Mega Menu - Outside navbar for full-screen --> 10314<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10315 <div class="mm2-view is-active" data-mm2="top"></div> 10316 <div class="mm2-view" data-mm2="detail"></div> 10317</div> 10318 10319<!-- Backdrop - Outside navbar for full-screen --> 10320<div class="mm2-backdrop" data-mm2="backdrop"></div> 10321 10322<!-- Mobile Bottom Navigation --> 10323<nav class="bottom-nav"> 10324 <div class="bottom-nav-inner"> 10325 <!-- Home --> 10326 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10327 <div class="bottom-nav-item-icon"> 10328 <!-- Outlined icon (default) --> 10329 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10330 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10331 <polyline points="9 22 9 12 15 12 15 22"/> 10332 </svg> 10333 <!-- Filled icon (active) --> 10334 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10335 <path d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 010 1.06l-.03.03a.75.75 0 01-1.06 0l-8.16-8.16-8.16 8.16a.75.75 0 01-1.06 0l-.03-.03a.75.75 0 010-1.06l8.69-8.69z"/> 10336 <path d="M12 5.5l-7.5 7.5v8.25c0 .414.336.75.75.75h3.75v-6h6v6h3.75a.75.75 0 00.75-.75V13l-7.5-7.5z"/> 10337 </svg> 10338 </div> 10339 <span class="bottom-nav-item-label">Home</span> 10340 </a> 10341 10342 <!-- Kategori --> 10343 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10344 <div class="bottom-nav-item-icon"> 10345 <!-- Outlined icon (default) --> 10346 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10347 <line x1="3" y1="12" x2="21" y2="12"/> 10348 <line x1="3" y1="6" x2="21" y2="6"/> 10349 <line x1="3" y1="18" x2="21" y2="18"/> 10350 </svg> 10351 <!-- Filled icon (active) --> 10352 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10353 <path d="M3 5.25A.75.75 0 013.75 4.5h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 5.25zM3 12a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 12zm0 6.75a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75a.75.75 0 01-.75-.75z"/> 10354 </svg> 10355 </div> 10356 <span class="bottom-nav-item-label">Kategori</span> 10357 </button> 10358 10359 <!-- Keranjang --> 10360 <button class="bottom-nav-item" id="bottomCartBtn"> 10361 <div class="bottom-nav-item-icon"> 10362 <!-- Outlined icon (default) --> 10363 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10364 <circle cx="9" cy="21" r="1"/> 10365 <circle cx="20" cy="21" r="1"/> 10366 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10367 </svg> 10368 <!-- Filled icon (active) -->
10369 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10370 <path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"/> 10371 </svg> 10372 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10373 </div> 10374 <span class="bottom-nav-item-label">Catatan Shio</span> 10375 </button> 10376 10377 <!-- Compare --> 10378 <button class="bottom-nav-item" id="bottomCompareBtn"> 10379 <div class="bottom-nav-item-icon"> 10380 <!-- Outlined icon (default) --> 10381 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10382 <polyline points="17 11 21 7 17 3"/> 10383 <path d="M21 7H9"/> 10384 <polyline points="7 21 3 17 7 13"/> 10385 <path d="M15 17H3"/> 10386 </svg> 10387 <!-- Filled icon (active) --> 10388 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10389 <path d="M21.75 6.75a.75.75 0 00-.75-.75H9a.75.75 0 000 1.5h10.19L15.22 11.47a.75.75 0 001.06 1.06l4.5-4.5a.75.75 0 00.22-.53.75.75 0 00-.22-.53l-.03-.03zM8.78 12.47L4.28 16.97a.75.75 0 000 1.06l4.5 4.5a.75.75 0 001.06-1.06L6.06 18H15a.75.75 0 000-1.5H4.81l3.97-3.97a.75.75 0 00-1.06-1.06z"/> 10390 </svg> 10391 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10392 </div> 10393 <span class="bottom-nav-item-label">Compare</span> 10394 </button> 10395 10396 <!-- Chat - Always show, check login in JavaScript --> 10397 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10398 <div class="bottom-nav-item-icon"> 10399 <!-- Outlined icon (default) --> 10400 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10401 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10402 </svg> 10403 <!-- Filled icon (active) --> 10404 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10405 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10406 </svg> 10407 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10408 </div> 10409 <span class="bottom-nav-item-label">Chat</span> 10410 </button> 10411 10412 <!-- Akun --> 10413 <button class="bottom-nav-item" id="bottomLoginBtn"> 10414 <div class="bottom-nav-item-icon"> 10415 <!-- Outlined icon (default) --> 10416 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10417 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10418 <circle cx="12" cy="7" r="4"/> 10419 </svg> 10420 <!-- Filled icon (active) --> 10421 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10422 <path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/> 10423 </svg> 10424 </div> 10425 <span class="bottom-nav-item-label">Login</span> 10426 </button> 10427 </div> 10428</nav> 10429 10430<!-- Mobile User Menu Bottom Sheet --> 10431
10432<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10433(function(){ 10434 const btn = document.getElementById('megaMoreBtn'); 10435 const dd = document.getElementById('megaMoreDropdown'); 10436 if(!btn || !dd) return; 10437 10438 function openDD(){ 10439 dd.hidden = false; 10440 btn.setAttribute('aria-expanded','true'); 10441 document.addEventListener('click', outside, {capture:true}); 10442 document.addEventListener('keydown', esc); 10443 } 10444 function closeDD(){ 10445 if(dd.hidden) return; 10446 dd.hidden = true; 10447 btn.setAttribute('aria-expanded','false'); 10448 document.removeEventListener('click', outside, {capture:true}); 10449 document.removeEventListener('keydown', esc); 10450 } 10451 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10452 function outside(e){ 10453 if(!dd.contains(e.target) && e.target !== btn){ 10454 closeDD(); 10455 } 10456 } 10457 function esc(e){ 10458 if(e.key === 'Escape'){ 10459 closeDD(); 10460 btn.focus(); 10461 } 10462 } 10463 btn.addEventListener('click', toggle); 10464})(); 10465 10466// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10467(function(){ 10468 const megaCats = document.getElementById('megaCatsScroll'); 10469 const fade = document.getElementById('megaCatsFade'); 10470 if (!megaCats || !fade) return; 10471 10472 function updateFade() { 10473 const scrollLeft = megaCats.scrollLeft; 10474 const scrollWidth = megaCats.scrollWidth; 10475 const clientWidth = megaCats.clientWidth; 10476 const maxScroll = scrollWidth - clientWidth; 10477 10478 // Hide fade when scrolled to end (within 10px tolerance) 10479 if (maxScroll - scrollLeft <= 10) { 10480 fade.style.opacity = '0'; 10481 } else { 10482 fade.style.opacity = '1'; 10483 } 10484 } 10485 10486 // Update on scroll 10487 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10488 10489 // Update on resize 10490 window.addEventListener('resize', updateFade, { passive: true }); 10491 10492 // Initial check 10493 setTimeout(updateFade, 100); 10494})(); 10495 10496document.addEventListener('DOMContentLoaded', function () { 10497 var input = document.getElementById('searchInput'); 10498 if (!input) return; 10499 input.addEventListener('keydown', function (e) { 10500 if (e.key === 'Enter') { 10501 e.preventDefault(); 10502 var form = input.form; 10503 if (!form) return; 10504 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10505 else form.submit(); 10506 } 10507 }); 10508}); 10509 10510 10511 10512(function(){ 10513 const cartBtn = document.getElementById('cartBtn'); 10514 if (cartBtn) { 10515 cartBtn.addEventListener('click', function(e) { 10516 e.preventDefault(); 10517 10518 const checkLogin = () => { 10519 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10520 }; 10521 10522 if (checkLogin()) { 10523 window.location.href = '/keranjang'; 10524 } else { 10525 showCartLoginModal(); 10526 } 10527 }); 10528 } 10529 10530 const loginBtn = document.getElementById('loginBtn'); 10531 if (loginBtn) { 10532 loginBtn.addEventListener('click', function(e) { 10533 e.preventDefault(); 10534 window.location.href = 'https://mgsco.org/'; 10535 }); 10536 } 10537 10538 const userMenuBtn = document.getElementById('userMenuBtn'); 10539 const userDropdown = document.getElementById('userDropdown'); 10540 10541 if (userMenuBtn && userDropdown) { 10542 function openDropdown() { 10543 userDropdown.hidden = false; 10544 userMenuBtn.setAttribute('aria-expanded', 'true'); 10545 document.addEventListener('click', closeOnOutside, { capture: true }); 10546 document.addEventListener('keydown', closeOnEscape); 10547 } 10548 10549 function closeDropdown() { 10550 if (userDropdown.hidden) return; 10551 userDropdown.hidden = true; 10552 userMenuBtn.setAttribute('aria-expanded', 'false'); 10553 document.removeEventListener('click', closeOnOutside, { capture: true }); 10554 document.removeEventListener('keydown', closeOnEscape); 10555 } 10556 10557 function closeOnOutside(e) { 10558 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10559 closeDropdown(); 10560 } 10561 } 10562 10563 function closeOnEscape(e) { 10564 if (e.key === 'Escape') { 10565 closeDropdown(); 10566 userMenuBtn.focus(); 10567 } 10568 } 10569 10570 userMenuBtn.addEventListener('click', function(e) { 10571 e.preventDefault(); 10572 userDropdown.hidden ? openDropdown() : closeDropdown(); 10573 });
10574 10575 // Close dropdown when clicking profile links 10576 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10577 link.addEventListener('click', function() { 10578 closeDropdown(); 10579 }); 10580 }); 10581 } 10582 10583 // Bottom nav profile button - toggle mobile bottom sheet 10584 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10585 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10586 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10587 10588 if (bottomProfileBtn && mobileUserSheet) { 10589 function openMobileSheet() { 10590 mobileUserSheet.hidden = false; 10591 document.body.style.overflow = 'hidden'; 10592 // Trigger animation 10593 requestAnimationFrame(() => { 10594 mobileUserSheet.classList.add('active'); 10595 }); 10596 } 10597 10598 function closeMobileSheet() { 10599 mobileUserSheet.classList.remove('active'); 10600 document.body.style.overflow = ''; 10601 setTimeout(() => { 10602 mobileUserSheet.hidden = true; 10603 }, 300); 10604 } 10605 10606 bottomProfileBtn.addEventListener('click', function(e) { 10607 e.preventDefault(); 10608 openMobileSheet(); 10609 }); 10610 10611 if (mobileUserSheetBackdrop) { 10612 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10613 } 10614 10615 // Close on escape key 10616 document.addEventListener('keydown', function(e) { 10617 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10618 closeMobileSheet(); 10619 } 10620 }); 10621 10622 // Close sheet when clicking profile links 10623 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10624 link.addEventListener('click', function() { 10625 closeMobileSheet(); 10626 }); 10627 }); 10628 } 10629 10630 // Handle Google avatar error in bottom-nav (fallback to initials) 10631 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10632 if (bottomNavAvatar) { 10633 bottomNavAvatar.addEventListener('error', function() { 10634 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10635 const initialsDiv = document.createElement('div'); 10636 initialsDiv.className = 'bottom-nav-avatar-initials'; 10637 initialsDiv.textContent = initials; 10638 this.parentNode.replaceChild(initialsDiv, this); 10639 }); 10640 } 10641 10642 // Mobile logout button 10643 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10644 if (mobileLogoutBtn) { 10645 mobileLogoutBtn.addEventListener('click', async function(e) { 10646 e.preventDefault(); 10647 10648 // Check push notification before logout 10649 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10650 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10651 if (!confirmLogout) { 10652 return; // User cancelled logout 10653 } 10654 } 10655 10656 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10657 10658 try { 10659 const response = await fetch('/auth/logout', { 10660 method: 'POST', 10661 credentials: 'same-origin', 10662 headers: { 10663 'Content-Type': 'application/json', 10664 'X-CSRF-Token': getCsrfToken() 10665 }, 10666 body: JSON.stringify({ 10667 csrf_token: getCsrfToken() 10668 }) 10669 }); 10670 10671 if (response.ok) { 10672 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10673 window.guestCart.clear(); 10674 } 10675 window.location.href = '/login?logged_out=1'; 10676 } else { 10677 console.error('Logout failed'); 10678 window.location.href = '/logout'; 10679 } 10680 } catch (error) { 10681 console.error('Logout error:', error); 10682 window.location.href = '/logout'; 10683 } 10684 }); 10685 } 10686 10687 const logoutBtn = document.getElementById('logoutBtn'); 10688 if (logoutBtn) { 10689 logoutBtn.addEventListener('click', async function(e) { 10690 e.preventDefault(); 10691 10692 // Check push notification before logout 10693 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10694 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10695 if (!confirmLogout) { 10696 return; // User cancelled logout 10697 } 10698 } 10699 10700 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10701
10702 try { 10703 const response = await fetch('/auth/logout', { 10704 method: 'POST', 10705 credentials: 'same-origin', 10706 headers: { 10707 'Content-Type': 'application/json', 10708 'X-CSRF-Token': getCsrfToken() 10709 }, 10710 body: JSON.stringify({ 10711 csrf_token: getCsrfToken() 10712 }) 10713 }); 10714 10715 if (response.ok) { 10716 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10717 window.guestCart.clear(); 10718 } 10719 10720 window.location.href = '/login?logged_out=1'; 10721 } else { 10722 console.error('Logout failed'); 10723 window.location.href = '/logout'; 10724 } 10725 } catch (error) { 10726 console.error('Logout error:', error); 10727 window.location.href = '/logout'; 10728 } 10729 }); 10730 } 10731 10732 const wishlistBtn = document.getElementById('wishlistBtn'); 10733 const notifBtn = document.getElementById('notifBtn'); 10734 10735 if (wishlistBtn) { 10736 wishlistBtn.addEventListener('click', function(e) { 10737 e.preventDefault(); 10738 alert('Fitur wishlist akan segera tersedia!'); 10739 }); 10740 } 10741 10742 if (notifBtn) { 10743 notifBtn.addEventListener('click', function(e) { 10744 e.preventDefault(); 10745 alert('Fitur notifikasi akan segera tersedia!'); 10746 }); 10747 } 10748})(); 10749 10750// ========== MOBILE BOTTOM NAVIGATION ========== 10751(function initBottomNav() { 10752 // Sync cart badge with bottom nav 10753 function syncBottomCartBadge() { 10754 const mainBadge = document.getElementById('cartCount'); 10755 const bottomBadge = document.getElementById('bottomCartCount'); 10756 if (mainBadge && bottomBadge) { 10757 const count = mainBadge.textContent; 10758 bottomBadge.textContent = count; 10759 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10760 } 10761 } 10762 10763 // Sync compare badge with bottom nav 10764 function syncBottomCompareBadge() { 10765 const mainBadge = document.getElementById('compareCount'); 10766 const bottomBadge = document.getElementById('bottomCompareCount'); 10767 if (mainBadge && bottomBadge) { 10768 const count = mainBadge.textContent; 10769 bottomBadge.textContent = count; 10770 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10771 } 10772 } 10773 10774 // Initial sync 10775 syncBottomCartBadge(); 10776 syncBottomCompareBadge(); 10777 10778 // Observe badge changes 10779 const observer = new MutationObserver(function() { 10780 syncBottomCartBadge(); 10781 syncBottomCompareBadge(); 10782 }); 10783 10784 const cartBadge = document.getElementById('cartCount'); 10785 const compareBadge = document.getElementById('compareCount'); 10786 10787 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10788 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10789 10790 // Bottom Cart Button 10791 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10792 if (bottomCartBtn) { 10793 bottomCartBtn.addEventListener('click', function(e) { 10794 e.preventDefault(); 10795 10796 const checkLogin = () => { 10797 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10798 }; 10799 10800 if (checkLogin()) { 10801 window.location.href = '/keranjang'; 10802 } else { 10803 showCartLoginModal(); 10804 } 10805 }); 10806 } 10807 10808 // Bottom Compare Button 10809 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10810 if (bottomCompareBtn) { 10811 bottomCompareBtn.addEventListener('click', function(e) { 10812 e.preventDefault(); 10813 10814 // Close chat widget if open 10815 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10816 window.plazaChat.closeWidget(); 10817 } 10818 10819 const mainCompareBtn = document.getElementById('compareBtn'); 10820 if (mainCompareBtn) { 10821 mainCompareBtn.click(); 10822 } 10823 }); 10824 } 10825 10826 // Bottom Category Button - triggers mega menu 10827 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10828 if (bottomCategoryBtn) { 10829 bottomCategoryBtn.addEventListener('click', function(e) { 10830 e.preventDefault(); 10831 // Trigger desktop mega menu 10832 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10833 if (desktopTrigger) { 10834 desktopTrigger.click(); 10835 } 10836 });
10837 } 10838 10839 // Bottom Login Button 10840 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10841 if (bottomLoginBtn) { 10842 bottomLoginBtn.addEventListener('click', function(e) { 10843 e.preventDefault(); 10844 window.location.href = '/login'; 10845 }); 10846 } 10847 10848 // Bottom Chat Button 10849 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10850 if (bottomChatBtn) { 10851 bottomChatBtn.addEventListener('click', function(e) { 10852 e.preventDefault(); 10853 10854 // Trigger chat widget (try multiple times if needed) 10855 const triggerChat = () => { 10856 const chatToggle = document.getElementById('chat-toggle-btn'); 10857 if (chatToggle) { 10858 chatToggle.click(); 10859 return true; 10860 } 10861 return false; 10862 }; 10863 10864 // Immediate attempt 10865 if (!triggerChat()) { 10866 // Retry after 100ms if not found 10867 setTimeout(() => { 10868 if (!triggerChat()) { 10869 // Last resort: manually show popup 10870 const popup = document.getElementById('chat-popup'); 10871 if (popup) { 10872 popup.style.display = 'block'; 10873 } 10874 } 10875 }, 100); 10876 } 10877 }); 10878 } 10879 10880 // Sync chat unread badge 10881 function syncBottomChatBadge() { 10882 const chatBadge = document.getElementById('chat-unread-badge'); 10883 const bottomChatBadge = document.getElementById('bottomChatCount'); 10884 if (chatBadge && bottomChatBadge) { 10885 const count = chatBadge.textContent; 10886 bottomChatBadge.textContent = count; 10887 bottomChatBadge.style.display = chatBadge.style.display; 10888 } 10889 } 10890 10891 // Observe chat badge changes 10892 const chatBadge = document.getElementById('chat-unread-badge'); 10893 if (chatBadge) { 10894 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10895 syncBottomChatBadge(); 10896 } 10897 10898 // Active state management based on current page 10899 function updateActiveState() { 10900 const currentPath = window.location.pathname; 10901 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10902 10903 bottomNavItems.forEach(item => { 10904 item.classList.remove('active'); 10905 10906 const itemId = item.id; 10907 const page = item.getAttribute('data-page'); 10908 const href = item.getAttribute('href'); 10909 10910 // Check by ID for specific pages 10911 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10912 item.classList.add('active'); 10913 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10914 item.classList.add('active'); 10915 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10916 item.classList.add('active'); 10917 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10918 item.classList.add('active'); 10919 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10920 item.classList.add('active'); 10921 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 10922 item.classList.add('active'); 10923 } 10924 // Fallback: check by data-page attribute 10925 else if (page === 'home' && currentPath === '/') { 10926 item.classList.add('active'); 10927 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 10928 item.classList.add('active'); 10929 } 10930 // Fallback: check by href 10931 else if (href && currentPath === href) { 10932 item.classList.add('active'); 10933 } 10934 }); 10935 } 10936 10937 // Update active state on load 10938 updateActiveState(); 10939 10940 // Bottom nav always visible - auto-hide disabled for better UX 10941 // const bottomNav = document.querySelector('.bottom-nav'); 10942 // if (bottomNav && window.innerWidth <= 640) { 10943 // // Auto-hide feature disabled 10944 // } 10945})(); 10946// Cart Login Modal Handler (Global scope) 10947function showCartLoginModal() { 10948 const modal = document.getElementById('cartLoginModal'); 10949 if (!modal) return; 10950 10951 modal.classList.add('show'); 10952 10953 const confirmBtn = document.getElementById('cartLoginConfirm'); 10954 const registerBtn = document.getElementById('cartRegisterBtn');
10955 const cancelBtn = document.getElementById('cartLoginCancel'); 10956 10957 const handleLogin = () => { 10958 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 10959 }; 10960 10961 const handleRegister = () => { 10962 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 10963 }; 10964 10965 const handleCancel = () => { 10966 modal.classList.remove('show'); 10967 confirmBtn.removeEventListener('click', handleLogin); 10968 registerBtn.removeEventListener('click', handleRegister); 10969 cancelBtn.removeEventListener('click', handleCancel); 10970 }; 10971 10972 confirmBtn.removeEventListener('click', handleLogin); 10973 registerBtn.removeEventListener('click', handleRegister); 10974 cancelBtn.removeEventListener('click', handleCancel); 10975 10976 confirmBtn.addEventListener('click', handleLogin); 10977 registerBtn.addEventListener('click', handleRegister); 10978 cancelBtn.addEventListener('click', handleCancel); 10979 10980 modal.addEventListener('click', (e) => { 10981 if (e.target === modal) { 10982 handleCancel(); 10983 } 10984 }); 10985} 10986</script>
vendor: 2 bytes, line 10986
10986
10987<script data-inlined="cart-badge.js"> 10988(function(){ 10989 function selBadge(){ 10990 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 10991 } 10992 function setBadge(n){ 10993 const el = selBadge(); 10994 if (!el) return; 10995 const count = Math.max(0, parseInt(n||0,10) || 0); 10996 el.textContent = String(count); 10997 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 10998 if (count <= 0) { el.classList.add('is-empty'); } 10999 else { el.classList.remove('is-empty'); } 11000 } 11001 async function refresh(){ 11002 try{ 11003 // Primary: Use server-side endpoint (handles both logged and guest users) 11004 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11005 if (res.ok) { 11006 const data = await res.json(); 11007 if (data && data.ok) { 11008 setBadge(data.items_count || 0); 11009 return; 11010 } 11011 } 11012 11013 // Fallback hanya untuk guest user: check localStorage 11014 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11015 document.body.classList.contains('logged-in') || 11016 window.userLoggedIn === true; 11017 11018 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11019 setBadge(window.guestCart.getItemsCount()); 11020 } 11021 }catch(e){ 11022 // Final fallback hanya untuk guest 11023 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11024 if (!isLoggedIn) { 11025 try { 11026 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11027 const items = guestCartData.items || {}; 11028 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11029 setBadge(count); 11030 } catch(e2) { 11031 setBadge(0); 11032 } 11033 } else { 11034 setBadge(0); 11035 } 11036 } 11037 } 11038 11039 // Ekspos untuk dipanggil dari skrip lain (opsional) 11040 window.CartBadge = { set: setBadge, refresh }; 11041 window.updateCartCounter = refresh; // Alias for compatibility 11042 11043 // Update saat halaman load 11044 if (document.readyState === 'loading') { 11045 document.addEventListener('DOMContentLoaded', refresh); 11046 } else { 11047 refresh(); 11048 } 11049 11050 // Dengarkan event global dari add/remove/update 11051 document.addEventListener('cart:updated', function(ev){ 11052 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11053 ? ev.detail.items_count : null; 11054 if (c !== null) setBadge(c); 11055 else refresh(); 11056 }); 11057 11058 // Dengarkan event khusus cart remove untuk update badge 11059 document.addEventListener('cart:removed', function(ev){ 11060 // Langsung refresh untuk memastikan data terbaru 11061 refresh(); 11062 }); 11063 11064 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11065 const prevAdd = window.addToCart; 11066 if (typeof prevAdd === 'function'){ 11067 window.addToCart = async function(pid, qty, vid, meta){ 11068 const res = await prevAdd(pid, qty, vid, meta); 11069 try{ 11070 const cnt = res && res.cart ? res.cart.items_count : null; 11071 if (cnt !== null) setBadge(cnt); 11072 // tetap siarkan event agar listener lain ikut sync 11073 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11074 }catch(_){} 11075 return res; 11076 }; 11077 } 11078 11079 // Helper function to check if user is logged in 11080 function isUserLoggedIn() { 11081 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11082 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11083 } 11084 11085 // Make login check globally accessible 11086 window.isUserLoggedIn = isUserLoggedIn; 11087})(); 11088</script>
11088 11089 11090<!-- Product Compare Sidebar --> 11091<div class="compare-sidebar" id="compareSidebar"> 11092 <div class="compare-header"> 11093 <h4 class="compare-title"> 11094 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11095 <path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"/> 11096 </svg> 11097 Bandingkan Panduan 11098 <span class="compare-count" id="compareCountSidebar">(0)</span> 11099 </h4> 11100 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11101 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11102 <path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7a1 1 0 1 0-1.4 1.4L10.6 12l-4.9 4.9a1 1 0 0 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z"/> 11103 </svg> 11104 </button> 11105 </div> 11106 11107 <div class="compare-content"> 11108 <div class="compare-empty" id="compareEmpty"> 11109 <div class="empty-icon"> 11110 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11111 <path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"/> 11112 </svg> 11113 </div> 11114 <h5>Belum ada panduan dipilih</h5> 11115 <p>Pilih panduan shio yang ingin dibandingkan melalui tombol "Bandingkan Panduan" pada halaman ini.</p> 11116 </div> 11117 11118 <div class="compare-list" id="compareList"> 11119 <!-- Products will be populated here --> 11120 </div> 11121 </div> 11122 11123 <div class="compare-actions" id="compareActions"> 11124 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11125 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11126 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 11127 </svg> 11128 Hapus Semua 11129 </button> 11130 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11131 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11132 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/> 11133 </svg> 11134 Analisis AI 11135 </button> 11136 </div> 11137 11138 <!-- AI Disclaimer in Sidebar --> 11139 <div class="compare-disclaimer"> 11140 <div class="ai-disclaimer"> 11141 <div class="disclaimer-text"> 11142 <strong>ð¡ Catatan:</strong> Variasi game dihadirkan sebagai referensi hiburan interaktif. 11143 Pilihan permainan tidak menjamin hasil atau kemenangan tertentu. 11144 Untuk informasi lebih lanjut mengenai layanan BOYTOTO, silakan hubungi tim BOYTOTO. 11145 </div> 11146 </div> 11147 </div> 11148</div><style data-inlined="pd-lightbox.css"> 11149/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11150.pd-lb { 11151 position: fixed; inset: 0; z-index: 99999; 11152 background: rgba(0,0,0,.95); 11153 display: none; 11154 color: #fff; 11155 /* Transition untuk swipe-down feedback */ 11156 transition: transform .18s ease, opacity .18s ease; 11157} 11158.pd-lb.show { display: block; } 11159 11160/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11161.pd-lb-canvas { 11162 position: absolute; inset: 56px 0 56px 0; 11163 display: flex; align-items: center; justify-content: center; 11164 overflow: hidden; 11165 touch-action: none; 11166 z-index: 1; 11167} 11168 11169.pd-lb-img { 11170 max-width: 96vw; max-height: 86vh; 11171 will-change: transform; 11172 user-select: none; 11173 pointer-events: none; 11174 transform-origin: 50% 50%; 11175} 11176 11177.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11178 position: absolute; 11179 z-index: 2; 11180} 11181 11182.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11183 background: rgba(17,24,39,.7); 11184 color: #fff; border: 0; cursor: pointer; 11185 width: 40px; height: 40px; border-radius: 999px; 11186 display: inline-flex; align-items: center; justify-content: center; 11187 transition: background-color .15s ease, transform .05s ease; 11188 -webkit-tap-highlight-color: transparent; 11189} 11190.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11191.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11192 11193.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11194.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11195.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11196 11197.pd-lb-counter { 11198 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11199 transform: translateX(-50%); 11200 font-weight: 700; font-size: 13px; letter-spacing: .4px;
11201 background: rgba(17,24,39,.65); 11202 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11203} 11204 11205@media (max-width: 640px){ 11206 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11207 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11208 .pd-lb-close { width: 38px; height: 38px; } 11209} 11210</style> 11211<style>.paylater-promo{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#f0fdf4 0,#dcfce7 100%);border:1px solid #86efac;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin:8px 0}.paylater-promo-sm{padding:6px 10px;font-size:11px;gap:6px}.paylater-promo-md{padding:8px 12px;font-size:13px;gap:8px}.paylater-promo-lg{padding:10px 16px;font-size:14px;gap:10px}.paylater-icon{font-size:1.2em;flex-shrink:0}.paylater-text{display:flex;flex-direction:column;gap:2px}.paylater-text strong{color:#15803d;font-weight:600}.paylater-text small{color:#16a34a;font-size:.85em;opacity:.9}.paylater-desktop-only{display:none}.paylater-mobile-only{display:flex}@media (min-width:641px){.paylater-desktop-only{display:flex}.paylater-mobile-only{display:none}}@media (max-width:640px){.paylater-promo{font-size:12px;padding:7px 10px}.paylater-promo-lg{font-size:13px;padding:8px 12px}}@media print{.paylater-promo{display:none!important}}.paylater-compact{display:inline-block;font-size:11px;font-weight:600;color:#059669;background:#d1fae5;padding:3px 8px;border-radius:4px;margin:6px 0 4px;line-height:1.3}@media (max-width:640px){.paylater-compact{font-size:10px;padding:2px 6px;margin:5px 0 3px}}@media print{.paylater-compact{display:none!important}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.pd-voucher-minimal{margin:10px 0;display:flex;flex-direction:column;gap:6px}.pdv-min-item{display:flex;align-items:center;gap:8px;padding:0;height:32px;transition:all .2s ease}.pdv-min-hidden{display:none}.pdv-min-disabled{opacity:.5}.pdv-badge-btn{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border:none;border-radius:8px;padding:3px;cursor:pointer;transition:all .2s ease;width:auto}.pdv-badge-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,.3)}.pdv-badge-btn:disabled{opacity:.6;cursor:not-allowed}.pdv-badge-icon{color:#fff;flex-shrink:0}.pdv-badge-text{font-size:14px;font-weight:700;color:#fff;white-space:nowrap}.pdv-badge-code{font-family:'Courier New',monospace;font-size:13px;font-weight:700;color:#fff;background:rgba(255,255,255,.2);padding:4px 8px;border-radius:4px;border:none;white-space:nowrap}.pdv-min-copy,.pdv-min-more{padding:4px 12px;font-size:12px;font-weight:600;border-radius:5px;border:1px solid #e2e8f0;background:#fff;color:#667eea;cursor:pointer;transition:all .2s ease;white-space:nowrap}.pdv-min-copy:hover:not(:disabled),.pdv-min-more:hover{background:#667eea;color:#fff;border-color:#667eea}.pdv-min-copy:disabled{opacity:.4;cursor:not-allowed}.pdv-min-copy.copied{background:#22c55e;color:#fff;border-color:#22c55e}.pdv-min-more{margin-left:auto}.modal-close{position:absolute;top:20px;right:20px;background:none;border:none;cursor:pointer;color:#64748b;padding:4px;transition:color .2s ease}.modal-close:hover{color:#1e293b}.modal-body{margin-bottom:8px}.voucher-modal-content{text-align:center}.voucher-modal-badge{display:inline-flex;flex-direction:row;align-items:center;gap:6px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:10px;padding:8px 16px;margin-bottom:12px}.vmb-value{font-size:18px;font-weight:700;color:#fff;line-height:1}.vmb-label{font-size:18px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.5px}.voucher-modal-code{font-family:'Courier New',monospace;font-size:14px;font-weight:700;color:#667eea;background:rgba(102,126,234,.1);padding:6px 12px;border-radius:6px;border:2px dashed rgba(102,126,234,.3);display:inline-block;margin-bottom:10px}.voucher-modal-name{font-size:14px;font-weight:600;color:#1e293b;margin-bottom:16px}.voucher-modal-details{text-align:left;background:#f8fafc;border-radius:8px;padding:12px;margin-bottom:16px}.vmd-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px;color:#64748b;border-bottom:1px solid #e2e8f0}.vmd-item:last-child{border-bottom:none}.vmd-item svg{color:#667eea;flex-shrink:0;margin-top:2px}.vmd-item strong{color:#1e293b}.modal-actions{padding:20px}.modal-btn-primary{background:linear-gradient(135deg,#667eea 0,#764ba2 100%);color:#fff}.modal-btn-primary:hover{opacity:.9;transform:translateY(-1px)}@media (max-width:640px){.pdv-min-item{gap:6px}.pdv-badge-btn{padding:3px;gap:6px}.pdv-badge-text{font-size:13px}.pdv-badge-code{font-size:12px;padding:3px 6px}.pdv-min-copy,.pdv-min-more{padding:3px 10px;font-size:11px}.modal-dialog{max-width:95%;margin:20px}.vmb-value{font-size:16px}.vmb-label{font-size:16px}.voucher-modal-code{font-size:13px;padding:5px 10px}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.promo-endyear-mini-banner{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(255,255,255,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(242,140,26,.2);border-radius:12px;margin:16px 0;box-shadow:0 4px 16px rgba(0,0,0,.08);transition:all .3s ease}.promo-endyear-mini-banner:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.12);border-color:rgba(242,140,26,.4)}.pey-icon{font-size:32px;line-height:1;flex-shrink:0}.pey-content{flex:1;min-width:0}.pey-content strong{display:block;font-size:14px;font-weight:700;color:#F28C1A;margin-bottom:2px}
11211.pey-content span{display:block;font-size:13px;color:#64748b;line-height:1.4}.pey-link{flex-shrink:0;padding:8px 16px;background:linear-gradient(135deg,#F28C1A,#FFB84D);color:#fff;text-decoration:none;font-size:13px;font-weight:700;border-radius:8px;white-space:nowrap;transition:all .2s ease;box-shadow:0 2px 8px rgba(242,140,26,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(242,140,26,.35)}@media(max-width:640px){.promo-endyear-mini-banner{flex-wrap:wrap;gap:8px;padding:10px 12px}.pey-icon{font-size:28px;align-self:flex-start}.pey-content{flex:1;min-width:0}.pey-content strong{font-size:12px}.pey-content span{font-size:11px;line-height:1.3}.pey-link{width:100%;flex-basis:100%;font-size:11px;padding:6px 12px;text-align:center}}@media print{.promo-endyear-mini-banner{display:none!important}}</style> 11212<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11213.pd-title-row{position:relative;margin-bottom:12px;padding-right:52px}.pd-title{margin:0!important}.pd-share-btn{position:absolute;top:0;right:0;width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid #e2e8f0;background:#fff;color:#64748b;cursor:pointer;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.pd-share-btn:hover{background:#f8fafc;border-color:#cbd5e1;color:#0f172a;transform:translateY(-1px)}.pd-share-btn:active{transform:translateY(0)}.pd-share-btn svg{display:block} 11214 11215.pd-share-modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.75);backdrop-filter:blur(4px);z-index:99999;display:none;align-items:center;justify-content:center;padding:0;animation:fadeIn .2s ease}.pd-share-modal-overlay.show{display:flex}.pd-share-modal{background:#fff;border-radius:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:480px;width:100%;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}} 11216 11217.pd-share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f1f5f9}.pd-share-title{font-size:18px;font-weight:700;color:#0f172a;margin:0}.pd-share-close{width:32px;height:32px;border-radius:8px;border:0;background:transparent;color:#64748b;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s ease}.pd-share-close:hover{background:#f1f5f9;color:#0f172a}.pd-share-body{padding:24px;overflow-y:auto} 11218 11219.pd-share-preview{display:flex;gap:12px;padding:16px;background:#f8fafc;border-radius:12px;margin-bottom:24px}.pd-share-preview-img{width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0;border:1px solid #e2e8f0}.pd-share-preview-info{flex:1;min-width:0}.pd-share-preview-name{font-size:14px;font-weight:600;color:#0f172a;margin:0 0 4px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pd-share-preview-price{font-size:16px;font-weight:700;color:#0ea5e9;margin:0} 11220 11221.pd-share-label{font-size:13px;font-weight:600;color:#475569;margin:0 0 12px 0;text-transform:uppercase;letter-spacing:.5px}.pd-share-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}.pd-share-option{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 8px;border:1.5px solid #e2e8f0;border-radius:12px;background:#fff;cursor:pointer;transition:all .2s ease;text-decoration:none;color:inherit}.pd-share-option:hover{border-color:#94a3b8;background:#f8fafc;transform:translateY(-2px)}.pd-share-option:active{transform:translateY(0)} 11222 11223.pd-share-icon{width:40px;height:40px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;position:relative}.pd-share-icon svg{width:24px;height:24px;display:block}.pd-share-icon-whatsapp{background:linear-gradient(135deg,#25D366 0%,#128C7E 100%);color:#fff}.pd-share-icon-facebook{background:linear-gradient(135deg,#1877F2 0%,#0c63d4 100%);color:#fff}.pd-share-icon-twitter{background:linear-gradient(135deg,#1DA1F2 0%,#0d8bd9 100%);color:#fff}.pd-share-icon-telegram{background:linear-gradient(135deg,#0088cc 0%,#0077b3 100%);color:#fff}.pd-share-icon-email{background:linear-gradient(135deg,#EA4335 0%,#c5331f 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#f1db63 0%,#e5d146 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11224 11225.pd-share-copy-section{background:#f8fafc;padding:16px;border-radius:12px}.pd-share-copy-input-wrap{display:flex;gap:8px}.pd-share-copy-input{flex:1;padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;font-size:14px;color:#0f172a;background:#fff;font-family:'Segoe UI',system-ui,sans-serif}.pd-share-copy-input:focus{outline:0;border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.1)}.pd-share-copy-btn{padding:12px 20px;border:0;border-radius:10px;background:linear-gradient(135deg,#0ea5e9 0%,#0284c7 100%);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}.pd-share-copy-btn:hover{background:linear-gradient(135deg,#0284c7 0%,#0369a1 100%);transform:translateY(-1px);box-shadow:0 4px 12px rgba(14,165,233,.3)}
11225.pd-share-copy-btn:active{transform:translateY(0)}.pd-share-copy-btn svg{width:16px;height:16px}.pd-share-copy-btn.copied{background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);pointer-events:none} 11226 11227@media(max-width:640px){.pd-share-modal{border-radius:20px 20px 0 0;margin-top:auto;max-height:85vh}.pd-share-grid{grid-template-columns:repeat(3,1fr)}.pd-share-header{padding:16px 20px}.pd-share-body{padding:20px}.pd-share-btn{width:36px;height:36px}.pd-share-btn svg{width:18px;height:18px}}@media(max-width:480px){.pd-share-grid{grid-template-columns:repeat(3,1fr);gap:10px}.pd-share-option{padding:12px 6px}.pd-share-icon{width:36px;height:36px}.pd-share-icon svg{width:20px;height:20px}.pd-share-name{font-size:11px}} 11228 11229.mf-share{background:#fff;color:#fb923c;border:2px solid #fb923c;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11230 11231@media(max-width:640px){.pd-share-btn{display:none!important}} 11232 11233.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#d50000;color:#14b1d7;border-radius:999px;padding:4px 10px;font-weight:700} 11234 11235.stock-info-btn{--c:#0ea5e9;--b:#bae6fd;--bg:#f0f9ff;--bg-h:#e0f2fe;--b-h:#0ea5e9;margin-left:0;display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;padding:4px 10px;border-radius:999px;border:1.5px solid var(--b);background:var(--bg);color:var(--c);cursor:pointer;font-size:12px;font-weight:600;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.stock-info-btn:hover{background:var(--bg-h);border-color:var(--b-h);color:#0284c7;transform:translateY(-1px);box-shadow:0 2px 8px rgba(14,165,233,.15)}.stock-info-btn:active{transform:translateY(0)}.stock-info-btn:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(14,165,233,.2)}.stock-info-btn svg{width:14px;height:14px;flex-shrink:0}.stock-info-btn .btn-text{white-space:nowrap} 11236 11237.stock-popover{position:absolute;z-index:9999;min-width:280px;max-width:92vw;background:#0b1220;color:#eef3ff;border:1px solid rgba(255,255,255,.08);border-radius:14px;box-shadow:0 20px 40px rgba(12,12,20,.45);padding:12px 12px 8px;display:none}.stock-popover.show{display:block}.stock-popover::before{content:"";position:absolute;top:-8px;left:24px;width:14px;height:14px;background:#0b1220;border-left:1px solid rgba(255,255,255,.08);border-top:1px solid rgba(255,255,255,.08);transform:rotate(45deg)}.sp-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.sp-title{font-weight:800;font-size:14px}.sp-close{background:transparent;border:0;color:#98a2b3;cursor:pointer} 11238 11239.sp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.sp-item{display:grid;grid-template-columns:1fr auto 32px;column-gap:12px;align-items:center;padding:10px 10px;min-height:44px;border-radius:10px;background:rgba(255,255,255,.03)}.sp-left{display:flex;align-items:center;gap:10px}.sp-dot{width:10px;height:10px;border-radius:999px;background:var(--dot,#60a5fa);display:inline-block}.sp-label{font-weight:600;font-size:13px} 11240 11241 11242 11243.sp-qty{grid-column:2;justify-self:end;min-width:40px;text-align:right;font-weight:800;font-size:13px;color:#cfe4ff;font-variant-numeric:tabular-nums} 11244 11245.sp-wa{ 11246 grid-column:3; justify-self:end; 11247 width:32px; height:32px; min-width:32px; min-height:32px; 11248 box-sizing:border-box; 11249 padding:0; line-height:0; text-decoration:none; outline:0; 11250 display:inline-flex; align-items:center; justify-content:center; 11251 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11252 background:rgba(37,211,102,.08); color:#25D366; 11253 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11254 -webkit-tap-highlight-color:transparent; 11255} 11256.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11257.sp-wa:active{transform:translateY(.5px) scale(.98)} 11258 11259.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11260.stock-popover a{text-decoration:none} 11261 11262#deskripsi img{max-width:450px!important;height:unset!important}@media(max-width:640px){.stock-popover{position:fixed;left:0;right:0;bottom:max(12px,env(safe-area-inset-bottom,12px));margin:0 10px;padding:14px 12px 10px;border-radius:16px}.stock-popover::before{display:none}.stock-info-btn{padding:4px 8px;gap:4px;border:0;background:transparent}.stock-info-btn:hover{background:transparent;box-shadow:none;transform:none}.stock-info-btn .btn-text{display:none}.stock-info-btn svg{width:16px;height:16px}.sp-item{grid-template-columns:1fr auto 36px}.sp-wa{width:36px;height:36px;min-width:36px;min-height:36px}.sp-wa svg{width:19px;height:19px}} 11263 11264.stock-dot{position:absolute;bottom:8px;right:8px;width:8px;height:8px;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,0.4);z-index:2;cursor:help}.stock-dot.ready{background:#10b981}.stock-dot.low{background:#f59e0b}.stock-dot.out{background:#ef4444}.stock-dot:hover{transform:scale(1.2);transition:transform 0.2s ease}@media(max-width:640px){.stock-dot{width:7px;height:7px;bottom:6px;right:6px}} 11265 11266.pd-related-card .spec-chips{font-size:11px;color:#64748b;margin:4px 0 6px 0;line-height:1.3;font-weight:500}.pd-related-card .spec-chips .spec-divider{margin:0 4px;opacity:0.6}@media(max-width:640px){.pd-related-card .spec-chips{font-size:10px;margin:3px 0 5px 0}#deskripsi img{max-width:300px!important;height:unset!important;justify-self:center!important}} 11267 11268.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#fbbf24 0%,#f59e0b 100%);color:#78350f;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11269.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border-radius:12px;border:2px solid #fbbf24}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#f59e0b;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#78350f}.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#92400e;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#92400e}.pwp-spinner{width:40px;height:40px;border:3px solid #fbbf24;border-top-color:#f59e0b;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11270.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #fbbf24;display:flex;align-items:center;padding:8px;gap:10px}.pwp-deal-card:hover{background:#fef3c7}.pwp-deal-thumb{flex-shrink:0;width:50px;height:50px;background:#f8fafc;overflow:hidden;border-radius:4px}.pwp-deal-thumb img{width:100%;height:100%;object-fit:cover}.pwp-deal-badge{background:#dc2626;color:#fff;padding:2px 5px;border-radius:3px;font-size:9px;font-weight:700;margin-left:auto}.pwp-deal-info{flex:1;display:flex;flex-direction:column;gap:4px}.pwp-deal-name{font-size:12px;font-weight:600;color:#0f172a;margin:0;line-height:1.2}.pwp-deal-prices{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.pwp-deal-price{font-size:14px;font-weight:800;color:#dc2626;white-space:nowrap}.pwp-deal-original{font-size:11px;color:#64748b;text-decoration:line-through;white-space:nowrap}.pwp-cart-note{margin-top:8px;padding:6px 10px;background:#e0f2fe;border-left:2px solid#0ea5e9;font-size:11px;color:#0c4a6e;border-radius:4px} 11271@media(max-width:640px){.pwp-deals-section{margin:12px 0;padding:10px;border-radius:10px}.pwp-deals-header{gap:8px}.pwp-text .pwp-title{font-size:13px}.pwp-text .pwp-subtitle{font-size:10px}.pwp-deal-card{padding:6px;gap:8px}.pwp-deal-thumb{width:45px;height:45px}.pwp-deal-name{font-size:11px}.pwp-deal-price{font-size:13px}.pwp-deal-original{font-size:10px}} 11272 11273.preorder-badge{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;background:linear-gradient(135deg,#8b5cf6 0%,#7c3aed 100%);color:#fff;border-radius:8px;font-size:13px;font-weight:700;border:0;box-shadow:0 2px 8px rgba(139,92,246,.25)}.preorder-badge svg{width:16px;height:16px;flex-shrink:0}.preorder-info{margin:12px 0;padding:14px;background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 100%);border:1.5px solid #c4b5fd;border-radius:12px}.preorder-info-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}.preorder-info-icon{width:20px;height:20px;color:#7c3aed;flex-shrink:0}.preorder-info-title{font-size:14px;font-weight:700;color:#5b21b6;margin:0}.preorder-info-items{display:flex;flex-direction:column;gap:8px}.preorder-info-row{display:flex;align-items:center;justify-content:space-between;font-size:13px}.preorder-info-label{color:#6b21a8;font-weight:600}.preorder-info-value{color:#5b21b6;font-weight:700}.preorder-info-value.highlight{color:#dc2626;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #f59e0b;font-size:12px;color:#78350f;border-radius:6px;line-height:1.4}@media(max-width:640px){.preorder-badge{font-size:12px;padding:5px 10px}.preorder-info{margin:10px 0;padding:12px}.preorder-info-title{font-size:13px}.preorder-info-row{font-size:12px}.preorder-info-note{font-size:11px;padding:6px 8px}} 11274</style> 11275 11276<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler</h2><div class="pd-sticky-price-row"><span class="pd-sticky-price" id="pd-sticky-price">Referensi Shio</span><span class="pd-sticky-price-original" id="pd-sticky-price-original">Nomor 4D</span><span class="pd-sticky-discount" id="pd-sticky-discount">-99.5%</span></div></div><div class="pd-sticky-actions"><button type="button" class="pd-sticky-share-btn" id="pd-sticky-share-btn" aria-label="Bagikan panduan" title="Bagikan"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
11276<path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button><button type="button" class="pd-sticky-btn pd-sticky-btn-chat" id="pd-sticky-chat"><svg viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg>Tanya</button><button type="button" class="pd-sticky-btn pd-sticky-btn-cart" id="pd-sticky-add-cart" disabled>Simpan Panduan</button><button type="button" class="pd-sticky-btn pd-sticky-btn-buy" id="pd-sticky-buy" disabled>Lihat Daftar BOYTOTO Terbaru</button></div></div></div> 11277 11278<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://mgsco.org/">BOYTOTO</a><span>-</span><a href="https://mgsco.org/">BOYTOTO RESMI</a><span>-</span><a href="https://mgsco.org/">BOYTOTO GACOR</a><span>-</span><a href="https://mgsco.org/">BOYTOTO TERPERCAYA</a><span>-</span><a href="https://mgsco.org/">BOYTOTO LOGIN</a><span>-</span><span class="current">BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler 11279 11280</span></nav> 11281 11282<div class="pd-top-layout"><section class="pd-media-card"><figure class="pd-main-image" id="pd-main-figure"><img id="pd-main-img" src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" data-slug="" loading="eager"> 11283</figure> 11284 11285<div class="pd-mobile-slider-wrapper"> 11286<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="eager"></div><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11287<div class="pd-thumb-gallery" id="pd-thumb-gallery" aria-label="Galeri gambar produk"><button type="button" class="pd-thumb-btn active" data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt="BOYTOTO" data-slug="" aria-label="Gambar 1"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt="BOYTOTO" data-slug="https://ik.imagekit.io/boytoto/banner235.png" aria-label="Gambar 2"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt="BOYTOTO" data-slug="BOYTOTO" aria-label="Gambar 3"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt
11287="BOYTOTO" data-slug="BOYTOTO" aria-label="Gambar 4"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt="BOYTOTO" data-slug="BOYTOTO" aria-label="Gambar 5"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://ik.imagekit.io/boytoto/banner235.png" data-alt="BOYTOTO" data-slug="BOYTOTO" aria-label="Gambar 6"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" loading="lazy"></button></div></section><section class="pd-info-card"><div id="pd-variant-combined" class="pd-variant-combined" hidden></div><div class="pd-title-row"> 11288 <h1 class="pd-title" id="pd-title">BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler</h1><button type="button" class="pd-share-btn" id="pd-share-btn" aria-label="Bagikan panduan" title="Bagikan"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button></div> 11289<div id="pd-price-anchor"> 11290</div> 11291<div class="pd-price-qr-container"> 11292 <div class="pd-price-block" id="pd-price-block"> 11293 <div class="pd-price-row"> 11294 <span class="pd-price-current" id="pd-price-current" >Daftar Shio</span> 11295 <span class="pd-price-original" id="pd-price-original">Kategori 4D</span> 11296 <span class="pd-discount-badge" id="pd-discount-badge">-95%</span></div> 11297 <div class="pd-meta-row"> 11298 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11299 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11300 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11301 <path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>Bisa PWP</span></div> 11302 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">BOYTOTO</span> 11303 <span class="sku">SKU: <span id="pd-sku">BOYTOTO</span></span></div> 11304<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11305 <span class="paylater-icon">ð³</span> 11306 <span class="paylater-text"> 11307 <strong>Autorized Situs BOYTOTO Online</strong> 11308 <small>BOYTOTO</small> 11309 </span> 11310</div> 11311 11312<div class="stock-popover" id="pd-stock-popover" role="dialog" aria-hidden="true" aria-label="Informasi stok cabang"><div class="sp-head"><div class="sp-title">Info Daftar Shio</div><button type="button" class="sp-close" id="pd-stock-popover-close" aria-label="Tutup"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M18.3 5.71L12 12.01l-6.3-6.3-1.4 1.41 6.3 6.3-6.3 6.3 1.4 1.41 6.3-6.3 6.3 6.3 1.41-1.41-6.3-6.3 6.3-6.3z"/></svg></button></div><ul class="sp-list"> 11313<li class="sp-item"><div class="sp-left"><span class="sp-dot" style="--dot:#60a5fa;"></span><span class="sp-label">Asia Mega Mas</span></div><span class="sp-qty" id="sp-qty-meg">0</span><a class="sp-wa" href="https://wa.me/6281380009898?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Asia%20Mega%20Mas%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2FBOYTOTO" target="_blank" rel="noopener" aria-label="WhatsApp Asia Mega Mas"><svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
11313<path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/></svg></a></li> 11314 11315 <li class="sp-item"> 11316 <div class="sp-left"> 11317 <span class="sp-dot" style="--dot:#34d399;"></span> 11318 <span class="sp-label">Medan Fair</span> 11319 </div> 11320 <span class="sp-qty" id="sp-qty-mdf">0</span> 11321 <a class="sp-wa" 11322 href="https://wa.me/6285388992009?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Medan%20Fair%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2FBOYTOTO" 11323 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11324 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11325 <path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/> 11326 </svg> 11327 </a> 11328 </li> 11329 11330 <li class="sp-item"> 11331 <div class="sp-left"> 11332 <span class="sp-dot" style="--dot:#f59e0b;"></span> 11333 <span class="sp-label">Banda Aceh</span> 11334 </div> 11335 <span class="sp-qty" id="sp-qty-bda">0</span> 11336 <a class="sp-wa" 11337 href="https://wa.me/628116833411?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Banda%20Aceh%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2FBOYTOTO" 11338 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11339 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11340 <path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/> 11341 </svg> 11342 </a> 11343 </li> 11344 </ul> 11345 </div> 11346 11347 </div> 11348 <div class="pd-qr-print" style="display:none;"><div class="pd-qr-label">Scan untuk lihat panduan:</div><img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https%3A%2F%2Fplazait.co.id%2Fproduct%2FBOYTOTO" alt="QR Code" class="pd-qr-image"></div> 11349 </div> 11350 <div id="pd-variants-anchor"></div> 11351 11352 11353 11354 11355 11356 <div class="pd-compare-section"> 11357 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11358 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11359 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11360 </svg>
11361 <span class="compare-text">Bandingkan Panduan</span> 11362 <span class="compare-remove" hidden> 11363 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11364 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 11365 </svg> 11366 Hapus dari Perbandingan 11367 </span> 11368 </button> 11369 <div class="pd-compare-info"> 11370 <small class="compare-note" id="compare-note">Bandingkan hingga 5 panduan dengan bantuan AI</small> 11371 </div> 11372 </div> 11373 11374 <div class="pd-short-desc"><p><strong>Informasi BOYTOTO:</strong></p><ul><li>Fokus Panduan: Hiburan Game Online</li><li>Fungsi: Menikmati sensasi permainan di atas langit</li><li>Kategori: Pilihan game seru & modern</li><li>Penggunaan: Media hiburan interaktif</li><li>Format: Akses Serbaguna</li><li>Akses: Smartphone & Komputer</li><li>Catatan: Selalu utamakan kenyamanan & strategi bermain</li><li>Daftar Game: <a href="https://mgsco.org/">Lihat Di Sini</a></li></ul></div><div class="BOYTOTO-container-v1 mini"> 11375<div class="BOYTOTO-container"> 11376 11377 11378 <!-- HEADER --> 11379<div class="BOYTOTO-container-v3 mini"> 11380 <div class="rtp-container-v3"> 11381 <div class="rtp-machine-v3"> 11382 <h2 class="rtp-title-v3">ð BOYTOTO - Di Balik Langit Ada Game Seru Menanti ð</h2> 11383 11384 <div class="rtp-controls-v3"> 11385 <label> 11386 PILIH TIPE 11387 <select id="providerFilter"> 11388 <option value="ALL">SEMUA PERMAINAN</option> 11389 </select> 11390 </label> 11391 11392 <label> 11393 LEVEL SERU 11394 <select id="slotTypeFilter"> 11395 <option value="HIGH">RINGAN</option> 11396 <option value="MEDIUM">SEDANG</option> 11397 <option value="BOOST">MAKSIMAL</option> 11398 </select> 11399 </label> 11400 11401 <label> 11402 NOMOR RUANG 11403 <input id="minRtp" type="number" min="0" max="9999" step="1" value="0"> 11404 </label> 11405 11406 <button id="btnRtpRandom" type="button"> 11407 <span class="icon"></span> TEMUKAN SERUNYA 11408 </button> 11409 </div> 11410 11411 <div class="rtp-main-result-v3" id="rtpResult"> 11412 <div class="rtp-status-v3" id="rtpStatus">â TERSEDIA</div> 11413 11414 <div class="rtp-highlight-box"> 11415 <div class="rtp-col-left"> 11416 <div class="k">Ruang Game</div> 11417 <div class="v-game" id="rProvider">-</div> 11418 11419 <div class="provider-info"> 11420 <span class="k-small">Sensasi:</span> 11421 <span class="v-small" id="rSlotType">-</span> 11422 <span class="k-small" style="margin-left:10px;">Indeks:</span> 11423 <span class="v-small" id="rPercent">-</span> 11424 </div> 11425 </div> 11426 11427 <div class="rtp-col-right"> 11428 <div class="k-rtp">TEMA</div> 11429 <div class="v-rtp-huge" id="rRtpValue">-</div> 11430 </div> 11431 </div> 11432 11433 <div class="rtp-details-v3"> 11434 <div class="rtp-detail-item"> 11435 <div class="k-detail">Katalog</div> 11436 <div class="v-detail" id="rJam">-</div> 11437 </div> 11438 11439 <div class="rtp-detail-item"> 11440 <div class="k-detail">Fitur</div> 11441 <div class="v-detail" id="rPola">-</div> 11442 </div> 11443 11444 <div class="rtp-detail-item"> 11445 <div class="k-detail">Aksesibilitas</div> 11446 <div class="v-detail" id="rStatusText">-</div> 11447 </div> 11448 </div> 11449 11450 <div class="rtp-tip-v3" id="rTip"> 11451 Saran: Rasakan sensasi hiburan game interaktif dengan atmosfer kekinian bersama BOYTOTO. 11452 </div> 11453 </div> 11454 11455 <div class="rtp-alts-v3"> 11456 <div class="rtp-alts-title-v3">3 Langkah Menuju Sensasi Game Seru</div> 11457 <div class="rtp-alt-list-v3" id="rtpAltList"></div> 11458 </div> 11459 11460 <div class="rtp-note-v3"> 11461 Copyright © BOYTOTO 11462 </div> 11463 11464 <!-- HEADER --> 11465<div class="BOYTOTO-container-v3 mini"> 11466 <div class="rtp-container-v3"> 11467 <div class="rtp-machine-v3"> 11468 <h2 class="rtp-title-v3">⨠BOYTOTO - Di Balik Langit Ada Game Seru Menanti â¨</h2> 11469 11470 <div class="rtp-controls-v3"> 11471 <label> 11472 PILIH HIBURAN 11473 <select id="providerFilter"> 11474 <option value="ALL">SEMUA APLIKASI</option> 11475 </select> 11476 </label> 11477 11478 <label> 11479 MODE PERMAINAN 11480 <select id="slotTypeFilter"> 11481 <option value="HIGH">SANTAI</option> 11482 <option value="MEDIUM">PETUALANG</option> 11483 <option value="BOOST">TANTANGAN</option> 11484 </select> 11485 </label> 11486 11487 <label> 11488 KODE AKSES 11489 <input id="minRtp" type="number" min="0" max="9999" step="1" value="
114890"> 11490 </label> 11491 11492 <button id="btnRtpRandom" type="button"> 11493 <span class="icon"></span> MULAI JELAJAHI 11494 </button> 11495 </div> 11496 11497 <div class="rtp-main-result-v3" id="rtpResult"> 11498 <div class="rtp-status-v3" id="rtpStatus">â SIAP MAIN</div> 11499 11500 <div class="rtp-highlight-box"> 11501 <div class="rtp-col-left"> 11502 <div class="k">Kategori Game</div> 11503 <div class="v-game" id="rProvider">-</div> 11504 11505 <div class="provider-info"> 11506 <span class="k-small">Mode:</span> 11507 <span class="v-small" id="rSlotType">-</span> 11508 <span class="k-small" style="margin-left:10px;">ID:</span> 11509 <span class="v-small" id="rPercent">-</span> 11510 </div> 11511 </div> 11512 11513 <div class="rtp-col-right"> 11514 <div class="k-rtp">ATMOSFER</div> 11515 <div class="v-rtp-huge" id="rRtpValue">-</div> 11516 </div> 11517 </div> 11518 11519 <div class="rtp-details-v3"> 11520 <div class="rtp-detail-item"> 11521 <div class="k-detail">Koleksi</div> 11522 <div class="v-detail" id="rJam">-</div> 11523 </div> 11524 11525 <div class="rtp-detail-item"> 11526 <div class="k-detail">Petunjuk</div> 11527 <div class="v-detail" id="rPola">-</div> 11528 </div> 11529 11530 <div class="rtp-detail-item"> 11531 <div class="k-detail">Kondisi</div> 11532 <div class="v-detail" id="rStatusText">-</div> 11533 </div> 11534 </div> 11535 11536 <div class="rtp-tip-v3" id="rTip"> 11537 Tips: Nikmati ragam pilihan permainan seru dan nuansa kekinian yang menanti di balik langit bersama BOYTOTO. 11538 </div> 11539 </div> 11540 11541 <div class="rtp-alts-v3"> 11542 <div class="rtp-alts-title-v3">3 Tahap Menemukan Keseruan Game</div> 11543 <div class="rtp-alt-list-v3" id="rtpAltList"></div> 11544 </div> 11545 11546 <div class="rtp-note-v3"> 11547 Hak Cipta © BOYTOTO 11548 </div> 11549 </div> 11550 </div> 11551 11552 <style> 11553@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11554 11555.rtp-container-v3{ 11556 display:flex; 11557 justify-content:center; 11558 padding:16px 10px; 11559 font-family:"Orbitron","Poppins",system-ui,sans-serif; 11560} 11561 11562.rtp-machine-v3{ 11563 position: relative; 11564 width: 100%; 11565 max-width: 520px; 11566 padding: 16px; 11567 border-radius: 18px; 11568 overflow: hidden; 11569 isolation: isolate; 11570 background: radial-gradient(circle at 15% 0%, rgb(255 0 0 / 56%), transparent 48%), radial-gradient(circle at 85% 100%, rgb(255 0 0 / 63%), #8a0000f5 55%), linear-gradient(145deg, #3f0808, #050805); 11571 border: 1px solid rgb(231 0 0); 11572 box-shadow: inset 0 0 18px rgb(0 0 0), 0 0 18px rgb(255 0 0), 0 0 40px rgb(255 0 0); 11573 color: #ff0000; 11574 filter: drop-shadow(0 0 14px rgba(170, 255, 0, .35)); 11575} 11576 11577.rtp-machine-v3::before{ 11578 content:""; 11579 position:absolute; 11580 inset:0; 11581 background: 11582 linear-gradient( 11583 120deg, 11584 transparent 40%, 11585 rgba(183,255,0,.08), 11586 transparent 65% 11587 ); 11588 animation:hudSweep 7s linear infinite; 11589 pointer-events:none; 11590 opacity:.8; 11591} 11592 11593.rtp-machine-v3::after{ 11594 content:""; 11595 position:absolute; 11596 inset:0; 11597 background: 11598 repeating-linear-gradient( 11599 0deg, 11600 rgba(255,255,255,.02) 0px, 11601 rgba(255,255,255,.02) 1px, 11602 transparent 1px, 11603 transparent 3px 11604 ); 11605 mix-blend-mode:soft-light; 11606 pointer-events:none; 11607} 11608 11609.rtp-title-v3{ 11610 text-align:center; 11611 margin:0 0 12px; 11612 font-size:15px; 11613 font-weight:900; 11614 letter-spacing:1px; 11615 color:#000000; 11616 text-shadow: 11617 0 0 8px rgb(255 0 0 / 90%), 0 0 18px rgb(255 0 0 / 45%); 11618} 11619 11620.rtp-controls-v3{ 11621 display:grid; 11622 grid-template-columns:1fr 1fr; 11623 gap:10px; 11624 padding:12px; 11625 border-radius:14px; 11626 11627 background:rgba(10,18,5,0.72); 11628 11629 border:1px solid rgba(166,255,0,0.28); 11630 11631 box-shadow: 11632 inset 0 0 14px rgba(166,255,0,0.12), 11633 0 0 12px rgba(60,255,0,0.08); 11634} 11635 11636.rtp-controls-v3 label{ 11637 display:grid; 11638 gap:6px; 11639 font-size:10px; 11640 letter-spacing:.8px; 11641 text-transform:uppercase; 11642 color:#ff5757; 11643} 11644 11645.rtp-controls-v3 select, 11646.rtp-controls-v3 input{ 11647 background: rgba(5, 10, 4, .95);
11648 color: #ffd7d7; 11649 border: 1px solid rgb(255 0 0 / 35%); 11650 border-radius: 10px; 11651 padding: 7px 9px; 11652 font-weight: 700; 11653 font-size: 12px; 11654 outline: none; 11655 box-shadow: inset 0 0 10px rgb(255 0 0 / 80%), 0 0 8px rgba(120, 255, 0, .05); 11656} 11657 11658.rtp-controls-v3 select:focus, 11659.rtp-controls-v3 input:focus{ 11660 border-color:#b7ff00; 11661 box-shadow: 11662 0 0 12px rgba(166,255,0,.35), 11663 inset 0 0 12px rgba(166,255,0,.15); 11664} 11665 11666#btnRtpRandom{ 11667 grid-column:span 2; 11668 11669 display:flex; 11670 align-items:center; 11671 justify-content:center; 11672 gap:8px; 11673 11674 background:linear-gradient(145deg, #ff0000, #ff0000); 11675 11676 border:none; 11677 color:#111; 11678 11679 font-weight:900; 11680 letter-spacing:.8px; 11681 11682 border-radius:14px; 11683 cursor:pointer; 11684 11685 padding:10px 12px; 11686 font-size:12px; 11687 11688 box-shadow: 11689 0 0 18px rgb(255 0 0), inset 0 0 10px rgba(255, 255, 255, .18); 11690 11691 transition:.25s ease; 11692} 11693 11694#btnRtpRandom:hover{ 11695 transform:translateY(-2px); 11696 box-shadow: 116970 0 24px rgba(139, 0, 0, 0.8), 116980 0 45px rgba(139, 0, 0, 0.45); 11699} 11700 11701#btnRtpRandom:active{ 11702 transform:scale(.98); 11703} 11704 11705#btnRtpRandom:disabled{ 11706 opacity:.75; 11707 cursor:not-allowed; 11708} 11709 11710.rtp-main-result-v3{ 11711 margin-top: 12px; 11712 padding: 12px; 11713 border-radius: 14px; 11714 background: rgb(57 6 6 / 82%); 11715 border: 1px solid rgb(255 0 0); 11716 box-shadow: inset 0 0 16px rgb(255 0 0 / 80%), 0 0 10px rgb(255 0 0 / 65%); 11717} 11718 11719.rtp-status-v3{ 11720 text-align:center; 11721 font-weight:800; 11722 letter-spacing:.8px; 11723 color:#f60000; 11724 margin-bottom:10px; 11725 font-size:11px; 11726} 11727 11728.rtp-highlight-box{ 11729 display:flex; 11730 justify-content:space-between; 11731 gap:12px; 11732 padding-bottom:10px; 11733 margin-bottom:10px; 11734 11735 border-bottom:1px dashed rgba(166,255,0,0.22); 11736} 11737 11738.k, 11739.k-rtp{ 11740 font-size:10px; 11741 color:#ff0000; 11742 text-transform:uppercase; 11743} 11744 11745.v-game{ 11746 font-size:14px; 11747 font-weight:900; 11748 color:#ffffff; 11749 margin-top:6px; 11750 line-height:1.2; 11751 11752 text-shadow:0 0 12px rgba(166,255,0,.22); 11753} 11754 11755.provider-info{ 11756 margin-top:8px; 11757 font-size:11px; 11758 color:#e9ffd0; 11759 opacity:.95; 11760} 11761 11762.k-small{ 11763 color:#d10000; 11764 font-size:10px; 11765} 11766 11767.v-small{ 11768 color:#ffffff; 11769 font-weight:900; 11770} 11771 11772.v-rtp-huge{ 11773 font-size:2.4em; 11774 font-weight:900; 11775 11776 color:#b7ff00; 11777 11778 text-shadow: 11779 0 0 10px rgba(166,255,0,0.7), 11780 0 0 20px rgba(60,255,0,0.4); 11781 11782 line-height:1; 11783 letter-spacing:1px; 11784} 11785 11786.rtp-details-v3{ 11787 display:grid; 11788 grid-template-columns:repeat(3,1fr); 11789 gap:8px; 11790 11791 background:rgba(5,12,4,.45); 11792 11793 padding:10px; 11794 border-radius:12px; 11795} 11796 11797.k-detail{ 11798 font-size:9px; 11799 color:#ff0000; 11800 text-transform:uppercase; 11801} 11802 11803.v-detail{ 11804 font-size:12px; 11805 font-weight:800; 11806 color:#ffffff; 11807 margin-top:4px; 11808 letter-spacing:.8px; 11809} 11810 11811.rtp-tip-v3{ 11812 margin-top:10px; 11813 font-size:10px; 11814 color:#e6ffd0; 11815 opacity:.9; 11816 text-align:center; 11817} 11818 11819.rtp-alts-v3{ 11820 margin-top:12px; 11821 padding:12px; 11822 border-radius:14px; 11823 11824 background:rgba(5,10,4,.62); 11825 11826 border:1px solid rgba(166,255,0,0.22); 11827} 11828 11829.rtp-alts-title-v3{ 11830 font-weight:900; 11831 color:#ed0000; 11832 margin-bottom:10px; 11833 font-size:12px; 11834} 11835 11836.rtp-alt-list-v3{ 11837 display:grid; 11838 grid-template-columns:1fr; 11839 gap:8px; 11840} 11841 11842.rtp-alt-item{ 11843 background:rgba(5,15,5,.88); 11844 11845 border-radius:12px; 11846 padding:10px; 11847 11848 border-left:3px solid rgba(166,255,0,0.9); 11849 11850 box-shadow: 11851 0 0 12px rgba(60,255,0,.08); 11852} 11853 11854.rtp-alt-item .g{ 11855 font-weight:900; 11856 color:#ffffff; 11857 font-size:12px; 11858} 11859 11860.rtp-alt-item .p{ 11861 font-size:10px; 11862 color:#d9ff7a; 11863 margin-top:4px; 11864} 11865 11866.rtp-alt-item .r{ 11867 margin-top:6px; 11868 font-weight:900; 11869 color:#b7ff00; 11870 letter-spacing:.8px; 11871 font-size:12px; 11872 11873 text-shadow:0 0 8px rgba(166,255,0,.4); 11874} 11875 11876.rtp-note-v3{ 11877 margin-top:10px; 11878 font-size:10px; 11879 color:#e8ffd4; 11880 opacity:.85; 11881 text-align:center; 11882} 11883 11884@media(max-width:420px){ 11885 .rtp-highlight-box{ 11886 flex-direction:column; 11887 } 11888
11889 .rtp-details-v3{ 11890 grid-template-columns:1fr; 11891 } 11892} 11893 11894@keyframes hudSweep{ 11895 0%{ 11896 transform:translateX(-110%); 11897 } 11898 100%{ 11899 transform:translateX(110%); 11900 } 11901} 11902</style> 11903 11904
11904<script> 11905 const SHIO_LIST = ["Tikus","Kerbau","Macan","Kelinci","Naga","Ular","Kuda","Kambing","Monyet","Ayam","Anjing","Babi"]; 11906 const $ = (q) => document.querySelector(q); 11907 11908 function renderGuide(){ 11909 const wrap = $("#rtpAltList"); 11910 if(!wrap) return; 11911 const steps = [ 11912 ["1. Tentukan Nomor", "Siapkan nomor 2D, 3D, atau 4D yang ingin kamu cek."], 11913 ["2. Buka Daftar Shio", "Gunakan daftar shio yang sedang berlaku pada halaman hari ini."], 11914 ["3. Cocokkan Kelompok", "Cari nomor tersebut pada kelompok shio yang sesuai dan jadikan sebagai referensi kategori."] 11915 ]; 11916 wrap.innerHTML = ""; 11917 steps.forEach(([title,text]) => { 11918 const el = document.createElement("div"); 11919 el.className = "rtp-alt-item"; 11920 el.innerHTML = `<div class="g">${title}</div><div class="p">${text}</div>`; 11921 wrap.appendChild(el); 11922 }); 11923 } 11924 11925 (function initShioGuide(){ 11926 const select = $("#providerFilter"); 11927 const format = $("#slotTypeFilter"); 11928 const input = $("#minRtp"); 11929 const button = $("#btnRtpRandom"); 11930 if(!select || !format || !input || !button) return; 11931 11932 SHIO_LIST.forEach(name => { 11933 const option = document.createElement("option"); 11934 option.value = name; 11935 option.textContent = name.toUpperCase(); 11936 select.appendChild(option); 11937 }); 11938 11939 renderGuide(); 11940 $("#rtpStatus").textContent = "â SIAP â"; 11941 $("#rTip").textContent = "Tips: Shio hanya dipakai sebagai kategori angka dan tidak menjamin hasil permainan."; 11942 11943 button.addEventListener("click", () => { 11944 const shio = select.value === "ALL" ? "Pilih salah satu dari 12 shio" : select.value; 11945 const formatLabel = format.options[format.selectedIndex].textContent + "D".replace("DD","D"); 11946 const nomor = String(Math.max(0, Number(input.value) || 0)); 11947 11948 $("#rProvider").textContent = shio; 11949 $("#rSlotType").textContent = format.options[format.selectedIndex].textContent; 11950 $("#rPercent").textContent = `Nomor ${nomor}`; 11951 $("#rRtpValue").textContent = format.options[format.selectedIndex].textContent; 11952 $("#rJam").textContent = "Daftar Shio Hari Ini"; 11953 $("#rPola").textContent = "Cocokkan nomor ke kelompok shio"; 11954 $("#rStatusText").textContent = "REFERENSI"; 11955 $("#rtpStatus").textContent = "â CATATAN SIAP â"; 11956 }); 11957 })(); 11958 </script>
11958 11959 11960 11961 11962 11963 11964 11965 11966 11967 11968<style> 11969body{background:#ebf2fc; font-family:Poppins,sans-serif; color:white;} 11970.BOYTOTO-container{ 11971max-width: 520px; 11972 margin: auto; 11973 padding: 15px; 11974 border-radius: 12px; 11975 background: #a10808; 11976 border: 2px solid #ca0000; 11977 box-shadow: 0 0 20px #d90000; 11978} 11979 11980.BOYTOTO-header{ 11981 font-weight:900; 11982 text-align:center; 11983 color:#ff4c4c; 11984 text-shadow:0 0 12px #5f90d9; 11985 margin-bottom:12px; 11986} 11987 11988.BOYTOTO-controls{ 11989 display:grid; 11990 grid-template-columns:1fr 1fr; 11991 gap:10px; 11992 margin-bottom:12px; 11993} 11994.BOYTOTO-controls label{ 11995 display:flex; flex-direction:column; 11996 font-size:11px; color:#ff9999; 11997} 11998.BOYTOTO-controls select, 11999.BOYTOTO-controls input{ 12000 padding:8px; 12001 border-radius:8px; 12002 border:1px solid #ff4c4c; 12003 background:#2b0000; 12004 color:white; 12005} 12006#btnGenerate{ 12007 grid-column:span 2; 12008 padding:12px; 12009 background:#5f90d9; 12010 border:none; 12011 color:black; 12012 font-weight:900; 12013 cursor:pointer; 12014 margin-top:5px; 12015 box-shadow:0 0 15px #ff4c4c; 12016} 12017 12018.BOYTOTO-result{ 12019 margin-top:12px; 12020 padding:12px; 12021 border-radius:10px; 12022 background:rgba(255,0,0,0.05); 12023 border:1px solid #ff4c4c; 12024} 12025 12026.BOYTOTO-main div, .BOYTOTO-details div{ 12027 margin:5px 0; 12028} 12029 12030#rPrediction{ 12031 font-size:24px; 12032 font-weight:900; 12033 color:#5f90d9; 12034} 12035 12036.BOYTOTO-alts{ 12037 margin-top:10px; 12038} 12039.BOYTOTO-alts h4{margin-bottom:6px;} 12040#BOYTOTOAltList div{ 12041 background:#2b0000; 12042 padding:6px 10px; 12043 margin:3px 0; 12044 border-radius:8px; 12045 border-left:3px solid #5f90d9; 12046} 12047 .bank-tab.active, .bank-tab:hover { 12048 background: #df0000; 12049 color: #fff; 12050 border-color: #df0000; 12051 box-shadow: 0 3px 10px -4px rgb(240 29 29 / 60%); 12052} 12053 12054.BOYTOTO-disclaimer{ 12055 font-size:10px; 12056 margin-top:8px; 12057 text-align:center; 12058 color:#ff9999; 12059} 12060</style> 12061
12062<script> 12063const leagues = ["Premier League","La Liga","Serie A"]; 12064const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12065function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12066 12067var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12068 const type = document.getElementById("betTypeFilter").value; 12069 const league = rand(leagues); 12070 12071 let pred; 12072 if(type==="1X2") pred=rand(["1","X","2"]); 12073 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12074 else pred=rand(["Home -1","Away +1","Draw"]); 12075 12076 document.getElementById("rLeague").innerText=league; 12077 document.getElementById("rPrediction").innerText=pred; 12078 document.getElementById("rBetType").innerText=type; 12079 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12080 document.getElementById("rTip").innerText=rand(tips); 12081 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12082 12083 // alternatif 12084 const alt=document.getElementById("BOYTOTOAltList"); alt.innerHTML=""; 12085 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12086 12087 document.getElementById("BOYTOTOStatus").innerText="â READY â"; 12088} 12089</script>
12089 12090 12091 12092 12093 12094 12095 <div class="pwp-deals-section" id="pwp-deals-section" hidden data-product-id="325" data-variant-id="0" data-category-path="Panduan Shio/BOYTOTO/Kategori Angka"><div class="pwp-deals-header"><svg class="pwp-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg><div class="pwp-text"><h3 class="pwp-title">Penawaran Spesial PWP (Purchase with Purchase)</h3><p class="pwp-subtitle">Gunakan daftar shio ini sebagai referensi kategori angka 4D</p></div></div><div class="pwp-deals-loading"><div class="pwp-spinner"></div><p>Memuat penawaran...</p></div><div class="pwp-deals-grid" id="pwp-deals-grid"></div><p class="pwp-cart-note">ð¡ Cocokkan nomor ke kelompok shio yang sesuai dan gunakan sebagai catatan pendukung</p></div> 12096 </div> 12097 </div> 12098 </section> 12099 12100 <aside class="pd-sidebar"> 12101 <div class="pd-side-card pd-buy-card"> 12102 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12103 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12104 <div> 12105 <label for="qty" class="qty-label">Qty</label> 12106 <input type="number" id="qty" name="qty" value="0" min="1" 12107 max="0" 12108 disabled> 12109 </div> 12110 <div class="pd-actions"> 12111 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>Simpan Panduan</button> 12112 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12113 <button type="button" class="btn-chat-icon" id="btn-chat-icon" aria-label="Tanya Shio" title="Tanya Shio"><svg viewBox="0 0 640 640" fill="currentColor"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg></button> 12114 </div> 12115 </form> 12116 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12117 <span class="paylater-icon">ð³</span> 12118 <span class="paylater-text"> 12119 <strong>Layanan BOYTOTO</strong> 12120 <small>Keseruan Game Di Balik Langit</small> 12121 </span> 12122</div> </div> 12123 12124 12125 12126 <div class="pd-side-card qoala-banner-card"> 12127 <div class="qoala-banner"> 12128 <div class="qoala-banner-content"> 12129 <div class="qoala-icon"> 12130 <center><img src="https://ik.imagekit.io/boytoto/logo.webp" alt="BOYTOTO" class="qoala-shield-img"></center> 12131 </div> 12132 <div class="qoala-text"> 12133 <div class="qoala-title">Lihat Daftar BOYTOTO Terbaru</div> 12134 <div class="qoala-subtitle">Verified</div> 12135 </div> 12136 </div> 12137 <a href="https://mgsco.org/boykita/" class="qoala-cta" 12138 data-ga-event="qoala_banner_click" 12139 data-product-id="325" 12140 data-product-name="BOYTOTO" 12141 data-category-path="Panduan Shio/BOYTOTO/Kategori Angka" target="_blank">Info</a> 12142 </div> 12143<style> 12144.qoala-banner-card{padding:0;overflow:hidden} 12145 12146.qoala-banner{ 12147 background:linear-gradient(135deg, #cd0000 0%, #c90000 100%); 12148 padding:12px 16px; 12149 display:flex; 12150 align-items:center; 12151 justify-content:space-between; 12152 color:white; 12153 text-decoration:none; 12154 min-height:60px; 12155} 12156 12157.qoala-banner-content{ 12158 display:flex; 12159 align-items:center; 12160 gap:12px; 12161} 12162 12163.qoala-icon{ 12164 display:flex; 12165 align-items:center; 12166 justify-content:center; 12167} 12168 12169.qoala-shield-img{ 12170 width:36px; 12171 height:36px; 12172 object-fit:contain; 12173 opacity:0.9; 12174} 12175 12176.qoala-title{ 12177 font-weight:600; 12178 font-size:14px; 12179 line-height:1.2; 12180} 12181 12182.qoala-subtitle{ 12183 font-size:12px; 12184 opacity:0.9; 12185 line-height:1.2; 12186} 12187 12188.qoala-cta{ 12189 background:rgba(255,255,255,0.15); 12190 color:white; 12191 padding:6px 12px; 12192 border-radius:6px; 12193 font-size:12px; 12194 font-weight:600; 12195 text-decoration:none; 12196 transition:background 0.2s ease; 12197 border:1px solid rgba(255,80,80,0.4); 12198} 12199 12200.qoala-cta:hover{ 12201 background:rgba(255,60,60,0.25); 12202 color:white; 12203} 12204 12205@media(max-width:768px){ 12206 .qoala-banner{padding:10px 12px} 12207 .qoala-title{font-size:17px} 12208 .qoala-subtitle{font-size:11px} 12209} 12210</style> 12211 </div> 12212 12213 <div class="pd-side-card pd-installment-card2" 12214 id="inst-card" 12215 data-base-price="20789000" 12216 data-zero-smart="0"> 12217 <div class="inst-head"> 12218 <h3>Panduan & Fitur Game BOYTOTO</h3> 12219 </div> 12220 12221 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12222 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Koleksi Game</button> 12223 <button type="button" class="bank-tab" data-bank="Cara Cocokkan Nomor" aria-selected="false">Cara Bermain</button> 12224 <button type="button" class="bank-tab" data-bank="Catatan Penggunaan" aria-selected="false">Panduan Fitur</button> 12225 </div> 12226 12227 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12228 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12229 </div> 12230 12231 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12232<div class="tnt-review-mecha"> 12233 12234<div class="tnt-header"> 12235<h2>Komentar Pengguna Tentang Keseruan BOYTOTO</h2> 12236 12237</div> 12238 12239<div class="tnt-slider" id="tntSlider"> 12240 12241<div class="tnt-slide active"> 12242 <div class="tnt-name">Bintang â Jakarta</div> 12243 <div class="tnt-comment">"Main di BOYTOTO beneran seru banget! Pilihan game di balik langit ini beragam dan tampilannya kekinian, bikin betah buat ngisi waktu luang sambil nikmati hiburan menarik."</div> 12244</div> 12245 12246<div class="tnt-slide"> 12247 <div class="tnt-name">Reza â Medan</div> 12248 <div class="tnt-comment">"Sensasi gamenya terasa beda dan fresh. Penataan mode permainannya simpel jadi gampang diakses dari HP, pas banget buat yang cari suasana bermain baru yang interaktif."</div> 12249</div> 12250 12251<div class="tnt-slide"> 12252 <div class="tnt-name">Aditya â Yogyakarta</div> 12253 <div class="tnt-comment">"Fitur-fiturnya lengkap dan banyak pilihan mode seru. Atmosfer gamenya bener-bener modern, jadi makin asik jelajahi tiap tantangan game yang ada di BOYTOTO."</div> 12254</div> 12255 12256 12257 12258 12259 12260</div> 12261</div> 12262 12263</aside> 12264</div> 12265 12266<style> 12267.tnt-review-mecha{ 12268 background: radial-gradient(circle at center, #da0000, #350000); 12269 border: 2px solid #aa0000; 12270 border-radius: 14px; 12271 padding: 20px; 12272 color: #dbdbdb; 12273 font-family: 'Orbitron', sans-serif; 12274 position: relative; 12275 overflow: hidden; 12276 box-shadow: 0 0 25px #990000, inset 0 0 20px rgba(255, 0, 0, 0.2); 12277} 12278 12279/* scanline */ 12280.tnt-review-mecha::before{ 12281 content:""; 12282 position:absolute; 12283 inset:0; 12284 background: repeating-linear-gradient( 12285 to bottom, 12286 rgba(255,0,0,0.08), 12287 rgba(255,0,0,0.08) 1px, 12288 transparent 2px, 12289 transparent 4px 12290 ); 12291 pointer-events:none; 12292}.bank-tab.active, .bank-tab:hover 12293 12294 12295 12296.tnt-header h2{ 12297 font-size: 18px; 12298 color:#000000; 12299 text-shadow:0 0 10px #d71414, 0 0 20px #d10000; 12300 margin-bottom:5px; 12301} 12302 12303.tnt-header p{ 12304 font-size:12px; 12305 opacity:0.85; 12306 margin-bottom:15px; 12307} 12308 12309.tnt-slider{ 12310 position:relative; 12311 height:90px; 12312} 12313 12314.tnt-slide{ 12315 position:absolute; 12316 width:100%; 12317 opacity:0; 12318 transform:translateX(40px); 12319 transition:all .6s ease; 12320} 12321 .tenor-badge { 12322 background: #df0000; 12323 color: #fff; 12324 font-weight: 700; 12325 min-width: 42px; 12326 text-align: center; 12327 padding: .38rem .4rem; 12328 font-size: .8rem; 12329 border-radius: 10px; 12330 box-shadow: 0 3px 10px -4px rgb(211 3 3); 12331} 12332 12333.tnt-slide.active{ 12334 opacity:1; 12335 transform:translateX(0); 12336} 12337 12338.tnt-name{ 12339font-size: 14px; 12340 color: #000000; 12341 margin-bottom: 5px; 12342 text-shadow: 0 0 6px #290000; 12343} 12344 12345.tnt-comment{ 12346 font-size:13px; 12347 line-height:1.5; 12348} 12349 12350/* glowing border animation */ 12351@keyframes pulseGlow{ 12352 0%{box-shadow:0 0 10px #d71414;} 12353 50%{box-shadow:0 0 25px #d71414;} 12354 100%{box-shadow:0 0 10px #d71414;} 12355} 12356 12357.tnt-review-mecha{ 12358 animation:pulseGlow 2.5s infinite; 12359} 12360</style> 12361
12362<script> 12363let slides = document.querySelectorAll('.tnt-slide'); 12364let index = 0; 12365 12366setInterval(()=>{ 12367 slides[index].classList.remove('active'); 12368 index = (index + 1) % slides.length; 12369 slides[index].classList.add('active'); 12370}, 3000); 12371</script>
12371 12372 12373 <div class="artikel-glow-BOYTOTO"> 12374 12375 <div class="artikel-shine"></div> 12376 12377 <h1>BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler</h1> 12378 <p style="text-align: justify;">Di tengah berkembangnya dunia permainan online, banyak pemain mencari platform yang mampu menghadirkan pengalaman bermain yang nyaman, lengkap, dan mudah diakses. <a href="https://mgsco.org/">BOYTOTO</a> hadir sebagai showroom situs toto online yang menyediakan beragam pilihan permainan menarik dari provider ternama. Dengan koleksi game yang selalu diperbarui, setiap pemain dapat menikmati variasi permainan tanpa merasa bosan.</p> 12379 12380 <p style="text-align: justify;">Boytoto menghadirkan berbagai permainan <a href="https://mgsco.org/">Toto Online</a> dari sejumlah provider populer yang dikenal memiliki kualitas grafis, efek suara, dan fitur permainan yang menarik. Kehadiran banyak provider membuat variasi game semakin lengkap sehingga pemain dapat mencoba pengalaman bermain yang berbeda setiap harinya. Selain menghadirkan tema permainan yang beragam, setiap provider juga memiliki ciri khas tersendiri dalam menyajikan fitur bonus, animasi, dan mekanisme permainan.</p> 12381</div> 12382 12383<style> 12384.artikel-glow-BOYTOTO{ 12385 max-width: 820px; 12386 margin: 40px auto; 12387 padding: 36px; 12388 box-sizing: border-box; 12389 position: relative; 12390 overflow: hidden; 12391 border-radius: 30px; 12392 font-family: Arial, sans-serif; 12393 color: #ffffff; 12394 background: radial-gradient(circle at top left, rgba(177 2 2), transparent 20%), radial-gradient(circle at bottom right, rgb(75 0 0), transparent 28%), linear-gradient(135deg, #cd0707, #a90000, #c90303); 12395 border: 2px solid rgba(255, 255, 255, .55); 12396 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgb(225 0 0), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(145 0 0); 12397} 12398 12399.artikel-glow-BOYTOTO:before{ 12400 content:""; 12401 position:absolute; 12402 inset:10px; 12403 border-radius:24px; 12404 border:1px solid rgba(255,255,255,.32); 12405 pointer-events:none; 12406} 12407 12408.artikel-shine{ 12409 position:absolute; 12410 top:-120px; 12411 left:-180px; 12412 width:280px; 12413 height:650px; 12414 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12415 transform:rotate(25deg); 12416 animation:artikelShine 4.5s infinite; 12417 pointer-events:none; 12418} 12419 12420@keyframes artikelShine{ 12421 0%{left:-220px;} 12422 55%{left:110%;} 12423 100%{left:110%;} 12424} 12425 12426.artikel-glow-BOYTOTO h1, 12427.artikel-glow-BOYTOTO h2{ 12428 position:relative; 12429 color:#000000; 12430 text-align:center; 12431 line-height:1.35; 12432 text-shadow: 12433 0 0 8px rgba(179 0 0 / 95%), 12434 0 0 18px rgba(197 0 0), 12435 0 2px 8px rgba(149 0 0); 12436} 12437 12438.artikel-glow-BOYTOTO h1{ 12439 font-size:31px; 12440 margin:0 0 20px; 12441} 12442 12443.artikel-glow-BOYTOTO h2{ 12444 font-size:24px; 12445 margin:28px 0 14px; 12446} 12447 12448.artikel-glow-BOYTOTO p{ 12449 position:relative; 12450 margin:0 0 16px; 12451 color:#f8ffff; 12452 font-size:15px; 12453 line-height:1.85; 12454 text-align:justify; 12455 text-shadow:0 1px 4px rgba(0,0,0,.28); 12456} 12457 12458.artikel-glow-BOYTOTO a{ 12459 color:#000000; 12460 font-weight:800; 12461 text-shadow:0 0 8px rgba(153 0 0); 12462} 12463 12464.artikel-glow-BOYTOTO img{ 12465 position:relative; 12466 display:block; 12467 max-width:100%; 12468 margin:16px auto 24px; 12469 border-radius:16px; 12470 border:1px solid rgba(255,255,255,.55); 12471 box-shadow: 12472 0 0 18px rgba(255,255,255,.55), 12473 0 12px 28px rgba(0,0,0,.28); 12474} 12475 12476.artikel-highlight{ 12477 position:relative; 12478 margin:24px 0; 12479 padding:22px; 12480 border-radius:22px;
12481 background:rgba(255,255,255,.20); 12482 border:1px solid rgba(255,255,255,.45); 12483 box-shadow: 12484 inset 0 1px 0 rgba(255,255,255,.65), 12485 inset 0 0 22px rgba(255,255,255,.14), 12486 0 10px 25px rgba(0,0,0,.18); 12487 backdrop-filter:blur(8px); 12488} 12489 12490.artikel-highlight:before{ 12491 content:""; 12492 position:absolute; 12493 left:0; 12494 right:0; 12495 top:0; 12496 height:45%; 12497 border-radius:22px 22px 0 0; 12498 background:linear-gradient(rgba(255,255,255,.35),rgba(255,255,255,.04)); 12499 pointer-events:none; 12500} 12501 12502.artikel-glow-BOYTOTO ul{ 12503 position:relative; 12504 margin:15px 0 0 20px; 12505 padding:0; 12506} 12507 12508.artikel-glow-BOYTOTO li{ 12509 margin-bottom:11px; 12510 padding:10px 12px; 12511 border-radius:12px; 12512 background:rgba(255,255,255,.16); 12513 color:#ffffff; 12514 line-height:1.6; 12515 box-shadow:inset 0 0 10px rgba(255,255,255,.08); 12516} 12517 12518@media(max-width:600px){ 12519 .artikel-glow-BOYTOTO{ 12520 margin:25px 12px; 12521 padding:25px; 12522 border-radius:24px; 12523 } 12524 12525 .artikel-glow-BOYTOTO h1{ 12526 font-size:24px; 12527 } 12528 12529 .artikel-glow-BOYTOTO h2{ 12530 font-size:21px; 12531 } 12532} 12533</style> 12534 12535<div class="BOYTOTO-faq-rainbow"> 12536 <div class="BOYTOTO-shine"></div> 12537 12538 <div class="BOYTOTO-faq-top"> 12539 <span>â FAQ â </span> 12540 <h2>Pertanyaan Umum Game Seru BOYTOTO</h2> 12541 12542 </div> 12543 12544 <div class="BOYTOTO-rainbow-item"> 12545 <button type="button">Apa yang membuat keseruan BOYTOTO berbeda?</button> 12546 <div class="BOYTOTO-rainbow-answer"> 12547 <p>BOYTOTO menghadirkan hiburan di balik langit dengan ragam pilihan game seru kekinian. Platform ini dirancang interaktif agar setiap pengguna bisa menikmati sensasi bermain yang menyegarkan kapan saja.</p> 12548 </div> 12549 </div> 12550 <div class="BOYTOTO-rainbow-item"> 12551 <button type="button">Bagaimana cara mulai menikmati permainan di BOYTOTO hari ini?</button> 12552 <div class="BOYTOTO-rainbow-answer"> 12553 <p>Pilih kategori game favorit Anda dari daftar yang tersedia, lalu sesuaikan mode permainan yang diinginkan. Akses yang fleksibel memudahkan Anda langsung menjelajahi tantangan seru secara instan.</p> 12554 </div> 12555 </div> 12556 <div class="BOYTOTO-rainbow-item"> 12557 <button type="button">Apakah ada banyak pilihan mode permainan yang bisa dimainkan?</button> 12558 <div class="BOYTOTO-rainbow-answer"> 12559 <p>Ya. BOYTOTO menyediakan berbagai variasi permainan mulai dari tingkat santai hingga tantangan seru, sehingga setiap pemain dapat memilih mode yang paling sesuai dengan selera.</p> 12560 </div> 12561 </div> 12562 <div class="BOYTOTO-rainbow-item"> 12563 <button type="button">Untuk apa variasi game dihadirkan di platform ini?</button> 12564 <div class="BOYTOTO-rainbow-answer"> 12565 <p>Pilihan game yang beragam berfungsi memberikan referensi hiburan yang kaya, sehingga pengguna tidak merasa bosan dan dapat terus merasakan pengalaman bermain dengan nuansa kekinian.</p> 12566 </div> 12567 </div> 12568 <div class="BOYTOTO-rainbow-item"> 12569 <button type="button">Apakah ada jaminan kemenangan dalam setiap game?</button> 12570 <div class="BOYTOTO-rainbow-answer"> 12571 <p>Tidak. Setiap permainan bergantung pada keberuntungan serta strategi masing-masing pemain. Pilihan game di sini hadir sebagai media hiburan untuk dinikmati dengan seru dan menyenangkan.</p> 12572 </div> 12573 </div> 12574</div> 12575 12576<style> 12577.BOYTOTO-faq-rainbow{ 12578 max-width: 760px; 12579 margin: 40px auto; 12580 padding: 30px; 12581 box-sizing: border-box; 12582 position: relative; 12583 overflow: hidden; 12584 border-radius: 32px; 12585 font-family: Arial, sans-serif; 12586 background: radial-gradient(circle at 15% 10%, rgba(177 2 2), transparent 18%), radial-gradient(circle at 85% 15%, rgb(207 3 3), transparent 25%), radial-gradient(circle at 20% 90%, rgb(207 3 3), transparent 25%), linear-gradient(135deg, #c70000, #8e0b0b, #960e0e, #c30000); 12587 border: 2px solid rgba(177 2 2); 12588 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgba(223 0 0), inset 0 0 35px rgba(145 0 0); 12589} 12590 12591.BOYTOTO-shine{ 12592 position:absolute; 12593 top:-90px; 12594 left:-120px; 12595 width:260px; 12596 height:520px; 12597 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12598 transform:rotate(25deg); 12599 animation:BOYTOTOShine 4s infinite; 12600 pointer-events:none; 12601} 12602 12603@keyframes BOYTOTOShine{ 12604 0%{left:-180px;} 12605 55%{left:110%;} 12606 100%{left:110%;} 12607} 12608 12609.BOYTOTO-faq-top{ 12610 position:relative; 12611 text-align:center; 12612 margin-bottom:24px; 12613} 12614 12615.BOYTOTO-faq-top span{ 12616 display:inline-block; 12617 padding:8px 22px; 12618 border-radius:999px; 12619 background:rgba(185 0 0 / 0%); 12620 color:#dbdbdb; 12621 font-size:12px; 12622 font-weight:900; 12623 letter-spacing:2px; 12624 border:1px solid rgba(215 215 215 / 62%); 12625 box-shadow:inset 0 0 14px rgba(255,255,255,.25); 12626} 12627 12628.BOYTOTO-faq-top h2{ 12629 margin:16px 0 8px; 12630 color:#000000; 12631 font-size:34px; 12632 line-height:1.15; 12633 text-shadow: 12634 0 0 10px rgba(177 0 0), 12635 0 0 22px rgba(173 0 0); 12636} 12637 12638.BOYTOTO-faq-top p{ 12639 margin:0; 12640 color:#f3ffff; 12641 font-size:14px; 12642} 12643 12644.BOYTOTO-rainbow-item{ 12645 position:relative; 12646 overflow:hidden; 12647 margin-bottom:14px; 12648 border-radius:20px; 12649 background:rgba(255,255,255,.20); 12650 border:1px solid rgba(255,255,255,.48); 12651 box-shadow: 12652 inset 0 1px 0 rgba(255,255,255,.6), 12653 0 10px 25px rgba(0,0,0,.18); 12654 backdrop-filter:blur(10px); 12655} 12656 12657.BOYTOTO-rainbow-item:before{ 12658 content:""; 12659 position:absolute; 12660 top:0; 12661 left:0; 12662 right:0; 12663 height:45%; 12664 background:linear-gradient(rgba(255,255,255,.32),rgba(255,255,255,.04)); 12665 pointer-events:none; 12666} 12667 12668.BOYTOTO-rainbow-item button{ 12669 width:100%; 12670 padding:18px 58px 18px 20px; 12671 border:0; 12672 background: #af0000;
12673 color:#dbdbdb; 12674 text-align:left; 12675 cursor:pointer; 12676 font-size:15px; 12677 font-weight:900; 12678 position:relative; 12679 z-index:2; 12680 text-shadow:0 2px 6px rgba(0,0,0,.35); 12681} 12682 12683.BOYTOTO-rainbow-item button:after{ 12684 content:"+"; 12685 position:absolute; 12686 right:18px; 12687 top:50%; 12688 transform:translateY(-50%); 12689 width:34px; 12690 height:34px; 12691 border-radius:50%; 12692 background:linear-gradient(135deg,#b10000, #9b0000); 12693 color:#d1d1d1; 12694 text-align:center; 12695 line-height:34px; 12696 font-size:24px; 12697 font-weight:900; 12698 box-shadow: 12699 0 0 14px rgba(255,255,255,.8), 12700 inset 0 -4px 8px rgba(0,0,0,.18); 12701} 12702 12703.BOYTOTO-rainbow-item.active button:after{ 12704 content:"â"; 12705 background:linear-gradient(135deg,#ff0000, #5b0000); 12706} 12707 12708.BOYTOTO-rainbow-answer{ 12709 max-height:0; 12710 overflow:hidden; 12711 transition:max-height .35s ease; 12712 position:relative; 12713 z-index:2; 12714} 12715 12716.BOYTOTO-rainbow-item.active .BOYTOTO-rainbow-answer{ 12717 max-height:170px; 12718} 12719 12720.BOYTOTO-rainbow-answer p{ 12721 margin:0; 12722 padding:0 20px 18px; 12723 color:#dbdbdb; 12724 font-size:14px; 12725 line-height:1.7; 12726 text-shadow:0 2px 5px rgba(0 0 0); 12727} 12728 12729@media(max-width:600px){ 12730 .BOYTOTO-faq-rainbow{ 12731 margin:25px 12px; 12732 padding:22px; 12733 border-radius:24px; 12734 } 12735 12736 .BOYTOTO-faq-top h2{ 12737 font-size:26px; 12738 } 12739} 12740</style> 12741
12742<script> 12743document.querySelectorAll(".BOYTOTO-rainbow-item button").forEach(function(btn){ 12744 btn.addEventListener("click", function(){ 12745 this.parentElement.classList.toggle("active"); 12746 }); 12747}); 12748</script>
12748 12749<style> 12750@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12751 12752.cyber-mecha { 12753 max-width: 800px; 12754 margin: 40px auto; 12755 12756 /* deep luxury red */ 12757 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12758 12759 border-radius: 16px; 12760 border: 1px solid rgba(255, 90, 90, 0.35); 12761 12762 box-shadow: 12763 0 0 25px rgba(140, 30, 30, 0.35), 12764 inset 0 0 20px rgba(255, 60, 60, 0.10); 12765 12766 padding: 25px; 12767 font-family: 'Orbitron', sans-serif; 12768 color: #f7e6e6; 12769 position: relative; 12770 overflow: hidden; 12771} 12772 12773/* scanline overlay */ 12774.cyber-mecha::before { 12775 content: ""; 12776 position: absolute; 12777 inset: 0; 12778 background: repeating-linear-gradient( 12779 to bottom, 12780 rgba(255,90,90,0.03), 12781 rgba(255,90,90,0.03) 1px, 12782 transparent 1px, 12783 transparent 3px 12784 ); 12785 pointer-events: none; 12786} 12787 12788.pd-full-desc-title { 12789 text-align: center; 12790 font-size: 22px; 12791 12792 /* elegant muted red */ 12793 color: #e08b8b; 12794 12795 letter-spacing: 2px; 12796 text-shadow: 0 0 10px rgba(224, 139, 139, 0.4); 12797 margin-bottom: 20px; 12798} 12799 12800.mecha-header h3 { 12801 text-align: center; 12802 font-size: 18px; 12803 color: #ffb3b3; 12804 text-shadow: 0 0 8px rgba(255, 90, 90, 0.5); 12805 margin-bottom: 15px; 12806} 12807 12808.mecha-image img { 12809 width: 100%; 12810 border-radius: 10px; 12811 border: 1px solid rgba(255, 90, 90, 0.4); 12812 box-shadow: 0 0 20px rgba(255, 60, 60, 0.25); 12813 margin-bottom: 20px; 12814} 12815 12816.mecha-content p { 12817 font-size: 14px; 12818 line-height: 1.8; 12819 margin-bottom: 14px; 12820 color: #f3dede; 12821} 12822 12823.glow { 12824 color: #e08b8b; 12825 text-shadow: 0 0 6px rgba(224, 139, 139, 0.5); 12826 font-weight: 600; 12827} 12828 12829.glow-strong { 12830 color: #ff6b6b; 12831 text-shadow: 12832 0 0 10px rgba(255, 90, 90, 0.6), 12833 0 0 20px rgba(255, 60, 60, 0.4); 12834 font-weight: 700; 12835} 12836 12837.final-line { 12838 margin-top: 18px; 12839 padding: 10px; 12840 border: 1px solid rgba(255, 90, 90, 0.35); 12841 background: rgba(60, 0, 0, 0.25); 12842 border-radius: 8px; 12843 text-align: center; 12844 color: #ffd0d0; 12845} 12846</style> 12847 12848 12849 12850 12851 12852 <section class="pd-related-full"> 12853 <h2 class="pd-related-title">Panduan Shio Terkait</h2> 12854 <div class="pd-related-grid pd-related-grid--full"> 12855 <a class="pd-related-card" href="https://mgsco.org/"> 12856 <div class="thumb"> 12857 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO"> 12858 <span class="mini-disc">SHIO</span> </div> 12859 <div class="info"> 12860 <h3>BOYTOTO</h3> 12861 <div class="spec-chips">12 Shio <span class="spec-divider">|</span> Kategori Angka <span class="spec-divider">|</span> Referensi 4D</div> <span class="price"> 12862 <span class="final">Daftar Shio</span> 12863 <span class="ori">Referensi 4D</span> 12864 </span>s 12865 <span class="sold">Kategori Angka</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12866 </div> 12867 </a> 12868 <a class="pd-related-card" href="https://mgsco.org/"> 12869 <div class="thumb"> 12870 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO"> 12871 <span class="mini-disc">SHIO</span> </div> 12872 <div class="info"> 12873 <h3>BOYTOTO</h3> 12874 <div class="spec-chips">Shio Harian <span class="spec-divider">|</span> Cocokkan Nomor <span class="spec-divider">|</span> Referensi 4D</div> <span class="price"> 12875 <span class="final">Daftar Shio</span> 12876 <span class="ori">Referensi 4D</span> 12877 </span> 12878 <span class="sold">Kategori Angka</span> <span class="stock-dot out" title="Habis"></span> 12879 </div> 12880 </a> 12881 <a class="pd-related-card" href="https://mgsco.org/"> 12882 <div class="thumb"> 12883 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO">
12884 <span class="mini-disc">SHIO</span> </div> 12885 <div class="info"> 12886 <h3>BOYTOTO</h3> 12887 <div class="spec-chips">Nomor 4D <span class="spec-divider">|</span> Kelompok Shio <span class="spec-divider">|</span> Catatan Angka</div> <span class="price"> 12888 <span class="final">Daftar Shio</span> 12889 <span class="ori">Referensi 4D</span> 12890 </span> 12891 <span class="sold">Kategori Angka</span> <span class="stock-dot ready" title="Ready"></span> 12892 </div> 12893 </a> 12894 <a class="pd-related-card" href="https://mgsco.org/"> 12895 <div class="thumb"> 12896 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="SITUS BOYTOTO"> 12897 <span class="mini-disc">SHIO</span> </div> 12898 <div class="info"> 12899 <h3>SITUS BOYTOTO</h3> 12900 <div class="spec-chips">Nomor Harian <span class="spec-divider">|</span> Daftar Shio <span class="spec-divider">|</span> Kategori Angka</div> <span class="price"> 12901 <span class="final">Daftar Shio</span> 12902 <span class="ori">Referensi 4D</span> 12903 </span> 12904 <span class="sold">Kategori Angka</span> <span class="stock-dot ready" title="Ready"></span> 12905 </div> 12906 </a> 12907 <a class="pd-related-card" href="https://mgsco.org/"> 12908 <div class="thumb"> 12909 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="SITUS PARLAY"> 12910 <span class="mini-disc">SHIO</span> </div> 12911 <div class="info"> 12912 <h3>BANDAR BOYTOTO</h3> 12913 <div class="spec-chips">Shio Harian <span class="spec-divider">|</span> Cocokkan Nomor <span class="spec-divider">|</span> Referensi 4D</div> <span class="price"> 12914 <span class="final">Daftar Shio</span> 12915 <span class="ori">Referensi 4D</span> 12916 </span> 12917 <span class="sold">Kategori Angka</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12918 </div> 12919 </a> 12920 <a class="pd-related-card" href="https://mgsco.org/"> 12921 <div class="thumb"> 12922 <img src="https://ik.imagekit.io/boytoto/banner235.png" alt="MIX PARLAY"> 12923 <span class="mini-disc">SHIO</span> </div> 12924 <div class="info"> 12925 <h3>BOYTOTO ONLINE</h3> 12926 <div class="spec-chips">Toto 4D <span class="spec-divider">|</span> Shio <span class="spec-divider">|</span> Referensi Kategori</div> <span class="price"> 12927 <span class="final">Daftar Shio</span> 12928 <span class="ori">Referensi 4D</span> 12929 </span> 12930 <span class="stock-dot low" title="Stok Terbatas"></span> 12931 </div> 12932 </a> 12933 12934 </div> 12935 </section> 12936 12937 <div class="pd-mobile-fab" id="pd-mobile-fab" data-pid="325" data-price="20789000" data-voucher-code="" data-voucher-value="0"><div class="mf-actions"><button type="button" class="mf-btn mf-share" id="mf-share" aria-label="Bagikan"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/>
12937</svg></button><button type="button" class="mf-btn mf-chat" id="mf-chat" aria-label="Tanya Shio"><svg viewBox="0 0 640 640" fill="currentColor"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg></button><button type="button" class="mf-btn mf-cart" id="mf-cart" disabled aria-label="Tambah ke keranjang"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg></button><button type="button" class="mf-btn mf-buy" id="mf-buy" disabled><span class="mf-buy-text">Lihat Daftar BOYTOTO Terbaru</span><span class="mf-buy-price">Referensi Shio</span></button></div></div> 12938 12939 <button type="button" 12940 class="pd-scroll-top" 12941 id="pd-scroll-top" 12942 aria-label="Kembali ke atas" 12943 title="Kembali ke atas"> 12944 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 12945 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 12946 </svg> 12947 </button> 12948</main> 12949 12950<div class="pd-share-modal-overlay" id="pd-share-modal-overlay"><div class="pd-share-modal"><div class="pd-share-header"><h2 class="pd-share-title">Bagikan Panduan</h2><button type="button" class="pd-share-close" id="pd-share-close" aria-label="Tutup"><svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button></div><div class="pd-share-body"><div class="pd-share-preview"><img src="https://ik.imagekit.io/boytoto/banner235.png" alt="BOYTOTO" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">BOYTOTO</h3><p class="pd-share-preview-price">Referensi 4D</p></div></div><p class="pd-share-label">Bagikan ke</p><div class="pd-share-grid"><a href="#" class="pd-share-option" data-share="whatsapp" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-whatsapp"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/></svg></span><span class="pd-share-name">WhatsApp</span></a><a href="#" class="pd-share-option" data-share="facebook" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-facebook"><svg viewBox="0 0 24 24">
12950<path fill="currentColor" d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></span><span class="pd-share-name">Facebook</span></a><a href="#" class="pd-share-option" data-share="twitter" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-twitter"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></span><span class="pd-share-name">Twitter</span></a><a href="#" class="pd-share-option" data-share="telegram" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-telegram"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg></span><span class="pd-share-name">Telegram</span></a><a href="#" class="pd-share-option" data-share="email"><span class="pd-share-icon pd-share-icon-email"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></span><span class="pd-share-name">Email</span></a><button type="button" class="pd-share-option" data-share="copy"><span class="pd-share-icon pd-share-icon-copy"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg></span><span class="pd-share-name">Salin Link</span></button><button type="button" class="pd-share-option" data-share="pdf"><span class="pd-share-icon pd-share-icon-pdf"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zM8 15.5h1.5c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5H8v4zm6 0h1c1.1 0 2-.9 2-2v-2c0-1.1-.9-2-2-2h-1v6zm-4.5-1.5H8v-1h1.5v1zM14 13h1v1h-1v-1z"/></svg></span><span class="pd-share-name">Export PDF</span></button></div><div class="pd-share-copy-section"><div class="pd-share-copy-input-wrap"><input type="text" class="pd-share-copy-input" id="pd-share-url-input" value="https://mgsco.org/" readonly><button type="button" class="pd-share-copy-btn" id="pd-share-copy-url-btn"><svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg><span class="pd-share-copy-text">Salin</span></button></div></div></div></div></div> 12951 12952<style>.toast-root{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);display:flex;flex-direction:column;gap:10px;z-index:99999;pointer-events:none;max-width:min(92vw,540px)}.toast{display:grid;grid-template-columns:28px 1fr auto;gap:12px;background:#0b1220;color:#eef3ff;border:1px solid rgba(255,255,255,.08);padding:12px 14px;border-radius:12px;box-shadow:0 10px 30px rgba(10,10,20,.35);pointer-events:auto;overflow:hidden}.toast .ico{align-self:start;margin-top:2px}.toast .ttl{font-weight:700;line-height:1.25;font-size:14px;margin-bottom:4px}.toast .msg{opacity:.95;line-height:1.45;font-size:13px}.toast .actions{display:flex;gap:8px;align-self:center}.toast .btn{background:#0ea5e9;color:#fff;border:0;border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;cursor:pointer}.toast .btn.alt{background:transparent;color:#8fb5ff;border:1px solid rgba(143,181,255,.35)}.toast .close{background:transparent;border:0;color:#98a2b3;cursor:pointer;padding:4px;margin-left:6px}.toast.success{border-left:4px solid #22c55e}.toast.info{border-left:4px solid #0ea5e9}.toast.warn{border-left:4px solid #f59e0b}.toast .progress{position:absolute;
12952left:0;bottom:0;height:3px;background:linear-gradient(90deg,#60a5fa,#22c55e);width:100%;transform-origin:left;animation:toastBar linear forwards}@keyframes toastBar{from{transform:scaleX(1)}to{transform:scaleX(0)}}@media(max-width:640px){.toast{grid-template-columns:24px 1fr auto;padding:12px}.toast .btn{padding:7px 10px}}</style> 12953<style> 12954.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 12955 12956.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 12957 12958@media (max-width: 540px) { 12959 .toast-root { 12960 left: 0; right: 0; transform: none; 12961 padding: 0 12px; 12962 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 12963 max-width: none; 12964 } 12965 .toast { 12966 grid-template-columns: 1fr; /* 1 kolom */ 12967 position: relative; 12968 padding: 14px 14px 12px 14px; 12969 border-radius: 16px; 12970 } 12971 .toast .ico { 12972 position: absolute; left: 14px; top: 14px; 12973 margin: 0; transform: translateY(1px); 12974 } 12975 .toast .cnt { padding-left: 34px; } 12976 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 12977 .toast .msg { font-size: 13px; opacity: .96; } 12978 .toast .actions { 12979 margin-top: 12px; gap: 10px; width: 100%; 12980 display: grid; grid-template-columns: 1fr 1fr; 12981 } 12982 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 12983 .toast .close { position: absolute; right: 6px; top: 6px; } 12984} 12985</style> 12986
12987<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 12988(function adjustToastFabOffset(){ 12989 const root = document.getElementById('toastRoot'); 12990 if (!root) return; 12991 12992 function isVisible(el){ 12993 if (!el) return false; 12994 const cs = getComputedStyle(el); 12995 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 12996 const r = el.getBoundingClientRect(); 12997 return r.width > 0 && r.height > 0; 12998 } 12999 13000 function update(){ 13001 const fab = document.getElementById('pd-mobile-fab'); 13002 let extra = 16; 13003 if (isVisible(fab)) { 13004 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13005 extra = h + 16; // FAB height + margin 13006 } 13007 root.style.setProperty('--fab-offset', extra + 'px'); 13008 } 13009 13010 window.addEventListener('resize', update, { passive: true }); 13011 const fab = document.getElementById('pd-mobile-fab'); 13012 if (window.ResizeObserver && fab) { 13013 const ro = new ResizeObserver(update); 13014 ro.observe(fab); 13015 } 13016 13017 update(); 13018})(); 13019</script>
13019 13020<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13021
13022<script nonce="se3iyzEHMc9FQEB4wSyd0w==">(function(){const root=document.getElementById('toastRoot');function svgIcon(kind){if(kind==='success')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#22c55e"><path d="M9 16.2l-3.5-3.5 1.4-1.4L9 13.4l7.7-7.7 1.4 1.4z"/></svg>';if(kind==='warn')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#f59e0b"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>';return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#0ea5e9"><path d="M11 7h2v2h-2zm0 4h2v6h-2z"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z" fill="none" stroke="#0ea5e9"/></svg>'} 13023window.showToast=function(opts){const o=Object.assign({title:'',message:'',type:'info',duration:4000,actions:[]},opts||{});const el=document.createElement('div');el.className='toast '+(o.type||'info');el.setAttribute('role','status');el.style.position='relative';const acts=(o.actions||[]).map(a=>{const lab=a.label||'OK';const cls='btn'+(a.variant==='alt'?' alt':'');return `<button type="button" class="${cls}" data-act="${lab}">${lab}</button>`}).join('');el.innerHTML=`<div class="ico" aria-hidden="true">${svgIcon(o.type)}</div><div class="cnt">${o.title?`<div class="ttl">${o.title}</div>`:''}${o.message?`<div class="msg">${o.message}</div>`:''}</div><div class="actions">${acts}</div><button type="button" class="close" aria-label="Tutup" title="Tutup"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M18.3 5.71L12 12.01l-6.3-6.3-1.4 1.41 6.3 6.3-6.3 6.3 1.4 1.41 6.3-6.3 6.3 6.3 1.41-1.41-6.3-6.3 6.3-6.3z"/></svg></button><div class="progress" style="animation-duration:${Math.max(1500,o.duration)}ms"></div>`; 13024 13025 const close = ()=> { 13026 if (!el.parentNode) return; 13027 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13028 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13029 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13030 }; 13031 13032 el.querySelector('.close').addEventListener('click', close); 13033 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13034 btn.addEventListener('click', ()=>{ 13035 const lab = btn.getAttribute('data-act'); 13036 try { 13037 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13038 } catch(_){} 13039 close(); 13040 }); 13041 }); 13042 13043 root.appendChild(el); 13044 setTimeout(close, o.duration); 13045 return el; 13046 }; 13047})(); 13048 13049const _0x1a2b=[{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":""},{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":"https://ik.imagekit.io/boytoto/banner235.png"},{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":"BOYTOTO"},{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":"BOYTOTO"},{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":"BOYTOTO"},{"url":"https://ik.imagekit.io/boytoto/banner235.png","alt":"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler","slug":"BOYTOTO"}],_0x3c4d="BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler",_0x5e6f="SB0B3T";function getVariantSummaryText(){const a=document.getElementById('pd-variant-summary');if(!a)return '';const b=Array.from(a.querySelectorAll('.vs-item strong')).map(c=>c.textContent.trim()).filter(Boolean);return b.length?b.join(' / '):''} 13050function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13051function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13052 13053function showSuccessToast(productId,qty,variantId){const variantText=getVariantSummaryText();const skuText=(function(){const el=document.getElementById('pd-sku');return el?el.textContent.trim():PD_SKU})();showToast({type:'warn',title:'Fitur Segera Hadir',message:`<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)}
13053 · Qty: ${qty}`,duration:5000,actions:[{label:'Lihat Keranjang',onClick:()=>{}},{label:'Lanjut Belanja',variant:'alt',onClick:()=>{}}]});try{if(window.ga4Ecomm){const qtySend=qty||1;window.ga4Ecomm.addToCart({currency:'IDR',price:20789000,quantity:qtySend,item_id:"SB0B3T",item_name:"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler",item_brand:"BOYTOTO",item_category:(["Komputer & Laptop","Laptop","Gaming"]||[]).join(' / ')})}}catch(_){}return false}function showErrorToast(){showToast({type:'error',title:'Gagal Menambah ke Keranjang',message:'Terjadi kesalahan saat memproses permintaan. Silakan coba lagi.',duration:5000})} 13054 13055function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13056 13057 let willRedirect = false; 13058 try { 13059 if (window.addToCart) { 13060 window.addToCart(id, 1, variantId); 13061 willRedirect = false; 13062 } 13063 } catch(_){} 13064 13065 if (willRedirect) { 13066 showToast({ 13067 type: 'info', 13068 title: 'Menuju Keranjang', 13069 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13070 duration: 1800 13071 }); 13072 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13073 } else { 13074 showToast({ 13075 type: 'warn', 13076 title: 'Fitur Beli Langsung Segera Hadir', 13077 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13078 duration: 6000, 13079 actions: [ 13080 { label: 'Simpan Panduan', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13081 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13082 ] 13083 }); 13084 } 13085} 13086 13087document.addEventListener('DOMContentLoaded',function(){var a=document.getElementById('pd-buy-form');if(a){a.addEventListener('submit',function(e){var b=parseInt(a.getAttribute('data-pid'),10)||0;if(window.addToCartSubmit){return window.addToCartSubmit(e,b)}})}});document.addEventListener('DOMContentLoaded',function(){var a=document.getElementById('btn-buy');if(a){a.addEventListener('click',function(){var b=a.getAttribute('data-id');buyNow(b)})}}); 13088 13089function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13090 13091(function initGallery(){const mainImg=document.getElementById('pd-main-img');const gallery=document.getElementById('pd-thumb-gallery');if(!mainImg||!gallery)return;gallery.addEventListener('click',function(ev){const btn=ev.target.closest('.pd-thumb-btn');if(!btn)return;const full=btn.getAttribute('data-full');if(full){mainImg.src=full;const alt=btn.getAttribute('data-alt')||'';if(alt)mainImg.alt=alt;const slug=btn.getAttribute('data-slug')||'';if(slug)mainImg.dataset.slug=slug;gallery.querySelectorAll('.pd-thumb-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active')}})})(); 13092 13093(function initMobileSliderCounter(){const slider=document.getElementById('pd-mobile-slider');const counter=document.getElementById('pd-gallery-counter');if(!slider||!counter)return;let ticking=false;slider.addEventListener('scroll',function(){if(!ticking){window.requestAnimationFrame(function(){const slides=slider.querySelectorAll('.pd-mslide');if(!slides.length)return;const containerRect=slider.getBoundingClientRect();const containerCenter=containerRect.left+containerRect.width/2;let currentIdx=0;let minDist=Infinity;slides.forEach((slide,idx)=>{const slideRect=slide.getBoundingClientRect();const slideCenter=slideRect.left+slideRect.width/2;const dist=Math.abs(slideCenter-containerCenter);if(dist<minDist){minDist=dist;currentIdx=idx}});counter.textContent=(currentIdx+1)+'/'+slides.length;ticking=false});ticking=true}},{passive:true})})(); 13094 13095/* ================= Simulasi Cicilan ================= */ 13096(function initInstallmentList(){const card=document.getElementById('inst-card');
13096if(!card)return;function getBasePrice(){return parseInt(card.getAttribute('data-base-price')||'0',10)||0}function setBasePrice(v){card.setAttribute('data-base-price',String(parseInt(v||'0',10)||0))}const zeroEligible=card.getAttribute('data-zero-smart')==='1';window.initMobile=zeroEligible;const listEl=document.getElementById('inst-month-list');const zeroNote=document.getElementById('inst-zero-note');const tenors=[3,6,12];const rates={BRI:{3:0.975,6:0.975,12:0.975},Mandiri:{3:0.984,6:0.9725,12:0.96},BCA:{3:0.97,6:0.94,12:0.91}};function formatRupiah(num){return 'Rp '+(Math.round(num)).toString().replace(/B(?=(d{3})+(?!d))/g,'.')}function activeBank(){const tab=card.querySelector('.bank-tab.active');return tab?tab.dataset.bank:'BRI'} 13097 13098function _0x4o5p(bank){const a=getBasePrice();listEl.innerHTML='';const b=rates[bank]||{};let c=false;tenors.forEach(m=>{let d=b[m]??1;if(zeroEligible&&(bank==='BRI'||bank==='Mandiri')){d=1;c=true}const e=a/d;const f=m>0?Math.ceil((e/m)/100)*100:0;const g=document.createElement('li');g.className='inst-month-item';g.innerHTML=`<span class="tenor-badge">${m}x</span><span class="amount">${formatRupiah(f)} / bln</span>${zeroEligible&&(bank==='BRI'||bank==='Mandiri')?'<span class="tag tag-zero">0%</span>':''}`;listEl.appendChild(g)});zeroNote.style.display=c?'block':'none'} 13099 13100card.addEventListener('click',function(ev){const a=ev.target.closest('.bank-tab');if(!a)return;card.querySelectorAll('.bank-tab').forEach(b=>{b.classList.remove('active');b.setAttribute('aria-selected','false')});a.classList.add('active');a.setAttribute('aria-selected','true');_0x4o5p(a.dataset.bank)}); 13101 13102window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13103})(); 13104 13105/* ====== Fokus gambar varian ====== */ 13106function _0x6q7r(s){return String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'')}function _0x8s9t(u){try{return new URL(u,window.location.origin).pathname.split('/').pop().toLowerCase()}catch(e){try{return(u||'').split('/').pop().toLowerCase()}catch(_){return ''}}} 13107function _0xau1v(url,altOpt){const a=document.getElementById('pd-main-img'),b=document.getElementById('pd-thumb-gallery'),c=document.getElementById('pd-mobile-slider');if(a&&url){a.src=url;if(altOpt)a.alt=altOpt}if(b){const d=_0x8s9t(url);let e=null;b.querySelectorAll('.pd-thumb-btn').forEach(btn=>{const f=btn.getAttribute('data-full')||'',g=_0x8s9t(f)===d;if(g)e=btn;btn.classList.toggle('active',g)});if(e){const h=e.getAttribute('data-alt')||'',i=e.getAttribute('data-slug')||'';if(h&&a)a.alt=h;if(i&&a)a.dataset.slug=i}} 13108 if (c){ 13109 const targetName = _0x8s9t(url); 13110 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13111 if (slideImg){ 13112 13113 try { 13114 const container = c; 13115 const imgRect = slideImg.getBoundingClientRect(); 13116 const contRect = container.getBoundingClientRect(); 13117 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13118 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13119 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13120 } catch(_){ 13121 13122 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13123 } 13124 } 13125 } 13126} 13127function findImageForVariant(variant){ 13128 if (!variant || !variant.attrs) return null; 13129 const tokens = []; 13130 try{ 13131 Object.values(variant.attrs).forEach(v=>{ 13132 if (!v) return; 13133 const raw = String(v); 13134 tokens.push(raw); 13135 tokens.push(normalize(raw)); 13136 }); 13137 }catch(_){} 13138 if (variant.sku){ 13139 tokens.push(String(variant.sku)); 13140 tokens.push(normalize(String(variant.sku))); 13141 } 13142 if (variant.image_url) { 13143 tokens.push(String(variant.image_url)); 13144 } 13145 let best = null; 13146 let bestScore = -1; 13147 13148 for (const g of (galleryData||[])){ 13149 const slugN = normalize(g.slug||''); 13150 const altN = normalize(g.alt||''); 13151 const file = _0x8s9t(g.url||''); 13152 const fileN = normalize(file); 13153 const imgU = g.url; 13154 13155 for (const t of tokens){ 13156 const tN = normalize(t); 13157 if (!tN) continue; 13158 13159 let score = -1; 13160 if (imgU === tN) score = 300; 13161 if (slugN && slugN === tN) score = 300; 13162 else if (fileN && fileN.includes(tN)) score = 200; 13163 else if (altN && altN.includes(tN)) score = 120; 13164 13165 if (score > bestScore){ 13166 bestScore = score; 13167 best = g; 13168 } 13169 if (bestScore >= 300) break; 13170 } 13171 if (bestScore >= 300) break; 13172 } 13173 return bestScore >= 120 ? best : null; 13174} 13175function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13176 13177/* ====== Layout mobile ====== */ 13178(function _0xg04h(){const a=document.querySelector('.pd-media-card'),b=document.getElementById('pd-price-block'),c=document.getElementById('pd-price-anchor'),d=document.querySelector('.pd-info-card'),e=document.getElementById('pd-title'),f=document.getElementById('pd-variant-combined'),g=document.getElementById('pd-variants'),h=document.getElementById('pd-variants-anchor'),i=document.getElementById('pd-variant-summary'),j=document.getElementById('pd-summary-anchor'),titleRow=document.querySelector('.pd-title-row'),promoBanner=document.querySelector('.promo-endyear-mini-banner'),compareSection=document.querySelector('.pd-compare-section');if(!a||!b||!c||!d||!e)return; 13179 13180 let pricePlaced = false;
13181 let varPlaced = false; 13182 let titlePlaced = false; 13183 let promoPlaced = false; 13184 const mq = window.matchMedia('(max-width: 640px)'); 13185 13186 function apply(){ 13187 if (mq.matches) { 13188 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13189 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13190 if (!varPlaced && f && d && e) { 13191 try{ 13192 f.hidden = false; 13193 if (i && !f.contains(i)) f.appendChild(i); 13194 if (g && !f.contains(g)) f.appendChild(g); 13195 if(d.contains(e))d.insertBefore(f, e); 13196 varPlaced = true; 13197 }catch(_){} 13198 } 13199 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13200 try{ 13201 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13202 promoPlaced = true; 13203 }catch(_){} 13204 } 13205 } else { 13206 if (titlePlaced && titleRow && d) { 13207 try{ 13208 const vcombined = document.getElementById('pd-variant-combined'); 13209 if (vcombined && d.contains(vcombined)) { 13210 d.insertBefore(titleRow, vcombined); 13211 } else { 13212 d.insertBefore(titleRow, d.firstChild); 13213 } 13214 titlePlaced=false; 13215 }catch(_){} 13216 } 13217 if (pricePlaced && b && c && c.parentNode) { 13218 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13219 } 13220 if (varPlaced) { 13221 try{ 13222 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13223 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13224 if (f) { f.hidden = true; f.innerHTML = ''; } 13225 varPlaced = false; 13226 }catch(_){} 13227 } 13228 if (promoPlaced && promoBanner && b) { 13229 try{ 13230 b.appendChild(promoBanner); 13231 promoPlaced = false; 13232 }catch(_){} 13233 } 13234 } 13235 } 13236 13237 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13238 apply(); 13239 13240 const fab = document.getElementById('pd-mobile-fab'); 13241 if (fab) { 13242 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13243 const btnCart = document.getElementById('mf-cart'); 13244 13245 if (btnCart) btnCart.addEventListener('click', function(){ 13246 const varInput = document.getElementById('pd-variant-id'); 13247 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13248 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13249 const variantText = getVariantSummaryText(); 13250 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13251 showToast({ 13252 type: 'success', 13253 title: 'Berhasil ditambahkan ke Keranjang', 13254 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13255 duration: 5000, 13256 actions: [ 13257 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13258 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13259 ] 13260 }); 13261 }); 13262 } 13263})(); 13264 13265/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13266(function(){ 13267 function adjustSidebarBanner() { 13268 if (window.innerWidth <= 768) return; 13269 const sidebar = document.querySelector('.pd-sidebar'); 13270 const infoCard = document.querySelector('.pd-info-card'); 13271 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13272 if (!sidebar || !infoCard || !banner) return; 13273 const sidebarHeight = sidebar.offsetHeight; 13274 const infoCardHeight = infoCard.offsetHeight; 13275 if (sidebarHeight > infoCardHeight) { 13276 banner.style.display = 'none'; 13277 } else { 13278 banner.style.display = ''; 13279 } 13280 } 13281 if (document.readyState === 'loading') { 13282 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13283 } else { 13284 adjustSidebarBanner(); 13285 } 13286 let resizeTimer; 13287 window.addEventListener('resize', function() { 13288 clearTimeout(resizeTimer); 13289 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13290 });
13291 const pwpSection = document.getElementById('pwp-deals-section'); 13292 if (pwpSection) { 13293 const observer = new MutationObserver(function(mutations) { 13294 let shouldRecheck = false; 13295 mutations.forEach(function(mutation) { 13296 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13297 shouldRecheck = true; 13298 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13299 shouldRecheck = true; 13300 } 13301 }); 13302 if (shouldRecheck) { 13303 setTimeout(adjustSidebarBanner, 100); 13304 } 13305 }); 13306 observer.observe(pwpSection, { 13307 attributes: true, 13308 childList: true, 13309 subtree: true 13310 }); 13311 } 13312 window.adjustSidebarBanner = adjustSidebarBanner; 13313})(); 13314 13315/* ====== Global constants ====== */ 13316const IS_DIGITAL=false; 13317const IS_PREORDER=false; 13318 13319/* ====== Variants state (REVISED) ====== */ 13320(function initVariants(){ 13321const _0x7g8h=[];window.variants=_0x7g8h; 13322window.isPreorderProduct=false; 13323 const attrOrder = []; 13324 13325 if (!variants.length || !attrOrder.length) return; 13326 13327 function comboKey(sel){ 13328 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13329 } 13330 13331 const variantMap = {};
13332 variants.forEach(v=>{ 13333 const key = comboKey(v.attrs); 13334 variantMap[key] = v; 13335 }); 13336 13337 function readSelection(){ 13338 const sel = {}; 13339 attrOrder.forEach(a=>{ 13340 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13341 sel[a] = r ? r.value : ''; 13342 }); 13343 return sel; 13344 } 13345 13346 function findVariant(sel){ 13347 return variantMap[comboKey(sel)] || null; 13348 } 13349 13350 function autoFillMissing(sel){ 13351 let changed = false; 13352 for (const a of attrOrder) { 13353 if (sel[a]) continue; 13354 const possible = []; 13355 const seen = new Set(); 13356 variants.forEach(v=>{ 13357 let ok = true; 13358 for (const a2 of attrOrder) { 13359 if (a2 === a) continue; 13360 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13361 } 13362 if (ok) { 13363 const val = v.attrs[a]; 13364 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13365 } 13366 }); 13367 if (possible.length === 1) { 13368 const val = possible[0]; 13369 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13370 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13371 } 13372 } 13373 return changed; 13374 } 13375 13376 function rupiah(n){ 13377 n = parseInt(n||0,10)||0; 13378 return 'Rp ' + n.toString().replace(/B(?=(d{3})+(?!d))/g,'.'); 13379 } 13380 13381 function focusGalleryByUrl(imageUrl) { 13382 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13383 if (isMobile) { 13384 _0xau1v(imageUrl); 13385 return; 13386 } 13387 13388 const gallery = document.getElementById('pd-thumb-gallery'); 13389 const targetName = _0x8s9t(imageUrl); 13390 13391 if (gallery) { 13392 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13393 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13394 if (btn) { btn.click(); return; } 13395 } 13396 13397 _0xau1v(imageUrl); 13398 } 13399 13400 13401 13402function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13403function updateBranchPopover(v){try{if(!v)return;const a=document.getElementById('sp-qty-meg'),b=document.getElementById('sp-qty-mdf'),c=document.getElementById('sp-qty-bda');if(a)a.textContent=formatStockLabel(Math.max(0,v.smm|0));if(b)b.textContent=formatStockLabel(Math.max(0,v.smf|0));if(c)c.textContent=formatStockLabel(Math.max(0,v.sba|0))}catch(_){}} 13404 13405 function calcVoucherDisc(type,value,price,maxDisc){if(type==='PERCENTAGE'){let d=(price*value)/100;if(maxDisc>0&&d>maxDisc)d=maxDisc;return Math.round(d)}return Math.round(value)} 13406 13407 function applyVariant(v, focusImage = true){ 13408 const elCur = document.getElementById('pd-price-current'); 13409 const elOri = document.getElementById('pd-price-original'); 13410 const elDisc = document.getElementById('pd-discount-badge'); 13411 const elSku = document.getElementById('pd-sku'); 13412 const elStock= document.getElementById('pd-stock-badge'); 13413 const elQty = document.getElementById('qty'); 13414 const btnAdd = document.getElementById('btn-add-cart'); 13415 const btnBuy = document.getElementById('btn-buy'); 13416 const hidVar = document.getElementById('pd-variant-id'); 13417 13418 const mfCart= document.getElementById('mf-cart'); 13419 const mfBuy = document.getElementById('mf-buy'); 13420 const fab = document.getElementById('pd-mobile-fab'); 13421 13422 if (hidVar) hidVar.value = v ? v.id : 0; 13423 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13424 if (elSku && v && v.sku) elSku.textContent = v.sku; 13425 13426 if (v && v.ori > v.final) { 13427 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13428 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13429 } else { 13430 if (elOri) elOri.style.display='none'; 13431 if (elDisc) elDisc.style.display='none'; 13432 } 13433 13434 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13435 let effectiveStock = stock; 13436 let isActivePreorder = false; 13437 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13438 effectiveStock = parseInt(v.preorder_remaining, 10); 13439 if (isNaN(effectiveStock)) effectiveStock = 0; 13440 isActivePreorder = true; 13441 } 13442 if (elStock){ 13443 elStock.classList.remove('in','out'); 13444 if (effectiveStock>0){ 13445 elStock.classList.add('in'); 13446 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13447 } else { 13448 elStock.classList.add('out'); 13449 elStock.textContent = 'Habis'; 13450 } 13451 } 13452 if (elQty){ 13453 elQty.max = String(effectiveStock>0?effectiveStock:0); 13454 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13455 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13456 } 13457 13458 const shouldDisable = !(v && effectiveStock>0); 13459 const updateButtons = () => { 13460 const btnAddCart = document.getElementById('btn-add-cart'); 13461 const btnBuyNow = document.getElementById('btn-buy'); 13462 const mobileCart = document.getElementById('mf-cart'); 13463 const mobileBuy = document.getElementById('mf-buy'); 13464 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEac
13464h(btn => { 13465 if (!btn) return; 13466 if (shouldDisable) { 13467 btn.disabled = true; 13468 btn.setAttribute('disabled', 'disabled'); 13469 btn.classList.add('stock-disabled'); 13470 btn.setAttribute('data-stock-blocked', 'true'); 13471 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13472 } else { 13473 btn.disabled = false; 13474 btn.removeAttribute('disabled'); 13475 btn.classList.remove('stock-disabled'); 13476 btn.removeAttribute('data-stock-blocked'); 13477 btn.removeAttribute('data-stock-reason'); 13478 } 13479 }); 13480 }; 13481 updateButtons(); 13482 setTimeout(updateButtons, 10); 13483 setTimeout(updateButtons, 50); 13484 setTimeout(updateButtons, 100); 13485 if (mfBuy && v) { 13486 const voucherBtn = document.getElementById('btn-buy-voucher'); 13487 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13488 let voucherDiscount = 0; 13489 13490 if (voucherBtn && voucherCode) { 13491 const vType = voucherBtn.dataset.voucherType; 13492 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13493 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13494 const finalPrice = v.final || 0; 13495 if (vType && vValue > 0) { 13496 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13497 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13498 } 13499 } 13500 13501 const finalPrice = v.final || 0; 13502 const textEl = mfBuy.querySelector('.mf-buy-text'); 13503 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13504 13505 if (effectiveStock > 0) { 13506 if (IS_PREORDER) { 13507 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13508 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13509 } else if (voucherCode && voucherDiscount > 0) { 13510 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13511 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13512 } else { 13513 if (textEl) textEl.textContent = 'Lihat Daftar BOYTOTO Terbaru'; 13514 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13515 } 13516 } else { 13517 if (textEl) textEl.textContent = 'Stok Habis'; 13518 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13519 } 13520 } 13521 13522 const isMobile = window.initMobile ?? 0; 13523 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13524 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13525 updateBranchPopover(v); 13526 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13527 if (v) { 13528 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13529 detail: { 13530 price: v.final, 13531 originalPrice: v.ori, 13532 discount: v.disc, 13533 stock: effectiveStock, 13534 sku: v.sku, 13535 variant_id: v.id 13536 } 13537 })); 13538 13539 const encodedHash = encodeVariantHash(v.id); 13540 if (encodedHash && window.history && window.history.replaceState) { 13541 const newHash = '#v=' + encodedHash; 13542 const newUrl = window.location.pathname + window.location.search + newHash; 13543 window.history.replaceState(null, '', newUrl); 13544 } 13545 } 13546 } 13547 13548 let sel = readSelection(); 13549 if (autoFillMissing(sel)) sel = readSelection(); 13550 13551 let v = findVariant(sel); 13552 if (!v) { 13553 const candidates = variants.filter(variant=>{ 13554 return attrOrder.every(a=>{ 13555 if (!sel[a]) return true; 13556 return variant.attrs[a] === sel[a]; 13557 }); 13558 }); 13559 if (candidates.length >= 1) { 13560 v = candidates[0]; 13561 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13562 attrOrder.forEach(a=>{ 13563 const val = sel[a]; 13564 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13565 if (input) input.checked = true; 13566 }); 13567 } 13568 } 13569 13570 const initSel = readSelection(); 13571 const initVar = findVariant(initSel) || variants[0] || null; 13572 if (document.readyState === 'loading') { 13573 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13574 } else { 13575 applyVariant(initVar, false); 13576 } 13577 13578function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13579 function refreshOptions(sel){ 13580 attrOrder.forEach(a=>{ 13581 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13582 if (!group) return; 13583 const chips = group.querySelectorAll('.v-chip'); 13584 chips.forEach(ch=>{ 13585 const val = ch.getAttribute('data-value'); 13586 const possible = variants.some(v=>{ 13587 if (v.attrs[a] !== val) return false; 13588 for (const a2 of attrOrder) { 13589 if (a2 === a) continue; 13590 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13591 } 13592 return true; 13593 }); 13594 const input = ch.querySelector('input[type=radio]'); 13595 const disabled = !possible; 13596 if (input) input.disabled = disabled; 13597 ch.classList.toggle('disabled', disabled); 13598 13599 let candidateVariant = null; 13600 if (possible) { 13601 candidateVariant = variants.find(v=>{ 13602 if (v.attrs[a] !== val) return false; 13603 for (const a2 of attrOrder) { 13604 if (a2 === a) continue; 13605 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13606 } 13607 return true; 13608 }); 13609 } 13610 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13611 ch.classList.toggle('out', !disabled && outOfStock); 13612 const badge = ch.querySelector('.chip-badge'); 13613 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13614 }); 13615 group.querySelectorAll('.v-chip').forEach(ch=>{ 13616 const input = ch.querySelector('input[type=radio]'); 13617 ch.classList.toggle('selected', input && input.checked); 13618 }); 13619 }); 13620 } 13621 updateSummary(sel); 13622 refreshOptions(sel); 13623 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13624 13625 const container = document.getElementById('pd-variants'); 13626 container && container.addEventListener('change', function(ev){ 13627 const radio = ev.target.closest('input[type=radio]'); 13628 if (!radio) return; 13629 sel = readSelection(); 13630 if (autoFillMissing(sel)) sel = readSelection(); 13631 const chosen = findVariant(sel); 13632 applyVariant(chosen || null, true); 13633 13634 updateSummary(sel); 13635 refreshOptions(sel); 13636 });
13637 13638window.applyVariant=applyVariant; 13639})(); 13640 13641(function cleanBrokenDescImages(){ 13642 const containers = [ 13643 document.querySelector('.pd-full-desc-body'), 13644 document.querySelector('.pd-short-desc') 13645 ].filter(Boolean); 13646 13647 containers.forEach(root => { 13648 const imgs = root.querySelectorAll('img'); 13649 imgs.forEach(img => { 13650 if (img.complete && img.naturalWidth === 0) { 13651 img.remove(); 13652 return; 13653 } 13654 img.addEventListener('error', () => { 13655 img.remove(); 13656 }, { once:true }); 13657 }); 13658 }); 13659})(); 13660 13661/* ====== Scroll To Top ====== */ 13662(function _0xi25j(){const a=document.getElementById('pd-scroll-top');if(!a)return;function b(){if(window.matchMedia('(max-width:640px)').matches){const d=document.querySelector('.bottom-nav'),e=document.querySelector('.pd-mobile-fab');if(!d&&!e){setTimeout(b,100);return}const f=d&&window.getComputedStyle(d).display!=='none'&&window.getComputedStyle(d).visibility!=='hidden',g=e&&window.getComputedStyle(e).display!=='none'&&window.getComputedStyle(e).visibility!=='hidden';let h;if(f&&g){h='calc(68px + 54px + env(safe-area-inset-bottom))'}else if(g){h='calc(68px + 8px + env(safe-area-inset-bottom))'}else if(f){h='calc(60px + env(safe-area-inset-bottom))'}else{h='calc(20px + env(safe-area-inset-bottom))'}a.style.setProperty('--scroll-top-bottom',h)}else{a.style.removeProperty('--scroll-top-bottom')}}function c(){const d=window.pageYOffset||document.documentElement.scrollTop||0,e=d>300;a.classList.toggle('visible',e);a.setAttribute('aria-hidden',e?'false':'true');b()}window.addEventListener('scroll',c,{passive:!0});window.addEventListener('resize',b,{passive:!0});setTimeout(b,100);c();a.addEventListener('click',function(){window.scrollTo({top:0,behavior:'smooth'})})})(); 13663 13664/* Format stok 10+ */ 13665function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13666 13667/* Toggle popover stok per cabang */ 13668(function _0xm89n(){const a=document.getElementById('pd-stock-info-btn'),b=document.getElementById('pd-stock-popover'),c=document.getElementById('pd-stock-popover-close');if(!a||!b)return;function d(){if(window.matchMedia('(min-width: 641px)').matches){const e=document.getElementById('pd-stock-badge');if(!e)return;const f=e.getBoundingClientRect(),g=window.pageXOffset||document.documentElement.scrollLeft||0,h=window.pageYOffset||document.documentElement.scrollTop||0;b.style.left=(f.left+g-10)+'px';b.style.top=(f.bottom+h+10)+'px'}else{b.style.left='';b.style.top=''}} 13669function e(){d();b.classList.add('show');a.setAttribute('aria-expanded','true');b.setAttribute('aria-hidden','false');document.addEventListener('click',g,{capture:true});window.addEventListener('resize',d,{passive:true});window.addEventListener('scroll',d,{passive:true})}function f(){b.classList.remove('show');a.setAttribute('aria-expanded','false');b.setAttribute('aria-hidden','true');document.removeEventListener('click',g,{capture:true});window.removeEventListener('resize',d);window.removeEventListener('scroll',d)}function g(h){if(b.contains(h.target)||a.contains(h.target))return;f()} 13670a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13671 13672/* ===== Share Product Modal ===== */ 13673(function(){const a=document.getElementById('pd-share-btn'),z=document.getElementById('mf-share'),b=document.getElementById('pd-share-modal-overlay'),c=document.getElementById('pd-share-close'),d=document.getElementById('pd-share-copy-url-btn'),e=document.getElementById('pd-share-url-input');if((!a&&!z)||!b)return;const f="https://mgsco.org/",g="BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler",h="Referensi Shio";const discountPercent=1;let i='';if(discountPercent>0){i=`ð¥ Lagi Promo Nih! Diskon ${discountPercent}%nn⨠${g}nð° Cuma ${h} aja!nnð Buruan cek sebelum kehabisan!nð `}else{i=`⨠Ada barang bagus nih!nnð¦ ${g}nð¸ Harga: ${h}nnðï¸ Yuk, langsung cek!nð `}function j(){b.classList.add('show');document.body.style.overflow='hidden'}function k(){b.classList.remove('show');document.body.style.overflow=''}if(a)a.addEventListener('click',j);if(z)z.addEventListener('click',j);if(c)c.addEventListener('click',k);b.addEventListener('click',function(l){if(l.target===b)k()});document.addEventListener('keydown',function(l){if(l.key==='Escape'&&b.classList.contains('show'))k()});b.addEventListener('click',function(l){const m=l.target.closest('[data-share]');if(!m)return;const n=m.getAttribute('data-share'),o=encodeURIComponent(f),p=e
13673ncodeURIComponent(i),q=encodeURIComponent(g);let r='';switch(n){case 'whatsapp':r=`https://wa.me/?text=${p}${o}`;break;case 'facebook':r=`https://www.facebook.com/sharer/sharer.php?u=${o}`;break;case 'twitter':r=`https://twitter.com/intent/tweet?text=${p}${o}`;break;case 'telegram':r=`https://t.me/share/url?url=${o}&text=${p}`;break;case 'email':r=`mailto:?subject=${encodeURIComponent('ð¥ Cek Produk Keren Ini!')}&body=${p}${o}`;window.location.href=r;k();return;case 'copy':s();return;case 'pdf':const ps=document.querySelector('.product-single');if(ps){ps.setAttribute('data-print-date',new Date().toLocaleDateString('id-ID',{year:'numeric',month:'long',day:'numeric'}))}k();setTimeout(function(){window.print();try{if(window.ga4Ecomm&&window.ga4Ecomm.trackEvent)window.ga4Ecomm.trackEvent('share',{method:'pdf_export',content_type:'product',item_id:"SB0B3T"})}catch(_){}},300);return}if(r){window.open(r,'_blank','noopener,noreferrer');try{if(window.ga4Ecomm&&window.ga4Ecomm.trackEvent)window.ga4Ecomm.trackEvent('share',{method:n,content_type:'product',item_id:"SB0B3T"})}catch(_){}}});function s(){if(!e)return;e.select();e.setSelectionRange(0,99999);try{if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(f).then(function(){t()}).catch(function(){document.execCommand('copy');t()})}else{document.execCommand('copy');t()}}catch(u){console.error('Failed to copy:',u)}}function t(){if(!d)return;const v=d.innerHTML;d.classList.add('copied');d.innerHTML='<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg><span class="pd-share-copy-text">Tersalin!</span>';if(window.showToast)showToast({type:'success',title:'⨠Link Tersalin!',message:'Link produk berhasil disalin. Yuk bagikan ke teman!',duration:3000});setTimeout(function(){d.classList.remove('copied');d.innerHTML=v},2000)}if(d)d.addEventListener('click',s);if(navigator.share){const shareHandler=function(w){w.preventDefault();navigator.share({title:g,text:i+f,url:f}).catch(function(x){console.log('Native share cancelled or failed:',x)})};if(a)a.addEventListener('dblclick',shareHandler);if(z)z.addEventListener('dblclick',shareHandler)}})(); 13674 13675/* ===== Embedding variants (server â client) ===== */ 13676(function(){ 13677 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13678})(); 13679 13680/* ===== Helper: update angka popover ===== */ 13681function updateStockPopoverForVariant(v){ 13682 try{ 13683 const elMeg = document.getElementById('sp-qty-meg'); 13684 const elMdf = document.getElementById('sp-qty-mdf'); 13685 const elBda = document.getElementById('sp-qty-bda'); 13686 13687 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13688 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13689 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13690 }catch(_){} 13691} 13692 13693/* ===== applyVariant: 10+ dan update popover ===== */ 13694(function patchApplyVariant(){ 13695 if (typeof applyVariant !== 'function') return; 13696 const original = applyVariant; 13697 window.applyVariant = function(v){ 13698 original(v); 13699 13700 try { 13701 const elStock = document.getElementById('pd-stock-badge'); 13702 if (elStock){ 13703 let txt = elStock.textContent || ''; 13704 let m = txt.match(/d+/); 13705 if (m) { 13706 const n = parseInt(m[0]||'0',10)||0; 13707 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13708 } 13709 } 13710 } catch(_){} 13711if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13712 }; 13713})(); 13714 13715(function initCurrentVariantBranch(){ 13716 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13717 if (!list.length) return; 13718 const hid = document.getElementById('pd-variant-id'); 13719 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13720 const current = list.find(x => x.id === vid) || list[0] || null; 13721 if (current) updateStockPopoverForVariant(current); 13722})(); 13723 13724(function(){ 13725 if (!window.ga4Ecomm) return; 13726 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13727 window.ga4Ecomm.viewItem({ 13728 currency: 'IDR', 13729 value: 20789000, 13730 item_id: "SB0B3T", 13731 item_name: "BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler", 13732 item_brand: "BOYTOTO",
13733 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13734 }); 13735 })(); 13736 13737(function patchApplyVariantFocus(){ 13738 if (typeof applyVariant !== 'function') return; 13739 const original = applyVariant; 13740 window.applyVariant = function(v, focusImage = true){ 13741 13742 original(v, focusImage); 13743 13744 if (focusImage && v && v.image_url) { 13745 _0xau1v(v.image_url); 13746 } 13747 }; 13748})(); 13749 13750(function() { 13751 var soldMap = []; 13752 var soldTotalLabel = "Terjual 2"; 13753 13754 function formatSoldLabel(qty) { 13755 if (qty >= 1000000) { 13756 var v = Math.floor((qty / 100000) ) / 10; 13757 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13758 } 13759 if (qty >= 1000) { 13760 var v2 = Math.floor((qty / 100) ) / 10; 13761 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13762 } 13763 if (qty >= 100) { 13764 var v2 = Math.floor((qty / 100) ) * 100; 13765 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13766 } 13767 if (qty >= 10) { 13768 var v2 = Math.floor((qty / 10) ) * 10; 13769 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13770 } 13771 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13772 } 13773 13774 function updateSoldLabel(labelText) { 13775 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13776 if (el) el.textContent = labelText; 13777 } 13778 13779 window.updateSoldBySku = function(sku) { 13780 if (!sku || typeof sku !== 'string') { 13781 updateSoldLabel(soldTotalLabel); 13782 return; 13783 } 13784 var qty = soldMap[sku] || 0; 13785 updateSoldLabel(formatSoldLabel(qty)); 13786 }; 13787 13788 updateSoldLabel(soldTotalLabel); 13789 })(); 13790 13791(function(){const h=document.getElementById('pd-sticky-header');if(!h)return;const p=document.getElementById('pd-price-anchor');const ac=document.getElementById('pd-sticky-add-cart');const by=document.getElementById('pd-sticky-buy');let lt=window.scrollY;let tk=false;function u(){if(!p)return;const r=p.getBoundingClientRect();const s=window.scrollY;if(r.top<0){h.classList.add('visible')}else{h.classList.remove('visible')}lt=s;tk=false}function rt(){if(!tk){window.requestAnimationFrame(u);tk=true}}window.addEventListener('scroll',rt,{passive:true});u();function sy(){const ma=document.getElementById('btn-add-cart');const mb=document.getElementById('btn-buy');if(ac&&ma){ac.disabled=ma.disabled}if(by&&mb){by.disabled=mb.disabled}}sy();const ob=new MutationObserver(sy);const ma=document.getElementById('btn-add-cart');const mb=document.getElementById('btn-buy');if(ma)ob.observe(ma,{attributes:true,attributeFilter:['disabled']});if(mb)ob.observe(mb,{attributes:true,attributeFilter:['disabled']});const ss=document.getElementById('pd-sticky-share-btn');const ms=document.getElementById('pd-share-btn');if(ss&&ms){ss.addEventListener('click',function(e){e.preventDefault();ms.click()})}if(ac){ac.addEventListener('click',function(e){e.preventDefault();if(ac.disabled)return;const m=document.getElementById('btn-add-cart');if(m&&!m.disabled){m.click()}})}if(by){by.addEventListener('click',function(e){e.preventDefault();if(by.disabled)return;const m=document.getElementById('btn-buy');if(m&&!m.disabled&&window.buyNow){window.buyNow(e)}})} 13792function getChatProductData(){const varInput=document.getElementById('pd-variant-id');const variantId=varInput?parseInt(varInput.value||'0',10):0;const variantSummary=getVariantSummaryText();const priceEl=document.getElementById('pd-price-current');const currentPrice=priceEl?priceEl.textContent.trim():'Referensi Shio';const pd={id:325,name:"BOYTOTO: Showroom Situs Boy Toto Kita Online dengan Beragam Games Menarik dan Terpopuler",price:currentPrice,image:"/uploads/products/BOYTOTO",url:window.location.href};if(variantId>0){pd.variant_id=variantId}if(variantSummary){pd.variant_summary=variantSummary}return pd} 13793const ch=document.getElementById('pd-sticky-chat');if(ch){ch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}const dch=document.getElementById('btn-chat-icon');if(dch){dch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}const mch=document.getElementById('mf-chat');if(mch){mch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}window.addEventListener('pd-variant-changed',function(e){const d=e.detail||{};if(d.price!==undefined){const sp=document.getElementById('pd-sticky-price');if(sp){sp.textContent=formatRupiah(d.price)}}if(d.originalPrice!==undefined&&d.originalPrice>d.price){const so=document.getElementById('pd-sticky-price-original');if(so){so.textContent=formatRupiah(d.originalPrice);so.style.display=''}}else{const so=document.getElementById('pd-sticky-price-original');if(so){so.style.display='none'}}if(d.discount!==undefined&&d.discount>0){const sd=document.getElementById('pd-sticky-discount');if(sd){sd.textContent='-'+d.discount+'%';sd.style.display=''}}else{const sd=document.getElementById('pd-sticky-discount');if(sd){sd.style.display='none'}}const hs=d.stock>0;if(ac){ac.disabled=!hs}if(by){by.disabled=!hs}});
13793function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13794 13795(function(){const t=document.getElementById('pdv-toggle-btn');const v=document.getElementById('pd-voucher-section');if(t&&v){const h=v.querySelectorAll('.pdv-min-hidden');const n=h.length;t.addEventListener('click',function(){const e=t.classList.toggle('expanded');h.forEach(function(i){if(e){i.classList.remove('pdv-min-hidden')}else{i.classList.add('pdv-min-hidden')}});t.textContent=e?'Tutup':'+'+n})}const m=document.getElementById('voucher-detail-modal');const mc=document.getElementById('voucher-modal-close');const ma=document.getElementById('voucher-modal-cancel');const mcp=document.getElementById('voucher-modal-copy');if(m){const bb=document.querySelectorAll('.pdv-badge-btn');bb.forEach(function(btn){btn.addEventListener('click',function(){if(btn.disabled)return;const c=btn.getAttribute('data-voucher-code');const nm=btn.getAttribute('data-voucher-name');const dl=btn.getAttribute('data-discount-label');const bl=btn.getAttribute('data-badge-label');const mp=parseInt(btn.getAttribute('data-min-purchase'));const md=parseInt(btn.getAttribute('data-max-discount'));const vu=btn.getAttribute('data-valid-until');document.getElementById('voucher-modal-badge').innerHTML='<span class="vmb-value">'+dl+'</span><span class="vmb-label">'+bl+'</span>';document.getElementById('voucher-modal-code').textContent=c;document.getElementById('voucher-modal-name').textContent=nm;const me=document.getElementById('voucher-modal-min');if(mp>0){me.style.display='';document.getElementById('voucher-modal-min-val').textContent=fr(mp)}else{me.style.display='none'}const xe=document.getElementById('voucher-modal-max');if(md>0){xe.style.display='';document.getElementById('voucher-modal-max-val').textContent=fr(md)}else{xe.style.display='none'}if(vu){const d=new Date(vu);const ft=d.toLocaleDateString('id-ID',{day:'numeric',month:'long',year:'numeric',hour:'2-digit',minute:'2-digit'});document.getElementById('voucher-modal-valid-val').textContent=ft}mcp.setAttribute('data-code',c);m.classList.add('show');document.body.style.overflow='hidden'})});function cm(){m.classList.remove('show');document.body.style.overflow=''}if(mc)mc.addEventListener('click',cm);if(ma)ma.addEventListener('click',cm);m.addEventListener('click',function(e){if(e.target===m)cm()});if(mcp){mcp.addEventListener('click',function(){const c=mcp.getAttribute('data-code');if(!c)return;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(c).then(function(){smc()}).catch(function(err){fc(c)})}else{fc(c)}})}}const b=document.querySelectorAll('.pdv-min-copy');b.forEach(function(btn){btn.addEventListener('click',function(){if(btn.disabled)return;const code=btn.getAttribute('data-code');if(!code)return;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(code).then(function(){sc(btn)}).catch(function(err){fc(code,btn)})}else{fc(code,btn)}})});function fc(text,btn){const tx=document.createElement('textarea');tx.value=text;tx.style.position='fixed';tx.style.opacity='0';document.body.appendChild(tx);tx.select();try{document.execCommand('copy');if(btn)sc(btn);else smc()}catch(err){}document.body.removeChild(tx)}function sc(btn){const o=btn.textContent;btn.classList.add('copied');btn.textContent='â Tersalin';setTimeout(function(){btn.classList.remove('copied');btn.textContent=o},2000)}function smc(){const o=mcp.textContent;mcp.textContent='â Kode Tersalin';mcp.style.background='#22c55e';setTimeout(function(){mcp.textContent=o;mcp.style.background=''},2000)}function fr(a){return'Rp'+Math.floor(a).toLocaleString('id-ID')}})(); 13796</script>
vendor: 2 bytes, line 13796
13796
13797<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13798window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13799window.IS_DIGITAL=IS_DIGITAL; 13800window.isPreorderProduct=false; 13801window.PD_PREORDER_DATA=null; 13802</script>
13802 13803
13804<script data-inlined="pd-lightbox.js"> 13805// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13806(function(){ 13807 const state = { 13808 items: [], 13809 open: false, 13810 idx: 0, 13811 scale: 1, 13812 minScale: 1, 13813 maxScale: 4, 13814 tx: 0, ty: 0, 13815 isDragging: false, 13816 dragStartX: 0, dragStartY: 0, 13817 baseTx: 0, baseTy: 0, 13818 touchStartX: 0, touchStartY: 0, 13819 lastTap: 0, 13820 swipeMode: 'none', // 'none' | 'h' | 'v' 13821 totalDx: 0, totalDy: 0 13822 }; 13823 13824 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13825 13826 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13827 13828 function absUrl(u){ 13829 if (!u) return ''; 13830 try { 13831 if (/^https?:///i.test(u)) return u; 13832 return new URL(u, ORIGIN).href; 13833 } catch(_) { return u; } 13834 } 13835 function fileName(u){ 13836 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13837 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13838 } 13839 13840 function buildItems(){ 13841 const gd = (window.galleryData || []).filter(Boolean); 13842 if (gd.length){ 13843 state.items = gd.map(g => ({ 13844 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13845 })).filter(it => !!it.src); 13846 return; 13847 } 13848 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13849 state.items = Array.from(thumbs).map(btn => { 13850 const raw = btn.getAttribute('data-full') || ''; 13851 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13852 }).filter(it => !!it.src); 13853 } 13854 13855 function ensureUI(){ 13856 if (root) return; 13857 root = document.createElement('div'); 13858 root.className = 'pd-lb'; 13859 root.id = 'pd-lightbox'; 13860 root.setAttribute('role','dialog'); 13861 root.setAttribute('aria-modal','true'); 13862 root.setAttribute('aria-hidden','true'); 13863 root.setAttribute('tabindex','-1'); 13864 13865 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13866 root.innerHTML = ` 13867 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13868 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13869 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13870 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13871 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13872 `; 13873 document.body.appendChild(root); 13874 13875 imgEl = root.querySelector('#pd-lb-img'); 13876 counterEl = root.querySelector('#pd-lb-counter'); 13877 btnPrev = root.querySelector('.pd-lb-prev'); 13878 btnNext = root.querySelector('.pd-lb-next'); 13879 btnClose = root.querySelector('.pd-lb-close'); 13880 canvas = root.querySelector('.pd-lb-canvas'); 13881 13882 // Button clicks 13883 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13884 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13885 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13886 13887 // Klik backdrop untuk tutup 13888 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13889 13890 // Keyboard global 13891 document.addEventListener('keydown', (e)=>{ 13892 if (!state.open) return; 13893 if (e.key === 'Escape') { e.preventDefault(); close(); } 13894 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13895 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13896 }); 13897 13898 // Wheel zoom (desktop) 13899 root.addEventListener('wheel', (e)=>{ 13900 if (!state.open) return; 13901 e.preventDefault(); 13902 const delta = -Math.sign(e.deltaY) * 0.2; 13903 zoomAt(delta, e.clientX, e.clientY); 13904 }, { passive: false }); 13905 13906 // Drag pan (mouse) 13907 canvas.addEventListener('mousedown', (e)=>{ 13908 if (state.scale <= 1) return; 13909 state.isDragging = true; 13910 state.dragStartX = e.clientX; 13911 state.dragStartY = e.clientY; 13912 state.baseTx = state.tx; 13913 state.baseTy = state.ty; 13914 e.preventDefault(); 13915 });
13916 window.addEventListener('mousemove', (e)=>{ 13917 if (!state.isDragging) return; 13918 const dx = e.clientX - state.dragStartX; 13919 const dy = e.clientY - state.dragStartY; 13920 setTranslate(state.baseTx + dx, state.baseTy + dy); 13921 }); 13922 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 13923 13924 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 13925 canvas.addEventListener('touchstart', (e)=>{ 13926 if (!state.open) return; 13927 if (e.touches.length === 1) { 13928 const t = e.touches[0]; 13929 state.touchStartX = t.clientX; 13930 state.touchStartY = t.clientY; 13931 state.totalDx = 0; 13932 state.totalDy = 0; 13933 state.swipeMode = 'none'; 13934 13935 state.isDragging = state.scale > 1; 13936 state.dragStartX = t.clientX; 13937 state.dragStartY = t.clientY; 13938 state.baseTx = state.tx; 13939 state.baseTy = state.ty; 13940 13941 // Double-tap toggle zoom 13942 const now = Date.now(); 13943 if (now - state.lastTap < 300) { 13944 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 13945 state.lastTap = 0; 13946 e.preventDefault(); 13947 return; 13948 } 13949 state.lastTap = now; 13950 } 13951 }, { passive: true }); 13952 13953 canvas.addEventListener('touchmove', (e)=>{ 13954 if (!state.open) return; 13955 13956 // Drag pan saat zoom 13957 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 13958 e.preventDefault(); 13959 const t = e.touches[0]; 13960 const dx = t.clientX - state.dragStartX; 13961 const dy = t.clientY - state.dragStartY; 13962 setTranslate(state.baseTx + dx, state.baseTy + dy); 13963 return; 13964 } 13965 13966 // Gesture saat scale <= 1 13967 if (state.scale <= 1 && e.touches.length === 1) { 13968 const t = e.touches[0]; 13969 const dx = t.clientX - state.touchStartX; 13970 const dy = t.clientY - state.touchStartY; 13971 state.totalDx = dx; state.totalDy = dy; 13972 13973 // Tentukan mode swipe di awal gerakan 13974 if (state.swipeMode === 'none') { 13975 const absX = Math.abs(dx), absY = Math.abs(dy); 13976 if (absY > 12 && absY > absX * 1.3) { 13977 state.swipeMode = 'v'; // swipe vertical (untuk close) 13978 } else if (absX > 12 && absX > absY * 1.3) { 13979 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 13980 } 13981 } 13982 13983 if (state.swipeMode === 'v') { 13984 // Ikuti jari, kurangi opacity untuk feedback 13985 e.preventDefault(); 13986 setRootDrag(dy); 13987 } else if (state.swipeMode === 'h') { 13988 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 13989 } 13990 } 13991 }, { passive: false }); 13992 13993 canvas.addEventListener('touchend', (e)=>{ 13994 if (!state.open) return; 13995 13996 if (state.scale <= 1) { 13997 const dx = state.totalDx; 13998 const dy = state.totalDy; 13999 14000 if (state.swipeMode === 'v') { 14001 const TH = 80; // threshold jarak piksel untuk menutup 14002 if (Math.abs(dy) > TH) { 14003 // Animasi keluar arah swipe 14004 animateClose(dy); 14005 } else { 14006 // Snap back 14007 resetRootDrag(true); 14008 } 14009 } else if (state.swipeMode === 'h') { 14010 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14011 } 14012 } 14013 14014 state.isDragging = false; 14015 state.swipeMode = 'none'; 14016 state.totalDx = 0; 14017 state.totalDy = 0; 14018 }); 14019 } 14020 14021 function setImage(idx){ 14022 if (!state.items.length) return; 14023 state.idx = (idx + state.items.length) % state.items.length; 14024 const it = state.items[state.idx]; 14025 imgEl.src = it.src; 14026 imgEl.alt = it.alt || ''; 14027 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14028 resetTransform(); 14029 preloadNeighbors(); 14030 } 14031 14032 function resetTransform(){ 14033 state.scale = 1; 14034 state.tx = 0; state.ty = 0; 14035 applyTransform(); 14036 } 14037 14038 function setTranslate(tx, ty){ 14039 state.tx = tx; state.ty = ty; 14040 applyTransform(); 14041 } 14042 14043 function applyTransform(){ 14044 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14045 } 14046 14047 function zoomAt(delta, clientX, clientY, snapTo){ 14048 const old = state.scale; 14049 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14050 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14051 14052 if (next !== old) { 14053 const rect = imgEl.getBoundingClientRect(); 14054 const cx = clientX - rect.left - rect.width/2; 14055 const cy = clientY - rect.top - rect.height/2; 14056 state.tx -= cx * (next/old - 1); 14057 state.ty -= cy * (next/old - 1); 14058 state.scale = next; 14059 applyTransform(); 14060 } 14061 } 14062 14063 function preloadNeighbors(){ 14064 if (state.items.length < 2) return; 14065 const nextIdx = (state.idx + 1) % state.items.length; 14066 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14067 [nextIdx, prevIdx].forEach(i=>{ 14068 const img = new Image(); img.src = state.items[i].src; 14069 }); 14070 } 14071 14072 // ----- Swipe-down visual helpers ----- 14073 function setRootDrag(dy){ 14074 const damped = dy * 0.6; // redam gerakan agar lembut 14075 const abs = Math.abs(dy); 14076 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14077 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14078 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14079 } 14080 function resetRootDrag(withAnim){ 14081 if (withAnim) { 14082 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14083 root.style.transform = 'translate3d(0,0,0)'; 14084 root.style.opacity = '1'; 14085 setTimeout(()=>{ root.style.transition = ''; }, 190); 14086 } else { 14087 root.style.transition = ''; 14088 root.style.transform = ''; 14089 root.style.opacity = ''; 14090 } 14091 } 14092 function animateClose(dy){ 14093 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14094 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14095 root.style.transform = `translate3d(0, ${off}px, 0)`; 14096 root.style.opacity = '0'; 14097 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14098 } 14099 14100 function setSiblingsInert(makeInert){ 14101 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14102 const nodes = Array.from(document.body.children).filter(el => el !== root); 14103 nodes.forEach(el=>{ 14104 if (makeInert) { 14105 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14106 } else { 14107 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14108 } 14109 }); 14110 } 14111 14112 function open(idx){ 14113 if (!state.items.length) buildItems();
14114 if (!state.items.length) return; 14115 ensureUI(); 14116 14117 // Reset efek drag visual 14118 resetRootDrag(false); 14119 14120 document.body.style.overflow = 'hidden'; 14121 root.classList.add('show'); 14122 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14123 setSiblingsInert(true); 14124 14125 state.open = true; 14126 setImage(typeof idx === 'number' ? idx : 0); 14127 14128 // Fokuskan dialog agar keyboard pasti tertangkap 14129 try { root.focus(); } catch(_){} 14130 } 14131 14132 function close(){ 14133 state.open = false; 14134 root.classList.remove('show'); 14135 root.setAttribute('aria-hidden','true'); 14136 setSiblingsInert(false); 14137 document.body.style.overflow = ''; 14138 14139 // Bersihkan transform/opacity 14140 resetRootDrag(false); 14141 } 14142 14143 function next(){ setImage(state.idx + 1); } 14144 function prev(){ setImage(state.idx - 1); } 14145 14146 function findIndexByUrl(url){ 14147 const target = fileName(absUrl(url)); 14148 const idx = state.items.findIndex(it => fileName(it.src) === target); 14149 return idx >= 0 ? idx : 0; 14150 } 14151 14152 function initPdLightbox(){ 14153 buildItems();
14154 ensureUI(); 14155 14156 // Klik gambar utama â buka LB di index yang sama 14157 const mainImg = document.getElementById('pd-main-img'); 14158 if (mainImg) { 14159 mainImg.style.cursor = 'zoom-in'; 14160 mainImg.addEventListener('click', ()=>{ 14161 open(findIndexByUrl(mainImg.src)); 14162 }); 14163 } 14164 14165 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14166 const slider = document.getElementById('pd-mobile-slider'); 14167 if (slider) { 14168 slider.addEventListener('click', (e)=>{ 14169 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14170 if (!im) return; 14171 open(findIndexByUrl(im.src)); 14172 }); 14173 } 14174 14175 // (Opsional) double-click pada thumbnail untuk buka langsung 14176 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14177 thumbs.forEach((btn)=>{ 14178 let last = 0; 14179 btn.addEventListener('click', ()=>{ 14180 const now = Date.now(); 14181 if (now - last < 300) { 14182 const src = btn.getAttribute('data-full') || ''; 14183 open(findIndexByUrl(absUrl(src))); 14184 last = 0; 14185 } else { 14186 last = now; 14187 } 14188 }); 14189 }); 14190 14191 // Ekspos API 14192 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14193 } 14194 14195 if (document.readyState === 'loading') { 14196 document.addEventListener('DOMContentLoaded', initPdLightbox); 14197 } else { 14198 initPdLightbox(); 14199 } 14200})(); 14201</script>
vendor: 2 bytes, line 14201
14201
14202<script data-inlined="cart.js"> 14203(function(){ 14204 function $(s,ctx=document){ return ctx.querySelector(s); } 14205 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14206 14207 // CSRF token helper 14208 function getCsrfToken() { 14209 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14210 return metaTag ? metaTag.getAttribute('content') : ''; 14211 } 14212 14213 function guessPdMeta(){ 14214 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14215 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14216 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14217 const imgEl = $('#pd-main-img'); 14218 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14219 return { name, sku, image_url }; // price removed for security 14220 } 14221 14222 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14223 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14224 const payload = { 14225 product_id: parseInt(productId||0,10) || 0, 14226 qty: parseInt(qty||1,10) || 1, 14227 variant_id: parseInt(variantId||0,10) || 0, 14228 meta 14229 }; 14230 14231 // Check if user is logged in 14232 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14233 document.body.classList.contains('logged-in') || 14234 window.userLoggedIn === true || 14235 document.body.dataset.userLoggedIn === 'true'; 14236 14237 // Skip localStorage untuk logged user - mereka pakai database 14238 if (!isLoggedIn && window.guestCart) { 14239 try { 14240 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14241 } catch (e) { 14242 console.warn('Failed to add to localStorage cart:', e); 14243 } 14244 } 14245 14246 const res = await fetch('/api/cart/add.php', { 14247 method: 'POST', 14248 headers: { 14249 'Content-Type':'application/json', 14250 'X-CSRF-Token': getCsrfToken() 14251 }, 14252 credentials: 'same-origin', 14253 body: JSON.stringify({ 14254 ...payload, 14255 csrf_token: getCsrfToken() 14256 }), 14257 }); 14258 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14259 if (!res.ok || !data.ok) { 14260 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14261 throw new Error(msg); 14262 } 14263 14264 // Update badge di navbar dengan data dari response 14265 try { 14266 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14267 if (el && data.cart && typeof data.cart.items_count === 'number') { 14268 el.textContent = String(data.cart.items_count); 14269 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14270 } 14271 } catch(_){} 14272 14273 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14274 try { 14275 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14276 } catch(_){} 14277 14278 // Update CartBadge dengan data dari response (lebih reliable) 14279 try { 14280 if (window.CartBadge && window.CartBadge.set) { 14281 window.CartBadge.set(data.cart.items_count||0); 14282 } 14283 } catch(_){} 14284 14285 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14286 setTimeout(() => { 14287 if (window.updateCartCounter) { 14288 window.updateCartCounter(); 14289 } 14290 }, 100); 14291 14292 return data; 14293 } 14294 14295 // Function to update cart counter from server 14296 async function updateCartCounter(){ 14297 try { 14298 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14299 const data = await res.json().catch(()=>({ok:false})); 14300 if (data.ok && typeof data.items_count === 'number') { 14301 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14302 if (el) { 14303 el.textContent = String(data.items_count); 14304 el.classList.toggle('is-empty', data.items_count <= 0); 14305 } 14306 // Dispatch event 14307 try { 14308 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14309 } catch(_){} 14310 } 14311 } catch (err) { 14312 console.log('Could not update cart counter:', err); 14313 } 14314 } 14315 14316 // Function to manually merge guest cart (if needed) 14317 async function mergeGuestCart(){ 14318 try { 14319 const res = await fetch('/api/cart/merge-guest.php', { 14320 method: 'POST', 14321 headers: { 14322 'Content-Type': 'application/json', 14323 'X-CSRF-Token': getCsrfToken() 14324 }, 14325 credentials: 'same-origin', 14326 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14327 }); 14328 const data = await res.json().catch(()=>({ok:false})); 14329 14330 if (data.ok && data.merged_items > 0) { 14331 console.log(`Guest cart merged: ${data.merged_items} items`); 14332 // Update cart counter after merge 14333 await updateCartCounter(); 14334 return data; 14335 } 14336 return data; 14337 } catch (err) { 14338 console.log('Could not merge guest cart:', err); 14339 return { ok: false, error: err.message }; 14340 } 14341 } 14342 14343 window.addToCart = addToCart; 14344 window.updateCartCounter = updateCartCounter; 14345 window.mergeGuestCart = mergeGuestCart; 14346 14347 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14348 document.addEventListener('click', async (e)=>{ 14349 const btn = e.target.closest('[data-addtocart]'); 14350 if (!btn) return; 14351 e.preventDefault(); 14352 if (btn.disabled) return; 14353 14354 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14355 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14356 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14357 14358 try { 14359 const data = await addToCart(pid, qty, vid); 14360 if (window.showToast) { 14361 const meta = guessPdMeta(); 14362 window.showToast({ 14363 type:'success', 14364 title:'Berhasil ditambahkan', 14365 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14366 duration: 3800, 14367 actions: [ 14368 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14369 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14370 ] 14371 }); 14372 } 14373 } catch (err) { 14374 if (window.showToast) { 14375 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14376 } else { 14377 console.error(err); 14378 } 14379 } 14380 }); 14381})(); 14382</script>
vendor: 2 bytes, line 14382
14382
14383<script data-inlined="stock-validator-vanilla.js"> 14384/** 14385 * Stock Validation - Vanilla JS Version 14386 * 14387 * This script provides client-side stock validation without jQuery dependency 14388 * - Works with native JavaScript and fetch API 14389 * - Graceful fallback for older browsers 14390 * - Same functionality as jQuery version 14391 */ 14392 14393window.StockValidator = (function() { 14394 'use strict'; 14395 14396 const config = { 14397 apiEndpoint: '/api/stock/check.php', 14398 modalSelector: '#stock-modal-overlay', // We create our own modal 14399 loadingClass: 'loading-stock', 14400 disabledClass: 'stock-disabled', 14401 debug: true // Enable debug logging 14402 }; 14403 14404 /** 14405 * Cross-browser event listener helper 14406 */ 14407 function addEventListener(element, event, handler) { 14408 if (element.addEventListener) { 14409 element.addEventListener(event, handler); 14410 } else if (element.attachEvent) { 14411 element.attachEvent('on' + event, handler); 14412 } 14413 } 14414 14415 /** 14416 * Get stock information from embedded variant data (instant, no AJAX needed) 14417 */ 14418 function getStockFromEmbeddedData(variantId) { 14419 // Check if embedded variant data exists 14420 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14421 return null; 14422 } 14423 14424 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14425 14426 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14427 if (variants.length === 0) { 14428 // For simple products, we cannot determine stock from embedded data 14429 // Return null to force API validation 14430 return null; 14431 } 14432 14433 // If no variant specified, check base product stock (all variants) 14434 if (!variantId || variantId <= 0) { 14435 // For base product, check if ANY variant has stock 14436 const isPreorder = window.isPreorderProduct || false; 14437 let hasAnyStock = false; 14438 let totalStock = 0; 14439 14440 if (isPreorder) { 14441 // For pre-order, check preorder_remaining 14442 hasAnyStock = variants.some(v => { 14443 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14444 return v.preorder_remaining > 0; 14445 } 14446 return v.stock > 0; 14447 }); 14448 totalStock = variants.reduce((sum, v) => { 14449 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14450 return sum + (v.preorder_remaining || 0); 14451 } 14452 return sum + (v.stock || 0); 14453 }, 0); 14454 } else { 14455 hasAnyStock = variants.some(v => v.stock > 0); 14456 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14457 } 14458 14459 return { 14460 hasStock: hasAnyStock, 14461 stock: totalStock, 14462 sku: 'Base Product', 14463 variant: null 14464 }; 14465 } 14466 14467 // Find specific variant 14468 const variant = variants.find(v => v.id === variantId); 14469 14470 if (!variant) { 14471 return { 14472 hasStock: false, 14473 stock: 0, 14474 sku: 'Unknown Variant', 14475 variant: null 14476 }; 14477 } 14478 14479 // For pre-order products, check preorder_remaining instead of stock 14480 const isPreorder = window.isPreorderProduct || false; 14481 let hasStock = false; 14482 let availableStock = 0; 14483 14484 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14485 availableStock = parseInt(variant.preorder_remaining, 10); 14486 if (isNaN(availableStock)) availableStock = 0; 14487 hasStock = availableStock > 0; 14488 } else { 14489 availableStock = variant.stock || 0; 14490 hasStock = availableStock > 0; 14491 } 14492 14493 return { 14494 hasStock: hasStock, 14495 stock: availableStock, 14496 sku: variant.sku, 14497 variant: variant 14498 }; 14499 } 14500 14501 /** 14502 * Cross-browser AJAX helper 14503 */
14504 function makeRequest(url, options) { 14505 return new Promise((resolve, reject) => { 14506 // Try fetch first (modern browsers) 14507 if (typeof fetch !== 'undefined') { 14508 const fetchOptions = { 14509 method: options.method || 'POST', 14510 headers: { 14511 'Content-Type': 'application/x-www-form-urlencoded', 14512 }, 14513 body: options.data ? new URLSearchParams(options.data).toString() : null 14514 }; 14515 14516 fetch(url, fetchOptions) 14517 .then(response => { 14518 if (!response.ok) { 14519 throw new Error(`HTTP ${response.status}`); 14520 } 14521 return response.json(); 14522 }) 14523 .then(resolve) 14524 .catch(reject); 14525 } else { 14526 // Fallback to XMLHttpRequest 14527 const xhr = new XMLHttpRequest(); 14528 xhr.open(options.method || 'POST', url); 14529 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14530 14531 xhr.onload = function() { 14532 if (xhr.status >= 200 && xhr.status < 300) { 14533 try { 14534 resolve(JSON.parse(xhr.responseText)); 14535 } catch (e) { 14536 reject(new Error('Invalid JSON response')); 14537 } 14538 } else { 14539 reject(new Error(`HTTP ${xhr.status}`)); 14540 } 14541 }; 14542 14543 xhr.onerror = function() { 14544 reject(new Error('Network error')); 14545 }; 14546 14547 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14548 xhr.send(data); 14549 } 14550 }); 14551 } 14552 14553 /** 14554 * Check stock via AJAX 14555 */ 14556 function checkStock(productId, variantId, qty) { 14557 return makeRequest(config.apiEndpoint, { 14558 method: 'POST', 14559 data: { 14560 product_id: productId, 14561 variant_id: variantId || 0, 14562 qty: qty || 1 14563 } 14564 }); 14565 } 14566 14567 /** 14568 * Create and show stock alert modal using modal.css structure 14569 */ 14570 function showStockModal(stockInfo, action = 'add to cart') { 14571 let title, message, type, iconClass; 14572 14573 // Handle both string and object formats 14574 if (typeof stockInfo === 'string') { 14575 title = 'Stok Tidak Tersedia'; 14576 message = stockInfo; 14577 type = 'warning'; 14578 iconClass = 'warning'; 14579 } else if (stockInfo && typeof stockInfo === 'object') { 14580 if (!stockInfo.is_active) { 14581 title = 'Produk Tidak Tersedia'; 14582 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14583 type = 'danger'; 14584 iconClass = 'danger'; 14585 } else if (!stockInfo.is_in_stock) { 14586 title = 'Stok Habis'; 14587 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14588 if (stockInfo.sku) { 14589 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14590 } 14591 type = 'warning'; 14592 iconClass = 'warning'; 14593 } else if (!stockInfo.can_add_to_cart) { 14594 title = 'Stok Tidak Mencukupi'; 14595 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14596 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14597 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14598 type = 'warning'; 14599 iconClass = 'warning'; 14600 } else { 14601 title = 'Stok Tidak Tersedia'; 14602 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14603 type = 'warning'; 14604 iconClass = 'warning'; 14605 } 14606 } else { 14607 title = 'Stok Tidak Tersedia'; 14608 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14609 type = 'warning'; 14610 iconClass = 'warning'; 14611 } 14612 14613 // Create modal HTML using modal.css structure 14614 const modalHTML = ` 14615 <div class="modal-overlay" id="stock-modal-overlay"> 14616 <div class="modal-dialog"> 14617 <div class="modal-header"> 14618 <div class="modal-icon ${iconClass}"> 14619 ${getIconSVG(iconClass)} 14620 </div> 14621 <h3 class="modal-title">${title}</h3> 14622 </div> 14623 <div class="modal-body"> 14624 <p class="modal-message">${message}</p> 14625 <div class="modal-actions"> 14626 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14627 Tutup 14628 </button> 14629 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14630 Muat Ulang 14631 </button> 14632 </div> 14633 </div> 14634 </div> 14635 </div> 14636 `; 14637 14638 // Remove existing modal if any 14639 const existingModal = document.getElementById('stock-modal-overlay'); 14640 if (existingModal) { 14641 existingModal.remove(); 14642 } 14643 14644 // Add modal to body 14645 document.body.insertAdjacentHTML('beforeend', modalHTML); 14646 14647 // Show modal with animation 14648 const modal = document.getElementById('stock-modal-overlay'); 14649 setTimeout(() => { 14650 modal.classList.add('show'); 14651 }, 10); 14652 14653 // Add event listeners to modal buttons 14654 const closeBtn = modal.querySelector('[data-action="close"]'); 14655 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14656 14657 if (closeBtn) { 14658 addEventListener(closeBtn, 'click', closeStockModal); 14659 } 14660 14661 if (reloadBtn) { 14662 addEventListener(reloadBtn, 'click', function() { 14663 closeStockModal(); 14664 window.location.reload(); 14665 }); 14666 } 14667 14668 // Close modal when clicking overlay 14669 addEventListener(modal, 'click', function(event) { 14670 if (event.target === modal) { 14671 closeStockModal(); 14672 } 14673 }); 14674 14675 // Auto-close after 8 seconds 14676 setTimeout(() => { 14677 closeStockModal(); 14678 }, 8000); 14679 } 14680 14681 /** 14682 * Get icon SVG based on type 14683 */ 14684 function getIconSVG(type) { 14685 const icons = { 14686 danger: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 14687 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14687<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14688 success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 14689 info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>' 14690 }; 14691 return icons[type] || icons.info; 14692 } 14693 14694 /** 14695 * Close stock modal 14696 */ 14697 function closeStockModal() { 14698 const modal = document.getElementById('stock-modal-overlay'); 14699 if (modal) { 14700 modal.classList.remove('show'); 14701 setTimeout(() => { 14702 modal.remove(); 14703 }, 200); 14704 } 14705 } 14706 14707 // Make closeStockModal globally available 14708 if (typeof window !== 'undefined') { 14709 window.closeStockModal = closeStockModal; 14710 } 14711 14712 /** 14713 * Get element data attribute with fallback 14714 */ 14715 function getDataAttribute(element, attr) { 14716 if (element.dataset) { 14717 return element.dataset[attr]; 14718 } else { 14719 return element.getAttribute('data-' + attr); 14720 } 14721 } 14722 14723 /** 14724 * Validate stock before cart action 14725 */ 14726 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14727 try { 14728 // SMART VALIDATION: For simple products, check if we already have cached stock info 14729 // If buttons are enabled and cached info shows stock available, skip API validation 14730 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14731 14732 return true; 14733 } 14734 14735 // For variant products or when cached info is not available, do API validation 14736 const stockInfo = await checkStock(productId, variantId, qty); 14737 14738 if (!stockInfo.ok) { 14739 showStockModal({ 14740 message: stockInfo.error || 'Gagal mengecek stok', 14741 is_active: false 14742 }, action); 14743 return false; 14744 } 14745 14746 if (!stockInfo.can_add_to_cart) { 14747 showStockModal(stockInfo, action); 14748 return false; 14749 } 14750 14751 return true; // Stock available, proceed 14752 14753 } catch (error) { 14754 console.error('Stock validation error:', error); 14755 showStockModal({ 14756 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14757 is_active: false 14758 }, action); 14759 return false; 14760 } 14761 } 14762 14763 /** 14764 * Handle button click with stock validation 14765 */ 14766 function handleButtonClick(event, action) { 14767 const button = event.target; 14768 14769 14770 14771 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14772 if (button.hasAttribute('data-stock-blocked')) { 14773 event.preventDefault(); 14774 event.stopImmediatePropagation(); 14775 14776 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14777 showStockModal(reason); 14778 14779 // Show security warning 14780 if (window.showToast) { 14781 window.showToast({ 14782 type: 'warning', 14783 title: 'Aksi Tidak Diizinkan', 14784 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14785 duration: 3000 14786 }); 14787 } 14788 return false; 14789 } 14790 14791 // SECOND CHECK: Real-time stock validation against cached stock info 14792 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14793 event.preventDefault(); 14794 event.stopImmediatePropagation(); 14795 14796 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14797 14798 // Re-enforce button disabling 14799 button.disabled = true; 14800 button.style.opacity = '0.5'; 14801 button.style.cursor = 'not-allowed'; 14802 button.setAttribute('data-stock-blocked', 'true'); 14803 14804 return false; 14805 } 14806 14807 // Skip if already processing 14808 if (button.classList.contains(config.loadingClass)) { 14809 event.preventDefault(); 14810 return false; 14811 } 14812 14813 // Get product data from button attributes or form 14814 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14815 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14816 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14817 14818 // Try to get from global variables (set by product detail page) 14819 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14820 productId = parseInt(window.PD_ID || 0); 14821 } 14822 14823 // Try to get from parent form if not in button 14824 if (productId <= 0) { 14825 const form = button.closest ? button.closest('form') : null; 14826 if (form) { 14827 const pidAttr = form.getAttribute('data-pid'); 14828 const pidInput = form.querySelector('[name="product_id"]'); 14829 const vidInput = form.querySelector('[name="variant_id"]');
14830 const qtyInput = form.querySelector('[name="qty"]'); 14831 14832 if (pidAttr) productId = parseInt(pidAttr); 14833 if (pidInput) productId = parseInt(pidInput.value || 0); 14834 if (vidInput) variantId = parseInt(vidInput.value || 0); 14835 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14836 } 14837 } 14838 14839 // CRITICAL: Always get current variant from selector (overrides button attributes) 14840 // This ensures we validate against the currently selected variant, not cached button data 14841 const variantSelector = document.getElementById('pd-variant-id'); 14842 if (variantSelector) { 14843 const currentVariantId = parseInt(variantSelector.value || 0); 14844 if (currentVariantId > 0) { 14845 variantId = currentVariantId; 14846 } 14847 } 14848 14849 14850 14851 if (productId <= 0) { 14852 return true; // Let default handler deal with it 14853 } 14854 14855 // Prevent default and validate stock 14856 event.preventDefault(); 14857 event.stopPropagation(); 14858 14859 // Show loading state 14860 button.classList.add(config.loadingClass); 14861 const originalDisabled = button.disabled; 14862 button.disabled = true; 14863 14864 14865 14866 validateBeforeCart(productId, variantId, qty, action) 14867 .then(isValid => { 14868 if (isValid) { 14869 // Stock OK, proceed with original action 14870 if (action === 'cart' && typeof window.addToCart === 'function') { 14871 window.addToCart(productId, qty, variantId); 14872 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14873 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14874 window.buyNow(); 14875 } else { 14876 // Add fallback cart/buy logic here if needed 14877 } 14878 } 14879 }) 14880 .catch(error => { 14881 }) 14882 .finally(() => { 14883 // Remove loading state 14884 button.classList.remove(config.loadingClass); 14885 button.disabled = originalDisabled; 14886 }); 14887 14888 return false; 14889 } 14890 14891 /** 14892 * Handle mobile button click with stock validation 14893 */ 14894 function handleMobileButtonClick(event, action) { 14895 const button = event.target; 14896 14897 14898 14899 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14900 if (button.hasAttribute('data-stock-blocked')) { 14901 event.preventDefault(); 14902 event.stopImmediatePropagation(); 14903 14904 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14905 showStockModal(reason); 14906 14907 // Show security warning 14908 if (window.showToast) { 14909 window.showToast({ 14910 type: 'warning', 14911 title: 'Aksi Tidak Diizinkan', 14912 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14913 duration: 3000 14914 }); 14915 } 14916 return false; 14917 } 14918 14919 // SECOND CHECK: Real-time stock validation against cached stock info 14920 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14921 event.preventDefault(); 14922 event.stopImmediatePropagation(); 14923 14924 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14925 14926 // Re-enforce button disabling 14927 button.disabled = true; 14928 button.style.opacity = '0.5'; 14929 button.style.cursor = 'not-allowed'; 14930 button.setAttribute('data-stock-blocked', 'true'); 14931 14932 return false; 14933 } 14934 14935 // Skip if already processing 14936 if (button.classList.contains(config.loadingClass)) { 14937 event.preventDefault(); 14938 return false; 14939 } 14940 14941 // Get product data from mobile FAB container 14942 let productId = 0; 14943 let variantId = 0; 14944 let qty = 1; 14945
14946 // Try to get from mobile FAB data-pid 14947 const mobileFab = document.getElementById('pd-mobile-fab'); 14948 if (mobileFab) { 14949 const pidAttr = getDataAttribute(mobileFab, 'pid'); 14950 if (pidAttr) productId = parseInt(pidAttr); 14951 } 14952 14953 // Try to get from global variables (set by product detail page) 14954 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14955 productId = parseInt(window.PD_ID || 0); 14956 } 14957 14958 // CRITICAL: Always get current variant from selector (real-time) 14959 // This ensures we validate against the currently selected variant 14960 const variantSelector = document.getElementById('pd-variant-id'); 14961 if (variantSelector) { 14962 variantId = parseInt(variantSelector.value || 0); 14963 } 14964 14965 // Try to get quantity from quantity input 14966 const qtySelector = document.querySelector('[name="qty"]'); 14967 if (qtySelector) { 14968 qty = parseInt(qtySelector.value || 1); 14969 } 14970 14971 // Ensure minimum quantity is 1 14972 qty = Math.max(1, qty); 14973 14974 14975 14976 if (productId <= 0) { 14977 return true; // Let default handler deal with it 14978 } 14979 14980 // Event already prevented at interceptor level, just proceed with validation 14981 14982 // Show loading state 14983 button.classList.add(config.loadingClass); 14984 const originalDisabled = button.disabled; 14985 const originalText = button.textContent; 14986 button.disabled = true; 14987 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 14988 14989 14990 14991 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 14992 if (window.addToCart && typeof window.addToCart === 'function') { 14993 window.addToCart._stockValidatorBlocked = true; 14994 } 14995 14996 // Also preemptively block other cart functions 14997 const originalAddToCartSubmit = window.addToCartSubmit; 14998 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 14999 15000 // Temporarily block ALL cart functions during validation 15001 window.addToCartSubmit = (...args) => { 15002 return Promise.resolve(false); 15003 }; 15004 15005 if (originalPdAddToCartSubmit) { 15006 window._pdAddToCartSubmit = (...args) => { 15007 return Promise.resolve(false); 15008 }; 15009 } 15010 15011 validateBeforeCart(productId, variantId, qty, action) 15012 .then(isValid => { 15013 if (isValid) { 15014 // Stock OK, proceed with original action 15015 15016 if (action === 'cart') { 15017 // For mobile cart, we need to recreate the original behavior 15018 // Strategy: Try multiple methods to ensure cart add + toast success 15019 15020 15021 15022 let cartAddSuccess = false; 15023 let errorMessage = ''; 15024 15025 // Method 1: Try original addToCart function 15026 const originalAddToCart = window.addToCart && window.addToCart._original; 15027 if (originalAddToCart && !cartAddSuccess) { 15028 try { 15029 const result = originalAddToCart(productId, qty, variantId); 15030 15031 // Handle both sync and async returns 15032 if (result && typeof result.then === 'function') { 15033 result.then(() => { 15034 cartAddSuccess = true; 15035 }).catch(err => { 15036 errorMessage = err.message || 'addToCart failed'; 15037 }); 15038 } else { 15039 cartAddSuccess = true; 15040 } 15041 } catch (error) { 15042 errorMessage = error.message || 'addToCart exception'; 15043 } 15044 } 15045 15046 // Method 2: Try pd-addtocart.js function (backup method) 15047 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15048 try { 15049 15050 // Create fake form for pd-addtocart.js 15051 const fakeForm = document.createElement('form');
15052 const qtyInput = document.createElement('input'); 15053 qtyInput.name = 'qty'; 15054 qtyInput.value = qty; 15055 fakeForm.appendChild(qtyInput); 15056 15057 // Set variant selector value for pd-addtocart to read 15058 const variantSelector = document.getElementById('pd-variant-id'); 15059 if (variantSelector) { 15060 variantSelector.value = variantId; 15061 } 15062 15063 const fakeEvent = { 15064 preventDefault: () => {}, 15065 target: fakeForm 15066 }; 15067 15068 // Call pd-addtocart.js function (this should handle toast internally) 15069 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15070 if (result && typeof result.then === 'function') { 15071 result.then(() => { 15072 cartAddSuccess = true; 15073 }); 15074 } else { 15075 cartAddSuccess = true; 15076 } 15077 } catch (error) { 15078 } 15079 } 15080 15081 // Method 3: Direct addToCart call (fallback) 15082 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15083 try { 15084 const result = window.addToCart(productId, qty, variantId); 15085 if (result && typeof result.then === 'function') { 15086 result.then(() => { 15087 cartAddSuccess = true; 15088 }); 15089 } else { 15090 cartAddSuccess = true; 15091 } 15092 } catch (error) { 15093 } 15094 } 15095 15096 // Always show success toast for mobile (regardless of which method worked) 15097 // This ensures user gets feedback for successful cart addition 15098 setTimeout(() => { 15099 if (window.showToast) { 15100 15101 // Get product details for toast 15102 const getVariantSummaryText = () => { 15103 const vs = document.getElementById('pd-variant-summary'); 15104 if (!vs) return ''; 15105 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15106 .map(el => el.textContent.trim()) 15107 .filter(Boolean); 15108 return items.length ? items.join(' / ') : ''; 15109 }; 15110 15111 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15112 '&':'&','<':'<','>':'>','"':'"',"'":''' 15113 }[m])); 15114 15115 const variantText = getVariantSummaryText(); 15116 const skuText = (function(){ 15117 const el = document.getElementById('pd-sku'); 15118 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15119 })(); 15120 const productName = window.PD_NAME || 'Product'; 15121 15122 window.showToast({ 15123 type: 'success', 15124 title: 'Berhasil ditambahkan ke Keranjang', 15125 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15125 ÷ Qty: ${qty}`, 15126 duration: 5000, 15127 actions: [ 15128 { 15129 label: 'Lihat Keranjang', 15130 onClick: () => { window.location.href = '/keranjang'; } 15131 }, 15132 { 15133 label: 'Lanjut Belanja', 15134 variant: 'alt', 15135 onClick: () => {} 15136 } 15137 ] 15138 }); 15139 } else { 15140 } 15141 }, 100); // Small delay to ensure cart operation completes first 15142 15143 } else if (action === 'buy') { 15144 // For mobile buy, call buyNow 15145 if (typeof window.buyNow === 'function') { 15146 window.buyNow(); 15147 } 15148 } 15149 } else { 15150 // Stock validation FAILED - absolutely NO cart function calls allowed 15151 15152 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15153 } 15154 15155 // Always restore functions after validation (success or failure) 15156 setTimeout(() => { 15157 // Remove blocking flag 15158 if (window.addToCart && typeof window.addToCart === 'function') { 15159 window.addToCart._stockValidatorBlocked = false; 15160 } 15161 15162 // Restore other functions 15163 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15164 window.addToCartSubmit = originalAddToCartSubmit; 15165 } 15166 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15167 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15168 } 15169 15170 }, 1000); // Shorter timeout since we want to restore quickly after success 15171 }) 15172 .catch(error => { 15173 // On error, also block the action 15174 }) 15175 .finally(() => { 15176 // Remove loading state 15177 button.classList.remove(config.loadingClass); 15178 button.disabled = originalDisabled; 15179 button.textContent = originalText; 15180 }); 15181 15182 return false; 15183 } 15184 15185 /** 15186 * Intercept cart buttons 15187 */ 15188 function interceptCartButtons() { 15189 15190 15191 15192 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15193 setTimeout(() => { 15194 const mobileCartBtn = document.getElementById('mf-cart'); 15195 if (mobileCartBtn) { 15196 // Clone the button to remove ALL existing event listeners 15197 const newBtn = mobileCartBtn.cloneNode(true); 15198 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15199 15200 15201 15202 // Add our own handler to the new button 15203 newBtn.addEventListener('click', function(event) { 15204 event.preventDefault(); 15205 event.stopPropagation(); 15206 event.stopImmediatePropagation(); 15207 handleMobileButtonClick(event, 'cart'); 15208 return false; 15209 }, true); 15210 } 15211 }, 50); 15212 15213 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15214 // This provides better integration with existing pd-addtocart.js toast system 15215 15216 // Buy now buttons - updated selectors (desktop + mobile) 15217 addEventListener(document, 'click', function(event) { 15218 const target = event.target; 15219 15220 // Desktop buy buttons 15221 if (target.classList.contains('btn-buy') || // Product detail buy button 15222 target.classList.contains('btn-buy-now') || 15223 target.classList.contains('buy-now-btn') || 15224 target.id === 'btn-buy' || // Specific ID match 15225 getDataAttribute(target, 'action') === 'buy-now' || 15226 getDataAttribute(target, 'action') === 'buy') { 15227 15228 handleButtonClick(event, 'buy'); 15229 } 15230 15231 // Mobile buy buttons 15232 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15233 target.id === 'mf-buy') { // Mobile buy button ID 15234 15235 handleMobileButtonClick(event, 'buy'); 15236 } 15237 15238 // Mobile cart buttons - ENHANCED BLOCKING 15239 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15240 target.id === 'mf-cart') { // Mobile cart button ID 15241 15242 15243 15244 // Immediately prevent all event propagation 15245 event.preventDefault(); 15246 event.stopPropagation(); 15247 event.stopImmediatePropagation(); 15248 15249 handleMobileButtonClick(event, 'cart'); 15250 15251 // Return false to ensure no other handlers run 15252 return false; 15253 } 15254 }, true);
15254 // Use capturing phase to ensure we intercept before other handlers 15255 15256 // Additional protection: Override addToCart function to add mobile button state checking 15257 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15258 if (originalAddToCart) { 15259 window.addToCart = async function(...args) { 15260 // Check if this call is coming from a mobile button that's currently being validated 15261 const mobileCartBtn = document.getElementById('mf-cart'); 15262 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15263 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15264 } 15265 15266 // Check if functions are temporarily blocked (during stock failure) 15267 if (window.addToCart._stockValidatorBlocked) { 15268 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15269 } 15270 15271 // Allow normal calls 15272 return originalAddToCart.apply(this, args); 15273 }; 15274 15275 // Store reference to original for restoration 15276 window.addToCart._original = originalAddToCart; 15277 } 15278 } 15279 15280 /** 15281 * Setup global click interceptor for security 15282 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15283 */ 15284 function setupGlobalClickInterceptor() { 15285 // Add capture-phase listener to catch clicks before other handlers 15286 document.addEventListener('click', function(event) { 15287 const target = event.target; 15288 15289 // Check if this is a cart/buy button 15290 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15291 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15292 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15293 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15294 15295 if (!isCartButton && !isBuyButton) { 15296 return; // Not our target, allow normal processing 15297 } 15298 15299 const button = isCartButton ? 15300 (target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') ? target : target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart')) : 15301 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15302 15303 // IMMEDIATE BLOCK: Prevent any processing first 15304 event.preventDefault(); 15305 event.stopImmediatePropagation(); 15306 15307 // SECURITY CHECK 1: Is button marked as stock-blocked? 15308 if (button && button.hasAttribute('data-stock-blocked')) { 15309 15310 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15311 showStockModal(reason); 15312 15313 // Show security alert 15314 if (window.showToast) { 15315 window.showToast({ 15316 type: 'error', 15317 title: 'Aksi Diblokir', 15318 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15319 duration: 4000 15320 }); 15321 } 15322 15323 return false; 15324 } 15325 15326 // SECURITY CHECK 2: Real-time validation against cached stock info 15327 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15328 15329 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15330 15331 // Re-enforce button disabling 15332 if (button) { 15333 button.disabled = true; 15334 button.style.opacity = '0.5'; 15335 button.style.cursor = 'not-allowed'; 15336 button.setAttribute('data-stock-blocked', 'true'); 15337 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15338 } 15339 15340 return false; 15341 } 15342 15343 // SECURITY CHECK 3: Smart client-side validation 15344 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15345 const variantSelector = document.getElementById('pd-variant-id'); 15346 const variantId = variantSelector ? parseInt(variantSelector.value || 0
15346) : 0; 15347 15348 if (productId > 0) { 15349 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15350 // and buttons are enabled, proceed immediately without re-validation 15351 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15352 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15353 15354 // Allow the action to proceed immediately with toast 15355 if (isCartButton) { 15356 // Trigger cart action with proper toast handling 15357 if (typeof window._pdAddToCartSubmit === 'function') { 15358 // Use pd-addtocart.js function for proper toast handling 15359 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15360 if (form) { 15361 const fakeEvent = { 15362 target: form, 15363 preventDefault: () => {} 15364 }; 15365 window._pdAddToCartSubmit(fakeEvent, productId); 15366 } 15367 } else if (typeof window.addToCartSubmit === 'function') { 15368 // Fallback to addToCartSubmit 15369 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15370 if (form) { 15371 const fakeEvent = { 15372 target: form, 15373 preventDefault: () => {} 15374 }; 15375 window.addToCartSubmit(fakeEvent, productId); 15376 } 15377 } else if (typeof window.addToCart === 'function') { 15378 // Direct addToCart call 15379 window.addToCart(productId, 1, variantId); 15380 } 15381 } else if (isBuyButton) { 15382 // Trigger buy action 15383 if (typeof window.buyNow === 'function') { 15384 window.buyNow(); 15385 } 15386 } 15387 15388 return false; // Action completed 15389 } 15390 15391 // Get stock info from embedded variant data (for variant products) 15392 const stockInfo = getStockFromEmbeddedData(variantId); 15393 15394 if (stockInfo && stockInfo.hasStock) { 15395 15396 // Update global stock cache 15397 window._currentStockInfo = { 15398 can_add_to_cart: true, 15399 is_in_stock: true, 15400 available_stock: stockInfo.stock, 15401 message: 'Stok tersedia' 15402 }; 15403 15404 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15405 // If data-stock-blocked is set, respect it and don't enable the button 15406 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15407 15408 if (!isExplicitlyBlocked) { 15409 // Remove stock blocking if it was set and enable button 15410 button.removeAttribute('data-stock-blocked'); 15411 button.removeAttribute('data-stock-reason'); 15412 button.disabled = false; 15413 button.style.opacity = ''; 15414 button.style.cursor = ''; 15415 button.classList.remove(config.disabledClass); 15416 } else { 15417 // Button is blocked by variant logic, show reason 15418 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15419 console.log('Button blocked by variant logic:', reason); 15420 e.preventDefault(); 15421 e.stopPropagation(); 15422 return false; 15423 } 15424 15425 // Allow the action to proceed immediately with toast 15426 if (isCartButton) { 15427 // Trigger cart action with proper toast handling 15428 if (typeof window._pdAddToCartSubmit === 'function') { 15429 // Use pd-addtocart.js function for proper toast handling 15430 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15431 if (form) { 15432 const fakeEvent = { 15433 target: form, 15434 preventDefault: () => {} 15435 }; 15436 window._pdAddToCartSubmit(fakeEvent, productId); 15437 } 15438 } else if (typeof window.addToCartSubmit === 'function') { 15439 // Fallback to addToCartSubmit 15440 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15441 if (form) { 15442 const fakeEvent = { 15443 target: form, 15444 preventDefault: () => {} 15445 }; 15446 window.addToCartSubmit(fakeEvent, productId); 15447 } 15448 } else if (typeof window.addToCart === 'function') { 15449 // Direct addToCart call 15450 window.addToCart(productId, 1, variantId); 15451 } 15452 } else if (isBuyButton) { 15453 // Trigger buy action 15454 if (typeof window.buyNow === 'function') { 15455 window.buyNow(); 15456 } 15457 } 15458 } else if (stockInfo && !stockInfo.hasStock) { 15459 15460 // Update global stock cache 15461 window._currentStockInfo = { 15462 can_add_to_cart: false, 15463 is_in_stock: false, 15464 available_stock: stockInfo.stock, 15465 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15466 }; 15467 15468 // Force block button 15469 button.disabled = true; 15470 button.style.opacity = '0.5'; 15471 button.style.cursor = 'not-allowed'; 15472 button.setAttribute('data-stock-blocked', 'true'); 15473 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15474 15475 // Show modal with variant info 15476 const modalMessage = stockInfo ? 15477 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15478 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15479 15480 showStockModal(modalMessage); 15481 15482 // Show security warning 15483 if (window.showToast) { 15484 window.showToast({ 15485 type: 'warning',
15486 title: 'Stok Tidak Tersedia', 15487 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15488 duration: 3000 15489 }); 15490 } 15491 } else { 15492 // stockInfo is null (simple product) - this means embedded data is not available 15493 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15494 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15495 15496 // Allow the action to proceed immediately with toast 15497 if (isCartButton) { 15498 // Trigger cart action with proper toast handling 15499 if (typeof window._pdAddToCartSubmit === 'function') { 15500 // Use pd-addtocart.js function for proper toast handling 15501 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15502 if (form) { 15503 const fakeEvent = { 15504 target: form, 15505 preventDefault: () => {} 15506 }; 15507 window._pdAddToCartSubmit(fakeEvent, productId); 15508 } 15509 } else if (typeof window.addToCartSubmit === 'function') { 15510 // Fallback to addToCartSubmit 15511 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15512 if (form) { 15513 const fakeEvent = { 15514 target: form, 15515 preventDefault: () => {} 15516 }; 15517 window.addToCartSubmit(fakeEvent, productId); 15518 } 15519 } else if (typeof window.addToCart === 'function') { 15520 // Direct addToCart call 15521 window.addToCart(productId, 1, variantId); 15522 } 15523 } else if (isBuyButton) { 15524 // Trigger buy action 15525 if (typeof window.buyNow === 'function') { 15526 window.buyNow(); 15527 } 15528 } 15529 } else { 15530 // Button is disabled, don't allow action 15531 showStockModal('Produk ini sedang tidak tersedia.'); 15532 } 15533 } 15534 } else { 15535 // No product ID, block action 15536 showStockModal('Data produk tidak valid.'); 15537 } 15538 15539 return false; // Always block at capture phase 15540 15541 }, true); // Use capture phase to run BEFORE other listeners 15542 15543 15544 15545 } 15546 15547 /** 15548 * Initialize button data attributes from current page 15549 */ 15550 function initializeButtonAttributes() { 15551 // Get product data from page globals or form 15552 let productId = 0, variantId = 0; 15553 15554 // Try to get from global variables (set by PHP) 15555 if (window.PD_ID) productId = parseInt(window.PD_ID); 15556 15557 // Try to get from form 15558 const form = document.getElementById('pd-buy-form'); 15559 if (form) { 15560 const pidAttr = form.getAttribute('data-pid'); 15561 if (pidAttr) productId = parseInt(pidAttr); 15562 15563 const varInput = document.getElementById('pd-variant-id'); 15564 if (varInput) variantId = parseInt(varInput.value || 0); 15565 } 15566 15567 // Try to get from mobile FAB 15568 const mobileFab = document.getElementById('pd-mobile-fab'); 15569 if (mobileFab && productId <= 0) { 15570 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15571 if (pidAttr) productId = parseInt(pidAttr); 15572 } 15573 15574 // Set attributes on desktop buttons 15575 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15576 desktopButtons.forEach(button => { 15577 if (productId > 0) { 15578 button.setAttribute('data-product-id', productId); 15579 if (variantId > 0) { 15580 button.setAttribute('data-variant-id', variantId); 15581 } 15582 } 15583 }); 15584 15585 // Set attributes on mobile buttons 15586 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15587 mobileButtons.forEach(button => { 15588 if (productId > 0) { 15589 button.setAttribute('data-product-id', productId); 15590 if (variantId > 0) { 15591 button.setAttribute('data-variant-id', variantId); 15592 } 15593 } 15594 }); 15595 15596 15597 15598 15599 15600 // Initial stock display update on page load 15601 if (productId > 0) { 15602 setTimeout(() => { 15603 updateStockDisplay(productId, variantId); 15604 15605 }, 100); 15606 } 15607 } 15608 15609 /** 15610 * Update button attributes when variant changes 15611 */ 15612 function updateButtonAttributesForVariant(variantId) { 15613 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15614 15615 if (productId <= 0) return; 15616 15617 // Update desktop buttons 15618 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15619 desktopButtons.forEach(button => { 15620 button.setAttribute('data-product-id', productId); 15621 if (variantId > 0) { 15622 button.setAttribute('data-variant-id', variantId); 15623 } else { 15624 button.removeAttribute('data-variant-id'); 15625 } 15626 }); 15627 15628 // Update mobile buttons 15629 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15630 mobileButtons.forEach(button => { 15631 button.setAttribute('data-product-id', productId); 15632 if (variantId > 0) { 15633 button.setAttribute('data-variant-id', variantId); 15634 } else { 15635 button.removeAttribute('data-variant-id'); 15636 } 15637 }); 15638 15639 15640 15641 } 15642 15643 /** 15644 * Setup variant change integration with existing product-detail.php handlers 15645 */ 15646 function setupVariantChangeIntegration() { 15647 // Strategy: Hook into existing applyVariant function instead of adding new listener 15648 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15649 15650 // Method 1: Hook into existing applyVariant function 15651 setTimeout(() => { 15652 if (typeof window.applyVariant === 'function') { 15653 const originalApplyVariant = window.applyVariant; 15654 window.applyVariant = function(v, focusImage) { 15655 // Call original function first 15656 const result = originalApplyVariant.call(this, v, focusImage); 15657 15658 // Then add our stock validator logic 15659 const variantId = v ? (v.id || v.ID || 0) : 0; 15660 15661 // Update button attributes for stock validator 15662 updateButtonAttributesForVariant(variantId); 15663 15664 // Update stock display using embedded data (instant) 15665 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15666 if (productId > 0) { 15667 updateStockDisplay(productId, variantId); 15668 } return result; 15669 }; 15670 15671 15672 15673 } else { 15674 15675 setupVariantObserver(); 15676 } 15677 }, 500); 15678 15679 // Method 2: Direct observation of hidden input value changes (fallback) 15680 setTimeout(() => { 15681 setupVariantObserver(); 15682 }, 1000); 15683 } 15684 15685 /** 15686 * Setup variant observer as fallback when applyVariant hook fail
15686s 15687 */ 15688 function setupVariantObserver() { 15689 const variantInput = document.getElementById('pd-variant-id'); 15690 if (variantInput) { 15691 let lastVariantId = parseInt(variantInput.value || '0'); 15692 15693 // Use MutationObserver to watch for value changes 15694 if (window.MutationObserver) { 15695 const observer = new MutationObserver((mutations) => { 15696 mutations.forEach((mutation) => { 15697 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15698 const newVariantId = parseInt(variantInput.value || '0'); 15699 if (newVariantId !== lastVariantId) { 15700 15701 updateButtonAttributesForVariant(newVariantId); 15702 15703 // Update stock display with embedded data 15704 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15705 if (productId > 0) { 15706 updateStockDisplay(productId, newVariantId); 15707 } 15708 15709 lastVariantId = newVariantId; 15710 } 15711 } 15712 }); 15713 }); 15714 15715 observer.observe(variantInput, { 15716 attributes: true, 15717 attributeFilter: ['value'] 15718 }); 15719 15720 15721 15722 } 15723 15724 // Additional polling as ultimate fallback 15725 setInterval(() => { 15726 const currentVariantId = parseInt(variantInput.value || '0'); 15727 if (currentVariantId !== lastVariantId) { 15728 15729 updateButtonAttributesForVariant(currentVariantId); 15730 15731 // Update stock display with embedded data 15732 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15733 if (productId > 0) { 15734 updateStockDisplay(productId, currentVariantId); 15735 } 15736 15737 lastVariantId = currentVariantId; 15738 } 15739 }, 1000); 15740 15741 15742 15743 } 15744 } 15745 15746 /** 15747 * Update stock display and enforce stock-based button blocking using embedded data 15748 */ 15749 function updateStockDisplay(productId, variantId = 0) { 15750 15751 15752 15753 // Get stock info from embedded data (instant) 15754 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15755 15756 let stockInfo; 15757 if (embeddedStockInfo) { 15758 // Convert embedded data to standard format 15759 stockInfo = { 15760 can_add_to_cart: embeddedStockInfo.hasStock, 15761 is_in_stock: embeddedStockInfo.hasStock, 15762 available_stock: embeddedStockInfo.stock, 15763 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15764 product_name: window.PD_NAME || 'Produk', 15765 sku: embeddedStockInfo.sku 15766 }; 15767 15768 15769 15770 } else { 15771 // Fallback to API call if embedded data not available 15772 15773 checkStock(productId, variantId, 1) 15774 .then(apiStockInfo => { 15775 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15776 }) 15777 .catch(error => { 15778 console.error('Failed to update stock display via API:', error); 15779 }); 15780 return; 15781 } 15782 15783 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15784 } 15785 15786 /** 15787 * Update stock display with provided stock info 15788 */ 15789 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15790 // Store current stock status globally for blocking checks 15791 window._currentStockInfo = stockInfo; 15792 15793 // Update stock indicators 15794 const stockIndicators = document.querySelectorAll('.stock-indicator'); 15795 stockIndicators.forEach(indicator => { 15796 if (stockInfo.is_in_stock) { 15797 indicator.classList.add('in-stock'); 15798 indicator.classList.remove('out-of-stock'); 15799 } else { 15800 indicator.classList.add('out-of-stock'); 15801 indicator.classList.remove('in-stock'); 15802 } 15803 }); 15804 15805 // Update stock count 15806 const stockCounts = document.querySelectorAll('.stock-count');
15807 stockCounts.forEach(count => { 15808 count.textContent = stockInfo.available_stock || 0; 15809 }); 15810 15811 // CRITICAL: For digital products, DO NOT modify button states here 15812 // PHP already handles button disable/enable based on LICENSE_POOL count 15813 // Only physical products need JavaScript stock validation 15814 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15815 15816 if (isDigitalProduct) { 15817 // For digital products: ONLY update global stock info, don't touch buttons 15818 // Let PHP-rendered disabled state remain authoritative 15819 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15820 return; // Exit early, preserve PHP-set button states 15821 } 15822 15823 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15824 const allButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15825 allButtons.forEach(button => { 15826 if (stockInfo.can_add_to_cart) { 15827 // STOCK AVAILABLE - ENABLE BUTTONS 15828 button.classList.remove(config.disabledClass); 15829 button.disabled = false; 15830 button.style.opacity = ''; 15831 button.style.cursor = ''; 15832 15833 // CRITICAL: Remove stock blocking attributes when stock becomes available 15834 button.removeAttribute('data-stock-blocked'); 15835 button.removeAttribute('data-stock-reason'); 15836 15837 15838 15839 } else { 15840 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15841 button.classList.add(config.disabledClass); 15842 button.disabled = true; 15843 button.style.opacity = '0.5'; 15844 button.style.cursor = 'not-allowed'; 15845 15846 // Mark as stock-blocked to prevent re-enabling 15847 button.setAttribute('data-stock-blocked', 'true'); 15848 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15849 15850 15851 15852 } 15853 }); 15854 15855 // Start aggressive button monitoring for out-of-stock variants 15856 if (!stockInfo.can_add_to_cart) { 15857 enforceStockDisabling(productId, variantId); 15858 } 15859 15860 15861 15862 } 15863 15864 /** 15865 * Aggressively enforce button disabling for out-of-stock items 15866 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15867 */ 15868 function enforceStockDisabling(productId, variantId) { 15869 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15870 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15871 if (isDigitalProduct) { 15872 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15873 return; // Exit early, let PHP-rendered disabled state remain 15874 } 15875 15876 const monitorInterval = setInterval(() => { 15877 // Check if variant changed 15878 const currentVariantSelector = document.getElementById('pd-variant-id'); 15879 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15880 15881 if (currentVariantId !== variantId) { 15882 // Variant changed, stop monitoring this variant 15883 clearInterval(monitorInterval); 15884 15885 return; 15886 } 15887 15888 // Check current stock status using embedded data (real-time check) 15889 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15890 15891 if (currentStockInfo && !currentStockInfo.hasStock) { 15892 // Still out of stock - continue monitoring for tampering 15893 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15894 buttons.forEach(button => { 15895 // Check if button was manually enabled (inspect attack) 15896 if (!button.disabled || button.style.opacity !== '0.5') { 15897 15898 // Force re-disable 15899 button.disabled = true; 15900 button.style.opacity = '0.5'; 15901 button.style.cursor = 'not-allowed'; 15902 button.setAttribute('data-stock-blocked', 'true'); 15903 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15904 15905 // Show warning toast 15906 if (window.showToast) { 15907 window.showToast({ 15908 type: 'warning',
15909 title: 'Aksi Tidak Diizinkan', 15910 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15911 duration: 3000 15912 }); 15913 } 15914 } 15915 }); 15916 } else { 15917 // Stock available OR embedded data shows stock - stop monitoring 15918 clearInterval(monitorInterval); 15919 15920 // For PHYSICAL products only, enable buttons 15921 // For DIGITAL products, skip (PHP is authoritative) 15922 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15923 15924 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15925 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15926 buttons.forEach(button => { 15927 button.disabled = false; 15928 button.style.opacity = ''; 15929 button.style.cursor = ''; 15930 button.removeAttribute('data-stock-blocked'); 15931 button.removeAttribute('data-stock-reason'); 15932 button.classList.remove(config.disabledClass); 15933 }); 15934 15935 } 15936 } 15937 }, 500); // Check every 500ms for tampering attempts 15938 15939 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15940 setTimeout(() => { 15941 clearInterval(monitorInterval); 15942 15943 }, 30000); 15944 } 15945 15946 /** 15947 * Override addToCartSubmit function to add stock validation 15948 * This function now works with pd-addtocart.js integration 15949 */ 15950 function overrideAddToCartSubmitFunction() { 15951 // Check if pd-addtocart.js is already loaded 15952 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 15953 window.addToCartSubmit.toString().includes('showToast'); 15954 15955 if (pdAddToCartExists) { 15956 15957 15958 15959 // Store the pd-addtocart.js function 15960 const pdAddToCartSubmit = window.addToCartSubmit; 15961 window._pdAddToCartSubmit = pdAddToCartSubmit; 15962 15963 // Override with stock validation that calls pd-addtocart.js 15964 window.addToCartSubmit = async function(e, productId) { 15965 15966 // Don't prevent default here - let pd-addtocart handle it if stock is valid 15967 15968 try { 15969 // Extract product data from form (same way as pd-addtocart.js) 15970 const form = e.target; 15971 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 15972 15973 // CRITICAL: Always get current variant from selector (real-time) 15974 let variantId = 0; 15975 const variantSelector = document.getElementById('pd-variant-id'); 15976 if (variantSelector) { 15977 variantId = parseInt(variantSelector.value || '0', 10) || 0; 15978 } 15979 15980 if (!productId || productId <= 0) { 15981 return pdAddToCartSubmit(e, productId); 15982 } 15983 15984 15985 15986 // Validate stock before proceeding 15987 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 15988 15989 if (isValid) { 15990 // Stock OK, call pd-addtocart.js function (it will handle toast) 15991 return pdAddToCartSubmit(e, productId); 15992 } else { 15993 // Prevent the form submission 15994 e.preventDefault(); 15995 // Stock validation failed, modal already shown by validateBeforeCart 15996 return false; 15997 } 15998 15999 } catch (error) { 16000 e.preventDefault(); 16001 // On error, show modal and don't proceed 16002 showStockModal({
16003 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16004 is_active: false 16005 }, 'cart'); 16006 return false; 16007 } 16008 }; 16009 } else { 16010 // Original logic for when pd-addtocart.js is not present 16011 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16012 16013 if (originalAddToCartSubmit) { 16014 window._originalAddToCartSubmit = originalAddToCartSubmit; 16015 } 16016 16017 // Override with stock validation 16018 window.addToCartSubmit = async function(e, productId) { 16019 16020 e.preventDefault(); 16021 16022 try { 16023 const form = e.target; 16024 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16025 16026 // CRITICAL: Always get current variant from selector (real-time) 16027 let variantId = 0; 16028 const variantSelector = document.getElementById('pd-variant-id'); 16029 if (variantSelector) { 16030 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16031 } 16032 16033 if (!productId || productId <= 0) { 16034 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16035 return false; 16036 } 16037 16038 16039 16040 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16041 16042 if (isValid) { 16043 if (originalAddToCartSubmit) { 16044 return originalAddToCartSubmit(e, productId); 16045 } else { 16046 if (typeof window.addToCart === 'function') { 16047 await window.addToCart(productId, qty, variantId); 16048 } 16049 } 16050 } else { 16051 } 16052 16053 } catch (error) { 16054 showStockModal({ 16055 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16056 is_active: false 16057 }, 'cart'); 16058 } 16059 16060 return false; 16061 }; 16062 } 16063 16064 16065 16066 } 16067 16068 /** 16069 * Override buyNow function to add stock validation 16070 */ 16071 function overrideBuyNowFunction() { 16072 // Store original buyNow function if it exists 16073 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16074 16075 // Override with stock validation 16076 window.buyNow = async function(e) { 16077 16078 try { 16079 // Get product data (same way as pd-buynow.js) 16080 const form = document.getElementById('pd-buy-form'); 16081 if (!form) { 16082 if (originalBuyNow) return originalBuyNow(e); 16083 return; 16084 } 16085 16086 const productId = parseInt(form.dataset.pid || '0', 10); 16087 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16088 16089 // CRITICAL: Always get current variant from selector (real-time) 16090 let variantId = 0; 16091 const variantSelector = document.getElementById('pd-variant-id'); 16092 if (variantSelector) { 16093 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16094 } 16095 16096 if (!productId || productId <= 0) { 16097 if (originalBuyNow) return originalBuyNow(e); 16098 return; 16099 } 16100 16101 16102 16103 // Validate stock before proceeding 16104 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16105 16106 if (isValid) { 16107 // Stock OK, call original buyNow function 16108 if (originalBuyNow) { 16109 return originalBuyNow(e); 16110 } else { 16111 } 16112 } else { 16113 // Stock validation failed, modal already shown by validateBeforeCart 16114 } 16115 16116 } catch (error) { 16117 // On error, show modal and don't proceed 16118 showStockModal({
16119 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16120 is_active: false 16121 }, 'buy'); 16122 } 16123 }; 16124 16125 16126 16127 } 16128 16129 /** 16130 * Initialize stock validation 16131 */ 16132 function init() { 16133 // Wait for DOM ready 16134 if (document.readyState === 'loading') { 16135 addEventListener(document, 'DOMContentLoaded', function() { 16136 initializeButtonAttributes(); 16137 setupVariantChangeIntegration(); // Setup variant change integration 16138 interceptCartButtons(); 16139 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16140 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16141 setTimeout(() => { 16142 overrideAddToCartSubmitFunction(); 16143 overrideBuyNowFunction(); 16144 }, 100); 16145 16146 setTimeout(() => { 16147 // Re-check and re-override if pd-addtocart.js loaded later 16148 overrideAddToCartSubmitFunction(); 16149 }, 500); 16150 16151 setTimeout(() => { 16152 // Final check for very late loading scripts 16153 overrideAddToCartSubmitFunction(); 16154 }, 1000); 16155 16156 16157 16158 }); 16159 } else { 16160 initializeButtonAttributes(); 16161 setupVariantChangeIntegration(); // Setup variant change integration 16162 interceptCartButtons(); 16163 setupGlobalClickInterceptor(); // Setup global click security interceptor 16164 16165 // Override functions after other scripts load 16166 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16167 setTimeout(() => { 16168 overrideAddToCartSubmitFunction(); 16169 overrideBuyNowFunction(); 16170 }, 100); 16171 16172 setTimeout(() => { 16173 // Re-check and re-override if pd-addtocart.js loaded later 16174 overrideAddToCartSubmitFunction(); 16175 }, 500); 16176 16177 setTimeout(() => { 16178 // Final check for very late loading scripts 16179 overrideAddToCartSubmitFunction(); 16180 }, 1000); 16181 16182 16183 16184 } 16185 } 16186 16187 // Public API 16188 return { 16189 init: init, 16190 checkStock: checkStock, 16191 validateBeforeCart: validateBeforeCart, 16192 updateStockDisplay: updateStockDisplay, 16193 showStockModal: showStockModal, 16194 config: config 16195 }; 16196})(); 16197 16198// Auto-initialize 16199if (typeof window !== 'undefined') { 16200 StockValidator.init(); 16201} 16202 16203</script>
vendor: 2 bytes, line 16203
16203
16204<script data-inlined="pd-addtocart.js"> 16205(function(){ 16206 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16207 function getVariantSummaryText(){ 16208 const vs = document.getElementById('pd-variant-summary'); 16209 if (!vs) return ''; 16210 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16211 return items.length ? items.join(' / ') : ''; 16212 } 16213 window.addToCartSubmit = async function(e, productId){ 16214 e.preventDefault(); 16215 try { 16216 const form = e.target; 16217 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16218 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16219 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16220 16221 if (window.PlazaAnalytics) { 16222 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16223 } 16224 16225 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16226 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16227 const variantText = getVariantSummaryText(); 16228 16229 if (window.showToast) { 16230 window.showToast({ 16231 type: 'success', 16232 title: 'Berhasil ditambahkan ke Keranjang', 16233 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16234 duration: 5000, 16235 actions: [ 16236 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16237 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16238 ] 16239 }); 16240 } 16241 } catch (err) { 16242 if (window.showToast) { 16243 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16244 } else { 16245 console.error(err); 16246 } 16247 } 16248 return false; 16249 }; 16250})(); 16251</script>
vendor: 2 bytes, line 16251
16251
16252<script data-inlined="pd-buynow.js"> 16253(function(){ 16254 16255 // CSRF Token Helper 16256 function getCsrfToken() { 16257 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16258 } 16259 16260 // Utility functions 16261 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16262 16263 function getVariantSummaryText(){ 16264 const vs = document.getElementById('pd-variant-summary'); 16265 if (!vs) return ''; 16266 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16267 return items.length ? items.join(' / ') : ''; 16268 } 16269 16270 function getCurrentProductData(){ 16271 const form = document.getElementById('pd-buy-form'); 16272 if (!form) return null; 16273 16274 const productId = parseInt(form.dataset.pid || '0', 10); 16275 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16276 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16277 16278 if (!productId || productId <= 0) { 16279 throw new Error('Product ID tidak valid'); 16280 } 16281 16282 if (qty <= 0) { 16283 throw new Error('Quantity harus lebih dari 0'); 16284 } 16285 16286 return { productId, qty, variantId }; 16287 } 16288 16289 function createBuyNowPayload(productId, qty, variantId) { 16290 // Get product metadata 16291 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16292 16293 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16294 let price = 0; 16295 let sku = ''; 16296 let weight = 0; 16297 let dimensions = null; 16298 16299 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16300 // Find the selected variant in the variants array 16301 const selectedVariant = window.variants.find(v => v.id === variantId); 16302 if (selectedVariant) { 16303 price = selectedVariant.final || 0; 16304 sku = selectedVariant.sku || ''; 16305 weight = selectedVariant.weight || 0; 16306 16307 // Include dimensions if available 16308 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16309 dimensions = { 16310 length_cm: selectedVariant.length_cm || 0, 16311 width_cm: selectedVariant.width_cm || 0, 16312 height_cm: selectedVariant.height_cm || 0 16313 }; 16314 } 16315 16316 // Debug info to verify fix is working 16317 if (window.console && window.console.debug) { 16318 console.debug('BuyNow: Using variant data', { 16319 variantId: variantId, 16320 price: price, 16321 sku: sku, 16322 weight: weight, 16323 dimensions: dimensions, 16324 variantData: selectedVariant 16325 }); 16326 } 16327 } 16328 } 16329 16330 // Fallback to DOM elements if variant data not found 16331 if (price === 0) { 16332 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16333 price = parseInt(String(priceTxt).replace(/[^d]/g,''), 10) || 0; 16334 16335 if (window.console && window.console.debug) { 16336 console.debug('BuyNow: Using DOM price fallback', { 16337 priceTxt: priceTxt, 16338 price: price, 16339 variantId: variantId, 16340 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16341 }); 16342 } 16343 } 16344 16345 if (!sku) { 16346 const skuEl = document.getElementById('pd-sku'); 16347 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16348 } 16349 16350 const imgEl = document.getElementById('pd-main-img'); 16351 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16352 16353 const variantText = getVariantSummaryText(); 16354 16355 // Get pre-order data if product is pre-order 16356 const preorderData = window.PD_PREORDER_DATA || null; 16357 16358 return { 16359 product_id: productId, 16360 qty: qty, 16361 variant_id: variantId, 16362 meta: { 16363 name, 16364 sku, 16365 price, 16366 image_url, 16367 variant_text: variantText, 16368 weight: weight, 16369 dimensions: dimensions 16370 }, 16371 is_preorder: preorderData ? 'Y' : 'N', 16372 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16373 preorder_note: preorderData ? preorderData.note : '' 16374 }; 16375 } 16376 16377 /** 16378 * Main Buy Now function
16379 * Performs validation and redirects to checkout with buy_now mode 16380 */ 16381 window.buyNow = async function(e) { 16382 if (e) e.preventDefault(); 16383 16384 try { 16385 // Check user login status 16386 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16387 document.body.classList.contains('logged-in') || 16388 window.userLoggedIn === true || 16389 document.body.dataset.userLoggedIn === 'true'; 16390 16391 if (!isLoggedIn) { 16392 // Redirect to login with return URL to current page 16393 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16394 window.location.href = '/login?return=' + currentUrl; 16395 return; 16396 } 16397 16398 // Get current product data 16399 const { productId, qty, variantId } = getCurrentProductData(); 16400 16401 // Create payload for buy now 16402 const payload = createBuyNowPayload(productId, qty, variantId); 16403 16404 // Store buy now data in sessionStorage for checkout page 16405 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16406 16407 // Track add_to_cart (buy now counts as purchase intent) 16408 if (window.PlazaAnalytics) { 16409 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16410 } 16411 16412 // Redirect to checkout with buy_now mode 16413 window.location.href = '/checkout?mode=buy_now'; 16414 16415 } catch (err) { 16416 console.error('Buy Now error:', err); 16417 16418 if (window.showToast) { 16419 window.showToast({ 16420 type: 'warn', 16421 title: 'Gagal memproses', 16422 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16423 duration: 4500 16424 }); 16425 } else { 16426 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16427 } 16428 } 16429 }; 16430 16431 /** 16432 * Alternative Buy Now with confirmation dialog 16433 */ 16434 window.buyNowWithConfirm = async function(e) { 16435 if (e) e.preventDefault(); 16436 16437 try { 16438 const { productId, qty, variantId } = getCurrentProductData(); 16439 16440 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16441 const variantText = getVariantSummaryText(); 16442 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16443 16444 const confirmMessage = `Lanjut ke checkout?nnProduk: ${name}${variantText ? `nVariant: ${variantText}` : ''}nQty: ${qty}nHarga: ${priceTxt}`; 16445 16446 if (!confirm(confirmMessage)) { 16447 return; 16448 } 16449 16450 // Proceed with buy now 16451 await window.buyNow(e); 16452 16453 } catch (err) { 16454 console.error('Buy Now with confirm error:', err); 16455 alert(err.message || 'Terjadi kesalahan'); 16456 } 16457 }; 16458 16459 /** 16460 * Attach event listener for Buy with Voucher button 16461 */ 16462 function setupBuyButtons() { 16463 // Setup desktop buy button (regular buy now without voucher) 16464 const buyBtn = document.getElementById('btn-buy'); 16465 if (buyBtn) { 16466 buyBtn.addEventListener('click', function(e) { 16467 e.preventDefault(); 16468 if (buyBtn.disabled) return; 16469 if (window.buyNow) { 16470 window.buyNow(e); 16471 } 16472 }); 16473 } 16474 16475 // Setup desktop buy with voucher button 16476 const voucherBtn = document.getElementById('btn-buy-voucher'); 16477 if (voucherBtn) { 16478 voucherBtn.addEventListener('click', function(e) { 16479 e.preventDefault(); 16480 16481 // Validate stock availability before proceeding 16482 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16483 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16484 16485 if (window.showToast) { 16486 window.showToast({ 16487 type: 'warn', 16488 title: 'Stok Tidak Tersedia', 16489 message: stockReason, 16490 duration: 3500 16491 }); 16492 } else { 16493 alert(stockReason); 16494 } 16495 return; 16496 } 16497 16498 const voucherCode = this.dataset.voucherCode; 16499 if (voucherCode) { 16500 window.buyNowWithVoucher(e, voucherCode); 16501 } 16502 }); 16503 } 16504 16505 } 16506 16507 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16508 // This is the ONLY way to guarantee it fires regardless of other scripts 16509 document.addEventListener('click', function(e) { 16510 // Check if click is on mobile buy button or its children 16511 const target = e.target.closest('#mf-buy'); 16512 if (!target) return; // Not our button 16513 16514 e.preventDefault(); 16515 e.stopPropagation(); 16516 e.stopImmediatePropagation(); 16517 16518 if (target.disabled) return; 16519
16520 // Get voucher code from parent FAB's data attribute 16521 const mobileFab = document.getElementById('pd-mobile-fab'); 16522 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16523 16524 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16525 window.buyNowWithVoucher(e, voucherCode); 16526 } else if (window.buyNow) { 16527 window.buyNow(e); 16528 } 16529 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16530 16531 // Execute immediately if DOM already loaded, otherwise wait 16532 if (document.readyState === 'loading') { 16533 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16534 } else { 16535 setupBuyButtons(); 16536 } 16537 16538 /** 16539 * Buy Now with Auto-Applied Voucher 16540 * Applies voucher to session before redirecting to checkout (like cart does) 16541 * 16542 * @param {Event} e - Click event 16543 * @param {string} voucherCode - Voucher code to apply 16544 */ 16545 window.buyNowWithVoucher = async function(e, voucherCode) { 16546 if (e) e.preventDefault(); 16547 16548 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16549 16550 try { 16551 // CRITICAL: Check stock availability first 16552 const voucherBtn = document.getElementById('btn-buy-voucher'); 16553 if (voucherBtn) { 16554 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16555 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16556 16557 if (window.showToast) { 16558 window.showToast({ 16559 type: 'warn', 16560 title: 'Stok Tidak Tersedia', 16561 message: stockReason, 16562 duration: 3500 16563 }); 16564 } else { 16565 alert(stockReason); 16566 } 16567 return; 16568 } 16569 } 16570 16571 // Check login status first 16572 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16573 document.body.classList.contains('logged-in') || 16574 window.userLoggedIn === true || 16575 document.body.dataset.userLoggedIn === 'true'; 16576 16577 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16578 16579 if (!isLoggedIn) { 16580 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16581 window.location.href = '/login?return=' + currentUrl; 16582 return; 16583 } 16584 16585 // Get current product data 16586 const { productId, qty, variantId } = getCurrentProductData(); 16587 const payload = createBuyNowPayload(productId, qty, variantId); 16588 16589 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16590 16591 // Get category path for voucher validation 16592 const categoryPath = window.PD_CATEGORY_PATH || ''; 16593 16594 // Step 1: Clear any existing voucher first (important for clean state) 16595 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16596 try { 16597 await fetch('/api/voucher/remove.php', { 16598 method: 'POST', 16599 headers: { 16600 'X-CSRF-Token': getCsrfToken() 16601 }, 16602 body: JSON.stringify({ 16603 csrf_token: getCsrfToken() 16604 }), 16605 credentials: 'same-origin' 16606 }); 16607 } catch (e) { 16608 // Ignore error if no voucher exists 16609 console.debug('No existing voucher to remove or removal failed'); 16610 } 16611 16612 // Step 2: Apply voucher to session (like cart does) 16613 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16614 const applyRes = await fetch('/api/voucher/apply.php', { 16615 method: 'POST', 16616 headers: { 16617 'Content-Type': 'application/json', 16618 'X-CSRF-Token': getCsrfToken() 16619 }, 16620 credentials: 'same-origin', 16621 body: JSON.stringify({ 16622 csrf_token: getCsrfToken(), 16623 code: voucherCode, 16624 items: [{ 16625 pid: productId, 16626 price: payload.meta.price, 16627 qty: qty, 16628 category_path: categoryPath 16629 }], 16630 shipping_cost: null // Will be calculated in checkout 16631 }) 16632 }); 16633 16634 const applyData = await applyRes.json(); 16635 16636 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16637 16638 if (!applyData.success) { 16639 // Voucher invalid - show error and fallback to regular buy now 16640 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16641 if (window.showToast) { 16642 window.showToast({ 16643 type: 'error', 16644 title: 'Voucher Tidak Valid', 16645 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16646 duration: 4000 16647 }); 16648 } else { 16649 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16650 } 16651 16652 // Fallback to regular buy now without voucher 16653 console.log('Voucher failed, proceeding with regular buy now'); 16654 return window.buyNow(e); 16655 } 16656 16657 // Step 3: Store buy now data in sessionStorage 16658 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16659 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16660 16661 // Step 4: Redirect to checkout 16662 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16663 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16664 window.location.href = '/checkout?mode=buy_now'; 16665 16666 } catch (err) { 16667 console.error('Buy Now with Voucher error:', err); 16668 16669 if (window.showToast) { 16670 window.showToast({ 16671 type: 'error', 16672 title: 'Terjadi Kesalahan', 16673 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16674 duration: 3000 16675 }); 16676 } else { 16677 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16678 } 16679 } 16680 }; 16681})(); 16682</script>
vendor: 2 bytes, line 16682
16682
16683<script data-inlined="pd-voucher-recalc.js"> 16684/** 16685 * Product Detail - Voucher Discount Recalculation 16686 * 16687 * Handles dynamic voucher discount recalculation when variant price changes 16688 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16689 */ 16690 16691(function() { 16692 'use strict'; 16693 16694 /** 16695 * Calculate voucher discount based on type and price 16696 * 16697 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16698 * @param {number} value - Voucher value (percentage or fixed amount) 16699 * @param {number} price - Current variant price 16700 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16701 * @return {number} Calculated discount amount 16702 */ 16703 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16704 if (type === 'PERCENTAGE') { 16705 // Calculate percentage discount 16706 let discount = (price * value) / 100; 16707 16708 // Apply max discount cap if set 16709 if (maxDiscount > 0 && discount > maxDiscount) { 16710 discount = maxDiscount; 16711 } 16712 16713 return Math.round(discount); 16714 } else { 16715 // FIXED_AMOUNT - discount doesn't change with price 16716 return Math.round(value); 16717 } 16718 } 16719 16720 /** 16721 * Format number as Rupiah without 'Rp' prefix 16722 * 16723 * @param {number} num - Number to format 16724 * @return {string} Formatted number (e.g., "20.000") 16725 */ 16726 function formatNumber(num) { 16727 return Math.round(num).toString().replace(/B(?=(d{3})+(?!d))/g, '.'); 16728 } 16729 16730 /** 16731 * Update voucher button discount label 16732 * 16733 * @param {number} newPrice - New variant price 16734 */ 16735 function updateVoucherDiscount(newPrice) { 16736 const voucherBtn = document.getElementById('btn-buy-voucher'); 16737 if (!voucherBtn) return; 16738 16739 // Get voucher data from button attributes 16740 const voucherType = voucherBtn.dataset.voucherType; 16741 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16742 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16743 16744 if (!voucherType || voucherValue === 0) return; 16745 16746 // Calculate new discount 16747 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16748 16749 // Update the discount label 16750 const saveLabel = voucherBtn.querySelector('.bv-save'); 16751 if (saveLabel) { 16752 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16753 } 16754 16755 // Also update mobile FAB voucher display if exists 16756 updateMobileFabVoucher(newDiscount); 16757 } 16758 16759 /** 16760 * Update mobile FAB voucher value 16761 * 16762 * @param {number} discountAmount - New discount amount 16763 */ 16764 function updateMobileFabVoucher(discountAmount) { 16765 const fab = document.getElementById('pd-mobile-fab'); 16766 if (fab) { 16767 fab.dataset.voucherValue = discountAmount.toString(); 16768 } 16769 } 16770 16771 /** 16772 * Listen to variant change events and recalculate discount 16773 */ 16774 function initVoucherRecalculation() { 16775 window.addEventListener('pd-variant-changed', function(e) { 16776 const detail = e.detail || {}; 16777 const newPrice = detail.price || 0; 16778 16779 if (newPrice > 0) { 16780 updateVoucherDiscount(newPrice); 16781 } 16782 }); 16783 } 16784 16785 // Initialize when DOM is ready 16786 if (document.readyState === 'loading') { 16787 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16788 } else { 16789 initVoucherRecalculation(); 16790 } 16791 16792})(); 16793 16794</script>
vendor: 2 bytes, line 16794
16794
16795<script nonce="se3iyzEHMc9FQEB4wSyd0w==">document.addEventListener('DOMContentLoaded',function(){if(typeof ProductReviews!=='undefined'){ProductReviews.init(325);}const r=document.getElementById('pd-rating-inline');if(r){r.addEventListener('click',function(e){e.preventDefault();const s=document.getElementById('reviews');if(s){s.scrollIntoView({behavior:'smooth',block:'start'});s.classList.add('highlight-flash');setTimeout(()=>{s.classList.remove('highlight-flash')},1500)}});}});</script>
16795 16796
16797<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16798(function(){function _0x9i0j(el){if(typeof gtag==='function'){gtag('event','qoala_insurance_click',{event_category:'Insurance',event_label:'Qoala Banner Click - Product Detail',link_url:'/plazait-protect',page_location:window.location.href,product_id:parseInt(el.getAttribute('data-product-id'))||0,product_name:el.getAttribute('data-product-name')||'',category_path:el.getAttribute('data-category-path')||'',custom_map:{custom_parameter_1:'qoala_banner_product_detail'}})}} 16799 16800function _0x1k2l(){var a=document.querySelector('.qoala-cta[data-ga-event="qoala_banner_click"]');if(a){a.addEventListener('click',function(e){_0x9i0j(this)})}}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',_0x1k2l)}else{_0x1k2l()}})(); 16801 16802(function(){const pwpSection=document.getElementById('pwp-deals-section');const pwpBadge=document.getElementById('pwp-trigger-badge');if(!pwpSection)return;const productId=parseInt(pwpSection.getAttribute('data-product-id'))||0;const variantId=parseInt(pwpSection.getAttribute('data-variant-id'))||0;const categoryPath=pwpSection.getAttribute('data-category-path')||'';if(!productId)return;function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}function loadPWPDeals(){const url=`/api/pwp/get_by_product.php?product_id=${productId}${variantId?`&variant_id=${variantId}`:''}${categoryPath?`&category_path=${encodeURIComponent(categoryPath)}`:''}`;fetch(url).then(r=>r.json()).then(data=>{if(data.success&&data.promotions&&data.promotions.length>0){renderPWPDeals(data.promotions);pwpSection.hidden=false;if(pwpBadge)pwpBadge.hidden=false;setTimeout(function(){if(typeof window.adjustSidebarBanner==='function'){window.adjustSidebarBanner()}},150)}else{pwpSection.hidden=true;if(pwpBadge)pwpBadge.hidden=true}}).catch(err=>{console.error('PWP load error:',err);pwpSection.hidden=true;if(pwpBadge)pwpBadge.hidden=true})}function renderPWPDeals(promotions){const loading=pwpSection.querySelector('.pwp-deals-loading');const grid=document.getElementById('pwp-deals-grid');if(loading)loading.hidden=true;if(!grid)return;grid.innerHTML='';promotions.forEach(promo=>{promo.bundled_products.forEach(product=>{const card=document.createElement('div');card.className='pwp-deal-card';const discount=product.discount>0?product.discount:0;const discountPercent=product.regular_price>0?Math.round((discount/product.regular_price)*100):0;const imgUrl=product.image_url?`/uploads/products/${product.image_url}`:'/assets/img/no-image.png';card.innerHTML=`<div class="pwp-deal-thumb"><img src="${imgUrl}" alt="${escapeHtml(product.name)}" loading="lazy"></div><div class="pwp-deal-info"><h4 class="pwp-deal-name">${escapeHtml(product.name)}</h4><div class="pwp-deal-prices"><span class="pwp-deal-price">${formatRupiah(product.pwp_price)}</span>${product.regular_price>product.pwp_price?`<span class="pwp-deal-original">${formatRupiah(product.regular_price)}</span>`:''}</div></div>${discountPercent>0?`<span class="pwp-deal-badge">-${discountPercent}%</span>`:''}`;grid.appendChild(card)})})}function escapeHtml(str){const div=document.createElement('div');div.textContent=str||'';return div.innerHTML}loadPWPDeals();window.addEventListener('pd-variant-changed',function(e){const detail=e.detail||{};const newVid=detail.variant_id||0;const currentVid=parseInt(pwpSection.getAttribute('data-variant-id'))||0;if(newVid!==currentVid){pwpSection.setAttribute('data-variant-id',newVid);loadPWPDeals()}})})(); 16803</script>
16803<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16804 <div class="ft-top ft-collapsible" id="ftTop"> 16805 <div class="container ft-grid"> 16806 16807 <div class="ft-col ft-help"> 16808 <h4 class="ft-title">Butuh Bantuan?</h4> 16809 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16810 <a class="ft-wa-btn" href="https://8822ina.com" target="_blank" rel="nofollow noopener"> 16811 <span class="ico" aria-hidden="true"> 16812 <svg viewBox="0 0 24 24" width="18" height="18"><path fill="#25D366" d="M12.04 2c-5.52 0-10 4.43-10 9.9 0 1.75.47 3.45 1.38 4.94L2 22l5.3-1.38c1.43.78 3.05 1.19 4.7 1.19 5.52 0 10-4.43 10-9.9S17.56 2 12.04 2Zm5.86 13.9c-.25.7-1.23 1.28-1.7 1.33-.45.06-1.02.08-1.65-.1-.38-.12-.88-.29-1.52-.56-2.66-1.15-4.4-3.83-4.53-4.01-.13-.18-1.08-1.44-1.08-2.75 0-1.31.68-1.95.92-2.22.25-.27.54-.33.72-.33.18 0 .36.01.52.01.17 0 .39-.06.62.47.25.6.85 2.07.92 2.22.07.15.12.33.02.53-.1.2-.15.33-.3.5-.15.17-.32.39-.46.53-.15.15-.3.31-.13.6.18.3.8 1.31 1.73 2.12 1.19 1.03 2.17 1.36 2.48 1.5.31.14.49.12.67-.07.18-.18.77-.82.98-1.1.21-.27.42-.23.7-.14.27.09 1.72.81 2.02.95.3.14.5.22.58.35.08.13.08.72-.17 1.42Z"/></svg> 16813 </span> 16814 Kirim Pesan 16815 </a> 16816 16817 <p class="ft-desc sm-gap">Ikuti kami:</p> 16818 <div class="ft-social"> 16819 <a aria-label="Facebook" href="https://facebook.com/PlazaIt/"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M13.5 22v-8.2h2.7l.4-3.2h-3.1V8c0-.9.3-1.5 1.6-1.5H17V3.7c-.3 0-1.2-.1-2.2-.1-2.2 0-3.8 1.3-3.8 3.9v2.2H8v3.2h3v8.1h2.5Z"/></svg></a> 16820 <a aria-label="Instagram" href="https://www.instagram.com/plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M7.8 2h8.4A5.8 5.8 0 0 1 22 7.8v8.4A5.8 5.8 0 0 1 16.2 22H7.8A5.8 5.8 0 0 1 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2Zm0 2A3.8 3.8 0 0 0 4 7.8v8.4A3.8 3.8 0 0 0 7.8 20h8.4a3.8 3.8 0 0 0 3.8-3.8
16820V7.8A3.8 3.8 0 0 0 16.2 4H7.8Zm9.9 1.7a1.1 1.1 0 1 1 0 2.3 1.1 1.1 0 0 1 0-2.3ZM12 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg></a> 16821 <a aria-label="TikTok" href="https://www.tiktok.com/@plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18.9 7.4a6.4 6.4 0 0 1-3.2-1.2v6.3a5.47 5.47 0 1 1-5.5-5.5c.2 0 .4 0 .6.02v2.37a2.92 2.92 0 0 0-.6-.07 2.8 2.8 0 1 0 2.8 2.8V2h2.3a4.6 4.6 0 0 0 4.5 4.1v2.3Z"/></svg></a> 16822 </div> 16823 </div> 16824 16825 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16826 <div class="ft-collapsible-content" id="ftCollapsible"> 16827 16828 <div class="ft-col"> 16829 <h4 class="ft-title">Tentang BOYTOTO</h4> 16830 <ul class="ft-links"> 16831 <li><a href="https://mgsco.org/">Tentang BOYTOTO</a></li> 16832 <li><a href="https://mgsco.org/">Panduan BOYTOTO</a></li> 16833 <li><a href="https://mgsco.org/">Informasi Layanan</a></li> 16834 <li><a href="https://mgsco.org/">FAQ</a></li> 16835 </ul> 16836 <div class="ft-address"> 16837 <p class="company">PT. MADU MEDIA KREATIF</p> 16838 <p>Jl. Angkasa Perdana No. 303, Kel. Srikandi,</p> 16839 <p>Kec. Skyview, Kab. Bogor, Jawa Barat, 16915</p> 16840 </div> 16841 </div> 16842 16843 <div class="ft-col"> 16844 <h4 class="ft-title">Informasi</h4> 16845 <ul class="ft-links"> 16846 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16847 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16848 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16849 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16850 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16851 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16852 </ul> 16853 16854 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16855 <div class="pay-row"> 16856 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-mandiri.png" alt="Mandiri" loading="lazy"> 16857 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bni.gif" alt="BNI" loading="lazy"> 16858 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bri.gif" alt="BRI" loading="lazy"> 16859 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-qris.gif" alt="QRIS" loading="lazy"> 16860 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bca.gif" alt="BCA" loading="lazy"> 16861 </div> 16862 </div> 16863 </div> 16864 16865 </div> 16866 <!-- End Collapsible Content --> 16867 16868 <!-- Expand Button (Mobile Only) --> 16869 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16870 <span class="expand-text">Selengkapnya</span> 16871 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16872 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16873 </svg> 16874 </button> 16875 16876 </div> 16877 </div> 16878 <div class="ft-bottom"> 16879 <div class="container ft-bottom-inner"> 16880 <div class="ft-copy"> 16881 <span>ANGIN-RIBUT © <span id="yearNow">2026</span></span> 16882 <span>PT ANGIN-RIBUT. All rights reserved.</span> 16883 </div> 16884 </div> 16885 </div> 16886 16887 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16888 /* Chat Widget Styles - Lightweight & Minimal */ 16889 .chat-widget { 16890 position: fixed; 16891 bottom: 20px; 16892 right: 20px; 16893 z-index: 9999; 16894 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16895 } 16896 16897 /* Hide floating button on mobile - use bottom nav instead */ 16898 @media (max-width: 640px) { 16899 .chat-toggle-btn { 16900 display: none !important; 16901 } 16902 16903 /* Ensure popup is still visible on mobile even when button is hidden */ 16904 .chat-widget { 16905 display: block !important; 16906 } 16907 16908 .chat-popup { 16909 display: none; /* Hidden by default, shown via JS */ 16910 } 16911 16912 .chat-popup[style*="display: block"], 16913 .chat-popup[style*="display:block"] { 16914 display: block !important; 16915 } 16916 } 16917 16918 .chat-toggle-btn { 16919 width: 60px; 16920 height: 60px; 16921 border-radius: 50%; 16922 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 16923 border: none; 16924 color: white; 16925 cursor: pointer; 16926 box-shadow: 0 4px 12px rgba(242, 140, 26, 0.3); 16927 position: relative; 16928 transition: transform 0.2s, box-shadow 0.2s; 16929 } 16930 16931 .chat-toggle-btn:hover { 16932 transform: scale(1.05); 16933 box-shadow: 0 6px 20px rgba(242, 140, 26, 0.4); 16934 } 16935 16936 #chat-toggle-btn .chat-icon { 16937 position: absolute; 16938 top: 50%; 16939 left: 50%; 16940 transform: translate(-50%, -50%); 16941 width: 28px !important; 16942 height: 28px !important; 16943 } 16944 16945 .chat-unread-badge { 16946 position: absolute; 16947 top: -5px; 16948 right: -5px; 16949 background: #ef4444; 16950 color: white; 16951 border-radius: 12px; 16952 padding: 2px 6px; 16953 font-size: 11px; 16954 font-weight: 600;
16955 min-width: 20px; 16956 text-align: center; 16957 } 16958 16959 .chat-popup { 16960 position: absolute; 16961 bottom: 80px; 16962 right: 0; 16963 width: 380px; 16964 max-width: calc(100vw - 40px); 16965 height: 500px; 16966 max-height: calc(100vh - 120px); 16967 background: white; 16968 border-radius: 12px; 16969 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 16970 display: flex; 16971 flex-direction: column; 16972 overflow: hidden; 16973 } 16974 16975 .chat-popup-header { 16976 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 16977 color: white; 16978 padding: 16px 20px; 16979 display: flex; 16980 justify-content: space-between; 16981 align-items: center; 16982 flex-shrink: 0; 16983 } 16984 16985 .chat-header-title { 16986 display: flex; 16987 flex-direction: column; 16988 gap: 2px; 16989 min-height: 0; 16990 } 16991 16992 .chat-popup-header h3 { 16993 margin: 0; 16994 font-size: 18px; 16995 font-weight: 600; 16996 line-height: 1.2; 16997 } 16998 16999 .chat-operating-hours { 17000 font-size: 11px; 17001 opacity: 0.9; 17002 font-weight: 400; 17003 display: flex; 17004 align-items: center; 17005 gap: 4px; 17006 line-height: 1.2; 17007 } 17008 17009 .chat-operating-hours svg { 17010 width: 12px; 17011 height: 12px; 17012 opacity: 0.8; 17013 flex-shrink: 0; 17014 } 17015 17016 .chat-header-actions { 17017 display: flex; 17018 align-items: center; 17019 gap: 8px; 17020 } 17021 17022 /* Notification Toggle Button */ 17023 .btn-notif-toggle { 17024 background: none; 17025 border: none; 17026 color: white; 17027 font-size: 18px; 17028 cursor: pointer; 17029 padding: 4px 8px; 17030 transition: all 0.2s; 17031 opacity: 0.8; 17032 } 17033 17034 .btn-notif-toggle:hover { 17035 opacity: 1; 17036 transform: scale(1.1); 17037 } 17038 17039 /* Icon states */ 17040 .btn-notif-toggle[data-status="not-subscribed"] svg { 17041 opacity: 0.7; 17042 } 17043 17044 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17045 opacity: 1; 17046 } 17047 17048 .btn-notif-toggle[data-status="subscribed"] svg { 17049 opacity: 1; 17050 animation: bellRing 2s ease-in-out; 17051 } 17052 17053 .btn-notif-toggle[data-status="blocked"] svg { 17054 color: #ff6b6b; 17055 } 17056 17057 .btn-notif-toggle[data-status="not-supported"] { 17058 display: none !important; 17059 } 17060 17061 @keyframes bellRing { 17062 0%, 100% { transform: rotate(0deg); } 17063 10%, 30% { transform: rotate(-10deg); } 17064 20%, 40% { transform: rotate(10deg); } 17065 } 17066 17067 /* Notification Info Bar */ 17068 .chat-notif-info-bar { 17069 position: absolute; 17070 top: 64px; 17071 left: 0; 17072 right: 0; 17073 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17074 border-bottom: 1px solid #bbdefb; 17075 padding: 8px 12px; 17076 display: flex; 17077 align-items: center; 17078 gap: 8px; 17079 font-size: 12px; 17080 color: #616161; 17081 z-index: 10; 17082 animation: slideDown 0.3s ease; 17083 } 17084 17085 .chat-notif-info-bar svg { 17086 flex-shrink: 0; 17087 } 17088 17089 .chat-notif-info-bar .small { 17090 flex: 1; 17091 line-height: 1.4; 17092 } 17093 17094 .chat-notif-info-bar .btn-dismiss { 17095 background: none; 17096 border: none; 17097 color: #9e9e9e; 17098 cursor: pointer; 17099 padding: 0 4px; 17100 font-size: 16px; 17101 transition: color 0.2s; 17102 } 17103 17104 .chat-notif-info-bar .btn-dismiss:hover { 17105 color: #616161; 17106 } 17107 17108 @keyframes slideDown { 17109 from { 17110 max-height: 0; 17111 opacity: 0; 17112 } 17113 to { 17114 max-height: 50px; 17115 opacity: 1;
17116 } 17117 } 17118 17119 .chat-close-btn { 17120 background: none; 17121 border: none; 17122 color: white; 17123 font-size: 28px; 17124 line-height: 1; 17125 cursor: pointer; 17126 padding: 0; 17127 width: 30px; 17128 height: 30px; 17129 } 17130 17131 .chat-popup-body { 17132 flex: 1 1 auto; 17133 display: flex; 17134 flex-direction: column; 17135 min-height: 0; 17136 overflow: hidden; 17137 height: calc(100% - 64px); 17138 position: relative; 17139 transition: padding-top 0.3s ease; 17140 } 17141 17142 /* Add padding when info bar is visible */ 17143 .chat-popup-body.has-info-bar { 17144 padding-top: 40px; 17145 } 17146 17147 .chat-view { 17148 flex: 1 1 auto; 17149 display: none; 17150 flex-direction: column; 17151 min-height: 0; 17152 overflow: hidden; 17153 } 17154 17155 #chat-list-view { 17156 display: flex; 17157 } 17158 17159 #chat-messages-view { 17160 display: none; 17161 } 17162 17163 #chat-messages-view.active { 17164 display: flex; 17165 flex-direction: column; 17166 } 17167 17168 #chat-new-view { 17169 display: none; 17170 } 17171 17172 #chat-new-view.active { 17173 display: flex; 17174 } 17175 17176 .chat-conversations-list { 17177 flex: 1; 17178 overflow-y: auto; 17179 padding: 0; 17180 background: white; 17181 } 17182 17183 /* Custom Scrollbar for Conversations List */ 17184 .chat-conversations-list::-webkit-scrollbar { 17185 width: 6px; 17186 } 17187 17188 .chat-conversations-list::-webkit-scrollbar-track { 17189 background: #f1f1f1; 17190 } 17191 17192 .chat-conversations-list::-webkit-scrollbar-thumb { 17193 background: #F28C1A; 17194 border-radius: 3px; 17195 } 17196 17197 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17198 background: #D97809; 17199 } 17200 17201 /* Firefox */ 17202 .chat-conversations-list { 17203 scrollbar-width: thin; 17204 scrollbar-color: #F28C1A #f1f1f1; 17205 } 17206 17207 .chat-conversation-item { 17208 padding: 12px 16px; 17209 cursor: pointer; 17210 display: flex; 17211 gap: 12px; 17212 align-items: flex-start; 17213 transition: background 0.15s ease; 17214 color: #33371f; 17215 border-bottom: 1px solid #f0f0f0; 17216 } 17217 17218 .chat-conversation-item:hover { 17219 background: #f5f5f5; 17220 } 17221 17222 .chat-conversation-item.active { 17223 background: #FFF5EB; 17224 } 17225 17226 /* WhatsApp-like Avatar */ 17227 .conversation-avatar { 17228 width: 48px; 17229 height: 48px; 17230 border-radius: 50%; 17231 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17232 flex-shrink: 0; 17233 display: flex; 17234 align-items: center; 17235 justify-content: center; 17236 color: white; 17237 font-weight: 600; 17238 font-size: 18px; 17239 } 17240 17241 .conversation-avatar svg { 17242 width: 24px; 17243 height: 24px; 17244 } 17245 17246 /* WhatsApp-like Content */ 17247 .conversation-content { 17248 flex: 1; 17249 min-width: 0; 17250 display: flex; 17251 flex-direction: column; 17252 gap: 4px; 17253 } 17254 17255 .conversation-header { 17256 display: flex; 17257 justify-content: space-between; 17258 align-items: center; 17259 gap: 8px; 17260 } 17261 17262 .conversation-name { 17263 font-weight: 600; 17264 font-size: 15px; 17265 color: #111; 17266 white-space: nowrap; 17267 overflow: hidden; 17268 text-overflow: ellipsis; 17269 } 17270 17271 .conversation-time { 17272 font-size: 12px; 17273 color: #7d8166; 17274 flex-shrink: 0; 17275 } 17276 17277 .conversation-preview { 17278 display: flex; 17279 justify-content: space-between; 17280 align-items: center; 17281 gap: 8px; 17282 } 17283 17284 .conversation-message { 17285 font-size: 14px; 17286 color: #667781; 17287 white-space: nowrap; 17288 overflow: hidden; 17289 text-overflow: ellipsis; 17290 margin: 0; 17291 flex: 1; 17292 } 17293 17294 .conversation-unread { 17295 background: #25D366; 17296 color: white; 17297 border-radius: 12px; 17298 padding: 2px 8px; 17299 font-size: 12px; 17300 font-weight: 600;
17301 min-width: 20px; 17302 text-align: center; 17303 flex-shrink: 0; 17304 } 17305 17306 /* Active item styling */ 17307 .chat-conversation-item.active .conversation-name { 17308 color: #F28C1A; 17309 } 17310 17311 .chat-conversation-item.active .conversation-time { 17312 color: #F28C1A; 17313 } 17314 17315 .chat-new-btn { 17316 margin: 12px; 17317 padding: 12px; 17318 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17319 color: white; 17320 border: none; 17321 border-radius: 8px; 17322 cursor: pointer; 17323 font-weight: 500; 17324 font-size: 14px; 17325 display: flex; 17326 align-items: center; 17327 justify-content: center; 17328 gap: 8px; 17329 transition: all 0.2s; 17330 flex-shrink: 0; 17331 } 17332 17333 .chat-new-btn:hover { 17334 transform: translateY(-1px); 17335 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17336 } 17337 17338 .chat-conversation-title { 17339 flex: 1; 17340 font-weight: 600; 17341 font-size: 16px; 17342 color: #1f2937; 17343 } 17344 17345 .chat-messages-container { 17346 flex: 1; 17347 overflow-y: auto; 17348 padding: 16px; 17349 display: flex; 17350 flex-direction: column; 17351 gap: 8px; 17352 min-height: 0; 17353 background: #E5DDD5; 17354 } 17355 17356 /* Custom Scrollbar for Messages Container */ 17357 .chat-messages-container::-webkit-scrollbar { 17358 width: 6px; 17359 } 17360 17361 .chat-messages-container::-webkit-scrollbar-track { 17362 background: rgba(0, 0, 0, 0.05); 17363 } 17364 17365 .chat-messages-container::-webkit-scrollbar-thumb { 17366 background: rgba(242, 140, 26, 0.6); 17367 border-radius: 3px; 17368 } 17369 17370 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17371 background: #F28C1A; 17372 } 17373 17374 /* Firefox */ 17375 .chat-messages-container { 17376 scrollbar-width: thin; 17377 scrollbar-color: rgba(242, 140, 26, 0.6) rgba(0, 0, 0, 0.05); 17378 } 17379 17380 /* WhatsApp Group-like Messages */ 17381 .chat-message { 17382 display: flex; 17383 margin-bottom: 4px; 17384 animation: fadeIn 0.2s ease; 17385 } 17386 17387 @keyframes fadeIn { 17388 from { opacity: 0; transform: translateY(10px); } 17389 to { opacity: 1; transform: translateY(0); } 17390 } 17391 17392 .chat-message.mine { 17393 justify-content: flex-end; 17394 } 17395 17396 .chat-message-bubble { 17397 max-width: 80%; 17398 padding: 6px 8px 8px 10px; 17399 border-radius: 8px; 17400 position: relative; 17401 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17402 } 17403 17404 /* Bubble from others (Admin/CS) */ 17405 .chat-message:not(.mine) .chat-message-bubble { 17406 background: #FFFFFF; 17407 border-radius: 0 8px 8px 8px; 17408 } 17409 17410 /* Bubble tail for others */ 17411 .chat-message:not(.mine) .chat-message-bubble::before { 17412 content: ''; 17413 position: absolute; 17414 top: 0; 17415 left: -8px; 17416 width: 0; 17417 height: 0; 17418 border-style: solid; 17419 border-width: 0 8px 13px 0; 17420 border-color: transparent #FFFFFF transparent transparent; 17421 } 17422 17423 /* Bubble from user (mine) */ 17424 .chat-message.mine .chat-message-bubble { 17425 background: #e4f5d0; /* Light blue for user messages */ 17426 border-radius: 8px 0 8px 8px; 17427 } 17428 17429 /* Bubble tail for mine */ 17430 .chat-message.mine .chat-message-bubble::before { 17431 content: ''; 17432 position: absolute; 17433 top: 0; 17434 right: -8px; 17435 width: 0; 17436 height: 0; 17437 border-style: solid; 17438 border-width: 0 0 13px 8px; 17439 border-color: transparent transparent transparent #eaf5d0; 17440 } 17441 17442 .message-sender { 17443 font-size: 12.5px; 17444 font-weight: 600; 17445 color: #F28C1A !important; 17446 margin-bottom: 4px;
17447 } 17448 17449 .chat-message.mine .message-sender { 17450 display: none; 17451 } 17452 17453 /* Message text */ 17454 .message-text { 17455 font-size: 14px; 17456 line-height: 19px; 17457 color: #ffffff !important; 17458 word-wrap: break-word; 17459 white-space: pre-wrap; 17460 margin-bottom: 4px; 17461 } 17462 17463 /* Message metadata (time + status) */ 17464 .message-meta { 17465 display: flex; 17466 align-items: center; 17467 justify-content: flex-end; 17468 gap: 4px; 17469 margin-top: 2px; 17470 } 17471 17472 .message-time { 17473 font-size: 11px; 17474 color: #728166 !important; 17475 line-height: 15px; 17476 } 17477 17478 /* Use multiple selectors to override global styles */ 17479 .chat-message-bubble .message-status, 17480 .message-meta .message-status, 17481 .chat-message .message-status { 17482 width: 16px !important; 17483 height: 16px !important; 17484 min-width: 16px !important; 17485 min-height: 16px !important; 17486 max-width: 16px !important; 17487 max-height: 16px !important; 17488 flex-shrink: 0; 17489 color: #728166 !important; 17490 fill: currentColor !important; 17491 } 17492 17493 .chat-message:not(.mine) .message-status { 17494 display: none; 17495 } 17496 17497 /* Loading dots animation */ 17498 @keyframes loadingDots { 17499 0%, 20% { content: '.'; } 17500 40% { content: '..'; } 17501 60%, 100% { content: '...'; } 17502 } 17503 17504 .message-status.loading::after { 17505 content: '...'; 17506 animation: loadingDots 1.5s infinite; 17507 font-size: 16px; 17508 line-height: 16px; 17509 } 17510 17511 .message-status.loading path { 17512 display: none; 17513 } 17514 17515 .message-status.failed { 17516 color: #ef4444 !important; 17517 cursor: pointer; 17518 } 17519 17520 .message-status.sent { 17521 color: #667781 !important; 17522 } 17523 17524 .chat-new-form { 17525 flex: 1; 17526 overflow-y: auto; 17527 padding: 16px; 17528 display: flex; 17529 flex-direction: column; 17530 } 17531 17532 .chat-new-form form { 17533 display: flex; 17534 flex-direction: column; 17535 gap: 16px; 17536 flex: 1; 17537 } 17538 17539 .chat-new-form .form-group { 17540 display: flex; 17541 flex-direction: column; 17542 gap: 6px; 17543 } 17544 17545 .chat-new-form label { 17546 font-size: 14px; 17547 font-weight: 500; 17548 color: #374151; 17549 } 17550 17551 .chat-new-form .form-control { 17552 width: 100%; 17553 padding: 10px 12px; 17554 border: 1px solid #d1d5db; 17555 border-radius: 8px; 17556 font-size: 14px; 17557 font-family: inherit; 17558 transition: border-color 0.2s; 17559 box-sizing: border-box; 17560 } 17561 17562 .chat-new-form .form-control:focus { 17563 outline: none; 17564 border-color: #F28C1A; 17565 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17566 } 17567 17568 .chat-new-form textarea.form-control { 17569 resize: vertical; 17570 min-height: 100px; 17571 } 17572 17573 .chat-new-form .btn { 17574 padding: 12px 20px; 17575 border: none; 17576 border-radius: 8px; 17577 font-size: 15px; 17578 font-weight: 500; 17579 cursor: pointer; 17580 transition: all 0.2s; 17581 text-align: center; 17582 } 17583 17584 .chat-new-form .btn-primary { 17585 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17586 color: white; 17587 } 17588 17589 .chat-new-form .btn-primary:hover { 17590 transform: translateY(-1px); 17591 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17592 } 17593 17594 .chat-new-form .btn-primary:active { 17595 transform: scale(0.98); 17596 } 17597 17598 .chat-new-form .btn-block { 17599 width: 100%; 17600 margin-top: auto; 17601 } 17602 17603 .chat-message-form { 17604 padding: 12px; 17605 border-top: 1px solid #e5e7eb; 17606 display: flex; 17607 flex-wrap: wrap; 17608 gap: 8px; 17609 align-items: flex-end; 17610 flex-shrink: 0; 17611 background: white; 17612 } 17613 17614 .chat-product-card { 17615 flex: 1 1 100%; 17616 max-width: 100%; 17617 display: flex; 17618 gap: 12px; 17619 padding: 12px; 17620 background: #f3f4f6; 17621 border-radius: 8px; 17622 margin-bottom: 4px;
17623 border: 1px solid #e5e7eb; 17624 } 17625 17626 .chat-product-card img { 17627 width: 60px; 17628 height: 60px; 17629 object-fit: cover; 17630 border-radius: 6px; 17631 flex-shrink: 0; 17632 } 17633 17634 .chat-product-card-content { 17635 flex: 1; 17636 min-width: 0; 17637 } 17638 17639 .chat-product-card-name { 17640 font-weight: 500; 17641 font-size: 14px; 17642 color: #1f2937; 17643 margin-bottom: 4px; 17644 white-space: nowrap; 17645 overflow: hidden; 17646 text-overflow: ellipsis; 17647 } 17648 17649 .chat-product-card-variant { 17650 font-size: 12px; 17651 color: #74806b; 17652 margin-bottom: 4px; 17653 } 17654 17655 .chat-product-card-price { 17656 font-size: 13px; 17657 color: #8bea66; 17658 font-weight: 600; 17659 } 17660 17661 .chat-product-card-remove { 17662 align-self: flex-start; 17663 background: none; 17664 border: none; 17665 color: #9ca3af; 17666 cursor: pointer; 17667 padding: 4px; 17668 font-size: 18px; 17669 line-height: 1; 17670 transition: color 0.2s; 17671 } 17672 17673 .chat-product-card-remove:hover { 17674 color: #ef4444; 17675 } 17676 17677 /* Product card in messages (read-only display) */ 17678 .message-product-card { 17679 display: flex; 17680 gap: 10px; 17681 padding: 10px; 17682 background: rgba(255, 255, 255, 0.5); 17683 border-radius: 8px; 17684 margin-bottom: 6px; 17685 border: 1px solid rgba(0, 0, 0, 0.08); 17686 align-items: center; 17687 } 17688 17689 .chat-message.mine .message-product-card { 17690 background: rgba(255, 255, 255, 0.7); 17691 } 17692 17693 .message-product-card img { 17694 width: 50px; 17695 height: 50px; 17696 object-fit: cover; 17697 border-radius: 6px; 17698 flex-shrink: 0; 17699 } 17700 17701 .message-product-info { 17702 flex: 1; 17703 min-width: 0; 17704 } 17705 17706 .message-product-name { 17707 font-weight: 500; 17708 font-size: 13px; 17709 color: #33371f; 17710 margin-bottom: 2px; 17711 overflow: hidden; 17712 text-overflow: ellipsis; 17713 display: -webkit-box; 17714 -webkit-line-clamp: 2; 17715 -webkit-box-orient: vertical; 17716 line-height: 1.3; 17717 } 17718 17719 .message-product-variant { 17720 font-size: 11px; 17721 color: #78806b; 17722 margin-bottom: 2px; 17723 } 17724 17725 .message-product-price { 17726 font-size: 12px; 17727 color: #c7ea66; 17728 font-weight: 600; 17729 } 17730 17731 .message-product-link { 17732 flex-shrink: 0; 17733 width: 32px; 17734 height: 32px; 17735 display: flex; 17736 align-items: center; 17737 justify-content: center; 17738 background: rgba(102, 126, 234, 0.1); 17739 border-radius: 6px; 17740 color: #c7ea66; 17741 text-decoration: none; 17742 transition: all 0.2s; 17743 } 17744 17745 .message-product-link:hover { 17746 background: #b7ea66; 17747 color: white; 17748 } 17749 17750 .message-product-link svg { 17751 width: 16px; 17752 height: 16px; 17753 } 17754 17755 .chat-message-input { 17756 flex: 1; 17757 border: 1px solid #d1d5db; 17758 border-radius: 20px; 17759 padding: 10px 16px; 17760 resize: none; 17761 max-height: 100px; 17762 font-family: inherit; 17763 font-size: 14px; 17764 line-height: 1.5; 17765 box-sizing: border-box; 17766 overflow-y: hidden; 17767 } 17768 17769 .chat-message-input:focus { 17770 outline: none; 17771 border-color: #F28C1A; 17772 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17773 } 17774 17775 .chat-send-btn { 17776 width: 40px; 17777 height: 40px; 17778 border-radius: 50%; 17779 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17780 border: none; 17781 color: white; 17782 cursor: pointer; 17783 flex-shrink: 0; 17784 display: flex; 17785 align-items: center; 17786 justify-content: center; 17787 transition: all 0.2s; 17788 } 17789 17790 .chat-send-btn:hover { 17791 transform: scale(1.05); 17792 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 17793 } 17794 17795 .chat-send-btn:active { 17796 transform: scale(0.95); 17797 } 17798 17799 .chat-loading { 17800 text-align: center; 17801 padding: 20px; 17802 color: #6b7280; 17803 } 17804 17805 /* Date Separator (WhatsApp-style) */ 17806 .date-separator { 17807 display: flex; 17808 align-items: center; 17809 gap: 12px; 17810 margin: 24px 0 16px; 17811 opacity: 0; 17812 animation: fadeInSeparator 0.3s ease forwards; 17813 } 17814 17815 @keyframes fadeInSeparator { 17816 from { 17817 opacity: 0; 17818 transform: translateY(-10px); 17819 } 17820 to { 17821 opacity: 1;
17822 transform: translateY(0); 17823 } 17824 } 17825 17826 .date-separator-line { 17827 flex: 1; 17828 height: 1px; 17829 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17830 } 17831 17832 .date-separator-text { 17833 flex-shrink: 0; 17834 padding: 4px 12px; 17835 background: #e9ecef; 17836 color: #6c757d; 17837 font-size: 11px; 17838 font-weight: 600; 17839 text-transform: uppercase; 17840 letter-spacing: 0.5px; 17841 border-radius: 12px; 17842 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17843 } 17844 17845 /* Mobile responsive */ 17846 @media (max-width: 640px) { 17847 .chat-popup { 17848 position: fixed !important; /* Override absolute positioning */ 17849 bottom: 45px !important; /* Above bottom nav */ 17850 right: 0 !important; 17851 left: 0 !important; 17852 width: 100% !important; 17853 max-width: 100% !important; 17854 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17855 height: calc(100dvh - 45px) !important; 17856 max-height: calc(100dvh - 45px) !important; 17857 /* Fallback for browsers that don't support dvh */ 17858 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17859 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17860 border-radius: 0 !important; 17861 z-index: 9999 !important; 17862 } 17863 17864 .chat-popup-header { 17865 border-radius: 0; 17866 } 17867 17868 .chat-messages-container, 17869 .chat-conversations-list { 17870 /* Adjust for mobile keyboard using dynamic viewport */ 17871 max-height: calc(100dvh - 190px); 17872 /* Fallback for browsers that don't support dvh */ 17873 max-height: calc(var(--viewport-height, 100vh) - 190px); 17874 } 17875 } 17876 </style> 17877 <!-- Chat Widget Button --> 17878 <div id="chat-widget" class="chat-widget" 17879 data-user-id="" 17880 data-is-logged-in="false" 17881 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17882 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17883 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17884 <path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/> 17885 </svg> 17886 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17887 </button> 17888 17889 <div id="chat-popup" class="chat-popup" style="display: none;"> 17890 <div class="chat-popup-header"> 17891 <div class="chat-header-title"> 17892 <h3>Bantuan</h3> 17893 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17894 </div> 17895 <div class="chat-header-actions"> 17896 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17897 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17898 <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/> 17899 </svg> 17900 </button> 17901 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">
17901Ã</button> 17902 </div> 17903 </div> 17904 17905 <!-- Notification Info Bar (conditional) --> 17906 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17907 <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffc107"> 17908 <path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/> 17909 </svg> 17910 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17911 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17912 </div> 17913 17914 <div class="chat-popup-body"> 17915 <!-- Conversations List View --> 17916 <div id="chat-list-view" class="chat-view"> 17917 <div id="chat-conversations-list" class="chat-conversations-list"> 17918 <div class="chat-loading">Memuat...</div> 17919 <!-- Conversation Item Structure (rendered by JS): 17920 <div class="chat-conversation-item"> 17921 <div class="conversation-avatar"> 17922 <svg>...</svg> 17923 </div> 17924 <div class="conversation-content"> 17925 <div class="conversation-header"> 17926 <span class="conversation-name">Admin Support</span> 17927 <span class="conversation-time">10:30</span> 17928 </div> 17929 <div class="conversation-preview"> 17930 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17931 <span class="conversation-unread">2</span> 17932 </div> 17933 </div> 17934 </div> 17935 --> 17936 </div> 17937 <button id="chat-new-btn" class="chat-new-btn"> 17938 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17939 <line x1="12" y1="5" x2="12" y2="19"></line> 17940 <line x1="5" y1="12" x2="19" y2="12"></line> 17941 </svg> 17942 Percakapan Baru 17943 </button> 17944 </div> 17945 17946 <!-- Chat Messages View --> 17947 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17948 <div id="chat-messages-container" class="chat-messages-container"> 17949 <div class="chat-loading">Memuat pesan...</div> 17950 <!-- Message Structure (rendered by JS): 17951 <div class="chat-message"> (or .chat-message.mine) 17952 <div class="chat-message-bubble"> 17953 <div class="message-sender">Admin Support</div> (only for non-mine) 17954 <div class="message-text">Halo, ada yang bisa dibantu?</div> 17955 <div class="message-meta"> 17956 <span class="message-time">10:30</span> 17957 <svg class="message-status">...</svg> (only for mine) 17958 </div> 17959 </div> 17960 </div> 17961 --> 17962 </div> 17963 17964 <form id="chat-message-form" class="chat-message-form"> 17965 <input type="hidden" id="chat-current-conversation-id" value=""> 17966 <textarea 17967 id="chat-message-input" 17968 class="chat-message-input" 17969 placeholder="Ketik pesan..." 17970 rows="1" 17971 maxlength="5000" 17972 ></textarea> 17973 <button type="submit" class="chat-send-btn" aria-label="Send message"> 17974 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17975 <line x1="22" y1="2" x2="11" y2="
1797513"></line> 17976 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 17977 </svg> 17978 </button> 17979 </form> 17980 </div> 17981 17982 <!-- New Chat View --> 17983 <div id="chat-new-view" class="chat-view" style="display: none;"> 17984 <div class="chat-messages-header"> 17985 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 17986 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17987 <path d="M19 12H5M12 19l-7-7 7-7"/> 17988 </svg> 17989 </button> 17990 <div class="chat-conversation-title">Percakapan Baru</div> 17991 </div> 17992 17993 <div class="chat-new-form"> 17994 <form id="chat-start-form"> 17995 <div class="form-group"> 17996 <label for="chat-new-subject">Subjek</label> 17997 <input 17998 type="text" 17999 id="chat-new-subject" 18000 class="form-control" 18001 placeholder="Bantuan Pelanggan" 18002 maxlength="255" 18003 > 18004 </div> 18005 <div class="form-group"> 18006 <label for="chat-new-message">Pesan</label> 18007 <textarea 18008 id="chat-new-message" 18009 class="form-control" 18010 rows="4" 18011 placeholder="Tulis pertanyaan Anda..." 18012 required 18013 maxlength="5000" 18014 ></textarea> 18015 </div> 18016 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18017 </form> 18018 </div> 18019 </div> 18020 </div> 18021 </div> 18022 </div> 18023 18024 <!-- Login Required Modal --> 18025 <div class="modal-overlay" id="chatLoginModal"> 18026 <div class="modal-dialog"> 18027 <div class="modal-header"> 18028 <h3 class="modal-title"> 18029 <div class="modal-icon info"> 18030 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18031 <path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"/> 18032 </svg> 18033 </div> 18034 Login Diperlukan 18035 </h3> 18036 </div> 18037 <div class="modal-body"> 18038 <p class="modal-message">Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18039 <div class="modal-actions"> 18040 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18041 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18042 </div> 18043 </div> 18044 </div> 18045 </div> 18046
18046<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18046<script data-inlined="chat-notification.js"> 18047/** 18048 * Customer Chat Push Notification Manager 18049 * Handles browser push notification subscription for customers 18050 * Version: 1.0.0 18051 * Date: 2026-01-19 18052 */ 18053 18054const ChatNotification = { 18055 // State 18056 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18057 subscription: null, 18058 vapidPublicKey: null, // Will be loaded from server 18059 18060 /** 18061 * Initialize notification manager 18062 */ 18063 async init() { 18064 // Check if user is logged in (only for logged-in users) 18065 if (!this.isUserLoggedIn()) { 18066 this.updateUI('not-supported'); 18067 return; 18068 } 18069 18070 // Check browser support 18071 if (!this.isSupported()) { 18072 this.updateUI('not-supported'); 18073 return; 18074 } 18075 18076 // Load VAPID key from server 18077 await this.loadVapidKey(); 18078 18079 if (!this.vapidPublicKey) { 18080 this.updateUI('not-supported'); 18081 return; 18082 } 18083 18084 // Register service worker if needed 18085 await this.registerServiceWorker(); 18086 18087 // Check current permission 18088 const permission = Notification.permission; 18089 18090 if (permission === 'denied') { 18091 this.updateUI('blocked'); 18092 } else if (permission === 'granted') { 18093 // Check if already subscribed 18094 const isSubscribed = await this.checkSubscriptionStatus(); 18095 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18096 } else { 18097 // Default: not subscribed yet 18098 this.updateUI('not-subscribed'); 18099 this.showInfoBar(); 18100 } 18101 }, 18102 18103 /** 18104 * Check if user is logged in 18105 */ 18106 isUserLoggedIn() { 18107 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18108 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18109 }, 18110 18111 /** 18112 * Check if browser supports push notifications 18113 */ 18114 isSupported() { 18115 return ( 18116 'serviceWorker' in navigator && 18117 'PushManager' in window && 18118 'Notification' in window 18119 ); 18120 }, 18121 18122 /** 18123 * Load VAPID public key from server 18124 */ 18125 async loadVapidKey() { 18126 try { 18127 const response = await fetch('/api/chat/notifications/vapid_key', { 18128 method: 'GET', 18129 headers: { 18130 'Accept': 'application/json' 18131 } 18132 }); 18133 18134 if (!response.ok) { 18135 return false; 18136 } 18137 18138 const data = await response.json(); 18139 if (data.success && data.data?.public_key) { 18140 this.vapidPublicKey = data.data.public_key; 18141 return true; 18142 } 18143 18144 return false; 18145 18146 } catch (error) { 18147 console.error('[ChatNotification] Error loading VAPID key:', error); 18148 return false; 18149 } 18150 }, 18151 18152 /** 18153 * Register service worker 18154 */ 18155 async registerServiceWorker() { 18156 if (!('serviceWorker' in navigator)) return null; 18157 18158 try { 18159 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18160 scope: '/' 18161 }); 18162 return registration; 18163 } catch (error) { 18164 return null; 18165 } 18166 }, 18167 18168 /** 18169 * Check subscription status from server 18170 */ 18171 async checkSubscriptionStatus() { 18172 try { 18173 const response = await fetch('/api/chat/notifications/status', { 18174 method: 'GET', 18175 credentials: 'include', 18176 headers: { 18177 'Accept': 'application/json' 18178 } 18179 }); 18180 18181 if (!response.ok) { 18182 return false; 18183 } 18184 18185 const data = await response.json(); 18186 return data.data?.subscribed || false; 18187 18188 } catch (error) { 18189 console.error('[ChatNotification] Error checking subscription:', error); 18190 return false; 18191 } 18192 }, 18193 18194 /** 18195 * Update UI based on status 18196 */ 18197 updateUI(status) { 18198 this.status = status; 18199 const btn = document.getElementById('chatNotifToggle'); 18200 18201 if (!btn) { 18202 // Retry after a short delay (button might not be in DOM yet) 18203 setTimeout(() => this.updateUI(status), 100); 18204 return; 18205 } 18206 18207 btn.setAttribute('data-status', status); 18208 18209 // Show/hide button based on support 18210 if (status === 'not-supported') { 18211 btn.style.display = 'none'; 18212 } else { 18213 btn.style.display = 'block'; 18214 } 18215 18216 // Update SVG icon (bell vs bell-slash) 18217 const svg = btn.querySelector('svg'); 18218 if (svg) { 18219 if (status === 'subscribed') { 18220 // Bell icon (active) 18221 svg.innerHTML = '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>'; 18222 } else { 18223 // Bell slash icon (inactive/blocked) 18224 svg.innerHTML = '<path d="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"/>'; 18225 } 18226 } 18227 18228 // Update tooltip 18229 const tooltips = { 18230 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18231 'subscribed': 'Notifikasi aktif', 18232 'blocked': 'Notifikasi diblokir'
18233 }; 18234 18235 if (tooltips[status]) { 18236 btn.title = tooltips[status]; 18237 } 18238 18239 // Hide info bar if subscribed or blocked 18240 if (status === 'subscribed' || status === 'blocked') { 18241 this.hideInfoBar(); 18242 } 18243 }, 18244 18245 /** 18246 * Show info bar (if not dismissed) 18247 */ 18248 showInfoBar() { 18249 if (localStorage.getItem('chat_notif_info_dismissed')) { 18250 return; 18251 } 18252 18253 const infoBar = document.getElementById('chatNotifInfoBar'); 18254 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18255 18256 if (infoBar) { 18257 infoBar.style.display = 'flex'; 18258 18259 // Bind dismiss button here so it's always attached when bar is visible 18260 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18261 if (btn && !btn._dismissBound) { 18262 btn._dismissBound = true; 18263 btn.addEventListener('click', () => { 18264 infoBar.style.display = 'none'; 18265 localStorage.setItem('chat_notif_info_dismissed', '1'); 18266 if (popupBody) popupBody.classList.remove('has-info-bar'); 18267 }); 18268 } 18269 } 18270 18271 if (popupBody) { 18272 popupBody.classList.add('has-info-bar'); 18273 } 18274 }, 18275 18276 /** 18277 * Hide info bar 18278 */ 18279 hideInfoBar() { 18280 const infoBar = document.getElementById('chatNotifInfoBar'); 18281 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18282 18283 if (infoBar) { 18284 infoBar.style.display = 'none'; 18285 } 18286 18287 if (popupBody) { 18288 popupBody.classList.remove('has-info-bar'); 18289 } 18290 }, 18291 18292 /** 18293 * Dismiss info bar permanently 18294 */ 18295 dismissInfo() { 18296 this.hideInfoBar(); 18297 localStorage.setItem('chat_notif_info_dismissed', '1'); 18298 }, 18299 18300 /** 18301 * Handle bell icon click 18302 */ 18303 async handleToggleClick() { 18304 if (this.status === 'not-subscribed') { 18305 await this.subscribe(); 18306 } else if (this.status === 'subscribed') { 18307 await this.unsubscribe(); 18308 } else if (this.status === 'blocked') { 18309 this.showUnblockHelp(); 18310 } 18311 }, 18312 18313 /** 18314 * Subscribe to push notifications 18315 */ 18316 async subscribe() { 18317 try { 18318 // Request permission 18319 const permission = await Notification.requestPermission(); 18320 18321 if (permission === 'denied') { 18322 this.updateUI('blocked'); 18323 this.showToast('Notifikasi diblokir', 'error'); 18324 return; 18325 } 18326 18327 if (permission !== 'granted') { 18328 return; 18329 } 18330 18331 // Get service worker registration 18332 const registration = await navigator.serviceWorker.ready; 18333 18334 // Subscribe to push 18335 const subscription = await registration.pushManager.subscribe({ 18336 userVisibleOnly: true, 18337 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18338 }); 18339 18340 // Send subscription to server 18341 const response = await fetch('/api/chat/notifications/subscribe', { 18342 method: 'POST', 18343 credentials: 'include', 18344 headers: { 18345 'Content-Type': 'application/json', 18346 'Accept': 'application/json' 18347 }, 18348 body: JSON.stringify(subscription.toJSON()) 18349 }); 18350 18351 if (!response.ok) { 18352 throw new Error('Failed to save subscription'); 18353 } 18354 18355 const result = await response.json(); 18356 18357 this.subscription = subscription; 18358 this.updateUI('subscribed'); 18359 this.hideInfoBar(); 18360 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18361 18362 // Send test notification 18363 setTimeout(() => this.sendTestNotification(), 1000); 18364 18365 } catch (error) { 18366 console.error('[ChatNotification] Subscribe error:', error); 18367 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18368 } 18369 }, 18370 18371 /** 18372 * Unsubscribe from push notifications 18373 */ 18374 async unsubscribe() { 18375 try { 18376 // Confirm first 18377 if (!confirm('Nonaktifkan notifikasi chat?')) { 18378 return; 18379 } 18380 18381 // Get current subscription 18382 const registration = await navigator.serviceWorker.ready; 18383 const subscription = await registration.pushManager.getSubscription(); 18384 18385 if (subscription) { 18386 // Unsubscribe from browser 18387 await subscription.unsubscribe(); 18388 18389 // Tell server 18390 await fetch('/api/chat/notifications/unsubscribe', { 18391 method: 'POST', 18392 credentials: 'include', 18393 headers: { 18394 'Content-Type': 'application/json', 18395 'Accept': 'application/json' 18396 }, 18397 body: JSON.stringify(subscription.toJSON()) 18398 }); 18399 } 18400 18401 this.subscription = null; 18402 this.updateUI('not-subscribed'); 18403 this.showInfoBar(); 18404 this.showToast('Notifikasi dinonaktifkan', 'info'); 18405 18406 } catch (error) { 18407 console.error('[ChatNotification] Unsubscribe error:', error); 18408 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18409 } 18410 }, 18411 18412 /** 18413 * Send test notification 18414 */ 18415 async sendTestNotification() { 18416 try { 18417 const response = await fetch('/api/chat/notifications/send_test', { 18418 method: 'POST', 18419 credentials: 'include', 18420 headers: { 18421 'Accept': 'application/json' 18422 } 18423 }); 18424 18425 if (response.ok) { 18426 // Test notification sent successfully 18427 } 18428 } catch (error) { 18429 // Test notification failed silently 18430 } 18431 }, 18432 18433 /** 18434 * Show unblock help modal/message 18435 */ 18436 showUnblockHelp() { 18437 const message = ` 18438 Notifikasi diblokir oleh browser.nn 18439 Untuk mengaktifkan:n 18440 1. Klik ikon gembok/info di address barn 18441 2. Cari "Notifications" atau "Notifikasi"n 18442 3. Ubah menjadi "Allow" atau "Izinkan"n 18443 4. Refresh halaman ini 18444 `.trim().replace(/s+/g, ' '); 18445 18446 alert(message); 18447 }, 18448 18449 /** 18450 * Show toast notification 18451 */ 18452 showToast(message, type = 'info') { 18453 // Simple toast implementation 18454 const toast = document.createElement('div'); 18455 toast.className = `chat-toast chat-toast-${type}`; 18456 toast.textContent = message; 18457 toast.style.cssText = ` 18458 position: fixed; 18459 bottom: 80px; 18460 right: 20px; 18461 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; 18462 color: white; 18463 padding: 12px 20px; 18464 border-radius: 4px; 18465 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18466 z-index: 10000; 18467 animation: slideInUp 0.3s ease; 18468 `; 18469 18470 document.body.appendChild(toast); 18471 18472 setTimeout(() => { 18473 toast.style.animation = 'slideOutDown 0.3s ease'; 18474 setTimeout(() => toast.remove(), 300); 18475 }, 3000); 18476 }, 18477 18478 /** 18479 * Convert VAPID key to Uint8Array 18480 */ 18481 urlBase64ToUint8Array(base64String) { 18482 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18483 const base64 = (base64String + padding) 18484 .replace(/-/g, '+') 18485 .replace(/_/g, '/'); 18486 18487 const rawData = window.atob(base64); 18488 const outputArray = new Uint8Array(rawData.length); 18489 18490 for (let i = 0; i < rawData.length; ++i) { 18491 outputArray[i] = rawData.charCodeAt(i); 18492 } 18493 return outputArray; 18494 }, 18495 18496 /** 18497 * Check if push notification is currently active 18498 * @returns {Promise<boolean>} 18499 */ 18500 async isPushActive() { 18501 try { 18502 // Check browser subscription 18503 if (!('serviceWorker' in navigator)) return false; 18504 18505 const registration = await navigator.serviceWorker.getRegistration(); 18506 if (!registration) return false; 18507 18508 const subscription = await registration.pushManager.getSubscription(); 18509 if (!subscription) return false; 18510 18511 // Also check server status 18512 const serverStatus = await this.checkSubscriptionStatus();
18513 18514 return serverStatus; 18515 } catch (error) { 18516 return false; 18517 } 18518 }, 18519 18520 /** 18521 * Show logout confirmation if push notification is active 18522 * Call this before logout action 18523 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18524 */ 18525 async confirmLogoutWithPushNotification() { 18526 const isActive = await this.isPushActive(); 18527 18528 if (!isActive) { 18529 // No active push, proceed with logout 18530 return true; 18531 } 18532 18533 // Show modal confirmation using modal.css 18534 return new Promise((resolve) => { 18535 // Create modal HTML 18536 const modalHTML = ` 18537 <div class="modal-overlay" id="logoutConfirmModal"> 18538 <div class="modal-dialog"> 18539 <div class="modal-header"> 18540 <h3 class="modal-title"> 18541 <span class="modal-icon warning">â ï¸</span> 18542 Konfirmasi Logout 18543 </h3> 18544 </div> 18545 <div class="modal-body"> 18546 <p class="modal-message"> 18547 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18548 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18549 Apakah Anda yakin ingin melanjutkan logout? 18550 </p> 18551 </div> 18552 <div class="modal-actions" style="padding: 0 20px 20px;"> 18553 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18554 Batal 18555 </button> 18556 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18557 Lanjutkan Logout 18558 </button> 18559 </div> 18560 </div> 18561 </div> 18562 `; 18563 18564 // Append to body 18565 const modalContainer = document.createElement('div'); 18566 modalContainer.innerHTML = modalHTML; 18567 document.body.appendChild(modalContainer.firstElementChild); 18568 18569 const modal = document.getElementById('logoutConfirmModal'); 18570 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18571 const cancelBtn = document.getElementById('logoutCancelBtn'); 18572 18573 // Show modal with animation 18574 requestAnimationFrame(() => { 18575 modal.classList.add('show'); 18576 }); 18577 18578 // Handle confirm 18579 const handleConfirm = () => { 18580 cleanup(); 18581 resolve(true); 18582 }; 18583 18584 // Handle cancel 18585 const handleCancel = () => { 18586 cleanup(); 18587 resolve(false); 18588 }; 18589 18590 // Cleanup function 18591 const cleanup = () => { 18592 modal.classList.remove('show'); 18593 setTimeout(() => { 18594 modal.remove(); 18595 }, 200); 18596 confirmBtn.removeEventListener('click', handleConfirm); 18597 cancelBtn.removeEventListener('click', handleCancel); 18598 modal.removeEventListener('click', handleBackdropClick); 18599 }; 18600 18601 // Handle backdrop click 18602 const handleBackdropClick = (e) => { 18603 if (e.target === modal) { 18604 handleCancel(); 18605 } 18606 }; 18607 18608 // Add event listeners 18609 confirmBtn.addEventListener('click', handleConfirm); 18610 cancelBtn.addEventListener('click', handleCancel); 18611 modal.addEventListener('click', handleBackdropClick); 18612 }); 18613 } 18614}; 18615 18616// Add CSS animations 18617const chatNotifStyle = document.createElement('style'); 18618chatNotifStyle.textContent = ` 18619 @keyframes slideInUp { 18620 from { 18621 transform: translateY(100%); 18622 opacity: 0; 18623 } 18624 to { 18625 transform: translateY(0); 18626 opacity: 1;
18627 } 18628 } 18629 18630 @keyframes slideOutDown { 18631 from { 18632 transform: translateY(0); 18633 opacity: 1; 18634 } 18635 to { 18636 transform: translateY(100%); 18637 opacity: 0; 18638 } 18639 } 18640`; 18641document.head.appendChild(chatNotifStyle); 18642 18643// Auto-initialize when DOM is ready 18644if (document.readyState === 'loading') { 18645 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18646} else { 18647 ChatNotification.init(); 18648} 18649 18650// Expose to window for access from other scripts 18651window.ChatNotification = ChatNotification; 18652 18653</script>
18653 18654 <!-- Footer Collapse Toggle Script --> 18655
18655<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18656 document.addEventListener('DOMContentLoaded', function() { 18657 const ftTop = document.getElementById('ftTop'); 18658 const expandBtn = document.getElementById('ftExpandBtn'); 18659 18660 if (ftTop && expandBtn) { 18661 const expandText = expandBtn.querySelector('.expand-text'); 18662 18663 expandBtn.addEventListener('click', function() { 18664 ftTop.classList.toggle('expanded'); 18665 const isExpanded = ftTop.classList.contains('expanded'); 18666 if (expandText) { 18667 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18668 } 18669 }); 18670 } 18671 }); 18672 </script>
18672 18673 </footer> 18674 18675<!-- Compare Sidebar Overlay --> 18676<div class="compare-overlay" id="compareOverlay"></div> 18677 18678<!-- Floating Compare Badge (Sticky) --> 18679<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18680 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18681 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18682 </svg> 18683 <span class="cfb-text">Bandingkan</span> 18684 <span class="cfb-count" id="cfbCount">0</span> 18685</button> 18686 18687<!-- AI Comparison Modal --> 18688<div class="ai-compare-modal" id="aiCompareModal"> 18689 <div class="modal-backdrop"></div> 18690 <div class="modal-container"> 18691 <div class="modal-header"> 18692 <h3 class="modal-title"> 18693 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18694 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/> 18695 </svg> 18696 Analisis AI: Perbandingan Panduan Shio 18697 </h3> 18698 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18699 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18700 <path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7a1 1 0 1 0-1.4 1.4L10.6 12l-4.9 4.9a1 1 0 0 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z"/> 18701 </svg> 18702 </button> 18703 </div> 18704 18705 <div class="modal-content"> 18706 <!-- Loading State --> 18707 <div class="ai-loading" id="aiLoading"> 18708 <div class="loading-animation"> 18709 <div class="typing-indicator"> 18710 <span></span> 18711 <span></span> 18712 <span></span> 18713 </div> 18714 <p>AI sedang membandingkan panduan shio...</p> 18715 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18716 </div> 18717 </div> 18718 18719 <!-- Error State --> 18720 <div class="ai-error" id="aiError" hidden> 18721 <div class="error-icon"> 18722 <svg width="48" height="48" viewBox="0 0 24 24" fill="#ef4444"> 18723 <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/> 18724 </svg> 18725 </div> 18726 <h4>Analisis AI Gagal</h4> 18727 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18728 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18729 </div> 18730 18731 <!-- Analysis Results --> 18732 <div class="ai-results" id="aiResults" hidden> 18733 <!-- Summary Section --> 18734 <div class="ai-summary-card"> 18735 <h4> 18736 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18737 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"/> 18738 </svg> 18739 Ringkasan Analisis 18740 </h4> 18741 <p id="aiSummary" class="summary-text"></p> 18742 <div class="winner-badge" id="winnerBadge"> 18743 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18744 <small id="winnerReason"></small> 18745 </div> 18746 </div> 18747 18748 <!-- Comparison Table --> 18749 <div class="ai-comparison-table"> 18750 <h4> 18751 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18752 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18753 </svg> 18754 Perbandingan Detail 18755 </h4> 18756 <div id="comparisonTable" class="comparison-grid"></div> 18757 </div> 18758 18759 <!-- Detailed Analysis --> 18760 <div class="ai-detailed-analysis"> 18761 <h4> 18762 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18763 <path d="M14,2A8,8 0 0,1 22,10A8,8 0 0,1 14,18A8,8 0 0,1 6,10A8,8 0 0,1 14,2M14,4A6,6 0 0,0 8,10A6,6 0 0,0 14,16A6,6 0 0,0 20,10A6,6 0 0,0 14,4M14,6A4,4 0 0,1 18,10A4,4 0 0,1 14,14A4,4 0 0,1 10,10A4,4 0 0,1 14,6M14,8A2,2 0 0,0 12,10A2,2 0 0,0 14,12A2,2 0 0,0 16,10A2,2 0 0,0 14,8Z"/> 18764 </svg> 18765 Analisis Mendalam 18766 </h4> 18767 <div id="detailedAnalysis" class="products-analysis"></div> 18768 </div> 18769 18770 <!-- Recommendations --> 18771 <div class="ai-recommendations"> 18772 <h4> 18773 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18774 <path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/> 18775 </svg> 18776 Rekomendasi Berdasarkan Kebutuhan 18777 </h4> 18778 <div class="recommendations-grid"> 18779 <div class="recommendation-card"> 18780 <h5>ð° Budget Conscious</h5> 18781 <p id="budgetRecommendation"></p> 18782 </div> 18783 <div class="recommendation-card"> 18784 <h5>â¡ Performance Focused</h5> 18785 <p id="performanceRecommendation"></p> 18786 </div> 18787 <div class="recommendation-card"> 18788 <h5>âï¸ Balanced Choice</h5> 18789 <p id="balancedRecommendation"></p> 18790 </div> 18791 </div> 18792 </div> 18793 18794 <!-- Buying Tips --> 18795 <div class="ai-tips"> 18796 <h4> 18797 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18798 <path d="M11,18H13V16H11V18M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6
18798M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3Z"/> 18799 </svg> 18800 Tips Pembelian 18801 </h4> 18802 <ul id="buyingTips" class="tips-list"></ul> 18803 </div> 18804 </div> 18805 </div> 18806 </div> 18807</div> 18808 18809<!-- Cart Login Modal --> 18810<div class="modal-overlay" id="cartLoginModal"> 18811 <div class="modal-dialog"> 18812 <div class="modal-header"> 18813 <h3 class="modal-title"> 18814 <div class="modal-icon info"> 18815 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18816 <path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/> 18817 </svg> 18818 </div> 18819 Login untuk Melanjutkan 18820 </h3> 18821 </div> 18822 <div class="modal-body"> 18823 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk menyimpan catatan dan panduan shio Anda.</p> 18824 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18825 <p style="font-size: 14px; color: #646947; margin: 0; line-height: 1.6;"> 18826 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18827 â Simpan catatan shio Anda<br> 18828 â Lacak status pesanan secara real-time<br> 18829 â Akses catatan lebih cepat dengan data tersimpan<br> 18830 â Dapatkan notifikasi promo & diskon eksklusif 18831 </p> 18832 </div> 18833 <div class="modal-actions"> 18834 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18835 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #ff6b35; border-color: #ff6b35;">Login</button> 18836 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #f7931e; border-color: #f7931e;">Daftar</button> 18837 </div> 18838 </div> 18839 </div> 18840</div> 18841 18842 18843
18844<script data-inlined="app.js"> 18845(function initLiveSearch(){ 18846 function setup() { 18847 const searchInput = document.getElementById('searchInput'); 18848 const suggest = document.getElementById('searchSuggest'); 18849 const searchBtn = document.getElementById('searchBtn'); 18850 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18851 18852 if (!searchInput || !suggest || !aiToggleBtn) { 18853 console.log('[LiveSearch] Elemen belum ada, retry...'); 18854 return false; 18855 } 18856 18857 let debounceTimer = null; 18858 let lastQuery = ''; 18859 let currentIndex = -1; 18860 let currentAbortController = null; 18861 let isAIModeEnabled = false; // AI mode OFF by default 18862 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18863 18864 // Trending cache 18865 let trendingCache = null; 18866 let trendingLoading = false; 18867 18868 // Trending products cache 18869 let trendingProductsCache = null; 18870 let trendingProductsLoading = false; 18871 18872 const MIN_LEN = 2; 18873 const DEBOUNCE = 300; 18874 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18875 18876 // Load AI mode preference from localStorage 18877 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18878 if (savedAIMode === 'enabled') { 18879 isAIModeEnabled = false; 18880 aiToggleBtn.classList.remove('active'); 18881 } 18882 18883 // Update placeholder based on AI mode 18884 function updatePlaceholder() { 18885 if (isAIModeEnabled) { 18886 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18887 } else { 18888 // Static placeholder for typing animation 18889 searchInput.placeholder = ''; 18890 } 18891 } 18892 18893 // Typing animation for placeholder 18894 let typingTimeout = null; 18895 let currentProductIndex = 0; 18896 let currentCharIndex = 0; 18897 let isTyping = false; 18898 let isDeleting = false; 18899 18900 const popularProducts = [ 18901 'Oppo A6', 18902 'Asus Ryzen 3', 18903 'Starlink V4', 18904 'HP Victus', 18905 'BOYTOTO Shio 4D', 18906 'ROG Strix', 18907 'iPhone 13', 18908 'Xpad 20', 18909 'Xiaomi 14', 18910 'Epson L3210', 18911 'Realme C85', 18912 'Brother T720', 18913 'Mouse Logitech' 18914 ]; 18915 18916 function typeWriter() { 18917 if (isAIModeEnabled || document.activeElement === searchInput) { 18918 return; // Skip typing if AI mode is on or input is focused 18919 } 18920 18921 const currentProduct = popularProducts[currentProductIndex]; 18922 18923 if (!isDeleting) { 18924 // Typing forward 18925 if (currentCharIndex < currentProduct.length) { 18926 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18927 currentCharIndex++; 18928 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18929 } else { 18930 // Finished typing, wait before deleting 18931 typingTimeout = setTimeout(() => { 18932 isDeleting = true; 18933 typeWriter(); 18934 }, 2000); // Pause before deleting 18935 } 18936 } else { 18937 // Deleting backward 18938 if (currentCharIndex > 0) { 18939 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18940 currentCharIndex--; 18941 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18942 } else { 18943 // Finished deleting, move to next product 18944 isDeleting = false; 18945 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18946 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18947 } 18948 } 18949 } 18950 18951 function startTypingAnimation() { 18952 if (!isAIModeEnabled && !typingTimeout) { 18953 currentCharIndex = 0; 18954 isDeleting = false; 18955 typeWriter(); 18956 } 18957 } 18958 18959 function stopTypingAnimation() { 18960 if (typingTimeout) { 18961 clearTimeout(typingTimeout); 18962 typingTimeout = null; 18963 } 18964 } 18965 18966 // Set initial placeholder 18967 updatePlaceholder(); 18968 18969 // Start typing animation for normal mode 18970 if (!isAIModeEnabled) { 18971 startTypingAnimation(); 18972 } 18973 18974 function esc(str){ 18975 if (str === null || str === undefined) return ''; 18976 return String(str) 18977 .replace(/&/g,'&') 18978 .replace(/</g,'<') 18979 .replace(/>/g,'>') 18980 .replace(/"/g,'"') 18981 .replace(/'/g,'''); 18982 } 18983 18984 function clearSuggest(){ 18985 suggest.innerHTML = ''; 18986 suggest.hidden = true; 18987 suggest.classList.remove('open', 'trending-mode'); 18988 currentIndex = -1; 18989 } 18990 18991 // ==================== TRENDING FUNCTIONS ==================== 18992 18993 async function fetchTrending() { 18994 if (trendingCache) return trendingCache; 18995 if (trendingLoading) return null; 18996 18997 trendingLoading = true; 18998
18999 try { 19000 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 19001 const data = await res.json(); 19002 19003 if (data.ok && Array.isArray(data.data)) { 19004 trendingCache = data.data; 19005 console.log('[Trending] Loaded', trendingCache.length, 'items'); 19006 return trendingCache; 19007 } 19008 } catch (err) { 19009 console.error('[Trending] Fetch error:', err); 19010 } finally { 19011 trendingLoading = false; 19012 } 19013 19014 return null; 19015 } 19016 19017 async function fetchTrendingProducts() { 19018 if (trendingProductsCache) return trendingProductsCache; 19019 if (trendingProductsLoading) return null; 19020 19021 trendingProductsLoading = true; 19022 19023 try { 19024 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19025 const data = await res.json(); 19026 19027 if (data.ok && Array.isArray(data.data)) { 19028 trendingProductsCache = data.data; 19029 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19030 return trendingProductsCache; 19031 } 19032 } catch (err) { 19033 console.error('[Trending Products] Fetch error:', err); 19034 } finally { 19035 trendingProductsLoading = false; 19036 } 19037 19038 return null; 19039 } 19040 19041 function renderTrendingSuggestions(trends, products) { 19042 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19043 clearSuggest(); 19044 return; 19045 } 19046 19047 let html = `<div class="trending-container">`; 19048 19049 // Left column: Trending searches 19050 if (trends && trends.length > 0) { 19051 html += ` 19052 <div class="trending-column trending-searches"> 19053 <div class="trending-header"> 19054 <span class="trending-icon">ð¥</span> 19055 <span class="trending-title">Pencarian Trending</span> 19056 </div> 19057 `; 19058 19059 html += trends.map((t, idx) => { 19060 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19061 return ` 19062 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19063 <span class="trending-rank"> 19064 <img src="/assets/img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19065 </span> 19066 <span class="trending-query">${esc(t.query)}</span> 19067 ${categoryHint} 19068 </div> 19069 `; 19070 }).join(''); 19071 19072 html += `</div>`; 19073 } 19074 19075 // Right column: Trending products 19076 if (products && products.length > 0) { 19077 html += ` 19078 <div class="trending-column trending-products"> 19079 <div class="trending-header"> 19080 <span class="trending-icon">â</span> 19081 <span class="trending-title">Produk Populer</span> 19082 </div> 19083 `; 19084 19085 html += products.map((p, idx) => { 19086 return ` 19087 <div class="trending-product-item" data-product-id="${esc(p.id)}" data-slug="${esc(p.slug)}" data-query="${esc(p.top_query || '')}" data-index="${idx}"> 19088 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19089 <div class="trending-product-info"> 19090 <div class="trending-product-name">${esc(p.name)}</div> 19091 <div class="trending-product-price">${esc(p.price_label)}</div> 19092 </div> 19093 </div> 19094 `; 19095 }).join(''); 19096 19097 html += `</div>`; 19098 } 19099 19100 html += `</div>`; 19101 19102 suggest.innerHTML = html; 19103 suggest.hidden = false; 19104 suggest.classList.add('open', 'trending-mode'); 19105 suggest.style.display = 'block'; 19106 19107 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19108 } 19109 19110 function trackSearchClick(query, productId) { 19111 fetch('/api/track_search_click.php', { 19112 method: 'POST', 19113 headers: { 'Content-Type': 'application/json' }, 19114 body: JSON.stringify({ 19115 query: query, 19116 product_id: productId,
19117 click_type: 'suggestion' 19118 }) 19119 }).catch(() => {}); // silent fail 19120 } 19121 19122 function trackTrendingClick(query) { 19123 fetch('/api/track_search_click.php', { 19124 method: 'POST', 19125 headers: { 'Content-Type': 'application/json' }, 19126 body: JSON.stringify({ 19127 query: query, 19128 click_type: 'trending' 19129 }) 19130 }).catch(() => {}); 19131 } 19132 19133 function trackTrendingProductClick(productId, query) { 19134 fetch('/api/track_search_click.php', { 19135 method: 'POST', 19136 headers: { 'Content-Type': 'application/json' }, 19137 body: JSON.stringify({ 19138 query: query || 'trending_product', 19139 product_id: productId, 19140 click_type: 'trending_product' 19141 }) 19142 }).catch(() => {}); 19143 } 19144 19145 function trackSearchSubmit(query) { 19146 fetch('/api/track_search_click.php', { 19147 method: 'POST', 19148 headers: { 'Content-Type': 'application/json' }, 19149 body: JSON.stringify({ 19150 query: query, 19151 click_type: 'submit' 19152 }) 19153 }).catch(() => {}); 19154 } 19155 19156 function logFinalSearch(query) { 19157 // Log the final search to analytics (not during typing) 19158 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19159 headers: { 'Accept': 'application/json' } 19160 }).catch(() => {}); 19161 } 19162 19163 // ==================== END TRENDING FUNCTIONS ==================== 19164 19165 function clearSuggest(){ 19166 suggest.innerHTML = ''; 19167 suggest.hidden = true; 19168 suggest.classList.remove('open', 'trending-mode'); 19169 currentIndex = -1; 19170 } 19171 19172 // AI Toggle Handler 19173 aiToggleBtn.addEventListener('click', (e) => { 19174 e.preventDefault(); 19175 e.stopPropagation(); 19176 19177 isAIModeEnabled = !isAIModeEnabled; 19178 19179 if (isAIModeEnabled) { 19180 aiToggleBtn.classList.add('active'); 19181 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19182 updatePlaceholder(); // Update placeholder text 19183 stopTypingAnimation(); // Stop typing animation in AI mode 19184 19185 // Show AI prompt if there's a query 19186 const query = searchInput.value.trim(); 19187 if (query.length >= MIN_LEN) { 19188 showAISearchPrompt(query); 19189 } else { 19190 showAIActivationMessage(); 19191 } 19192 } else { 19193 aiToggleBtn.classList.remove('active'); 19194 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19195 updatePlaceholder(); // Update placeholder text 19196 startTypingAnimation(); // Start typing animation in normal mode 19197 clearSuggest(); 19198 19199 // Switch to normal search if there's a query 19200 const query = searchInput.value.trim(); 19201 if (query.length >= MIN_LEN) { 19202 performSearch(query); 19203 } 19204 } 19205 }); 19206 19207 function showAIActivationMessage() { 19208 suggest.innerHTML = ` 19209 <div class="ai-progress-container"> 19210 <div class="ai-progress-header"> 19211 <div class="ai-progress-icon">â¨</div> 19212 <div class="ai-progress-text"> 19213 <div class="ai-progress-title">AI Search Mode Activated!</div> 19214 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19215 </div> 19216 </div> 19217 </div> 19218 `; 19219 suggest.hidden = false; 19220 suggest.classList.add('open'); 19221 suggest.style.display = 'block'; 19222 } 19223 19224 function renderAIInsights(insights){ 19225 if (!insights) return ''; 19226 19227 // Generate AI summary via backend endpoint 19228 const summary = insights.ai_summary || generateAISummary(insights); 19229 19230 if (!summary) return ''; 19231 19232 return ` 19233 <div class="ai-result-banner"> 19234 <div class="ai-result-icon">â¨</div> 19235 <div class="ai-result-text"> 19236 <div class="ai-result-title">AI Assistant</div> 19237 <div class="ai-result-description">${summary}</div> 19238 </div> 19239 </div> 19240 `; 19241 } 19242 19243 function generateAISummary(insights) { 19244 // Fallback client-side summary generation if backend doesn't provide ai_summary 19245 const productName = insights.product_name || ''; 19246 const categoryPath = insights.category_path || ''; 19247 const priceRange = insights.price_range || {}; 19248 const intent = insights.intent || ''; 19249 19250 let summary = ''; 19251 19252 // Parse category for natural display 19253 let categoryDisplay = ''; 19254 if (categoryPath) { 19255 const categoryParts = categoryPath.split('/');
19256 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19257 } 19258 19259 // Intent-based opening 19260 if (intent === 'cek_stok') { 19261 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19262 } else if (intent === 'rekomendasi_produk') { 19263 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19264 } else { 19265 // Default: cari_harga or general 19266 if (productName && categoryDisplay) { 19267 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19268 } else if (productName) { 19269 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19270 } else if (categoryDisplay) { 19271 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19272 } 19273 19274 // Add price context 19275 if (priceRange.min > 0 && priceRange.max > 0) { 19276 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19277 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19278 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19279 } else if (priceRange.min > 0) { 19280 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19281 summary += ` dengan harga mulai ${minPrice}`; 19282 } else if (priceRange.max > 0) { 19283 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19284 summary += ` dengan harga maksimal ${maxPrice}`; 19285 } 19286 19287 summary += ' yang sesuai dengan kebutuhan Anda.'; 19288 } 19289 19290 return summary || esc(insights.explanation || ''); 19291 } 19292 19293 function showAIProgressSteps() { 19294 return ` 19295 <div class="ai-progress-container"> 19296 <div class="ai-progress-header"> 19297 <div class="ai-progress-icon">ð¤</div> 19298 <div class="ai-progress-text"> 19299 <div class="ai-progress-title">AI sedang menganalisis...</div> 19300 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19301 </div> 19302 </div> 19303 <div class="ai-progress-steps"> 19304 <div class="ai-progress-step active"> 19305 <div class="ai-step-icon">1</div> 19306 <div class="ai-step-text">Memahami maksud pencarian...</div> 19307 </div> 19308 <div class="ai-progress-step pending"> 19309 <div class="ai-step-icon">2</div> 19310 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19311 </div> 19312 <div class="ai-progress-step pending"> 19313 <div class="ai-step-icon">3</div> 19314 <div class="ai-step-text">Mencari produk yang relevan...</div> 19315 </div> 19316 </div> 19317 </div> 19318 `; 19319 } 19320 19321 function updateAIProgressStep(step) { 19322 const steps = suggest.querySelectorAll('.ai-progress-step'); 19323 19324 steps.forEach((stepEl, idx) => { 19325 const icon = stepEl.querySelector('.ai-step-icon'); 19326 19327 if (idx < step) { 19328 stepEl.classList.remove('active', 'pending'); 19329 stepEl.classList.add('completed'); 19330 icon.textContent = 'â'; 19331 } else if (idx === step) { 19332 stepEl.classList.remove('pending', 'completed'); 19333 stepEl.classList.add('active'); 19334 } 19335 }); 19336 } 19337
19338 function renderSuggest(items, aiInsights){ 19339 if (!items.length){ 19340 clearSuggest(); 19341 return; 19342 } 19343 19344 let html = ''; 19345 19346 // Show AI insights if available 19347 if (aiInsights && aiInsights.explanation) { 19348 html += renderAIInsights(aiInsights); 19349 } 19350 19351 // Section header for products with hint 19352 html += ` 19353 <div class="suggest-section-header"> 19354 <div class="suggest-header-main"> 19355 <span class="suggest-header-icon">ð¦</span> 19356 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19357 </div> 19358 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19359 </div> 19360 `; 19361 19362 // Render product items 19363 html += items.map((it, idx)=>{ 19364 // Normalisasi & fallback 19365 const id = it.id != null ? it.id : ''; 19366 const name = esc(it.name); 19367 const slug = esc(it.slug != null ? it.slug : it.id); 19368 const img = esc(it.image || '/assets/img/no-image.png'); 19369 const price = esc(it.price_label || ''); 19370 19371 const url = '/product/' + encodeURIComponent(slug); 19372 19373 return ` 19374 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19375 <img src="${img}" alt="${name}" class="suggest-thumb"> 19376 <div class="suggest-meta"> 19377 <div class="suggest-name">${name}</div> 19378 <div class="suggest-price">${price}</div> 19379 </div> 19380 </div> 19381 `; 19382 }).join(''); 19383 19384 // Footer hint for more results - only show if 10 items (means there might be more) 19385 if (items.length >= 10) { 19386 html += ` 19387 <div class="suggest-footer-hint"> 19388 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19389 <path d="M9.29 6.71a.996.996 0 0 0 0 1.41L13.17 12l-3.88 3.88a.996.996 0 1 0 1.41 1.41l4.59-4.59a.996.996 0 0 0 0-1.41L10.7 6.7c-.38-.39-1.02-.39-1.41.01z"/> 19390 </svg> 19391 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19392 </div> 19393 `; 19394 } 19395 19396 suggest.innerHTML = html; 19397 suggest.hidden = false; 19398 suggest.classList.add('open'); 19399 suggest.style.display = 'block'; 19400 suggest.style.visibility = 'visible'; 19401 suggest.style.opacity = '1'; 19402 } 19403 19404 function fetchRemote(q, shouldLog = false){ 19405 // Cancel previous request if any 19406 if (currentAbortController) { 19407 currentAbortController.abort(); 19408 } 19409 currentAbortController = new AbortController(); 19410 19411 // Always use standard search for normal mode 19412 // Only add log=1 parameter when shouldLog is true (intentional search) 19413 const logParam = shouldLog ? '&log=1' : ''; 19414 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19415 const fetchOptions = { 19416 headers: {'Accept':'application/json'}, 19417 signal: currentAbortController.signal 19418 }; 19419 19420 return fetch(url, fetchOptions) 19421 .then(r=>r.json()) 19422 .then(j=>{ 19423 if (j.ok && Array.isArray(j.data)) { 19424 return j; 19425 } 19426 return { ok: false, data: [] }; 19427 }) 19428 .catch(err=>{ 19429 if (err.name === 'AbortError') { 19430 return { ok: false, data: [] }; 19431 } 19432 return { ok:false, data:[] }; 19433 }); 19434 } 19435 19436 function showNoResults(q, aiInsights) { 19437 suggest.innerHTML = ` 19438 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19439 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19440 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19441 Tidak ada hasil untuk "${esc(q)}" 19442 </div> 19443 ${aiInsights && aiInsights.explanation ? ` 19444 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19445 <strong style="color: #667eea;">ð¤ AI memahami:</strong><br> 19446 ${esc(aiInsights.explanation)} 19447 </div> 19448 ` : ''} 19449 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19450 Coba kata kunci lain atau hubungi kami untuk bantuan 19451 </div> 19452 </div> 19453 `; 19454 suggest.hidden = false; 19455 suggest.classList.add('open'); 19456 suggest.style.display = 'block'; 19457 } 19458 19459 function showErrorMessage() { 19460 suggest.innerHTML = ` 19461 <div style="padding: 32px 24px; text-align: center; color: #dc2626;"> 19462 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19463 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19464 Pencarian tidak tersedia 19465 </div> 19466 <div style="font-size: 12px; opacity: 0.8;">
19467 Silakan coba lagi dalam beberapa saat 19468 </div> 19469 </div> 19470 `; 19471 suggest.hidden = false; 19472 suggest.classList.add('open'); 19473 suggest.style.display = 'block'; 19474 } 19475 19476 function showAIErrorState(q) { 19477 suggest.innerHTML = ` 19478 <div class="ai-progress-container"> 19479 <div class="ai-progress-header"> 19480 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);">â¡</div> 19481 <div class="ai-progress-text"> 19482 <div class="ai-progress-title">AI sedang memproses...</div> 19483 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19484 </div> 19485 </div> 19486 <div style="padding: 16px; margin-top: 12px; background: #fef3c7; border-radius: 8px; border-left: 3px solid #f59e0b;"> 19487 <div style="font-size: 13px; color: #92400e; line-height: 1.6;"> 19488 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19489 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19490 Mohon tunggu sebentar... 19491 </div> 19492 </div> 19493 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19494 <button onclick="location.reload()" style="padding: 8px 16px; background: #fb923c; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;"> 19495 ð Refresh & Coba Lagi 19496 </button> 19497 </div> 19498 </div> 19499 `; 19500 suggest.hidden = false; 19501 suggest.classList.add('open'); 19502 suggest.style.display = 'block'; 19503 } 19504 19505 function triggerAISearch(q) { 19506 lastQuery = q; // Update lastQuery to prevent re-triggering 19507 isAIProcessing = true; // Set flag to prevent suggest box from closing 19508 19509 // Show AI progress UI - keep suggest box visible 19510 suggest.innerHTML = showAIProgressSteps(); 19511 suggest.hidden = false; 19512 suggest.classList.add('open'); 19513 suggest.style.display = 'block'; 19514 19515 setTimeout(() => updateAIProgressStep(1), 1000); 19516 setTimeout(() => updateAIProgressStep(2), 2500); 19517 19518 // Cancel previous request if any 19519 if (currentAbortController) { 19520 currentAbortController.abort(); 19521 } 19522 currentAbortController = new AbortController(); 19523 19524 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19525 const fetchOptions = { 19526 headers: {'Accept':'application/json'}, 19527 signal: currentAbortController.signal 19528 }; 19529 19530 // Add timeout 19531 const timeoutId = setTimeout(() => { 19532 currentAbortController.abort(); 19533 19534 // Show fallback loading state 19535 suggest.innerHTML = ` 19536 <div class="ai-progress-container"> 19537 <div class="ai-progress-header"> 19538 <div class="ai-progress-icon">â³</div> 19539 <div class="ai-progress-text"> 19540 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19541 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19542 </div> 19543 </div> 19544 </div> 19545 `; 19546 19547 // Fallback to traditional search 19548 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19549 headers: { 'Accept': 'application/json' } 19550 }) 19551 .then(r => r.json()) 19552 .then(j => { 19553 isAIProcessing = false; // Reset flag 19554 if (j.ok && Array.isArray(j.data) && j.data.length) { 19555 renderSuggest(j.data, null); 19556 } else { 19557 showNoResults(q, null); 19558 } 19559 }) 19560 .catch(() => { 19561 isAIProcessing = false; // Reset flag 19562 showAIErrorState(q); 19563 }); 19564 }, AI_TIMEOUT); 19565 19566 fetch(url, fetchOptions) 19567 .then(r => r.json()) 19568 .then(j => { 19569 clearTimeout(timeoutId); 19570 isAIProcessing = false; // Reset flag when done 19571 19572 if (j.ok && Array.isArray(j.data)) { 19573 if (j.data.length > 0) { 19574 renderSuggest(j.data, j.ai_insights || null); 19575 } else { 19576 showNoResults(q, j.ai_insights || null); 19577 } 19578 } else { 19579 showAIErrorState(q); 19580 } 19581 }) 19582 .catch(err => { 19583 clearTimeout(timeoutId); 19584 isAIProcessing = false; // Reset flag on error 19585 if (err.name !== 'AbortError') { 19586 showAIErrorState(q); 19587 } 19588 }); 19589 } 19590 19591 function performSearch(q){ 19592 if (q.length < MIN_LEN){ 19593 clearSuggest(); 19594 return; 19595 } 19596 if (q === lastQuery) return; 19597 lastQuery = q; 19598 19599 // Normal mode only - AI mode uses triggerAISearch() 19600 fetchRemote(q).then(j=>{ 19601 if (j.ok && Array.isArray(j.data)){ 19602 if (j.data.length > 0) { 19603 renderSuggest(j.data, null); 19604 } else { 19605 showNoResults(q, null); 19606 } 19607 } else { 19608 clearSuggest(); 19609 } 19610 }).catch(() => { 19611 clearSuggest(); 19612 }); 19613 } 19614 19615 function debounce(){ 19616 const q = searchInput.value.trim(); 19617 clearTimeout(debounceTimer); 19618 19619 // If empty, show trending suggestions and products 19620 if (q.length === 0) { 19621 debounceTimer = setTimeout(async () => { 19622 const trends = trendingCache || await fetchTrending(); 19623 const products = trendingProductsCache || await fetchTrendingProducts(); 19624 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19625 renderTrendingSuggestions(trends, products); 19626 } else { 19627 clearSuggest(); 19628 } 19629 }, 100); 19630 return; 19631 } 19632 19633 // For AI mode, just show search prompt (no auto search) 19634 if (isAIModeEnabled && q.length >= MIN_LEN) { 19635 showAISearchPrompt(q); 19636 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19637 // Normal mode: auto search with debounce 19638 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19639 } else { 19640 clearSuggest(); 19641 } 19642 } 19643 19644 function showAISearchPrompt(q) { 19645 suggest.innerHTML = ` 19646 <div class="ai-search-prompt"> 19647 <div class="ai-prompt-header"> 19648 <div class="ai-prompt-icon">ð¤</div> 19649 <div class="ai-prompt-text"> 19650 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19651 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19652 </div> 19653 </div> 19654 <div class="ai-prompt-query"> 19655 <div class="ai-query-label">
19655Query Anda:</div> 19656 <div class="ai-query-text">"${esc(q)}"</div> 19657 </div> 19658 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19659 <span class="ai-btn-icon">â¨</span> 19660 <span class="ai-btn-text">Cari dengan AI</span> 19661 </button> 19662 <div class="ai-prompt-footer"> 19663 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19664 </div> 19665 </div> 19666 `; 19667 suggest.hidden = false; 19668 suggest.classList.add('open'); 19669 suggest.style.display = 'block'; 19670 } 19671 19672 function moveHighlight(dir){ 19673 if (suggest.hidden) return; 19674 const items = suggest.querySelectorAll('.suggest-item'); 19675 if (!items.length) return; 19676 currentIndex += dir; 19677 if (currentIndex < 0) currentIndex = items.length - 1; 19678 if (currentIndex >= items.length) currentIndex = 0; 19679 items.forEach(it=> it.classList.remove('active')); 19680 const active = items[currentIndex]; 19681 if (active){ 19682 active.classList.add('active'); 19683 active.scrollIntoView({block:'nearest'}); 19684 } 19685 } 19686 19687 function activate(){ 19688 const active = suggest.querySelector('.suggest-item.active'); 19689 if (!active) return; 19690 window.location.href = active.getAttribute('data-url'); 19691 } 19692 19693 searchInput.addEventListener('input', debounce); 19694 19695 // Stop typing animation when focused & load trending if empty 19696 searchInput.addEventListener('focus', async () => { 19697 stopTypingAnimation(); 19698 19699 const q = searchInput.value.trim(); 19700 19701 // If empty, show trending suggestions and products 19702 if (q.length === 0) { 19703 const trends = trendingCache || await fetchTrending(); 19704 const products = trendingProductsCache || await fetchTrendingProducts(); 19705 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19706 renderTrendingSuggestions(trends, products); 19707 } 19708 return; 19709 } 19710 19711 // If AI mode and has query, show AI prompt 19712 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19713 showAISearchPrompt(q); 19714 } 19715 }); 19716 19717 // Resume typing animation when blurred (if suggest is closed) 19718 searchInput.addEventListener('blur', () => { 19719 // Add delay to allow click events to fire first 19720 setTimeout(() => { 19721 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19722 startTypingAnimation(); 19723 } 19724 }, 200); 19725 }); 19726 19727 searchInput.addEventListener('keydown', e=>{ 19728 switch(e.key){ 19729 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19730 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19731 case 'Enter': 19732 if (!suggest.hidden){ 19733 activate(); 19734 e.preventDefault(); 19735 } else { 19736 // Direct enter without suggestion - track search submit and log final search 19737 const q = searchInput.value.trim(); 19738 if (q.length >= MIN_LEN) { 19739 trackSearchSubmit(q); 19740 logFinalSearch(q); 19741 } 19742 } 19743 break; 19744 case 'Escape': clearSuggest(); break; 19745 } 19746 }); 19747 19748 suggest.addEventListener('click', e=>{ 19749 // Check if footer link clicked 19750 const footerLink = e.target.closest('#footerSearchLink'); 19751 if (footerLink) { 19752 e.preventDefault(); 19753 const q = searchInput.value.trim(); 19754 if (q.length >= MIN_LEN) { 19755 trackSearchSubmit(q); 19756 logFinalSearch(q); 19757 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19758 } 19759 return; 19760 } 19761 19762 // Check if AI search button clicked 19763 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19764 if (aiBtn) { 19765 const query = aiBtn.getAttribute('data-query'); 19766 if (query) { 19767 triggerAISearch(query); 19768 } 19769 return; 19770 } 19771 19772 // Handle trending item click 19773 const trendingItem = e.target.closest('.trending-item'); 19774 if (trendingItem) { 19775 const query = trendingItem.getAttribute('data-query'); 19776 if (query) { 19777 searchInput.value = query; 19778 trackTrendingClick(query); 19779 logFinalSearch(query); // Log final search when user clicks trending 19780 performSearch(query); 19781 } 19782 return; 19783 } 19784 19785 // Handle trending product click 19786 const trendingProduct = e.target.closest('.trending-product-item'); 19787 if (trendingProduct) { 19788 const productId = trendingProduct.getAttribute('data-product-id'); 19789 const slug = trendingProduct.getAttribute('data-slug'); 19790 const query = trendingProduct.getAttribute('data-query'); 19791 19792 if (productId && slug) {
19793 trackTrendingProductClick(productId, query); 19794 window.location.href = '/product/' + encodeURIComponent(slug); 19795 } 19796 return; 19797 } 19798 19799 // Handle product item click 19800 const it = e.target.closest('.suggest-item'); 19801 if (!it) return; 19802 19803 const productId = it.getAttribute('data-id'); 19804 trackSearchClick(lastQuery, productId); 19805 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19806 19807 window.location.href = it.getAttribute('data-url'); 19808 }); 19809 19810 document.addEventListener('click', e=>{ 19811 // Don't close suggest box if AI is processing 19812 if (isAIProcessing) return; 19813 19814 if (e.target === searchInput || e.target === aiToggleBtn || 19815 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19816 clearSuggest(); 19817 }); 19818 19819 if (searchBtn){ 19820 searchBtn.addEventListener('click', ()=>{ 19821 const q = searchInput.value.trim(); 19822 if (q.length >= MIN_LEN){ 19823 // Track search submit and log final search 19824 trackSearchSubmit(q); 19825 logFinalSearch(q); 19826 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19827 } 19828 }); 19829 } 19830 19831 // Pastikan style dasar muncul 19832 if (!suggest.classList.contains('wired')) { 19833 suggest.classList.add('wired'); 19834 suggest.style.position = 'absolute'; 19835 suggest.style.zIndex = '9999'; 19836 suggest.style.background = '#fff'; 19837 suggest.style.border = '1px solid #ccc'; 19838 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19839 suggest.style.maxHeight = '360px'; 19840 suggest.style.overflowY = 'auto'; 19841 } 19842 19843 return true; 19844 } 19845 19846 if (!setup()) { 19847 if (document.readyState === 'loading') { 19848 document.addEventListener('DOMContentLoaded', setup); 19849 } else { 19850 setTimeout(setup, 80); 19851 } 19852 } 19853})(); 19854</script>
vendor: 2 bytes, line 19854
19854
19855<script data-inlined="site-notice.js"> 19856/** 19857 * Site Notice Banner - Interactive Behavior 19858 * Handles dismiss functionality with localStorage persistence 19859 * Supports dynamic notice IDs for different campaigns 19860 */ 19861 19862(function() { 19863 'use strict'; 19864 19865 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19866 19867 const banner = document.getElementById('holidayNotice'); 19868 const closeBtn = document.getElementById('holidayNoticeClose'); 19869 19870 if (!banner || !closeBtn) return; 19871 19872 // Get notice ID from data attribute 19873 const noticeId = banner.dataset.noticeId; 19874 if (!noticeId) { 19875 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19876 } 19877 19878 /** 19879 * Check if banner was previously dismissed 19880 */ 19881 function wasDismissed() { 19882 if (!noticeId) return false; 19883 19884 try { 19885 const dismissed = localStorage.getItem(STORAGE_KEY); 19886 return dismissed === noticeId; 19887 } catch (e) { 19888 console.warn('localStorage not available:', e); 19889 return false; 19890 } 19891 } 19892 19893 /** 19894 * Mark banner as dismissed 19895 */ 19896 function markDismissed() { 19897 if (!noticeId) return; 19898 19899 try { 19900 localStorage.setItem(STORAGE_KEY, noticeId); 19901 } catch (e) { 19902 console.warn('Could not save to localStorage:', e); 19903 } 19904 } 19905 19906 /** 19907 * Hide banner with animation 19908 */ 19909 function hideBanner() { 19910 banner.classList.add('hidden'); 19911 document.body.classList.remove('has-holiday-notice'); 19912 19913 // Remove from DOM after animation 19914 setTimeout(() => { 19915 banner.remove(); 19916 }, 300); 19917 19918 markDismissed(); 19919 19920 // Analytics tracking (optional) 19921 if (window.gtag) { 19922 window.gtag('event', 'banner_dismiss', { 19923 'event_category': 'Site Notice', 19924 'event_label': noticeId || 'unknown', 19925 }); 19926 } 19927 } 19928 19929 /** 19930 * Initialize banner 19931 */ 19932 function init() { 19933 // Check if already dismissed 19934 if (wasDismissed()) { 19935 banner.remove(); 19936 return; 19937 } 19938 19939 // Add body class for layout adjustment 19940 document.body.classList.add('has-holiday-notice'); 19941 19942 // Close button click 19943 closeBtn.addEventListener('click', hideBanner); 19944 19945 // Keyboard support for close button 19946 closeBtn.addEventListener('keydown', (e) => { 19947 if (e.key === 'Enter' || e.key === ' ') { 19948 e.preventDefault(); 19949 hideBanner(); 19950 } 19951 }); 19952 19953 // Optional: Auto-hide after certain time (disabled by default) 19954 // setTimeout(hideBanner, 30000); // 30 seconds 19955 } 19956 19957 // Initialize when DOM is ready 19958 if (document.readyState === 'loading') { 19959 document.addEventListener('DOMContentLoaded', init); 19960 } else { 19961 init(); 19962 } 19963 19964})(); 19965 19966</script>
vendor: 2 bytes, line 19966
19966
19967<script data-inlined="guest-cart.js"> 19968/** 19969 * Guest Cart Manager - localStorage based cart for guest users 19970 * 19971 * Manages shopping cart items in browser localStorage for users who are not logged in. 19972 * Provides seamless transition to database when user logs in. 19973 */ 19974 19975class GuestCart { 19976 constructor() { 19977 this.storageKey = 'plazait_guest_cart'; 19978 this.apiBase = '/api/cart'; 19979 19980 // Initialize cart structure 19981 this.cart = this.loadFromStorage(); 19982 19983 // Bind methods 19984 this.add = this.add.bind(this); 19985 this.remove = this.remove.bind(this); 19986 this.update = this.update.bind(this); 19987 this.clear = this.clear.bind(this); 19988 this.getItems = this.getItems.bind(this); 19989 this.mergeToDatabase = this.mergeToDatabase.bind(this); 19990 } 19991 19992 /** 19993 * Load cart from localStorage 19994 */ 19995 loadFromStorage() { 19996 try { 19997 const stored = localStorage.getItem(this.storageKey); 19998 if (stored) { 19999 const parsed = JSON.parse(stored); 20000 if (parsed && typeof parsed === 'object' && parsed.items) { 20001 return parsed; 20002 } 20003 } 20004 } catch (error) { 20005 // Silent error handling 20006 } 20007 20008 return { 20009 items: {}, 20010 updated_at: Date.now(), 20011 version: '1.0' 20012 }; 20013 } 20014 20015 /** 20016 * Save cart to localStorage 20017 */ 20018 saveToStorage() { 20019 try { 20020 this.cart.updated_at = Date.now(); 20021 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20022 20023 // Dispatch custom event for cart updates 20024 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20025 detail: { 20026 itemsCount: this.getItemsCount(), 20027 linesCount: this.getLinesCount(), 20028 items: this.cart.items 20029 } 20030 })); 20031 20032 return true; 20033 } catch (error) { 20034 return false; 20035 } 20036 } 20037 20038 /** 20039 * Generate cart item key 20040 */ 20041 generateKey(productId, variantId = 0) { 20042 return `p${productId}v${variantId}`; 20043 } 20044 20045 /** 20046 * Add item to guest cart 20047 */ 20048 add(productId, qty = 1, variantId = 0, meta = {}) { 20049 const key = this.generateKey(productId, variantId); 20050 20051 // Validate inputs 20052 if (!productId || productId <= 0 || !qty || qty <= 0) { 20053 throw new Error('Invalid product ID or quantity'); 20054 } 20055 20056 qty = Math.max(1, Math.min(999, parseInt(qty))); 20057 20058 // Initialize item if not exists 20059 if (!this.cart.items[key]) { 20060 this.cart.items[key] = { 20061 pid: parseInt(productId), 20062 vid: parseInt(variantId) || 0, 20063 qty: 0, 20064 name: meta.name || '', 20065 sku: meta.sku || '', 20066 price: parseInt(meta.price) || 0, 20067 image_url: meta.image_url || '', 20068 product_url: meta.product_url || '/product/' + parseInt(productId), 20069 added_at: Date.now() 20070 }; 20071 } 20072 20073 // Update quantity 20074 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20075 20076 // Update metadata if provided 20077 if (meta.name) this.cart.items[key].name = meta.name; 20078 if (meta.sku) this.cart.items[key].sku = meta.sku; 20079 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20080 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20081 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20082 20083 this.saveToStorage(); 20084 20085 return { 20086 key, 20087 item: this.cart.items[key], 20088 itemsCount: this.getItemsCount(), 20089 linesCount: this.getLinesCount() 20090 }; 20091 } 20092 20093 /** 20094 * Remove item from guest cart 20095 */ 20096 remove(productId, variantId = 0) { 20097 const key = this.generateKey(productId, variantId); 20098 20099 if (this.cart.items[key]) { 20100 delete this.cart.items[key]; 20101 this.saveToStorage(); 20102 20103 return true; 20104 } 20105 20106 return false; 20107 } 20108 20109 /** 20110 * Update item quantity 20111 */ 20112 update(productId, qty, variantId = 0) {
20113 const key = this.generateKey(productId, variantId); 20114 20115 if (!this.cart.items[key]) { 20116 return false; 20117 } 20118 20119 qty = parseInt(qty); 20120 if (qty <= 0) { 20121 return this.remove(productId, variantId); 20122 } 20123 20124 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20125 this.saveToStorage(); 20126 20127 return true; 20128 } 20129 20130 /** 20131 * Clear entire cart 20132 */ 20133 clear() { 20134 this.cart = { 20135 items: {}, 20136 updated_at: Date.now(), 20137 version: '1.0' 20138 }; 20139 20140 this.saveToStorage(); 20141 20142 return true; 20143 } 20144 20145 /** 20146 * Get all cart items 20147 */ 20148 getItems() { 20149 return this.cart.items; 20150 } 20151 20152 /** 20153 * Get cart items as array 20154 */ 20155 getItemsArray() { 20156 return Object.values(this.cart.items); 20157 } 20158 20159 /** 20160 * Get total items count (sum of quantities) 20161 */ 20162 getItemsCount() { 20163 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20164 } 20165 20166 /** 20167 * Get lines count (number of different items) 20168 */ 20169 getLinesCount() { 20170 return Object.keys(this.cart.items).length; 20171 } 20172 20173 /** 20174 * Get cart summary 20175 */ 20176 getSummary() { 20177 const itemsCount = this.getItemsCount(); 20178 const linesCount = this.getLinesCount(); 20179 const totalValue = Object.values(this.cart.items).reduce( 20180 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20181 ); 20182 20183 return { 20184 itemsCount, 20185 linesCount, 20186 totalValue, 20187 updatedAt: this.cart.updated_at 20188 }; 20189 } 20190 20191 /** 20192 * Check if cart has items 20193 */ 20194 isEmpty() { 20195 return this.getLinesCount() === 0; 20196 } 20197 20198 /** 20199 * Merge guest cart to database after user login 20200 */ 20201 async mergeToDatabase() { 20202 const items = this.cart.items; 20203 20204 if (Object.keys(items).length === 0) { 20205 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20206 } 20207 20208 try { 20209 // Use new endpoint that handles localStorage format 20210 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20211 method: 'POST', 20212 headers: { 20213 'Content-Type': 'application/json', 20214 }, 20215 credentials: 'same-origin', 20216 body: JSON.stringify({ 20217 items: items // Send localStorage format directly 20218 }) 20219 }); 20220 20221 const result = await response.json(); 20222 20223 if (result.ok) { 20224 // Clear localStorage cart after successful merge 20225 this.clear(); 20226 20227 // Update cart counter 20228 if (window.updateCartCounter) { 20229 try { 20230 await window.updateCartCounter(); 20231 } catch (e) { 20232 // Silent error handling 20233 } 20234 } 20235 20236 // Dispatch merge completion event 20237 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20238 detail: result 20239 })); 20240 20241 return result; 20242 } else { 20243 throw new Error(result.error || 'Failed to merge cart'); 20244 } 20245 20246 } catch (error) { 20247 throw error; 20248 } 20249 } 20250 20251 /** 20252 * Export cart data (for debugging or backup) 20253 */ 20254 export() { 20255 return { 20256 ...this.cart, 20257 summary: this.getSummary() 20258 }; 20259 } 20260 20261 /** 20262 * Import cart data (for restore or testing) 20263 */ 20264 import(cartData) { 20265 if (cartData && typeof cartData === 'object' && cartData.items) { 20266 this.cart = { 20267 items: cartData.items, 20268 updated_at: cartData.updated_at || Date.now(), 20269 version: cartData.version || '1.0' 20270 }; 20271 20272 this.saveToStorage(); 20273 return true; 20274 } 20275 20276 return false; 20277 } 20278} 20279 20280// Create global instance 20281window.guestCart = new GuestCart(); 20282 20283// Auto-merge on page load if user is logged in 20284document.addEventListener('DOMContentLoaded', async function() { 20285 // Multiple ways to check if user is logged in 20286 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20287 document.body.classList.contains('logged-in') || 20288 window.userLoggedIn === true || 20289 document.body.dataset.userLoggedIn === 'true'; 20290 20291 if (isLoggedIn && !window.guestCart.isEmpty()) { 20292 // Small delay to ensure page is fully loaded 20293 setTimeout(async () => { 20294 try { 20295 const result = await window.guestCart.mergeToDatabase(); 20296 } catch (error) { 20297 // Silent error handling 20298 } 20299 }, 500); 20300 } 20301}); 20302 20303// Also listen for login events (if login happens via AJAX) 20304window.addEventListener('userLoggedIn', async function() { 20305 if (!window.guestCart.isEmpty()) { 20306 try { 20307 await window.guestCart.mergeToDatabase(); 20308 } catch (error) { 20309 // Silent error handling 20310 } 20311 } 20312}); 20313</script>
vendor: 2 bytes, line 20313
20313
20314<script data-inlined="cart.js"> 20315(function(){ 20316 function $(s,ctx=document){ return ctx.querySelector(s); } 20317 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20318 20319 // CSRF token helper 20320 function getCsrfToken() { 20321 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20322 return metaTag ? metaTag.getAttribute('content') : ''; 20323 } 20324 20325 function guessPdMeta(){ 20326 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20327 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20328 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20329 const imgEl = $('#pd-main-img'); 20330 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20331 return { name, sku, image_url }; // price removed for security 20332 } 20333 20334 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20335 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20336 const payload = { 20337 product_id: parseInt(productId||0,10) || 0, 20338 qty: parseInt(qty||1,10) || 1, 20339 variant_id: parseInt(variantId||0,10) || 0, 20340 meta 20341 }; 20342 20343 // Check if user is logged in 20344 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20345 document.body.classList.contains('logged-in') || 20346 window.userLoggedIn === true || 20347 document.body.dataset.userLoggedIn === 'true'; 20348 20349 // Skip localStorage untuk logged user - mereka pakai database 20350 if (!isLoggedIn && window.guestCart) { 20351 try { 20352 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20353 } catch (e) { 20354 console.warn('Failed to add to localStorage cart:', e); 20355 } 20356 } 20357 20358 const res = await fetch('/api/cart/add.php', { 20359 method: 'POST', 20360 headers: { 20361 'Content-Type':'application/json', 20362 'X-CSRF-Token': getCsrfToken() 20363 }, 20364 credentials: 'same-origin', 20365 body: JSON.stringify({ 20366 ...payload, 20367 csrf_token: getCsrfToken() 20368 }), 20369 }); 20370 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20371 if (!res.ok || !data.ok) { 20372 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20373 throw new Error(msg); 20374 } 20375 20376 // Update badge di navbar dengan data dari response 20377 try { 20378 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20379 if (el && data.cart && typeof data.cart.items_count === 'number') { 20380 el.textContent = String(data.cart.items_count); 20381 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20382 } 20383 } catch(_){} 20384 20385 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20386 try { 20387 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20388 } catch(_){} 20389 20390 // Update CartBadge dengan data dari response (lebih reliable) 20391 try { 20392 if (window.CartBadge && window.CartBadge.set) { 20393 window.CartBadge.set(data.cart.items_count||0); 20394 } 20395 } catch(_){} 20396 20397 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20398 setTimeout(() => { 20399 if (window.updateCartCounter) { 20400 window.updateCartCounter(); 20401 } 20402 }, 100); 20403 20404 return data; 20405 } 20406 20407 // Function to update cart counter from server 20408 async function updateCartCounter(){ 20409 try { 20410 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20411 const data = await res.json().catch(()=>({ok:false})); 20412 if (data.ok && typeof data.items_count === 'number') { 20413 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20414 if (el) { 20415 el.textContent = String(data.items_count); 20416 el.classList.toggle('is-empty', data.items_count <= 0); 20417 } 20418 // Dispatch event 20419 try { 20420 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20421 } catch(_){} 20422 } 20423 } catch (err) { 20424 console.log('Could not update cart counter:', err); 20425 } 20426 } 20427 20428 // Function to manually merge guest cart (if needed) 20429 async function mergeGuestCart(){ 20430 try { 20431 const res = await fetch('/api/cart/merge-guest.php', { 20432 method: 'POST', 20433 headers: { 20434 'Content-Type': 'application/json', 20435 'X-CSRF-Token': getCsrfToken() 20436 }, 20437 credentials: 'same-origin', 20438 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20439 }); 20440 const data = await res.json().catch(()=>({ok:false})); 20441 20442 if (data.ok && data.merged_items > 0) { 20443 console.log(`Guest cart merged: ${data.merged_items} items`); 20444 // Update cart counter after merge 20445 await updateCartCounter(); 20446 return data; 20447 } 20448 return data; 20449 } catch (err) { 20450 console.log('Could not merge guest cart:', err); 20451 return { ok: false, error: err.message }; 20452 } 20453 } 20454 20455 window.addToCart = addToCart; 20456 window.updateCartCounter = updateCartCounter; 20457 window.mergeGuestCart = mergeGuestCart; 20458 20459 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20460 document.addEventListener('click', async (e)=>{ 20461 const btn = e.target.closest('[data-addtocart]'); 20462 if (!btn) return; 20463 e.preventDefault(); 20464 if (btn.disabled) return; 20465 20466 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20467 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20468 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20469 20470 try { 20471 const data = await addToCart(pid, qty, vid); 20472 if (window.showToast) { 20473 const meta = guessPdMeta(); 20474 window.showToast({ 20475 type:'success', 20476 title:'Berhasil ditambahkan', 20477 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20478 duration: 3800, 20479 actions: [ 20480 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20481 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20482 ] 20483 }); 20484 } 20485 } catch (err) { 20486 if (window.showToast) { 20487 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20488 } else { 20489 console.error(err); 20490 } 20491 } 20492 }); 20493})(); 20494</script>
vendor: 2 bytes, line 20494
20494
20495<script data-inlined="mega-menu-tiles.js"> 20496// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20497// Includes safe-area hover handling (no backdrop flicker) 20498 20499(function(){ 20500 const trigger = document.querySelector('[data-mm2="trigger"]'); 20501 const panel = document.querySelector('[data-mm2="panel"]'); 20502 const topView = document.querySelector('[data-mm2="top"]'); 20503 const detailView= document.querySelector('[data-mm2="detail"]'); 20504 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20505 if (!trigger || !panel || !topView || !detailView) return; 20506 20507 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20508 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20509 20510 let data = []; 20511 let hoverTimer, closeTimer; 20512 let isLoading = false; 20513 20514 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20515 20516 // Mobile close button HTML 20517 const mobileCloseBtn = () => ` 20518 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20519 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 20520 <line x1="18" y1="6" x2="6" y2="18"></line> 20521 <line x1="6" y1="6" x2="18" y2="18"></line> 20522 </svg> 20523 </button> 20524 `; 20525 20526 // Category-specific gradient colors (16 unique combinations) 20527 const categoryGradients = [ 20528 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#4f46e5' }, // Indigo 20529 { from: '#dbeafe', to: '#fff', border: '#bfdbfe', color: '#1e40af' }, // Blue 20530 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20531 { from: '#fef3c7', to: '#fff', border: '#fde68a', color: '#d97706' }, // Amber 20532 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20533 { from: '#e9d5ff', to: '#fff', border: '#d8b4fe', color: '#9333ea' }, // Purple 20534 { from: '#ffedd5', to: '#fff', border: '#fed7aa', color: '#ea580c' }, // Orange 20535 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20536 { from: '#fef9c3', to: '#fff', border: '#fef08a', color: '#ca8a04' }, // Yellow 20537 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#e11d48' }, // Rose 20538 { from: '#e0f2fe', to: '#fff', border: '#bae6fd', color: '#0284c7' }, // Sky 20539 { from: '#f3e8ff', to: '#fff', border: '#e9d5ff', color: '#a855f7' }, // Violet 20540 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20541 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#dc2626' }, // Red 20542 { from: '#eff6ff', to: '#fff', border: '#dbeafe', color: '#2563eb' }, // Blue-600 20543 { from: '#f5f3ff', to: '#fff', border: '#ede9fe', color: '#7c3aed' }, // Violet-600 20544 ]; 20545 20546 const icon = () => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7h5l2 3h11v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v3"></path></svg>`; 20547 20548 // Recent (optional future use) 20549 const REC_KEY='mm2_recent'; 20550 const saveRecent=(path,label)=>{ try{ 20551 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20552 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20553 }catch{} }; 20554 20555 function isToSafeArea(e){
20556 const t = e && e.relatedTarget ? e.relatedTarget : null; 20557 if (!t) return false; 20558 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20559 } 20560 20561 function open(){ 20562 document.body.classList.add('mm2-open'); 20563 showTop(); 20564 // Add entrance animation class 20565 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20566 } 20567 function close(){ 20568 document.body.classList.remove('mm2-open'); 20569 panel.classList.remove('mm2-animated'); 20570 } 20571 function showTop(){ 20572 topView.classList.add('is-active'); 20573 detailView.classList.remove('is-active'); 20574 } 20575 20576 // Loading skeleton 20577 function showLoading() { 20578 const skeletons = Array.from({length: 12}, (_, i) => ` 20579 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s"> 20580 <span class="mm2-ico skeleton-box"></span> 20581 <span style="flex: 1;"> 20582 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20583 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20584 </span> 20585 </div> 20586 `).join(''); 20587 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20588 } 20589 20590 // Count total products in a category tree 20591 function countProducts(node) { 20592 let count = node.product_count || 0; 20593 if (Array.isArray(node.children)) { 20594 node.children.forEach(child => { 20595 count += countProducts(child); 20596 }); 20597 } 20598 return count; 20599 } 20600 20601 // Cache for fetched thumbnails to avoid duplicate requests 20602 const thumbnailCache = new Map(); 20603 20604 // Fetch product thumbnails for a category 20605 function fetchCategoryThumbnails(categoryPath) { 20606 // Check cache first 20607 if (thumbnailCache.has(categoryPath)) { 20608 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20609 return; 20610 } 20611 20612 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20613 20614 fetch(url) 20615 .then(r => { 20616 if (!r.ok) throw new Error('Network error'); 20617 return r.json(); 20618 }) 20619 .then(data => { 20620 if (data.success && data.products && data.products.length > 0) { 20621 thumbnailCache.set(categoryPath, data.products); 20622 injectThumbnails(categoryPath, data.products); 20623 } else { 20624 // No products, remove skeleton 20625 removeThumbSkeletons(categoryPath); 20626 } 20627 }) 20628 .catch(err => { 20629 console.warn('Failed to load thumbnails for', categoryPath, err); 20630 removeThumbSkeletons(categoryPath); 20631 }); 20632 } 20633 20634 // Inject thumbnails into the DOM 20635 function injectThumbnails(categoryPath, products) { 20636 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20637 links.forEach(link => { 20638 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20639 if (!thumbsContainer) return; 20640 20641 const thumbsHtml = products.slice(0, 2).map(p => 20642 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20643 ).join(''); 20644 20645 thumbsContainer.innerHTML = thumbsHtml; 20646 thumbsContainer.removeAttribute('data-loading'); 20647 }); 20648 } 20649 20650 // Remove skeleton loaders when no products 20651 function removeThumbSkeletons(categoryPath) { 20652 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20653 links.forEach(link => { 20654 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20655 if (thumbsContainer) { 20656 thumbsContainer.remove(); 20657 } 20658 }); 20659 } 20660 20661 function showDetail(node){ 20662 const title = node.label.split('/')[0]; 20663 const children = Array.isArray(node.children) ? node.children : []; 20664 const hasChildren = children.length > 0; 20665 20666 const subnav = hasChildren ? ` 20667 <nav class="mm2-subnav" role="tablist"> 20668 ${children.map((c2,i)=>{ 20669 const t = c2.label.split('/').slice(-1)[0]; 20670 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20671 }).join('')} 20672 </nav>` : ''; 20673 20674 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20675
20676 detailView.innerHTML = ` 20677 <div class="mm2-detail"> 20678 <div class="mm2-detail-header"> 20679 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20680 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"></polyline></svg> 20681 </button> 20682 <div class="mm2-detail-title">${esc(title)}</div> 20683 </div> 20684 20685 ${hasChildren ? ` 20686 <div class="mm2-detail-layout"> 20687 ${subnav} 20688 ${subcontent} 20689 </div>` : ` 20690 <div class="mm2-links"> 20691 <a class="mm2-link mm2-seeall" href="/product/kategori/?kategori_path=${encodeURIComponent(node.path)}" data-mm2="nav" data-path="${esc(node.path)}" data-label="${esc(node.label)}"> 20692 <strong>Lihat semua ${esc(title)}</strong> 20693 </a> 20694 </div> 20695 `} 20696 </div> 20697 `; 20698 20699 topView.classList.remove('is-active'); 20700 detailView.classList.add('is-active'); 20701 20702 if (hasChildren) { 20703 const subEl = detailView.querySelector('.mm2-subnav'); 20704 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20705 let activeIdx = 0; 20706 20707 const renderSubgrid = (idx) => { 20708 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20709 const title2 = c2.label.split('/').slice(-1)[0]; 20710 const l3 = Array.isArray(c2.children) ? c2.children : []; 20711 const l3Html = l3.map(c3 => ` 20712 <a class="mm2-link" href="/product/kategori/?kategori_path=${encodeURIComponent(c3.path)}" data-mm2="nav" data-path="${esc(c3.path)}" data-label="${esc(c3.label)}" data-cat-path="${esc(c3.path)}"> 20713 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20714 <span class="mm2-thumbs" data-loading="true"> 20715 <span class="mm2-thumb-skeleton"></span> 20716 <span class="mm2-thumb-skeleton"></span> 20717 </span> 20718 </a>`).join(''); 20719 20720 contEl.innerHTML = ` 20721 <div class="mm2-links"> 20722 <a class="mm2-link mm2-seeall" href="/product/kategori/?kategori_path=${encodeURIComponent(c2.path)}" data-mm2="nav" data-path="${esc(c2.path)}" data-label="${esc(c2.label)}"> 20723 <strong>Lihat semua ${esc(title2)}</strong> 20724 </a> 20725 ${l3Html} 20726 </div> 20727 `; 20728 20729 // Fetch product thumbnails for each level-3 category (desktop only) 20730 if (isDesktop()) { 20731 setTimeout(() => { 20732 l3.forEach(c3 => { 20733 fetchCategoryThumbnails(c3.path); 20734 }); 20735 }, 100); 20736 } 20737 }; 20738 20739 renderSubgrid(activeIdx); 20740 20741 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20742 subEl.addEventListener('mousemove', (e)=>{ 20743 if (!isDesktop()) return; 20744 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20745 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20746 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20747 it.classList.add('active'); 20748 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20749 activeIdx = idx; 20750 renderSubgrid(activeIdx); 20751 }); 20752 20753 subEl.addEventListener('click', (e)=>{ 20754 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20755 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20756 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20757 it.classList.add('active'); 20758 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20759 activeIdx = idx; 20760 renderSubgrid(activeIdx); 20761 }); 20762 20763 // Delegate link clicks to add recent 20764 detailView.addEventListener('click', (e)=>{ 20765 const nav = e.target.closest('[data-mm2="nav"]'); 20766 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20767 }); 20768 } 20769 } 20770 20771 function buildTop(){ 20772 const html = data.map((n, idx) => { 20773 const title = n.label.split('/')[0]; 20774 const childCount = (n.children||[]).length;
20775 const productCount = countProducts(n); 20776 const gradient = categoryGradients[idx % categoryGradients.length]; 20777 20778 // Dynamic subtitle based on content - shorter for mobile 20779 let sub = ''; 20780 if (productCount > 0) { 20781 sub = `${productCount} produk`; 20782 if (childCount > 0) sub += ` · ${childCount} kat`; 20783 } else if (childCount > 0) { 20784 sub = `${childCount} kategori`; 20785 } else { 20786 sub = 'Lihat semua'; 20787 } 20788 20789 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20790 20791 return `<a href="/product/kategori/?kategori_path=${encodeURIComponent(n.path)}" class="mm2-card" data-mm2="card" data-path="${esc(n.path)}" data-label="${esc(n.label)}"> 20792 <span class="mm2-ico" style="${iconStyle}">${icon()}</span> 20793 <span style="flex: 1;"> 20794 <div class="mm2-title">${esc(title)}</div> 20795 <div class="mm2-sub">${esc(sub)}</div> 20796 </span> 20797 </a>`; 20798 }).join(''); 20799 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20800 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20801 } 20802 20803 // Open/close with safe-area 20804 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20805 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20806 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20807 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20808 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20809 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20810 backdrop?.addEventListener('click', close); 20811 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20812 20813 // Card click â open detail (unless cmd/ctrl to open link) 20814 panel.addEventListener('click', (e)=>{ 20815 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20816 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20817 const card = e.target.closest('[data-mm2="card"]'); 20818 if (card && !e.metaKey && !e.ctrlKey) { 20819 e.preventDefault(); 20820 const n = data.find(d => d.path === card.dataset.path); 20821 if (n) showDetail(n); 20822 } 20823 const nav = e.target.closest('[data-mm2="nav"]'); 20824 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20825 }); 20826 20827 // Swipe-down gesture for mobile (bottom sheet close) 20828 if (!isDesktop()) { 20829 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20830 const SWIPE_THRESHOLD = 80; 20831 const VELOCITY_THRESHOLD = 0.3; 20832 20833 panel.addEventListener('touchstart', (e) => { 20834 if (!document.body.classList.contains('mm2-open')) return; 20835 const touch = e.touches[0]; 20836 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20837 20838 // Prevent drag if content is scrolled 20839 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20840 20841 // Only start drag from top area or handle 20842 const rect = panel.getBoundingClientRect(); 20843 if (touch.clientY - rect.top > 60) return; 20844 20845 startY = touch.clientY; 20846 currentY = startY; 20847 startTime = Date.now(); 20848 isDragging = true; 20849 }, { passive: true }); 20850 20851 panel.addEventListener('touchmove', (e) => { 20852 if (!isDragging) return; 20853 const touch = e.touches[0]; 20854 currentY = touch.clientY; 20855 const deltaY = currentY - startY; 20856 20857 // Only allow downward swipe 20858 if (deltaY > 0) { 20859 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20860 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20861 panel.style.opacity = opacity; 20862 } 20863 }, { passive: true });
20864 20865 const endDrag = () => { 20866 if (!isDragging) return; 20867 const deltaY = currentY - startY; 20868 const deltaTime = Date.now() - startTime; 20869 const velocity = deltaY / deltaTime; 20870 20871 // Close if swiped down enough or fast swipe 20872 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20873 close(); 20874 } 20875 20876 // Reset position 20877 panel.style.transform = ''; 20878 panel.style.opacity = ''; 20879 isDragging = false; 20880 }; 20881 20882 panel.addEventListener('touchend', endDrag, { passive: true }); 20883 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20884 } 20885 20886 // Fetch data with loading state 20887 if (!isLoading) { 20888 isLoading = true; 20889 showLoading(); 20890 20891 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20892 .then(r => { 20893 if (!r.ok) throw new Error('Network response was not ok'); 20894 return r.json(); 20895 }) 20896 .then(j => { 20897 data = Array.isArray(j.data) ? j.data : []; 20898 if (!data.length) { 20899 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20900 <p>Tidak ada kategori tersedia</p> 20901 </div></div>`; 20902 return; 20903 } 20904 buildTop(); 20905 isLoading = false; 20906 }) 20907 .catch(err => { 20908 console.error('Failed to load categories:', err); 20909 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20910 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20911 </div></div>`; 20912 isLoading = false; 20913 }); 20914 } 20915})(); 20916 20917// Add skeleton animation styles 20918const style = document.createElement('style'); 20919style.textContent = ` 20920.mm2-skeleton { 20921 pointer-events: none; 20922 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20923} 20924.skeleton-box, .skeleton-line { 20925 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20926 background-size: 200% 100%; 20927 animation: shimmer 1.5s infinite; 20928 border-radius: 8px; 20929} 20930@keyframes pulse { 20931 0%, 100% { opacity: 1; } 20932 50% { opacity: 0.6; } 20933} 20934@keyframes shimmer { 20935 0% { background-position: 200% 0; } 20936 100% { background-position: -200% 0; } 20937} 20938`; 20939document.head.appendChild(style); 20940</script>
20940 20941
20941<script data-inlined="benchmark.js"> 20942/** 20943 * Benchmark Data Loader 20944 * Handles AJAX fetch and display of benchmark data 20945 */ 20946 20947(function() { 20948 'use strict'; 20949 20950 // Wait for DOM ready 20951 if (document.readyState === 'loading') { 20952 document.addEventListener('DOMContentLoaded', initBenchmark); 20953 } else { 20954 initBenchmark(); 20955 } 20956 20957 function initBenchmark() { 20958 const section = document.querySelector('.pd-benchmark-section'); 20959 if (!section) return; 20960 20961 const button = document.getElementById('btn-load-benchmark'); 20962 const content = document.getElementById('benchmark-content'); 20963 const productId = section.dataset.productId; 20964 20965 if (!button || !content || !productId) return; 20966 20967 let isLoading = false; 20968 let dataLoaded = false; 20969 20970 button.addEventListener('click', function() { 20971 if (isLoading || dataLoaded) return; 20972 20973 loadBenchmarkData(productId, button, content); 20974 }); 20975 } 20976 20977 function loadBenchmarkData(productId, button, container) { 20978 // Update button state 20979 button.disabled = true; 20980 button.classList.add('loading'); 20981 const originalText = button.querySelector('.btn-text').textContent; 20982 button.querySelector('.btn-text').textContent = 'Memuat data...'; 20983 20984 // Show loading state 20985 container.style.display = 'block'; 20986 container.innerHTML = ` 20987 <div class="benchmark-loading"> 20988 <div class="benchmark-loading-spinner"></div> 20989 <p>Mengambil data benchmark...</p> 20990 </div> 20991 `; 20992 20993 // Fetch data 20994 fetch('/api/benchmark/fetch.php?product_id=' + productId) 20995 .then(function(response) { 20996 if (!response.ok) { 20997 throw new Error('HTTP error ' + response.status); 20998 } 20999 return response.json(); 21000 }) 21001 .then(function(result) { 21002 if (result.success && result.data) { 21003 renderBenchmarkData(result.data, container); 21004 button.style.display = 'none'; // Hide button after successful load 21005 } else { 21006 throw new Error(result.error || 'Data tidak tersedia'); 21007 } 21008 }) 21009 .catch(function(error) { 21010 console.error('Benchmark fetch error:', error); 21011 container.innerHTML = ` 21012 <div class="benchmark-error"> 21013 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21014 <p style="font-size: 14px; margin-top: 8px;"> 21015 ${error.message || 'Terjadi kesalahan saat memuat data'} 21016 </p> 21017 <button type="button" onclick="location.reload()" 21018 style="margin-top: 16px; padding: 8px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21019 Coba Lagi 21020 </button> 21021 </div> 21022 `; 21023 21024 // Reset button 21025 button.disabled = false; 21026 button.classList.remove('loading'); 21027 button.querySelector('.btn-text').textContent = originalText; 21028 }); 21029 } 21030 21031 function renderBenchmarkData(data, container) { 21032 let html = '<div class="benchmark-data-wrapper">'; 21033 21034 // Chipset info if available 21035 if (data.chipset) { 21036 html += ` 21037 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21038 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21039 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21040 </div> 21041 `; 21042 } 21043 21044 // Main benchmark scores 21045 if (data.antutu || data.geekbench_multi || data.overall_score) { 21046 html += '<div class="benchmark-data-grid">'; 21047 21048 if (data.antutu) { 21049 html += ` 21050 <div class="benchmark-card"> 21051 <div class="benchmark-card-label">ANTUTU</div> 21052 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21053 ${getScoreBadge(data.antutu, 'antutu')} 21054 </div> 21055 `; 21056 } 21057 21058 if (data.geekbench_multi) { 21059 html += ` 21060 <div class="benchmark-card"> 21061 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21062 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21063 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21064 </div> 21065 `; 21066 } 21067 21068 if (data.gaming_score) { 21069 html += ` 21070 <div class="benchmark-card"> 21071 <div class="benchmark-card-label">GAMING</div> 21072 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21073 </div> 21074 `; 21075 } 21076 21077 if (data.battery_score) { 21078 html += ` 21079 <div class="benchmark-card"> 21080 <div class="benchmark-card-label">BATTERY</div> 21081 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21082 </div> 21083 `; 21084 } 21085 21086 if (data.overall_score) { 21087 html += ` 21088 <div class="benchmark-card"> 21089 <div class="benchmark-card-label">OVERALL</div> 21090 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21091 </div> 21092 `; 21093 } 21094 21095 html += '</div>'; 21096 } 21097 21098 // AI Performance Description 21099 if (data.notes) { 21100 html += ` 21101 <div style="background: #f8f9fa; border-left: 3px solid #667eea; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21102 <div style="font-size: 11px; font-weight: 600; color: #667eea; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21103 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21104 </div> 21105 `; 21106 } 21107 21108 // Attribution 21109 html += ` 21110 <div class="benchmark-attribution"> 21111 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21112 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21113 </div> 21114 `; 21115 21116 html += '</div>'; 21117 21118 container.innerHTML = html; 21119 } 21120 21121 function formatNumber(num) { 21122 if (!num) return '-'; 21123 return num.toString().replace(/B(?=(d{3})+(?!d))/g, '.'); 21124 } 21125 21126 function escapeHtml(text) { 21127 if (!text) return ''; 21128 const div = document.createElement('div'); 21129 div.textContent = text; 21130 return div.innerHTML; 21131 } 21132 21133 function formatDate(dateStr) { 21134 try { 21135 const date = new Date(dateStr); 21136 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21137 return date.toLocaleDateString('id-ID', options); 21138 } catch (e) { 21139 return dateStr; 21140 } 21141 } 21142 21143 function getScoreBadge(score, type) { 21144 let category = ''; 21145 let label = ''; 21146 21147 if (type === 'antutu') { 21148 if (score >= 500000) { 21149 category = 'excellent'; 21150 label = 'Flagship'; 21151 } else if (score >= 350000) { 21152 category = 'good'; 21153 label = 'High-End'; 21154 } else if (score >= 200000) { 21155 category = 'average'; 21156 label = 'Mid-Range'; 21157 } else { 21158 category = 'average'; 21159 label = 'Entry-Level'; 21160 } 21161 } 21162 21163 if (label) { 21164 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21165 } 21166 21167 return ''; 21168 } 21169})(); 21170 21171</script>
21171 21172
21172<script data-inlined="product-compare.js"> 21173(function() { 21174 'use strict'; 21175 21176 // State management 21177 let compareList = []; 21178 let isAnalyzing = false; 21179 let currentAnalysis = null; 21180 21181 // DOM elements 21182 const compareBtn = document.getElementById('compareBtn'); 21183 const compareSidebar = document.getElementById('compareSidebar'); 21184 const compareOverlay = document.getElementById('compareOverlay'); 21185 const compareClose = document.getElementById('compareClose'); 21186 const compareModal = document.getElementById('aiCompareModal'); 21187 const compareModalClose = document.getElementById('aiCompareModalClose'); 21188 21189 // Initialize 21190 document.addEventListener('DOMContentLoaded', async function() { 21191 await initializeCompare(); 21192 setupEventListeners(); 21193 }); 21194 21195 /** 21196 * Initialize compare functionality 21197 */ 21198 async function initializeCompare() { 21199 // Load compare list first before setting up buttons 21200 await loadCompareList(); 21201 21202 // Setup product detail compare button if exists 21203 const productCompareBtn = document.getElementById('pd-compare-btn'); 21204 21205 if (productCompareBtn) { 21206 setupProductCompareButton(productCompareBtn); 21207 } 21208 21209 // Check for existing compare list and update UI 21210 if (compareList.length > 0) { 21211 // Only update badge and buttons, no sticky bar 21212 } 21213 } 21214 21215 /** 21216 * Setup event listeners 21217 */ 21218 function setupEventListeners() { 21219 // Navigation compare button 21220 if (compareBtn) { 21221 compareBtn.addEventListener('click', toggleCompareSidebar); 21222 } 21223 21224 // Sidebar controls 21225 if (compareClose) { 21226 compareClose.addEventListener('click', closeCompareSidebar); 21227 } 21228 21229 if (compareOverlay) { 21230 compareOverlay.addEventListener('click', closeCompareSidebar); 21231 } 21232 21233 // Sidebar actions 21234 const clearAllBtn = document.getElementById('compareClearAll'); 21235 if (clearAllBtn) { 21236 clearAllBtn.addEventListener('click', clearAllProducts); 21237 } 21238 21239 const analyzeBtn = document.getElementById('compareAnalyze'); 21240 if (analyzeBtn) { 21241 analyzeBtn.addEventListener('click', startAIAnalysis); 21242 } 21243 21244 // Modal controls 21245 if (compareModalClose) { 21246 compareModalClose.addEventListener('click', closeModal); 21247 } 21248 21249 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21250 if (modalBackdrop) { 21251 modalBackdrop.addEventListener('click', closeModal); 21252 } 21253 21254 // Modal actions 21255 const retryBtn = document.getElementById('aiRetry'); 21256 if (retryBtn) { 21257 retryBtn.addEventListener('click', startAIAnalysis); 21258 } 21259 21260 // Removed share and add to cart buttons - replaced with AI disclaimer 21261 } 21262 21263 /** 21264 * Setup product detail compare button 21265 */ 21266 function setupProductCompareButton(button) { 21267 const productId = parseInt(button.dataset.productId); 21268 if (!productId) return; 21269 21270 // Check if product is already in compare list 21271 updateProductCompareButtonState(button, productId); 21272 21273 button.addEventListener('click', async function() { 21274 // Get current variant ID (if any) 21275 const variantIdElement = document.getElementById('pd-variant-id'); 21276 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21277 21278 // Update button's variant data for current selection 21279 button.dataset.variantId = variantId.toString(); 21280 21281 await toggleProductInCompare(productId, button, variantId); 21282 }); 21283 } 21284 21285 /** 21286 * Toggle product in compare list 21287 */ 21288 async function toggleProductInCompare(productId, button, variantId = 0) { 21289 // Check by product ID (variants of same product treated as same item) 21290 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21291 21292 try { 21293 if (isInCompare) { 21294 await removeFromCompare(productId); 21295 showToast('Produk dihapus dari perbandingan', 'success'); 21296 } else { 21297 await addToCompare(productId, variantId); 21298 showToast('Produk ditambahkan ke perbandingan', 'success'); 21299 21300 // Trigger fly-to-compare animation 21301 triggerFlyToCompareAnimation(button); 21302 } 21303 21304 updateProductCompareButtonState(button, productId); 21305 updateCompareBadge(); 21306 updateAllCompareButtons(); 21307 21308 } catch (error) { 21309 showToast(error.message || 'Terjadi kesalahan', 'error'); 21310 } 21311 } 21312 21313 /** 21314 * Update product compare button state 21315 */ 21316 function updateProductCompareButtonState(button, productId) { 21317 // Ensure both IDs are compared as integers 21318 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21319 button.classList.toggle('added', isInCompare); 21320 21321 // Handle visibility of text elements 21322 const compareText = button.querySelector('.compare-text'); 21323 const compareRemove = button.querySelector('.compare-remove'); 21324 const compareNote = document.getElementById('compare-note'); 21325 21326 if (compareText) { 21327 compareText.style.display = isInCompare ? 'none' : 'inline'; 21328 } 21329 21330 if (compareRemove) { 21331 if (isInCompare) { 21332 compareRemove.removeAttribute('hidden'); 21333 compareRemove.style.display = 'flex'; 21334 } else { 21335 compareRemove.setAttribute('hidden', ''); 21336 compareRemove.style.display = 'none'; 21337 } 21338 } 21339 21340 // Update info note 21341 if (compareNote) { 21342 if (isInCompare) { 21343 const currentCount = compareList.length; 21344 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21345 } else { 21346 compareNote.textContent = 'Bandingkan hingga 5 panduan dengan bantuan AI'; 21347 } 21348 } 21349 21350 // Inline compare preview removed - keeping it simple 21351 } 21352 21353 /** 21354 * Add product to compare list 21355 */ 21356 async function addToCompare(productId, variantId = 0) { 21357 const requestData = { product_id: productId }; 21358 if (variantId > 0) { 21359 requestData.variant_id = variantId; 21360 } 21361 21362 const response = await fetch('/api/compare/add.php', { 21363 method: 'POST', 21364 headers: { 21365 'Content-Type': 'application/json', 21366 }, 21367 body: JSON.stringify(requestData), 21368 credentials: 'same-origin' 21369 });
21370 21371 const result = await response.json(); 21372 21373 if (!result.success) { 21374 throw new Error(result.error || 'Gagal menambahkan produk'); 21375 } 21376 21377 // Send Google Analytics event for add to compare 21378 if (typeof gtag === 'function') { 21379 gtag('event', 'add_to_compare', { 21380 event_category: 'Product Compare', 21381 event_label: 'Product Added to Compare', 21382 product_id: productId, 21383 variant_id: variantId || 0, 21384 custom_map: { 21385 custom_parameter_1: 'compare_add_product' 21386 } 21387 }); 21388 } 21389 21390 await loadCompareList(); 21391 return result; 21392 } 21393 21394 /** 21395 * Remove product from compare list 21396 */ 21397 async function removeFromCompare(productId) { 21398 const response = await fetch('/api/compare/remove.php', { 21399 method: 'POST', 21400 headers: { 21401 'Content-Type': 'application/json', 21402 }, 21403 body: JSON.stringify({ product_id: productId }), 21404 credentials: 'same-origin' 21405 }); 21406 21407 const result = await response.json(); 21408 21409 if (!result.success) { 21410 throw new Error(result.error || 'Gagal menghapus produk'); 21411 } 21412 21413 // Send Google Analytics event for remove from compare 21414 if (typeof gtag === 'function') { 21415 gtag('event', 'remove_from_compare', { 21416 event_category: 'Product Compare', 21417 event_label: 'Product Removed from Compare', 21418 product_id: productId, 21419 custom_map: { 21420 custom_parameter_1: 'compare_remove_product' 21421 } 21422 }); 21423 } 21424 21425 await loadCompareList(); 21426 return result; 21427 } 21428 21429 /** 21430 * Clear all products from compare list 21431 */ 21432 async function clearAllProducts() { 21433 if (compareList.length === 0) return; 21434 21435 // Show confirmation modal 21436 const confirmed = await showConfirmModal({ 21437 title: 'Hapus Semua Produk', 21438 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21439 confirmText: 'Hapus Semua', 21440 cancelText: 'Batal', 21441 type: 'danger' 21442 }); 21443 21444 if (!confirmed) return; 21445 21446 try { 21447 const response = await fetch('/api/compare/clear.php', { 21448 method: 'POST', 21449 credentials: 'same-origin' 21450 }); 21451 21452 const result = await response.json(); 21453 21454 if (!result.success) { 21455 throw new Error(result.error || 'Gagal menghapus produk'); 21456 } 21457 21458 // Send Google Analytics event for clear all products 21459 if (typeof gtag === 'function') { 21460 gtag('event', 'compare_clear_all', { 21461 event_category: 'Product Compare', 21462 event_label: 'Clear All Products', 21463 products_count: compareList.length, 21464 custom_map: { 21465 custom_parameter_1: 'compare_clear_action' 21466 } 21467 }); 21468 } 21469 21470 await loadCompareList(); 21471 showToast('Semua produk dihapus dari perbandingan', 'success'); 21472 21473 } catch (error) { 21474 showToast(error.message || 'Terjadi kesalahan', 'error'); 21475 } 21476 } 21477 21478 /** 21479 * Load compare list from server 21480 */ 21481 async function loadCompareList() { 21482 try { 21483 const response = await fetch('/api/compare/get.php', { 21484 credentials: 'same-origin' 21485 }); 21486 21487 const result = await response.json(); 21488 21489 if (result.success) { 21490 compareList = result.products || []; 21491 updateCompareSidebar(); 21492 updateCompareBadge(); 21493 updateAllCompareButtons(); 21494 } 21495 21496 } catch (error) { 21497 // Silently handle error - compare functionality will work with empty list 21498 } 21499 } 21500 21501 /** 21502 * Update compare sidebar content 21503 */ 21504 function updateCompareSidebar() { 21505 const emptyState = document.getElementById('compareEmpty'); 21506 const listContainer = document.getElementById('compareList'); 21507 const actionsContainer = document.getElementById('compareActions'); 21508 const countElement = document.getElementById('compareCountSidebar'); 21509 21510 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21511 21512 // Update count 21513 countElement.textContent = `(${compareList.length})`; 21514 21515 if (compareList.length === 0) { 21516 emptyState.style.display = 'block'; 21517 listContainer.style.display = 'none'; 21518 actionsContainer.style.display = 'none'; 21519 } else { 21520 emptyState.style.display = 'none'; 21521 listContainer.style.display = 'block'; 21522 actionsContainer.style.display = 'flex'; 21523 21524 // Render product list 21525 listContainer.innerHTML = compareList.map(product => { 21526 // Display variant name if available 21527 let displayName = product.name; 21528 if (product.combination_name) { 21529 displayName += ` (${product.combination_name})`; 21530 } 21531 21532 // Use variant price if available 21533 let displayPrice = product.price; 21534 if (product.variant_price !== undefined && product.variant_price !== null) { 21535 displayPrice = product.variant_price; 21536 } 21537 21538 return ` 21539 <div class="compare-item" data-product-id="${product.id}"> 21540 <div class="compare-item-image"> 21541 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21542 </div> 21543 <div class="compare-item-info"> 21544 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21545 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21546 </div> 21547 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21548 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21549 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 21550 </svg> 21551 </button> 21552 </div> 21553 `; 21554 }).join(''); 21555 21556 // Setup event listeners for remove buttons 21557 setupRemoveButtons(); 21558 21559 // Update analyze button state 21560 const analyzeBtn = document.getElementById('compareAnalyze'); 21561 if (analyzeBtn) { 21562 analyzeBtn.disabled = compareList.length < 2; 21563 } 21564 } 21565 } 21566 21567 /** 21568 * Setup remove buttons event listeners 21569 */ 21570 function setupRemoveButtons() { 21571 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21572 removeButtons.forEach(button => { 21573 button.addEventListener('click', async (e) => { 21574 e.preventDefault(); 21575 const productId = parseInt(button.dataset.productId); 21576 if (productId) { 21577 await removeProduct(productId); 21578 } 21579 }); 21580 }); 21581 } 21582 21583 /** 21584 * Update all compare buttons on the page 21585 */ 21586 function updateAllCompareButtons() { 21587 // Update product detail compare button 21588 const productBtn = document.getElementById('pd-compare-btn'); 21589 if (productBtn) { 21590 const productId = parseInt(productBtn.dataset.productId); 21591 if (productId) { 21592 updateProductCompareButtonState(productBtn, productId); 21593 } 21594 } 21595 21596 // Update any other compare buttons on the page 21597 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21598 allCompareButtons.forEach(button => { 21599 const productId = parseInt(button.dataset.productId); 21600 if (productId && button.id !== 'pd-compare-btn') { 21601 updateProductCompareButtonState(button, productId); 21602 } 21603 }); 21604 } 21605 21606 /** 21607 * Update compare badge 21608 */ 21609 function updateCompareBadge() { 21610 const badge = document.getElementById('compareCount'); 21611 if (!badge) return; 21612 21613 const oldCount = parseInt(badge.textContent) || 0; 21614 const newCount = compareList.length; 21615 21616 badge.textContent = newCount; 21617 badge.classList.toggle('active', newCount > 0); 21618 21619 // Animate badge if count increased 21620 if (newCount > oldCount) { 21621 animateCompareBadge(); 21622 highlightCompareButton(); 21623 } 21624 21625 // Sticky bar removed - keeping only fly-to-navbar animation 21626 } 21627 21628 /** 21629 * Animate compare badge when new product added 21630 */ 21631 function animateCompareBadge() { 21632 const badge = document.getElementById('compareCount'); 21633 if (!badge) return; 21634 21635 badge.style.transform = 'scale(1.3)'; 21636 badge.style.transition = 'transform 0.3s ease'; 21637 21638 setTimeout(() => { 21639 badge.style.transform = 'scale(1)'; 21640 }, 300); 21641 } 21642 21643 /** 21644 * Highlight compare button temporarily 21645 */ 21646 function highlightCompareButton() { 21647 const compareBtn = document.getElementById('compareBtn'); 21648 if (!compareBtn) return; 21649 21650 compareBtn.classList.add('pulse-highlight'); 21651 21652 setTimeout(() => { 21653 compareBtn.classList.remove('pulse-highlight'); 21654 }, 3000); 21655 } 21656 21657 /** 21658 * Trigger fly-to-compare animation 21659 */ 21660 function triggerFlyToCompareAnimation(sourceButton) { 21661 if (!sourceButton) { 21662 return; 21663 } 21664 21665 // Check if mobile view (bottom nav) or desktop (top nav) 21666 const isMobile = window.innerWidth <= 768; 21667 21668 // Get source position 21669 const sourceRect = sourceButton.getBoundingClientRect(); 21670 21671 // Calculate target position 21672 let targetTop, targetLeft; 21673 21674 if (isMobile) { 21675 // Mobile: fly to fixed bottom position (bottom nav area) 21676 // Use fixed coordinates regardless of bottom nav visibility 21677 targetTop = window.innerHeight - 80; // Just above bottom nav area 21678 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21679 } else { 21680 // Desktop: fly to top navbar compare button 21681 const targetElement = document.getElementById('compareBtn'); 21682 if (!targetElement) { 21683 return; 21684 } 21685 const targetRect = targetElement.getBoundingClientRect(); 21686 targetTop = targetRect.top + 10; 21687 targetLeft = targetRect.left + 10; 21688 } 21689 21690 // Get product image for animation - try multiple selectors 21691 let productImage = document.querySelector('#pd-main-img, .product-gallery img, .pd-image img, .product-image img, .product-main-image img, img[alt*="produk"], img[src*="product"]'); 21692 21693 // Create animated element 21694 const flyingElement = document.createElement('div'); 21695 flyingElement.className = 'flying-product'; 21696 21697 if (productImage) { 21698 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21699 flyingElement.querySelector('img').style.cssText = ` 21700 width: 100%; 21701 height: 100%; 21702 object-fit: cover; 21703 border-radius: 8px; 21704 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21705 `; 21706 } else { 21707 // Fallback: colored div with icon 21708 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21709 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21710 width: 100%; 21711 height: 100%; 21712 background: #3b82f6; 21713 border-radius: 8px; 21714 display: flex; 21715 align-items: center; 21716 justify-content: center; 21717 font-size: 24px; 21718 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21719 `; 21720 } 21721 21722 flyingElement.style.cssText = ` 21723 position: fixed; 21724 top: ${sourceRect.top}px; 21725 left: ${sourceRect.left}px; 21726 width: 60px; 21727 height: 60px; 21728 z-index: 9999; 21729 pointer-events: none; 21730 opacity: 1;
21731 transform: scale(1); 21732 `; 21733 21734 document.body.appendChild(flyingElement); 21735 21736 // Use Web Animations API for more reliable animation 21737 flyingElement.animate([ 21738 // Keyframe 1: Initial state 21739 { 21740 top: `${sourceRect.top}px`, 21741 left: `${sourceRect.left}px`, 21742 transform: 'scale(1)', 21743 opacity: 1 21744 }, 21745 // Keyframe 2: Final state 21746 { 21747 top: `${targetTop}px`, 21748 left: `${targetLeft}px`, 21749 transform: 'scale(0.3)', 21750 opacity: 0 21751 } 21752 ], { 21753 duration: 800, 21754 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21755 fill: 'forwards' 21756 }); 21757 21758 // Clean up after animation completes 21759 setTimeout(() => { 21760 if (flyingElement.parentNode) { 21761 flyingElement.parentNode.removeChild(flyingElement); 21762 } 21763 }, 850); 21764 } 21765 21766 // Sticky compare bar functions removed - using only fly-to-navbar animation 21767 21768 /** 21769 * Toggle compare sidebar 21770 */ 21771 function toggleCompareSidebar() { 21772 if (!compareSidebar) return; 21773 21774 const isOpen = compareSidebar.classList.contains('open'); 21775 21776 if (isOpen) { 21777 closeCompareSidebar(); 21778 } else { 21779 openCompareSidebar(); 21780 } 21781 } 21782 21783 /** 21784 * Open compare sidebar 21785 */ 21786 function openCompareSidebar() { 21787 if (!compareSidebar || !compareOverlay) return; 21788 21789 compareSidebar.classList.add('open'); 21790 compareOverlay.classList.add('active'); 21791 document.body.style.overflow = 'hidden'; 21792 21793 // Send Google Analytics event for compare sidebar open 21794 if (typeof gtag === 'function') { 21795 gtag('event', 'compare_sidebar_open', { 21796 event_category: 'Product Compare', 21797 event_label: 'Compare Sidebar Opened', 21798 products_count: compareList.length, 21799 custom_map: { 21800 custom_parameter_1: 'compare_sidebar_view' 21801 } 21802 }); 21803 } 21804 } 21805 21806 /** 21807 * Close compare sidebar 21808 */ 21809 function closeCompareSidebar() { 21810 if (!compareSidebar || !compareOverlay) return; 21811 21812 compareSidebar.classList.remove('open'); 21813 compareOverlay.classList.remove('active'); 21814 document.body.style.overflow = ''; 21815 } 21816 21817 /** 21818 * Start AI analysis 21819 */ 21820 async function startAIAnalysis() { 21821 if (compareList.length < 2) { 21822 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21823 return; 21824 } 21825 21826 if (isAnalyzing) return; 21827 21828 // Send Google Analytics event for AI Analysis start 21829 if (typeof gtag === 'function') { 21830 gtag('event', 'ai_analysis_start', { 21831 event_category: 'Product Compare', 21832 event_label: 'AI Analysis Button Click', 21833 products_count: compareList.length, 21834 product_ids: compareList.map(p => p.id).join(','), 21835 custom_map: { 21836 custom_parameter_1: 'compare_ai_analysis' 21837 } 21838 }); 21839 } 21840 21841 // Check if products are in similar categories 21842 const mismatchInfo = await checkProductCategoryMismatch(); 21843 if (mismatchInfo.shouldWarn) { 21844 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21845 const confirmed = await showConfirmModal({ 21846 title: 'Produk Berbeda Kategori', 21847 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21848 confirmText: 'Ya, Analisis Tetap', 21849 cancelText: 'Batal', 21850 type: 'warning' 21851 }); 21852 21853 if (!confirmed) return; 21854 } 21855 21856 openModal(); 21857 showLoadingState(); 21858 isAnalyzing = true; 21859 21860 try { 21861 const response = await fetch('/api/compare/ai-analysis.php', { 21862 method: 'POST', 21863 credentials: 'same-origin' 21864 }); 21865 21866 const result = await response.json(); 21867 21868 if (!result.success) { 21869 throw new Error(result.error || 'Analisis AI gagal'); 21870 } 21871 21872 currentAnalysis = result; 21873 showAnalysisResults(result); 21874 21875 // Send Google Analytics event for successful AI Analysis 21876 if (typeof gtag === 'function') { 21877 gtag('event', 'ai_analysis_success', { 21878 event_category: 'Product Compare', 21879 event_label: 'AI Analysis Completed', 21880 products_count: compareList.length, 21881 analysis_type: 'product_comparison', 21882 custom_map: { 21883 custom_parameter_1: 'compare_ai_success' 21884 } 21885 }); 21886 } 21887 21888 } catch (error) { 21889 showErrorState(error.message); 21890 21891 // Send Google Analytics event for failed AI Analysis 21892 if (typeof gtag === 'function') { 21893 gtag('event', 'ai_analysis_error', { 21894 event_category: 'Product Compare', 21895 event_label: 'AI Analysis Failed', 21896 products_count: compareList.length, 21897 error_message: error.message, 21898 custom_map: { 21899 custom_parameter_1: 'compare_ai_error' 21900 } 21901 }); 21902 } 21903 } finally { 21904 isAnalyzing = false; 21905 } 21906 } 21907 21908 /** 21909 * Show modal 21910 */ 21911 function openModal() { 21912 if (!compareModal) return; 21913 compareModal.classList.add('open'); 21914 document.body.style.overflow = 'hidden'; 21915 } 21916 21917 /** 21918 * Close modal 21919 */ 21920 function closeModal() { 21921 if (!compareModal) return; 21922 compareModal.classList.remove('open'); 21923 document.body.style.overflow = ''; 21924 } 21925 21926 /** 21927 * Show loading state 21928 */ 21929 function showLoadingState() { 21930 const loading = document.getElementById('aiLoading'); 21931 const error = document.getElementById('aiError'); 21932 const results = document.getElementById('aiResults'); 21933 21934 if (loading) loading.style.display = 'block'; 21935 if (error) error.hidden = true; 21936 if (results) results.hidden = true; 21937 } 21938 21939 /** 21940 * Show error state 21941 */ 21942 function showErrorState(message) { 21943 const loading = document.getElementById('aiLoading'); 21944 const error = document.getElementById('aiError'); 21945 const results = document.getElementById('aiResults'); 21946 const errorMessage = document.getElementById('aiErrorMessage'); 21947 21948 if (loading) loading.style.display = 'none'; 21949 if (error) error.hidden = false; 21950 if (results) results.hidden = true; 21951 if (errorMessage) errorMessage.textContent = message; 21952 } 21953 21954 /** 21955 * Show analysis results 21956 */ 21957 function showAnalysisResults(analysis) { 21958 const loading = document.getElementById('aiLoading'); 21959 const error = document.getElementById('aiError'); 21960 const results = document.getElementById('aiResults'); 21961 21962 if (loading) loading.style.display = 'none'; 21963 if (error) error.hidden = true; 21964 if (results) results.hidden = false; 21965 21966 const data = analysis.data; 21967 21968 // Update summary 21969 const summaryElement = document.getElementById('aiSummary'); 21970 if (summaryElement) summaryElement.textContent = data.summary || ''; 21971 21972 const winnerProduct = document.getElementById('winnerProduct'); 21973 const winnerReason = document.getElementById('winnerReason'); 21974 if (winnerProduct && winnerReason && data.winner_overall) { 21975 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 21976 winnerProduct.textContent = winner ? winner.name : 'Tidak diketa
21976hui'; 21977 winnerReason.textContent = data.winner_reason || ''; 21978 } 21979 21980 // Update comparison table 21981 updateComparisonTable(data.comparison_table); 21982 21983 // Update detailed analysis 21984 updateDetailedAnalysis(data.detailed_analysis); 21985 21986 // Update recommendations 21987 updateRecommendations(data.recommendations); 21988 21989 // Update buying tips 21990 updateBuyingTips(data.buying_tips); 21991 } 21992 21993 /** 21994 * Update comparison table 21995 */ 21996 function updateComparisonTable(comparisonData) { 21997 const container = document.getElementById('comparisonTable'); 21998 if (!container || !comparisonData?.categories) return; 21999 22000 // First, create product header row with names 22001 const productHeaderHtml = ` 22002 <div class="comparison-category"> 22003 <div class="category-header">Produk</div> 22004 <div class="category-products"> 22005 ${compareList.map(product => ` 22006 <div class="product-score"> 22007 <div class="product-name-header"> 22008 <strong>${escapeHtml(product.name)}</strong> 22009 </div> 22010 <div class="product-price"> 22011 ${formatPrice(product.price)} 22012 </div> 22013 </div> 22014 `).join('')} 22015 </div> 22016 </div> 22017 `; 22018 22019 // Then create comparison categories 22020 const categoriesHtml = comparisonData.categories.map(category => ` 22021 <div class="comparison-category"> 22022 <div class="category-header">${escapeHtml(category.name)}</div> 22023 <div class="category-products"> 22024 ${compareList.map(product => { 22025 const productData = category.products?.[product.id]; 22026 return ` 22027 <div class="product-score"> 22028 <div class="score-value"> 22029 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22030 <span>${productData?.score || 0}/5</span> 22031 </div> 22032 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22033 </div> 22034 `; 22035 }).join('')} 22036 </div> 22037 </div> 22038 `).join(''); 22039 22040 container.innerHTML = productHeaderHtml + categoriesHtml; 22041 } 22042 22043 /** 22044 * Update detailed analysis 22045 */ 22046 function updateDetailedAnalysis(analysisData) { 22047 const container = document.getElementById('detailedAnalysis'); 22048 if (!container || !analysisData) return; 22049 22050 container.innerHTML = compareList.map(product => { 22051 const productAnalysis = analysisData[product.id]; 22052 if (!productAnalysis) return ''; 22053 22054 // Use product name from API response if available, fallback to local name 22055 const productName = productAnalysis.product_name || product.name; 22056 22057 return ` 22058 <div class="product-analysis"> 22059 <h5>${escapeHtml(productName)}</h5> 22060 22061 ${productAnalysis.strengths ? ` 22062 <div class="analysis-section"> 22063 <h6 class="strengths">â Kelebihan</h6> 22064 <ul> 22065 ${productAnalysis.strengths.map(strength => 22066 `<li>${escapeHtml(strength)}</li>` 22067 ).join('')} 22068 </ul> 22069 </div> 22070 ` : ''} 22071 22072 ${productAnalysis.weaknesses ? ` 22073 <div class="analysis-section"> 22074 <h6 class="weaknesses">â Kekurangan</h6> 22075 <ul> 22076 ${productAnalysis.weaknesses.map(weakness => 22077 `<li>${escapeHtml(weakness)}</li>` 22078 ).join('')} 22079 </ul> 22080 </div> 22081 ` : ''} 22082 22083 ${productAnalysis.best_for ? ` 22084 <div class="analysis-section"> 22085 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22086 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22087 </div> 22088 ` : ''} 22089 </div> 22090 `; 22091 }).filter(Boolean).join(''); 22092 } 22093 22094 /** 22095 * Update recommendations 22096 */ 22097 function updateRecommendations(recommendations) { 22098 if (!recommendations) return; 22099 22100 const budgetEl = document.getElementById('budgetRecommendation'); 22101 const performanceEl = document.getElementById('performanceRecommendation'); 22102 const balancedEl = document.getElementById('balancedRecommendation'); 22103 22104 if (budgetEl && recommendations.budget_conscious) { 22105 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22106 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22107 } 22108 22109 if (performanceEl && recommendations.performance_focused) { 22110 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22111 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22112 } 22113 22114 if (balancedEl && recommendations.balanced) { 22115 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22116 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22117 } 22118 } 22119 22120 /** 22121 * Check if products are from different categories 22122 */ 22123 async function checkProductCategoryMismatch() { 22124 if (compareList.length < 2) { 22125 return { shouldWarn: false, categories: new Set() }; 22126 } 22127 22128 // Get the root categories from each product 22129 const rootCategories = new Set(); 22130 const productCategories = []; 22131 22132 for (const product of compareList) { 22133 // Extract root category from the product's category_path if available 22134 const rootCategory = await getProductRootCategory(product); 22135 if (rootCategory) { 22136 rootCategories.add(rootCategory); 22137 productCategories.push({ 22138 product: product.name, 22139 category: rootCategory 22140 }); 22141 } 22142 } 22143 22144 // If we have more than 1 different root categories, show warning 22145 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22146 22147 return { 22148 shouldWarn: shouldWarn, 22149 categories: rootCategories, 22150 productCategories: productCategories 22151 }; 22152 } 22153 22154 /** 22155 * Get product root category from category_path or fallback to name detection 22156 */ 22157 async function getProductRootCategory(product) { 22158 // First try to get from category_path if available 22159 if (product.category_path) { 22160 const rootCategory = product.category_path.split('/')[0]; 22161 return rootCategory; 22162 } 22163 22164 // If no category_path, try to fetch from product detail or use name detection 22165 const detectedCategory = detectProductCategory(product.name); 22166 return detectedCategory; 22167 } 22168 22169 /** 22170 * Detect product category from name (fallback method) 22171 */ 22172 function detectProductCategory(productName) { 22173 const name = productName.toLowerCase(); 22174 22175 // Map keywords to actual root categories from the API structure 22176 const categoryMappings = { 22177 // Komputer & Laptop 22178 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22179 22180 // Printer & Scanner 22181 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22182 22183 // Gadget 22184 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22185 22186 // Periferal
22187 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22188 22189 // Audio 22190 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22191 22192 // Jaringan 22193 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22194 22195 // Penyimpanan 22196 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22197 22198 // Komponen PC 22199 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22200 22201 // UPS & Power 22202 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22203 22204 // Proyektor 22205 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22206 22207 // Security System 22208 'Security System': ['cctv', 'security camera', 'surveillance'], 22209 22210 // Aksesoris 22211 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22212 22213 // Software 22214 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22215 22216 // Perlengkapan Kantor 22217 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22218 }; 22219 22220 // Check which root category matches 22221 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22222 for (const keyword of keywords) { 22223 if (name.includes(keyword)) { 22224 return rootCategory; 22225 } 22226 } 22227 } 22228 22229 return null; 22230 } 22231 22232 /** 22233 * Update buying tips 22234 */ 22235 function updateBuyingTips(tips) { 22236 const container = document.getElementById('buyingTips'); 22237 if (!container || !Array.isArray(tips)) return; 22238 22239 container.innerHTML = tips.map(tip => 22240 `<li>${escapeHtml(tip)}</li>` 22241 ).join(''); 22242 } 22243 22244 // Removed share and cart functions - replaced with AI disclaimer in UI 22245 22246 /** 22247 * Remove product from compare (public method) 22248 */ 22249 async function removeProduct(productId) { 22250 try { 22251 await removeFromCompare(productId); 22252 showToast('Produk dihapus dari perbandingan', 'success'); 22253 22254 // Update all compare buttons 22255 updateAllCompareButtons(); 22256 22257 } catch (error) { 22258 showToast(error.message || 'Terjadi kesalahan', 'error'); 22259 } 22260 } 22261 22262 /** 22263 * Utility functions 22264 */ 22265 function escapeHtml(text) { 22266 const div = document.createElement('div'); 22267 div.textContent = text; 22268 return div.innerHTML; 22269 } 22270 22271 function formatPrice(price) { 22272 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22273 } 22274 22275 /** 22276 * Show confirmation modal using the existing modal.css design 22277 */ 22278 function showConfirmModal(options = {}) { 22279 return new Promise((resolve) => { 22280 const { 22281 title = 'Konfirmasi', 22282 message = 'Apakah Anda yakin?', 22283 confirmText = 'Ya', 22284 cancelText = 'Tidak', 22285 type = 'warning' 22286 } = options; 22287 22288 // Create modal HTML 22289 const modalHTML = ` 22290 <div class="modal-overlay" id="confirmModal"> 22291 <div class="modal-dialog"> 22292 <div class="modal-header"> 22293 <h3 class="modal-title"> 22294 <div class="modal-icon ${type}"> 22295 ${getModalIcon(type)} 22296 </div> 22297 ${escapeHtml(title)} 22298 </h3> 22299 </div> 22300 <div class="modal-body"> 22301 <p class="modal-message">${escapeHtml(message)}</p> 22302 <div class="modal-actions"> 22303 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22304 ${escapeHtml(cancelText)} 22305 </button> 22306 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22307 ${escapeHtml(confirmText)} 22308 </button> 22309 </div> 22310 </div> 22311 </div> 22312 </div> 22313 `; 22314 22315 // Add to DOM 22316 document.body.insertAdjacentHTML('beforeend', modalHTML); 22317 const modal = document.getElementById('confirmModal'); 22318 const cancelBtn = document.getElementById('confirmCancel'); 22319 const okBtn = document.getElementById('confirmOk'); 22320 22321 // Show modal 22322 requestAnimationFrame(() => { 22323 modal.classList.add('show'); 22324 }); 22325 22326 // Handle events 22327 function cleanup() { 22328 modal.remove(); 22329 } 22330 22331 function handleCancel() { 22332 modal.classList.remove('show'); 22333 setTimeout(() => { 22334 cleanup(); 22335 resolve(false); 22336 }, 200); 22337 } 22338 22339 function handleConfirm() { 22340 modal.classList.remove('show'); 22341 setTimeout(() => { 22342 cleanup(); 22343 resolve(true); 22344 }, 200); 22345 } 22346 22347 cancelBtn.addEventListener('click', handleCancel); 22348 okBtn.addEventListener('click', handleConfirm); 22349 22350 // Close on overlay click 22351 modal.addEventListener('click', (e) => { 22352 if (e.target === modal) { 22353 handleCancel(); 22354 } 22355 }); 22356 22357 // Close on escape 22358 document.addEventListener('keydown', function escapeHandler(e) { 22359 if (e.key === 'Escape') { 22360 document.removeEventListener('keydown', escapeHandler); 22361 handleCancel(); 22362 } 22363 }); 22364 22365 // Focus on confirm button 22366 setTimeout(() => okBtn.focus(), 100); 22367 });
22368 } 22369 22370 /** 22371 * Get icon for modal type 22372 */ 22373 function getModalIcon(type) { 22374 const icons = { 22375 danger: 'ð¨ ', 22376 warning: 'â ï¸', 22377 success: 'â ', 22378 info: 'â¹ï¸' 22379 }; 22380 return icons[type] || icons.info; 22381 } 22382 22383 22384 22385 function showToast(message, type = 'info') { 22386 // Integration with existing toast system or create simple one 22387 22388 // Simple alert fallback - replace with proper toast system 22389 if (type === 'error') { 22390 alert('Error: ' + message); 22391 } 22392 } 22393 22394 // Inline compare preview functions removed - keeping it simple and clean 22395 22396 /** 22397 * Get current product ID from page 22398 */ 22399 function getCurrentProductId() { 22400 const button = document.getElementById('pd-compare-btn'); 22401 return button ? parseInt(button.dataset.productId) : null; 22402 } 22403 22404 // hideStickyCompareBar function removed - no longer using sticky bar 22405 22406 // Public API 22407 window.productCompare = { 22408 removeProduct: removeProduct, 22409 addToCompare: addToCompare, 22410 loadCompareList: loadCompareList, 22411 getCompareList: function() { return compareList; }, 22412 openSidebar: openCompareSidebar, 22413 startAnalysis: startAIAnalysis, 22414 // Helper function to add from external sources with variant support 22415 addProductWithVariant: async function(productId, variantId = 0) { 22416 return await addToCompare(productId, variantId); 22417 } 22418 }; 22419 22420})(); 22421 22422 22423 22424 22425 22426</script>
vendor: 2 bytes, line 22426
22426
22427<script data-inlined="product-card-actions.js"> 22428/** 22429 * Product Card Quick Actions 22430 * Bottom sheet menu for quick add to cart & compare 22431 * Integrates with existing product-compare.js and cart.js 22432 */ 22433 22434(function() { 22435 'use strict'; 22436 22437 let activeSheet = null; 22438 let activeProductData = null; 22439 22440 // DOM elements (will be created dynamically) 22441 let overlay = null; 22442 let bottomSheet = null; 22443 22444 /** 22445 * Initialize on DOM ready 22446 */ 22447 document.addEventListener('DOMContentLoaded', function() { 22448 createBottomSheet(); 22449 attachEventListeners(); 22450 }); 22451 22452 /** 22453 * Create bottom sheet HTML structure 22454 */ 22455 function createBottomSheet() { 22456 // Create overlay 22457 overlay = document.createElement('div'); 22458 overlay.className = 'pca-overlay'; 22459 overlay.addEventListener('click', closeBottomSheet); 22460 22461 // Create bottom sheet 22462 bottomSheet = document.createElement('div'); 22463 bottomSheet.className = 'pca-bottom-sheet'; 22464 bottomSheet.innerHTML = ` 22465 <div class="pca-sheet-handle"></div> 22466 <div class="pca-sheet-product" id="pca-product-preview"> 22467 <!-- Product info will be inserted here --> 22468 </div> 22469 <div class="pca-sheet-actions"> 22470 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22471 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22472 <path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/> 22473 </svg> 22474 <div class="pca-action-content"> 22475 <div class="pca-action-label">Tambah ke Keranjang</div> 22476 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22477 </div> 22478 </button> 22479 22480 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22481 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22482 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22483 </svg> 22484 <div class="pca-action-content"> 22485 <div class="pca-action-label">Bandingkan Panduan</div> 22486 <div class="pca-action-desc">Analisis dengan AI</div> 22487 </div> 22488 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22489 </button> 22490 </div> 22491 `; 22492 22493 // Append to body 22494 document.body.appendChild(overlay); 22495 document.body.appendChild(bottomSheet); 22496 22497 // Setup action buttons 22498 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22499 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22500 22501 // Prevent clicks inside bottom sheet from closing it 22502 bottomSheet.addEventListener('click', function(e) { 22503 e.stopPropagation(); 22504 }); 22505 } 22506 22507 /** 22508 * Attach event listeners to all 3-dot buttons 22509 */ 22510 function attachEventListeners() { 22511 // Use event delegation for dynamically loaded product cards 22512 document.addEventListener('click', function(e) { 22513 const btn = e.target.closest('.prod-card-actions-btn'); 22514 if (btn) { 22515 e.preventDefault(); 22516 e.stopPropagation(); 22517 handleActionsButtonClick(btn); 22518 } 22519 }); 22520 22521 // Handle ESC key 22522 document.addEventListener('keydown', function(e) { 22523 if (e.key === 'Escape' && activeSheet) { 22524 closeBottomSheet(); 22525 } 22526 }); 22527 } 22528 22529 /** 22530 * Handle 3-dot button click 22531 */ 22532 function handleActionsButtonClick(button) { 22533 // Get product ID directly from button 22534 const productId = parseInt(button.dataset.productId) || 0; 22535 if (!productId) { 22536 console.error('Product ID not found on button:', button); 22537 showToast('Data produk tidak ditemukan', 'error'); 22538 return; 22539 } 22540 22541 // Find parent card for additional data 22542 const card = button.closest('.prod-card, .pa-card'); 22543 if (!card) { 22544 console.error('Product card not found'); 22545 showToast('Data produk tidak ditemukan', 'error'); 22546 return; 22547 } 22548 22549 // Extract product data from card 22550 const productData = extractProductData(card, productId); 22551 if (!productData) { 22552 showToast('Data produk tidak ditemukan', 'error'); 22553 return; 22554 } 22555 22556 activeProductData = productData; 22557 openBottomSheet(productData); 22558 } 22559 22560 /** 22561 * Extract product data from card element 22562 */ 22563 function extractProductData(card, productId) { 22564 try { 22565 // Product ID already passed from button 22566 if (!productId) { 22567 console.error('Product ID not provided'); 22568 return null; 22569 } 22570 // Product ID already passed from button 22571 if (!productId) { 22572 console.error('Product ID not provided'); 22573 return null; 22574 } 22575 22576 const id = productId; 22577 22578 // Get product name 22579 const nameEl = card.querySelector('.prod-name, .title'); 22580 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22581 22582 // Get product image 22583 const imgEl = card.querySelector('.thumb img, img'); 22584 const image = imgEl ? imgEl.src : '/assets/img/placeholder.png'; 22585 22586 // Get product price and detect variants 22587 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22588 let price = 0; 22589 let hasVariants = false; 22590 let priceDisplay = ''; 22591 22592 if (priceEl) { 22593 // Get text content and clean it 22594 let priceText = priceEl.textContent.trim(); 22595 priceDisplay = priceText; 22596 22597 // Check if it's a range (indicates product has variants) 22598 if (priceText.includes('-')) { 22599 hasVariants = true; 22600 // Take the first price for calculation 22601 priceText = priceText.split('-')[0].trim(); 22602 } 22603 22604 // Remove all non-numeric characters 22605 priceText = priceText.replace(/[^0-9]/g, ''); 22606 price = parseInt(priceText) || 0; 22607 } 22608 22609 // Get stock status 22610 const stockDot = card.querySelector('.stock-dot'); 22611 let stockStatus = 'unknown'; 22612 let stockLabel = 'Cek stok'; 22613 22614 if (stockDot) { 22615 if (stockDot.classList.contains('ready')) { 22616 stockStatus = 'in'; 22617 stockLabel = 'Stok tersedia'; 22618 } else if (stockDot.classList.contains('low')) { 22619 stockStatus = 'low'; 22620 stockLabel = 'Stok terbatas'; 22621 } else if (stockDot.classList.contains('out')) { 22622 stockStatus = 'out'; 22623 stockLabel = 'Stok habis'; 22624 } 22625 } 22626 22627 // Get category path (if available) 22628 const categoryPath = card.dataset.category || ''; 22629 22630 // Get URL 22631 let url = '#'; 22632 if (card.tagName === 'A') { 22633 url = card.href; 22634 } else { 22635 const link = card.querySelector('a[href*="/product/"]'); 22636 if (link) url = link.href; 22637 } 22638 22639 return { 22640 id: id, 22641 name: name, 22642 image: image, 22643 price: price, 22644 priceDisplay: priceDisplay, 22645 hasVariants: hasVariants, 22646 stockStatus: stockStatus, 22647 stockLabel: stockLabel, 22648 categoryPath: categoryPath, 22649 url: url 22650 }; 22651 } catch (error) { 22652 console.error('Error extracting product data:', error); 22653 return null; 22654 } 22655 } 22656 22657 /** 22658 * Open bottom sheet 22659 */ 22660 function openBottomSheet(productData) { 22661 if (!overlay || !bottomSheet) return; 22662 22663 // Update product preview 22664 updateProductPreview(productData); 22665 22666 // Update cart button based on variant status
22667 updateCartButtonForVariant(productData); 22668 22669 // Update compare button state 22670 updateCompareButtonState(productData.id); 22671 22672 // Show overlay and sheet 22673 overlay.classList.add('active'); 22674 bottomSheet.classList.add('active'); 22675 document.body.style.overflow = 'hidden'; 22676 22677 activeSheet = true; 22678 22679 // Google Analytics 22680 if (typeof gtag === 'function') { 22681 gtag('event', 'quick_actions_open', { 22682 event_category: 'Product Card', 22683 event_label: 'Quick Actions Menu Opened', 22684 product_id: productData.id, 22685 product_name: productData.name 22686 }); 22687 } 22688 } 22689 22690 /** 22691 * Close bottom sheet 22692 */ 22693 function closeBottomSheet() { 22694 if (!overlay || !bottomSheet) return; 22695 22696 overlay.classList.remove('active'); 22697 bottomSheet.classList.remove('active'); 22698 document.body.style.overflow = ''; 22699 22700 activeSheet = false; 22701 activeProductData = null; 22702 } 22703 22704 /** 22705 * Update product preview in bottom sheet 22706 */ 22707 function updateProductPreview(productData) { 22708 const preview = document.getElementById('pca-product-preview'); 22709 if (!preview) return; 22710 22711 // Use full price display (shows range for variants) 22712 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22713 22714 preview.innerHTML = ` 22715 <img src="${escapeHtml(productData.image)}" 22716 alt="${escapeHtml(productData.name)}" 22717 class="pca-product-image" 22718 loading="lazy"> 22719 <div class="pca-product-info"> 22720 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22721 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22722 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22723 </div> 22724 `; 22725 } 22726 22727 /** 22728 * Update cart button for variant products 22729 */ 22730 function updateCartButtonForVariant(productData) { 22731 const cartBtn = document.getElementById('pca-add-cart'); 22732 if (!cartBtn) return; 22733 22734 if (productData.hasVariants) { 22735 // Product has variants - change to "View Detail" 22736 cartBtn.dataset.hasVariants = 'true'; 22737 cartBtn.dataset.productUrl = productData.url; 22738 22739 // Update icon to eye/external 22740 const iconSvg = cartBtn.querySelector('svg'); 22741 if (iconSvg) { 22742 iconSvg.innerHTML = '<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z"/>'; 22743 } 22744 22745 // Update text 22746 const label = cartBtn.querySelector('.pca-action-label'); 22747 const desc = cartBtn.querySelector('.pca-action-desc'); 22748 if (label) label.textContent = 'Lihat Detail Produk'; 22749 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22750 22751 } else { 22752 // Single product - normal cart button 22753 cartBtn.dataset.hasVariants = 'false'; 22754 22755 // Restore cart icon 22756 const iconSvg = cartBtn.querySelector('svg'); 22757 if (iconSvg) { 22758 iconSvg.innerHTML = '<path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/>'; 22759 } 22760 22761 // Restore text 22762 const label = cartBtn.querySelector('.pca-action-label'); 22763 const desc = cartBtn.querySelector('.pca-action-desc'); 22764 if (label) label.textContent = 'Tambah ke Keranjang'; 22765 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22766 } 22767 } 22768 22769 /** 22770 * Update compare button state 22771 */ 22772 function updateCompareButtonState(productId) { 22773 const compareBtn = document.getElementById('pca-add-compare'); 22774 const compareBadge = document.getElementById('pca-compare-count'); 22775 22776 if (!compareBtn) return; 22777 22778 // Check if product is in compare list (using global compareList if available) 22779 let isInCompare = false;
22780 let compareCount = 0; 22781 22782 // Try to get compare list from product-compare.js via window.productCompare 22783 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22784 const compareList = window.productCompare.getCompareList(); 22785 compareCount = compareList.length; 22786 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22787 } else { 22788 // Fallback: check badge count 22789 const mainBadge = document.getElementById('compareCount'); 22790 if (mainBadge) { 22791 compareCount = parseInt(mainBadge.textContent) || 0; 22792 } 22793 } 22794 22795 // Update button state 22796 if (isInCompare) { 22797 compareBtn.classList.add('active'); 22798 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22799 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22800 } else { 22801 compareBtn.classList.remove('active'); 22802 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Panduan'; 22803 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22804 } 22805 22806 // Update badge 22807 if (compareBadge && compareCount > 0) { 22808 compareBadge.textContent = compareCount; 22809 compareBadge.style.display = 'inline-flex'; 22810 } else if (compareBadge) { 22811 compareBadge.style.display = 'none'; 22812 } 22813 } 22814 22815 /** 22816 * Handle add to cart 22817 */ 22818 async function handleAddToCart() { 22819 if (!activeProductData) return; 22820 22821 const button = document.getElementById('pca-add-cart'); 22822 if (!button) return; 22823 22824 // If product has variants, redirect to detail page 22825 if (activeProductData.hasVariants) { 22826 window.location.href = activeProductData.url; 22827 return; 22828 } 22829 22830 // Check stock 22831 if (activeProductData.stockStatus === 'out') { 22832 showToast('Produk sedang habis', 'warning'); 22833 return; 22834 } 22835 22836 // Show loading state 22837 button.classList.add('loading'); 22838 button.disabled = true; 22839 22840 // CSRF Token Helper 22841 function getCsrfToken() { 22842 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22843 } 22844 22845 try { 22846 // Call cart API with proper format 22847 const response = await fetch('/api/cart/add.php', { 22848 method: 'POST', 22849 headers: { 22850 'Content-Type': 'application/json', 22851 'X-CSRF-Token': getCsrfToken() 22852 }, 22853 body: JSON.stringify({ 22854 csrf_token: getCsrfToken(), 22855 product_id: activeProductData.id, 22856 qty: 1, // Use 'qty' not 'quantity' 22857 variant_id: 0, // Default variant 22858 meta: { 22859 name: activeProductData.name, 22860 image_url: activeProductData.image 22861 } 22862 }), 22863 credentials: 'same-origin' 22864 }); 22865 22866 const result = await response.json(); 22867 22868 // Check both response status and result.ok 22869 if (!response.ok || !result.ok) { 22870 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22871 } 22872 22873 // Success animation 22874 button.classList.remove('loading'); 22875 button.classList.add('success'); 22876 22877 // Update cart badge 22878 updateCartBadge(); 22879 22880 // PlazaAnalytics tracking 22881 if (window.PlazaAnalytics) { 22882 const _slug = (activeProductData.url || '').replace(/.*/product//, ''); 22883 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22884 } 22885 22886 // Show success toast 22887 showToast('Produk ditambahkan ke keranjang', 'success'); 22888 22889 // Google Analytics 22890 if (typeof gtag === 'function') { 22891 gtag('event', 'add_to_cart', { 22892 event_category: 'Ecommerce', 22893 event_label: 'Quick Add to Cart', 22894 items: [{ 22895 item_id: activeProductData.id, 22896 item_name: activeProductData.name, 22897 price: activeProductData.price, 22898 quantity: 1 22899 }] 22900 }); 22901 } 22902 22903 // Close sheet after short delay 22904 setTimeout(() => { 22905 closeBottomSheet(); 22906 button.classList.remove('success'); 22907 button.disabled = false; 22908 }, 1000); 22909 22910 } catch (error) { 22911 console.error('Add to cart error:', error); 22912 button.classList.remove('loading'); 22913 button.disabled = false; 22914 showToast(error.message || 'Terjadi kesalahan', 'error'); 22915 } 22916 } 22917 22918 /** 22919 * Handle add to compare 22920 */ 22921 async function handleAddToCompare() { 22922 if (!activeProductData) return; 22923 22924 const button = document.getElementById('pca-add-compare'); 22925 if (!button) return; 22926 22927 // Check if already in compare 22928 const isActive = button.classList.contains('active'); 22929 22930 // Check 5-product limit before adding 22931 if (!isActive) { 22932 const mainBadge = document.getElementById('compareCount'); 22933 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22934 22935 if (currentCount >= 5) { 22936 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22937 return; 22938 } 22939 } 22940 22941 // Show loading state 22942 button.classList.add('loading'); 22943 button.disabled = true; 22944 22945 try { 22946 if (isActive) { 22947 // Remove from compare 22948 await removeFromCompare(activeProductData.id); 22949 showToast('Produk dihapus dari perbandingan', 'success'); 22950 } else { 22951 // Add to compare 22952 await addToCompare(activeProductData.id); 22953 showToast('Produk ditambahkan ke perbandingan', 'success'); 22954 22955 // Trigger fly animation 22956 triggerFlyToCompareAnimation(); 22957 } 22958 22959 // Success animation 22960 button.classList.remove('loading'); 22961 button.classList.add('success'); 22962 22963 // Update compare button state 22964 updateCompareButtonState(activeProductData.id); 22965 22966 // Close sheet after short delay 22967 setTimeout(() => { 22968 closeBottomSheet(); 22969 button.classList.remove('success'); 22970 button.disabled = false; 22971 }, 800); 22972 22973 } catch (error) { 22974 console.error('Compare error:', error); 22975 button.classList.remove('loading'); 22976 button.disabled = false; 22977 showToast(error.message || 'Terjadi kesalahan', 'error'); 22978 } 22979 } 22980 22981 /** 22982 * Add to compare (calls existing API) 22983 */ 22984 async function addToCompare(productId) { 22985 const response = await fetch('/api/compare/add.php', { 22986 method: 'POST', 22987 headers: { 22988 'Content-Type': 'application/json', 22989 }, 22990 body: JSON.stringify({ 22991 product_id: productId, 22992 variant_id: 0 22993 }), 22994 credentials: 'same-origin' 22995 });
22996 22997 const result = await response.json(); 22998 22999 if (!result.success) { 23000 throw new Error(result.error || 'Gagal menambahkan produk'); 23001 } 23002 23003 // Trigger compare list reload if function exists 23004 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23005 await window.productCompare.loadCompareList(); 23006 } 23007 23008 return result; 23009 } 23010 23011 /** 23012 * Remove from compare (calls existing API) 23013 */ 23014 async function removeFromCompare(productId) { 23015 const response = await fetch('/api/compare/remove.php', { 23016 method: 'POST', 23017 headers: { 23018 'Content-Type': 'application/json', 23019 }, 23020 body: JSON.stringify({ 23021 product_id: productId 23022 }), 23023 credentials: 'same-origin' 23024 }); 23025 23026 const result = await response.json(); 23027 23028 if (!result.success) { 23029 throw new Error(result.error || 'Gagal menghapus produk'); 23030 } 23031 23032 // Trigger compare list reload if function exists 23033 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23034 await window.productCompare.loadCompareList(); 23035 } 23036 23037 return result; 23038 } 23039 23040 /** 23041 * Update cart badge 23042 */ 23043 function updateCartBadge() { 23044 // Try to call global cart update function if exists 23045 if (typeof window.updateCartBadge === 'function') { 23046 window.updateCartBadge(); 23047 } else { 23048 // Fallback: increment badge manually 23049 const badge = document.getElementById('cartCount'); 23050 if (badge) { 23051 const currentCount = parseInt(badge.textContent) || 0; 23052 badge.textContent = currentCount + 1; 23053 badge.classList.add('active'); 23054 } 23055 } 23056 } 23057 23058 /** 23059 * Trigger fly to compare animation 23060 */ 23061 function triggerFlyToCompareAnimation() { 23062 const compareBtn = document.getElementById('compareBtn'); 23063 if (!compareBtn || !activeProductData) return; 23064 23065 // Create flying element 23066 const flyingElement = document.createElement('div'); 23067 flyingElement.className = 'flying-product'; 23068 flyingElement.style.cssText = ` 23069 position: fixed; 23070 width: 60px; 23071 height: 60px; 23072 z-index: 99999; 23073 pointer-events: none; 23074 `; 23075 23076 flyingElement.innerHTML = ` 23077 <img src="${activeProductData.image}" 23078 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23079 `; 23080 23081 // Position at bottom sheet 23082 const sheetRect = bottomSheet.getBoundingClientRect(); 23083 const targetRect = compareBtn.getBoundingClientRect(); 23084 23085 flyingElement.style.top = `${sheetRect.top + 20}px`; 23086 flyingElement.style.left = `${sheetRect.left + 20}px`; 23087 23088 document.body.appendChild(flyingElement); 23089 23090 // Animate to target 23091 requestAnimationFrame(() => { 23092 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23093 flyingElement.style.top = `${targetRect.top + 10}px`; 23094 flyingElement.style.left = `${targetRect.left + 10}px`; 23095 flyingElement.style.transform = 'scale(0.3)'; 23096 flyingElement.style.opacity = '0'; 23097 }); 23098 23099 // Clean up 23100 setTimeout(() => { 23101 if (flyingElement.parentNode) { 23102 flyingElement.parentNode.removeChild(flyingElement); 23103 } 23104 }, 800); 23105 } 23106 23107 /** 23108 * Format price as Rupiah 23109 */ 23110 function formatPrice(price) { 23111 if (!price || price === 0) return 'Rp 0'; 23112 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23113 } 23114 23115 /** 23116 * Escape HTML 23117 */ 23118 function escapeHtml(text) { 23119 const div = document.createElement('div'); 23120 div.textContent = text; 23121 return div.innerHTML; 23122 } 23123 23124 /** 23125 * Show toast notification (uses global showToast if available) 23126 */ 23127 function showToast(message, type = 'info') { 23128 if (typeof window.showToast === 'function') { 23129 window.showToast({ 23130 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23131 message: message, 23132 type: type, 23133 duration: 3000 23134 }); 23135 } else { 23136 // Fallback: console log 23137 console.log(`[${type.toUpperCase()}] ${message}`); 23138 } 23139 } 23140 23141 /** 23142 * Expose functions for external access 23143 */ 23144 window.ProductCardActions = {
23145 openSheet: openBottomSheet, 23146 closeSheet: closeBottomSheet, 23147 updateCompareState: updateCompareButtonState 23148 }; 23149 23150})(); 23151 23152</script>
23152 23153
23153<script data-inlined="product-reviews.js"> 23154/** 23155 * Product Reviews - Frontend JavaScript 23156 * Handles review display, submission, and helpful voting 23157 */ 23158 23159const ProductReviews = (function() { 23160 'use strict'; 23161 23162 let currentPage = 1; 23163 let currentSort = 'helpful'; 23164 let productId = null; 23165 let isLoading = false; 23166 23167 /** 23168 * Public API: Initialize reviews for a product 23169 */ 23170 function init(pid) { 23171 productId = pid; 23172 23173 if (!productId) { 23174 console.warn('Product ID not provided to ProductReviews.init()'); 23175 return; 23176 } 23177 23178 initializeReviewTab(); 23179 loadReviews(); 23180 } 23181 23182 /** 23183 * Initialize review tab functionality 23184 */ 23185 function initializeReviewTab() { 23186 // Tab switching 23187 const tabButtons = document.querySelectorAll('.tab-btn'); 23188 tabButtons.forEach(btn => { 23189 btn.addEventListener('click', function() { 23190 const tabName = this.dataset.tab; 23191 switchTab(tabName); 23192 }); 23193 }); 23194 23195 // Sort dropdown 23196 const sortSelect = document.getElementById('review-sort'); 23197 if (sortSelect) { 23198 sortSelect.addEventListener('change', function() { 23199 currentSort = this.value; 23200 currentPage = 1; 23201 loadReviews(); 23202 }); 23203 } 23204 23205 // Load more button
23206 const loadMoreBtn = document.getElementById('load-more-reviews'); 23207 if (loadMoreBtn) { 23208 loadMoreBtn.addEventListener('click', function() { 23209 currentPage++; 23210 loadReviews(true); // Append mode 23211 }); 23212 } 23213 23214 // Write review button 23215 const writeReviewBtn = document.getElementById('write-review-btn'); 23216 if (writeReviewBtn) { 23217 writeReviewBtn.addEventListener('click', showReviewModal); 23218 } 23219 } 23220 23221 /** 23222 * Switch between product tabs 23223 */ 23224 function switchTab(tabName) { 23225 // Update button states 23226 document.querySelectorAll('.tab-btn').forEach(btn => { 23227 btn.classList.toggle('active', btn.dataset.tab === tabName); 23228 }); 23229 23230 // Update content visibility 23231 document.querySelectorAll('.tab-content').forEach(content => { 23232 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23233 }); 23234 } 23235 23236 /** 23237 * Load reviews from API 23238 */ 23239 async function loadReviews(append = false) { 23240 if (isLoading) return; 23241 isLoading = true; 23242 23243 const container = document.getElementById('reviews-list');
23244 const loadMoreBtn = document.getElementById('load-more-reviews'); 23245 23246 // Check if review section exists (product might not have reviews section) 23247 if (!container) { 23248 console.log('Review section not found - product may not have reviews yet'); 23249 isLoading = false; 23250 return; 23251 } 23252 23253 try { 23254 if (!append) { 23255 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23256 } else { 23257 loadMoreBtn.disabled = true; 23258 loadMoreBtn.textContent = 'Memuat...'; 23259 } 23260 23261 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23262 const data = await response.json(); 23263 23264 if (!data.success) { 23265 throw new Error(data.message || 'Gagal memuat ulasan'); 23266 } 23267 23268 // Update statistics (only on initial load) 23269 if (!append && data.stats) { 23270 updateReviewStats(data.stats); 23271 } 23272 23273 // Render reviews 23274 if (!append) { 23275 container.innerHTML = ''; 23276 } 23277 23278 const reviews = data.reviews || []; 23279 23280 if (reviews.length === 0 && !append) { 23281 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23282 } else { 23283 reviews.forEach(review => { 23284 container.appendChild(createReviewElement(review)); 23285 }); 23286 } 23287 23288 // Update load more button 23289 if (loadMoreBtn) { 23290 if (data.pagination.has_more) { 23291 loadMoreBtn.style.display = 'block'; 23292 loadMoreBtn.disabled = false; 23293 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23294 } else { 23295 loadMoreBtn.style.display = 'none'; 23296 } 23297 } 23298 23299 } catch (error) { 23300 console.error('Load reviews error:', error); 23301 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23302 } finally { 23303 isLoading = false; 23304 } 23305 } 23306 23307 /** 23308 * Update review statistics display 23309 */ 23310 function updateReviewStats(stats) { 23311 const avgRatingEl = document.getElementById('avg-rating'); 23312 const totalReviewsEl = document.getElementById('total-reviews'); 23313 const ratingBarsContainer = document.getElementById('rating-bars'); 23314 23315 if (avgRatingEl) { 23316 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23317 } 23318 23319 if (totalReviewsEl) { 23320 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23321 } 23322 23323 // Update rating breakdown bars 23324 if (ratingBarsContainer) { 23325 ratingBarsContainer.innerHTML = ''; 23326 stats.rating_breakdown.forEach(item => { 23327 const bar = document.createElement('div'); 23328 bar.className = 'rating-bar'; 23329 bar.innerHTML = ` 23330 <span class="rating-label">${item.rating}â </span> 23331 <div class="bar-container"> 23332 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23333 </div> 23334 <span class="rating-count">${item.count}</span> 23335 `; 23336 ratingBarsContainer.appendChild(bar); 23337 }); 23338 } 23339 23340 // Update tab count 23341 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23342 if (reviewTab && stats.total_reviews > 0) { 23343 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23344 } 23345 } 23346 23347 /** 23348 * Create review element 23349 */ 23350 function createReviewElement(review) { 23351 const div = document.createElement('div'); 23352 div.className = 'review-item'; 23353 div.dataset.reviewId = review.ID; 23354 23355 // Build stars 23356 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23357 23358 // Build images if available 23359 let imagesHtml = ''; 23360 if (review.images && review.images.length > 0) { 23361 imagesHtml = '<div class="review-images">'; 23362 review.images.forEach(img => { 23363 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23364 }); 23365 imagesHtml += '</div>'; 23366 } 23367 23368 // Verified purchase badge 23369 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23370 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23371 : ''; 23372 23373 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23374 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23375 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23376 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23376an untuk privasi">ð</span>' 23377 : ''; 23378 23379 div.innerHTML = ` 23380 <div class="review-header"> 23381 <div class="review-author"> 23382 <strong>${escapeHtml(displayName)}</strong> 23383 ${anonymousIndicator} 23384 ${verifiedBadge} 23385 </div> 23386 <div class="review-rating"> 23387 <span class="stars">${stars}</span> 23388 </div> 23389 </div> 23390 <div class="review-date">${review.created_ago}</div> 23391 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/n/g, '<br>')}</div> 23392 ${imagesHtml} 23393 <div class="review-actions"> 23394 <button class="btn-helpful" data-review-id="${review.ID}"> 23395 <span class="helpful-icon">ð</span> 23396 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23397 </button> 23398 </div> 23399 `; 23400 23401 // Add helpful button listener 23402 const helpfulBtn = div.querySelector('.btn-helpful'); 23403 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23404 23405 return div; 23406 } 23407 23408 /** 23409 * Get CSRF token from meta tag 23410 */ 23411 function getCsrfToken() { 23412 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23413 return metaTag ? metaTag.getAttribute('content') : ''; 23414 } 23415 23416 /** 23417 * Mark review as helpful 23418 */ 23419 async function markAsHelpful(reviewId) { 23420 // Check if logged in 23421 const isLoggedIn = document.body.classList.contains('logged-in') || 23422 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23423 23424 if (!isLoggedIn) { 23425 showModal({ 23426 title: 'Login Diperlukan', 23427 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23428 icon: 'info', 23429 buttons: [ 23430 { text: 'Batal', className: 'modal-btn-secondary' }, 23431 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23432 ] 23433 }); 23434 return; 23435 } 23436 23437 // Get current button element 23438 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23439 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23440 23441 if (!helpfulBtn) return; 23442 23443 // Disable button during request 23444 helpfulBtn.disabled = true; 23445 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23446 23447 try { 23448 const response = await fetch('/api/reviews/helpful.php', { 23449 method: 'POST', 23450 headers: { 23451 'Content-Type': 'application/json', 23452 'X-CSRF-Token': getCsrfToken() 23453 }, 23454 credentials: 'same-origin', 23455 body: JSON.stringify({ 23456 csrf_token: getCsrfToken(), 23457 review_id: reviewId 23458 }) 23459 }); 23460 23461 const data = await response.json(); 23462 23463 if (data.success) { 23464 // Update button count immediately based on action 23465 const currentCount = parseInt(originalText.match(/d+/)?.[0] || 0); 23466 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23467 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23468 23469 // Show success message 23470 showToast(data.message, 'success'); 23471 23472 // Re-enable button 23473 helpfulBtn.disabled = false; 23474 } else { 23475 showToast(data.message, 'error'); 23476 helpfulBtn.disabled = false; 23477 } 23478 23479 } catch (error) { 23480 console.error('Mark helpful error:', error); 23481 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23482 helpfulBtn.disabled = false; 23483 } 23484 } 23485 23486 /** 23487 * Show review submission modal (placeholder) 23488 */ 23489 function showReviewModal() { 23490 // TODO: Implement review modal 23491 // For now, redirect to account page 23492 window.location.href = '/account/orders'; 23493 } 23494 23495 /** 23496 * Show toast notification 23497 */ 23498 function showToast(message, type = 'info') { 23499 const toast = document.createElement('div'); 23500 toast.className = `review-toast toast-${type}`; 23501 toast.textContent = message; 23502 23503 document.body.appendChild(toast); 23504 23505 setTimeout(() => { 23506 toast.classList.add('show'); 23507 }, 100); 23508 23509 setTimeout(() => { 23510 toast.classList.remove('show'); 23511 setTimeout(() => toast.remove(), 300); 23512 }, 3000); 23513 } 23514 23515 /** 23516 * Show modal dialog 23517 * @param {Object} options - Modal options 23518 * @param {string} options.title - Modal title 23519 * @param {string} options.message - Modal message 23520 * @param {string} options.icon - Icon type (info, success, warning, danger) 23521 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23522 */ 23523 function showModal(options) { 23524 const {
23525 title = 'Konfirmasi', 23526 message = '', 23527 icon = 'info', 23528 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23529 } = options; 23530 23531 // Icon emoji mapping 23532 const iconEmojis = { 23533 info: 'â¹ï¸', 23534 success: 'â', 23535 warning: 'â ï¸', 23536 danger: 'â' 23537 }; 23538 23539 // Create modal HTML 23540 const modalHTML = ` 23541 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23542 <div class="modal-dialog"> 23543 <div class="modal-header"> 23544 <h3 class="modal-title"> 23545 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23546 ${escapeHtml(title)} 23547 </h3> 23548 </div> 23549 <div class="modal-body"> 23550 <p class="modal-message">${escapeHtml(message)}</p> 23551 <div class="modal-actions"> 23552 ${buttons.map((btn, index) => 23553 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23554 ).join('')} 23555 </div> 23556 </div> 23557 </div> 23558 </div> 23559 `; 23560 23561 // Append to body 23562 const tempDiv = document.createElement('div'); 23563 tempDiv.innerHTML = modalHTML; 23564 const modal = tempDiv.firstElementChild; 23565 document.body.appendChild(modal); 23566 23567 // Add event listeners to buttons 23568 buttons.forEach((btn, index) => { 23569 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23570 btnElement.addEventListener('click', () => { 23571 closeModal(modal); 23572 if (btn.callback && typeof btn.callback === 'function') { 23573 btn.callback(); 23574 } 23575 }); 23576 }); 23577 23578 // Close on overlay click 23579 modal.addEventListener('click', (e) => { 23580 if (e.target === modal) { 23581 closeModal(modal); 23582 } 23583 }); 23584 23585 // Show modal with animation 23586 setTimeout(() => modal.classList.add('show'), 10); 23587 } 23588 23589 /** 23590 * Close modal dialog 23591 */ 23592 function closeModal(modal) { 23593 modal.classList.remove('show'); 23594 setTimeout(() => modal.remove(), 200); 23595 } 23596 23597 /** 23598 * Escape HTML for XSS protection 23599 */ 23600 function escapeHtml(text) { 23601 const div = document.createElement('div'); 23602 div.textContent = text; 23603 return div.innerHTML; 23604 } 23605 23606 /** 23607 * Get product ID from URL 23608 */ 23609 function getProductIdFromUrl() { 23610 const match = window.location.pathname.match(//product/(d+)/); 23611 return match ? match[1] : null; 23612 } 23613 23614 // Public API 23615 return { 23616 init: init 23617 }; 23618 23619})(); 23620 23621</script>
23621 23622
23622<script data-inlined="review-submission.js"> 23623/** 23624 * Review Submission Modal & Handler 23625 * Handles review form display, validation, and submission 23626 */ 23627 23628(function() { 23629 'use strict'; 23630 23631 let currentProductId = null; 23632 let currentOrderId = null; 23633 let currentSoRefId = null; 23634 let selectedRating = 0; 23635 let uploadedImages = []; 23636 23637 // Get CSRF token from meta tag 23638 function getCsrfToken() { 23639 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23640 return metaTag ? metaTag.getAttribute('content') : ''; 23641 } 23642 23643 // Initialize 23644 function init() { 23645 createReviewModal(); 23646 attachEventListeners(); 23647 loadReviewableOrders(); 23648 } 23649 23650 // Create review modal HTML 23651 function createReviewModal() { 23652 const modalHTML = ` 23653 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23654 <div class="modal-content review-modal"> 23655 <div class="modal-header"> 23656 <h3 class="modal-title">Tulis Ulasan</h3> 23657 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23658 <svg width="20" height="20" viewBox="0 0 24 24"> 23659 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 23660 </svg> 23661 </button> 23662 </div> 23663 23664 <div class="modal-body"> 23665 <form id="review-form" class="review-form"> 23666 <!-- Product Info --> 23667 <div class="review-product-info" id="review-product-info"> 23668 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23669 <div class="review-product-details"> 23670 <h4 id="review-product-name"></h4> 23671 <p id="review-product-sku"></p> 23672 </div> 23673 </div> 23674 23675 <!-- Rating --> 23676 <div class="form-group"> 23677 <label class="form-label">Rating <span style="color:#ef4444;">*</span></label> 23678 <div class="star-rating-input" id="star-rating-input"> 23679 <button type="button" class="star-btn" data-rating="1">â </button> 23680 <button type="button" class="star-btn" data-rating="2">â </button> 23681 <button type="button" class="star-btn" data-rating="3">â </button> 23682 <button type="button" class="star-btn" data-rating="4">â </button> 23683 <button type="button" class="star-btn" data-rating="5">â </button> 23684 </div> 23685 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23686 </div> 23687 23688 <!-- Review Text --> 23689 <div class="form-group"> 23690 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#ef4444;">*</span></label> 23691 <textarea 23692 id="review-text" 23693 name="review_text" 23694 class="form-control" 23695 rows="6" 23696 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23697 minlength="20" 23698 maxlength="5000" 23699 required></textarea> 23700 <p class="form-hint"> 23701 <span id="review-char-count">0</span>/5000 karakter
23702 <span style="color:#059669;font-weight:500;"> 23703 ð° +1,000 point untuk ulasan ⥠100 karakter 23704 </span> 23705 </p> 23706 </div> 23707 23708 <!-- Image Upload --> 23709 <div class="form-group"> 23710 <label class="form-label">Upload Foto (Opsional)</label> 23711 <div class="image-upload-container"> 23712 <input type="file" 23713 id="review-images" 23714 name="images[]" 23715 accept="image/jpeg,image/jpg,image/png,image/webp" 23716 multiple 23717 style="display:none;"> 23718 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23719 <svg width="24" height="24" viewBox="0 0 24 24"> 23720 <path fill="currentColor" d="M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z"/> 23721 </svg> 23722 <span>Pilih Foto</span> 23723 </button> 23724 <p class="form-hint"> 23725 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23726 <span style="color:#059669;font-weight:500;"> 23727 ð° +2,000 point untuk ulasan dengan foto 23728 </span> 23729 </p> 23730 </div> 23731 <div class="image-preview-grid" id="image-preview-grid"></div> 23732 </div> 23733 23734 <!-- Point Reward Info --> 23735 <div class="point-reward-info"> 23736 <div class="pri-header"> 23737 <span>ð</span> 23738 <span>Dapatkan Point Reward</span> 23739 </div> 23740 <ul class="pri-list"> 23741 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23742 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23743 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23744 </ul> 23745 <div class="pri-total"> 23746 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23747 </div> 23748 </div> 23749 23750 <!-- Privacy Option --> 23751 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23752 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23753 <input type="checkbox" 23754 id="review-anonymous" 23755 name="is_anonymous" 23756 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23757 <span style="font-size:14px;"> 23758 <strong>ð Sembunyikan nama saya</strong> 23759 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23760 Nama Anda akan ditampilkan de
23760ngan disamarkan untuk menjaga privasi 23761 </span> 23762 </span> 23763 </label> 23764 </div> 23765 23766 <!-- Error Alert --> 23767 <div class="form-error" id="review-form-error" style="display:none;"></div> 23768 23769 <!-- Submit Buttons --> 23770 <div class="modal-footer"> 23771 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23772 <button type="submit" class="btn btn-primary" id="review-submit"> 23773 Kirim Ulasan 23774 </button> 23775 </div> 23776 </form> 23777 </div> 23778 </div> 23779 </div> 23780 `; 23781 23782 document.body.insertAdjacentHTML('beforeend', modalHTML); 23783 } 23784 23785 // Attach event listeners 23786 function attachEventListeners() { 23787 // Modal triggers - event delegation 23788 document.addEventListener('click', function(e) { 23789 const btn = e.target.closest('.btn-write-review'); 23790 if (btn) { 23791 e.preventDefault(); 23792 openReviewModal( 23793 btn.dataset.productId, 23794 btn.dataset.orderId, 23795 btn.dataset.productName, 23796 btn.dataset.productImage, 23797 btn.dataset.productSku, 23798 btn.dataset.soRefId || null 23799 ); 23800 } 23801 }); 23802 23803 // Close modal 23804 setTimeout(() => { 23805 const closeBtn = document.getElementById('review-modal-close'); 23806 const cancelBtn = document.getElementById('review-cancel'); 23807 const overlay = document.getElementById('review-modal-overlay'); 23808 23809 if (closeBtn) { 23810 closeBtn.addEventListener('click', closeReviewModal); 23811 } 23812 if (cancelBtn) { 23813 cancelBtn.addEventListener('click', closeReviewModal); 23814 } 23815 if (overlay) { 23816 overlay.addEventListener('click', function(e) { 23817 if (e.target === overlay) closeReviewModal(); 23818 }); 23819 } 23820 }, 100); 23821 23822 // Star rating 23823 setTimeout(() => { 23824 const starButtons = document.querySelectorAll('.star-btn'); 23825 starButtons.forEach(btn => { 23826 btn.addEventListener('click', function(e) { 23827 e.preventDefault(); 23828 const rating = parseInt(this.dataset.rating); 23829 setRating(rating); 23830 }); 23831 }); 23832 }, 100); 23833 23834 // Character count 23835 setTimeout(() => { 23836 const textarea = document.getElementById('review-text'); 23837 if (textarea) { 23838 textarea.addEventListener('input', function() { 23839 updateCharCount(); 23840 calculatePotentialPoints(); 23841 }); 23842 } 23843 }, 100); 23844 23845 // Image upload 23846 setTimeout(() => { 23847 const uploadTrigger = document.getElementById('upload-trigger'); 23848 const fileInput = document.getElementById('review-images'); 23849 23850 if (uploadTrigger && fileInput) { 23851 uploadTrigger.addEventListener('click', () => fileInput.click()); 23852 fileInput.addEventListener('change', handleImageSelection); 23853 } 23854 }, 100); 23855 23856 // Form submit 23857 setTimeout(() => { 23858 const form = document.getElementById('review-form'); 23859 if (form) { 23860 form.addEventListener('submit', handleSubmit); 23861 } 23862 }, 100); 23863 } 23864 23865 // Open review modal 23866 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23867 const overlay = document.getElementById('review-modal-overlay'); 23868 if (!overlay) { 23869 console.error('Modal overlay not found!'); 23870 return; 23871 } 23872 23873 currentProductId = productId; 23874 currentOrderId = orderId; 23875 currentSoRefId = soRefId; 23876 selectedRating = 0; 23877 uploadedImages = []; 23878 23879 // Set product info 23880 const nameEl = document.getElementById('review-product-name'); 23881 const imageEl = document.getElementById('review-product-image'); 23882 const skuEl = document.getElementById('review-product-sku'); 23883 23884 if (nameEl) nameEl.textContent = productName; 23885 if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; 23886 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23887 23888 // Update privacy preview with actual user name 23889 updatePrivacyPreview(); 23890 23891 // Reset form 23892 const form = document.getElementById('review-form'); 23893 const previewGrid = document.getElementById('image-preview-grid'); 23894 const errorEl = document.getElementById('review-form-error'); 23895 23896 if (form) form.reset(); 23897 if (previewGrid) previewGrid.innerHTML = ''; 23898 if (errorEl) errorEl.style.display = 'none'; 23899 23900 setRating(0); 23901 updateCharCount(); 23902 calculatePotentialPoints();
23903 23904 // Show modal 23905 overlay.style.display = 'flex'; 23906 overlay.classList.add('show'); 23907 document.body.style.overflow = 'hidden'; 23908 } 23909 23910 // Close review modal 23911 function closeReviewModal() { 23912 const overlay = document.getElementById('review-modal-overlay'); 23913 if (overlay) { 23914 overlay.classList.remove('show'); 23915 setTimeout(() => { 23916 overlay.style.display = 'none'; 23917 }, 200); // Wait for fade animation 23918 } 23919 document.body.style.overflow = ''; 23920 } 23921 23922 // Set star rating 23923 function setRating(rating) { 23924 selectedRating = rating; 23925 const stars = document.querySelectorAll('.star-btn'); 23926 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23927 23928 stars.forEach((star, index) => { 23929 if (index < rating) { 23930 star.classList.add('active'); 23931 } else { 23932 star.classList.remove('active'); 23933 } 23934 }); 23935 23936 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23937 } 23938 23939 // Update character count 23940 function updateCharCount() { 23941 const textarea = document.getElementById('review-text'); 23942 const count = textarea.value.length; 23943 document.getElementById('review-char-count').textContent = count; 23944 } 23945 23946 // Calculate potential points 23947 function calculatePotentialPoints() { 23948 const textarea = document.getElementById('review-text'); 23949 if (!textarea) return; 23950 23951 const charCount = textarea.value.trim().length; 23952 const hasImages = uploadedImages.length > 0; 23953 23954 let totalPoints = 5000; 23955 let longTextActive = charCount >= 100; 23956 let imagesActive = hasImages; 23957 23958 if (longTextActive) totalPoints += 1000; 23959 if (imagesActive) totalPoints += 2000; 23960 23961 // Update UI 23962 const longTextEl = document.getElementById('pri-long-text'); 23963 const imagesEl = document.getElementById('pri-images'); 23964 const totalEl = document.getElementById('pri-total-points'); 23965 23966 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 23967 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 23968 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 23969 } 23970 23971 // Handle image selection 23972 function handleImageSelection(e) { 23973 const files = Array.from(e.target.files); 23974 23975 if (files.length > 5) { 23976 showError('Maksimal 5 foto yang dapat diupload'); 23977 e.target.value = ''; // Reset input 23978 return; 23979 } 23980 23981 uploadedImages = []; 23982 const previewGrid = document.getElementById('image-preview-grid'); 23983 previewGrid.innerHTML = ''; 23984 23985 files.forEach((file, index) => { 23986 // Validate file type 23987 if (!file.type.match(/image/(jpeg|jpg|png|webp)/)) { 23988 showError('Format file harus JPG, PNG, atau WebP'); 23989 return; 23990 } 23991 23992 // Validate file size (2MB) 23993 if (file.size > 2 * 1024 * 1024) { 23994 showError('Ukuran file maksimal 2MB'); 23995 return; 23996 } 23997 23998 uploadedImages.push(file); 23999 24000 // Create preview 24001 const reader = new FileReader(); 24002 reader.onload = function(e) { 24003 const previewHTML = ` 24004 <div class="image-preview-item"> 24005 <img src="${e.target.result}" alt="Preview ${index + 1}"> 24006 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 24007 <svg width="16" height="16" viewBox="0 0 24 24"> 24008 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 24009 </svg> 24010 </button> 24011 </div> 24012 `; 24013 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24014 };
24015 reader.readAsDataURL(file); 24016 }); 24017 24018 calculatePotentialPoints(); 24019 24020 // Attach remove handlers 24021 setTimeout(() => { 24022 document.querySelectorAll('.remove-image').forEach(btn => { 24023 btn.addEventListener('click', function() { 24024 const index = parseInt(this.dataset.index); 24025 removeImage(index); 24026 }); 24027 }); 24028 }, 100); 24029 } 24030 24031 // Remove image 24032 function removeImage(index) { 24033 uploadedImages.splice(index, 1); 24034 24035 const previewGrid = document.getElementById('image-preview-grid'); 24036 previewGrid.innerHTML = ''; 24037 24038 uploadedImages.forEach((file, idx) => { 24039 const reader = new FileReader(); 24040 reader.onload = function(e) { 24041 const previewHTML = ` 24042 <div class="image-preview-item"> 24043 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24044 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus"> 24045 <svg width="16" height="16" viewBox="0 0 24 24"> 24046 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 24047 </svg> 24048 </button> 24049 </div> 24050 `; 24051 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24052 }; 24053 reader.readAsDataURL(file); 24054 }); 24055 24056 calculatePotentialPoints(); 24057 24058 setTimeout(() => { 24059 document.querySelectorAll('.remove-image').forEach(btn => { 24060 btn.addEventListener('click', function() { 24061 const idx = parseInt(this.dataset.index); 24062 removeImage(idx); 24063 }); 24064 }); 24065 }, 100); 24066 } 24067 24068 // Handle form submission 24069 async function handleSubmit(e) { 24070 e.preventDefault(); 24071 24072 // Validate 24073 if (selectedRating === 0) { 24074 showError('Silakan pilih rating terlebih dahulu'); 24075 return; 24076 } 24077 24078 const reviewText = document.getElementById('review-text').value.trim(); 24079 if (reviewText.length < 20) { 24080 showError('Ulasan minimal 20 karakter'); 24081 return; 24082 } 24083 24084 const submitBtn = document.getElementById('review-submit'); 24085 submitBtn.disabled = true; 24086 24087 try { 24088 // Step 1: Upload images if any 24089 let imageUrls = []; 24090 if (uploadedImages.length > 0) { 24091 submitBtn.textContent = 'Mengupload foto...'; 24092 imageUrls = await uploadImages(); 24093 if (!imageUrls) { 24094 // Upload failed 24095 submitBtn.disabled = false; 24096 submitBtn.textContent = 'Kirim Ulasan'; 24097 return; 24098 } 24099 } 24100 24101 // Step 2: Submit review with image URLs 24102 submitBtn.textContent = 'Mengirim ulasan...'; 24103 24104 const payload = { 24105 csrf_token: getCsrfToken(), 24106 product_id: currentProductId, 24107 order_id: currentOrderId || 0, 24108 so_ref_id: currentSoRefId || undefined, 24109 rating: selectedRating, 24110 review_text: reviewText, 24111 is_anonymous: document.getElementById('review-anonymous').checked, 24112 images: imageUrls 24113 }; 24114 24115 const response = await fetch('/api/reviews/submit.php', { 24116 method: 'POST', 24117 headers: { 24118 'Content-Type': 'application/json', 24119 'X-CSRF-Token': getCsrfToken() 24120 }, 24121 body: JSON.stringify(payload) 24122 }); 24123 24124 const result = await response.json(); 24125 24126 if (result.success) { 24127 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24128 closeReviewModal(); 24129 24130 // Refresh reviewable orders if on account page 24131 if (typeof loadReviewableOrders === 'function') { 24132 setTimeout(loadReviewableOrders, 500); 24133 } 24134 } else { 24135 showError(result.message || 'Gagal mengirim ulasan'); 24136 } 24137 } catch (error) { 24138 console.error('Submit error:', error); 24139 showError('Terjadi kesalahan saat mengirim ulasan'); 24140 } finally { 24141 submitBtn.disabled = false; 24142 submitBtn.textContent = 'Kirim Ulasan'; 24143 } 24144 } 24145 24146 /** 24147 * Upload images to server 24148 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24149 */ 24150 async function uploadImages() { 24151 if (uploadedImages.length === 0) { 24152 return []; 24153 } 24154 24155 const formData = new FormData();
24156 uploadedImages.forEach(file => { 24157 formData.append('images[]', file); 24158 }); 24159 24160 try { 24161 const response = await fetch('/api/reviews/upload_images.php', { 24162 method: 'POST', 24163 headers: { 24164 'X-CSRF-Token': getCsrfToken() 24165 }, 24166 body: formData 24167 }); 24168 24169 // Check if response is JSON 24170 const contentType = response.headers.get('content-type'); 24171 if (!contentType || !contentType.includes('application/json')) { 24172 const text = await response.text(); 24173 console.error('Non-JSON response:', text); 24174 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24175 return null; 24176 } 24177 24178 const result = await response.json(); 24179 24180 if (result.success && result.images) { 24181 // Extract URLs from uploaded images 24182 return result.images.map(img => img.url); 24183 } else { 24184 showError(result.message || 'Gagal mengupload foto'); 24185 return null; 24186 } 24187 } catch (error) { 24188 console.error('Upload images error:', error); 24189 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24190 return null; 24191 } 24192 } 24193 24194 // Load reviewable orders (for account page) 24195 async function loadReviewableOrders() { 24196 // Only run if on account/profile page 24197 const container = document.getElementById('reviewable-orders-list'); 24198 const loading = document.getElementById('reviewable-orders-loading'); 24199 const empty = document.getElementById('reviewable-orders-empty'); 24200 24201 if (!container) { 24202 return; 24203 } 24204 24205 // Show loading 24206 if (loading) loading.style.display = 'block'; 24207 if (empty) empty.style.display = 'none'; 24208 container.innerHTML = ''; 24209 24210 try { 24211 const response = await fetch('/api/reviews/reviewable.php'); 24212 const result = await response.json(); 24213 24214 if (loading) loading.style.display = 'none'; 24215 24216 if (result.success && result.data.length > 0) { 24217 renderReviewableOrders(result.data); 24218 if (empty) empty.style.display = 'none'; 24219 } else { 24220 if (empty) empty.style.display = 'block'; 24221 } 24222 } catch (error) { 24223 console.error('Load reviewable orders error:', error); 24224 if (loading) loading.style.display = 'none'; 24225 if (empty) { 24226 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24227 empty.style.display = 'block'; 24228 } 24229 } 24230 } 24231 24232 // Render reviewable orders 24233 function renderReviewableOrders(orders) { 24234 const container = document.getElementById('reviewable-orders-list'); 24235 if (!container) return; 24236 24237 let html = ''; 24238 24239 orders.forEach(order => { 24240 html += ` 24241 <div class="reviewable-order-item"> 24242 <img src="${escapeHtml(order.product_image || '/assets/img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24243 <div class="roi-info"> 24244 <h4>${escapeHtml(order.product_name)}</h4> 24245 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24246 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24247 </div> 24248 <button type="button" 24249 class="btn btn-primary btn-write-review" 24250 data-product-id="${order.product_id}" 24251 data-order-id="${order.order_id || ''}" 24252 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24253 data-product-name="${escapeHtml(order.product_name)}" 24254 data-product-image="${escapeHtml(order.product_image || '')}" 24255 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24256 âï¸ Tulis Ulasan 24257 </button> 24258 </div> 24259 `; 24260 }); 24261 24262 container.innerHTML = html; 24263 } 24264 24265 // Escape HTML for XSS protection 24266 function escapeHtml(text) { 24267 const div = document.createElement('div'); 24268 div.textContent = text; 24269 return div.innerHTML; 24270 } 24271 24272 // Show error 24273 function showError(message) { 24274 const errorEl = document.getElementById('review-form-error'); 24275 errorEl.textContent = message; 24276 errorEl.style.display = 'block'; 24277 24278 setTimeout(() => { 24279 errorEl.style.display = 'none'; 24280 }, 5000); 24281 } 24282 24283 // Show success 24284 function showSuccess(message) { 24285 // Create toast notification 24286 const toast = document.createElement('div'); 24287 toast.className = 'toast-notification success'; 24288 toast.textContent = message; 24289 toast.style.cssText = 'position:fixed;top:20px;right:20px;background:#059669;color:white;padding:16px 24px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);z-index:10000;font-size:14px;font-weight:500;'; 24290 24291 document.body.appendChild(toast); 24292 24293 setTimeout(() => { 24294 toast.style.opacity = '0'; 24295 toast.style.transition = 'opacity 0.3s'; 24296 setTimeout(() => toast.remove(), 300); 24297 }, 3000); 24298 } 24299 24300 // Mask user name for privacy preview (like PHP maskReviewerName) 24301 function maskUserName(fullName) { 24302 if (!fullName || fullName.trim() === '') { 24303 return 'Anonymous'; 24304 } 24305 24306 const words = fullName.trim().split(/s+/); 24307 const maskedWords = words.map(word => { 24308 const len = word.length; 24309 if (len <= 1) return word; 24310 return word.charAt(0) + '*'.repeat(len - 1); 24311 }); 24312 24313 return maskedWords.join(' '); 24314 } 24315 24316 // Update privacy preview text with user's actual masked name 24317 function updatePrivacyPreview() { 24318 const previewEl = document.getElementById('review-anonymous-preview'); 24319 if (!previewEl) return; 24320 24321 const userName = window.userFullName || ''; 24322 if (userName) { 24323 const maskedName = maskUserName(userName); 24324 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24325 } else { 24326 previewEl.textContent = 'Nama Anda akan ditampilkan de
24326ngan disamarkan untuk menjaga privasi'; 24327 } 24328 } 24329 24330 // Initialize when DOM ready 24331 if (document.readyState === 'loading') { 24332 document.addEventListener('DOMContentLoaded', init); 24333 } else { 24334 init(); 24335 } 24336 24337 // Export openReviewModal to global scope for use in other scripts 24338 window.openReviewModal = openReviewModal; 24339 24340})(); 24341 24342</script>
24342 24343 <style> 24344/* ========================================================= 24345 â¡ FIXED FOOTER â PINK ELEGANT MODE 24346========================================================= */ 24347 24348.sbo-top-fixed-footer{ 24349 position: fixed; 24350 left: 0; 24351 right: 0; 24352 bottom: 0; 24353 z-index: 999; 24354 display: flex; 24355 align-items: center; 24356 justify-content: space-around; 24357 gap: 8px; 24358 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24359 overflow: hidden; 24360 background: radial-gradient(circle at 50% 0%, #000000, rgb(0 0 0 / 92%) 58%, rgb(0 0 0 / 98%)), linear-gradient(180deg, rgba(35, 0, 20, 0.92), rgb(0 0 0 / 98%)); 24361 backdrop-filter: blur(12px); 24362 border-top: 1px solid rgb(219, 39, 119); 24363 border-radius: 18px 18px 0 0; 24364 box-shadow: 0 -12px 30px #500028, inset 0 1px 0 rgba(255, 255, 255, .05); 24365 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24366} 24367 24368/* scanline */ 24369.sbo-top-fixed-footer::before{ 24370 content:""; 24371 position:absolute; 24372 inset:0; 24373 pointer-events:none; 24374 background: 24375 repeating-linear-gradient( 24376 0deg, 24377 rgba(255,255,255,.015), 24378 rgba(255,255,255,.015) 1px, 24379 transparent 1px, 24380 transparent 6px 24381 ); 24382 opacity:.85; 24383 mix-blend-mode:overlay; 24384} 24385 24386/* shine sweep */ 24387.sbo-top-fixed-footer::after{ 24388 content:""; 24389 position:absolute; 24390 top:0; 24391 left:-160%; 24392 width:60%; 24393 height:100%; 24394 background: linear-gradient(120deg, transparent, rgba(244, 114, 182, 0.25), transparent); 24395 transform: skewX(-25deg); 24396 animation: footerFinalShine 3.8s infinite; 24397 mix-blend-mode: screen; 24398 pointer-events:none; 24399} 24400 24401@keyframes footerFinalShine{ 24402 0%{left:-160%;} 24403 68%{left:160%;} 24404 100%{left:160%;} 24405} 24406 24407/* ========================================================= 24408 ITEM 24409========================================================= */ 24410 24411.sbo-top-fixed-footer a{ 24412 flex:1; 24413 max-width:110px; 24414 24415 display:flex; 24416 flex-direction:column; 24417 align-items:center; 24418 justify-content:center; 24419 gap:6px; 24420 24421 padding: 8px 6px; 24422 24423 text-decoration:none; 24424 text-align:center; 24425 24426 color: rgb(244, 114, 182); 24427 font-size: 12px; 24428 font-weight: 800; 24429 letter-spacing: .25px; 24430 24431 border-radius: 14px; 24432 position:relative; 24433 z-index:1; 24434 24435 transition: all .18s ease; 24436} 24437 24438.sbo-top-fixed-footer a:hover{ 24439 color:#ffffff; 24440 background: rgba(244, 114, 182, 0.12); 24441 filter: drop-shadow(0 0 12px rgba(244, 114, 182, 0.25)); 24442 transform: translateY(-1px); 24443} 24444 24445.sbo-top-fixed-footer a:active{ 24446 transform: translateY(0) scale(.99); 24447} 24448 24449/* ========================================================= 24450 ICON 24451========================================================= */ 24452 24453.sbo-top-fixed-footer img{ 24454 width:26px; 24455 height:26px; 24456 transition: .18s ease; 24457 filter: drop-shadow(0 0 8px rgba(244, 114, 182, 0.35)); 24458} 24459 24460.sbo-top-fixed-footer a:hover img{ 24461 transform: scale(1.08); 24462 filter: drop-shadow(0 0 16px rgba(244, 114, 182, 0.6)); 24463} 24464 24465/* ========================================================= 24466 HUD CORNERS 24467========================================================= */ 24468 24469.sbo-top-fixed-footer .hud-corner{ 24470 position:absolute; 24471 width:18px;height:18px; 24472 pointer-events:none; 24473 border: 1px solid rgba(244, 114, 182, 0.5); 24474 filter: drop-shadow(0 0 10px rgba(244, 114, 182, 0.2)); 24475 opacity:.85; 24476} 24477 24478.sbo-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24479.sbo-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24480.sbo-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24481.sbo-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24482 24483/* ========================================================= 24484 CENTER CTA 24485========================================================= */ 24486 24487.sbo-top-fixed-footer .tada{ 24488 color: #ffffff; 24489 background: radial-gradient(circle at 50% 0%, #ec4899, rgba(0, 0, 0, 0) 70%), rgba(244, 114, 182, 0.12); 24490 border: 1px solid rgba(244, 114, 182, 0.4); 24491 box-shadow: 0 0 18px #ec4899, inset 0 0 16px rgba(244, 114, 182, 0.2); 24492} 24493 24494.sbo-top-fixed-footer .tada img{ 24495 width:30px; 24496 height:30px; 24497 filter: drop-shadow(0 0 16px rgba(244, 114, 182, 0.75)); 24498} 24499 24500/* pulse */ 24501.sbo-top-fixed-footer .tada::before{ 24502 content:""; 24503 position:absolute; 24504 inset:-12px; 24505 border-radius:999px; 24506 background: 24507 radial-gradient(circle, #000000, transparent 70%), 24508 radial-gradient(circle, rgba(244, 114, 182, 0.12), transparent 72%); 24509 filter: blur(12px); 24510 opacity:.55; 24511 animation: corePulse 2.9s ease-in-out infinite; 24512 z-index:-1; 24513} 24514 24515@keyframes corePulse{ 24516 0%,100%{opacity:.35; transform:scale(.99);} 24517 50%{opacity:.75; transform:scale(1.02);} 24518} 24519 24520/* ACTIVE */ 24521.sbo-top-fixed-footer a.is-active{ 24522 color:#ffffff; 24523 background: rgba(244, 114, 182, 0.15); 24524 box-shadow: inset 0 0 0 1px rgba(244, 114, 182, 0.35); 24525} 24526 24527.sbo-top-fixed-footer a.is-active img{ 24528 filter: drop-shadow(0 0 18px rgba(244, 114, 182, 0.85)); 24529} 24530 24531/* RESPONSIVE */ 24532@media (max-width:500px){ 24533 .sbo-top-fixed-footer{ 24534 padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); 24535 border-radius: 16px 16px 0 0; 24536 gap:6px; 24537 } 24538 .sbo-top-fixed-footer a{ 24539 font-size:11px; 24540 padding:7px 5px; 24541 gap:5px; 24542 max-width:104px; 24543 } 24544 .sbo-top-fixed-footer img{width:22px;height:22px;} 24545 .sbo-top-fixed-footer .tada img{width:26px;height:26px;} 24546} 24547 24548 24549/* ===== BOYTOTO PINK THEME OVERRIDE ===== */ 24550:root{ 24551 --brand-50:#fdf2f8; 24552 --brand-100:#fce7f3; 24553 --brand-500:#ec4899; 24554 --brand-600:#db2777; 24555 --brand-700:#be185d; 24556 --ina-gold:#f472b6; 24557 --ina-amber:#ec4899; 24558 --ina-deep:#290416; 24559 --ina-panel:#380720; 24560 --ina-cream:#fdf2f8; 24561} 24562body{ 24563 background:linear-gradient(180deg,#fdf2f8 0%,#fce7f3 45%,#fdf2f8 100%) !important; 24564 color:#290416 !important; 24565 position:relative; 24566} 24567body::before{ 24568 content:""; 24569 position:fixed; 24570 z-index:490; 24571 pointer-events:none; 24572 top:0;left:0;right:0; 24573 height:300px; 24574 opacity:.34; 24575 background-repeat:no-repeat; 24576 background-position:12% 0,50% 0,86% 0; 24577 background-size:220px 280px,280px 300px,210px 270px; 24578 background-image: 24579 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300'%3E%3Cpath d='M75 0 L105 70 L83 70 L121 132 L98 132 L143 210' fill='none' stroke='%23f472b6' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M105 70 L145 95 L130 125' fill='none' stroke='%23fbcfe8' stroke-width='2.4'/%3E%3C/svg%3E"), 24580 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 260 320'%3E%3Cpath d='M145 0 L116 72 L142 72 L104 142 L128 142 L82 235' fill='none' stroke='%23ec4899' stroke-width='5.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M116 72 L76 100 L90 132' fill='none' stroke='%23fbcfe8' stroke-width='2.5'/%3E%3C/svg%3E"), 24581 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 300'%3E%3Cpath d='M130 0 L102 66 L126 66 L92 125 L116 125 L74 205' fill='none' stroke='%23f472b6' stroke-width='4.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M102 66 L70 92 L84 116' fill='none' stroke='%23fbcfe8' stroke-width='2.2'/%3E%3C/svg%3E"); 24582 filter:drop-shadow(0 0 7px rgba(236,72,153,.75)); 24583 animation:inaLightning 7s steps(1,end) infinite; 24584} 24585@keyframes inaLightning{ 24586 0%,8%,11%,15%,100%{opacity:.06} 24587 9%,12%{opacity:.55} 24588 13%{opacity:.22} 24589 52%{opacity:.08} 24590 53%{opacity:.42} 24591 54%{opacity:.09} 24592} 24593@media (prefers-reduced-motion:reduce){body::before{animation:none;opacity:.16}} 24594.navbar,.navbar-logo{ 24595 background:linear-gradient(135deg,rgba(255,255,255,.97),rgba(252,231,243,.94)) !important; 24596 border-bottom-color:rgba(219,39,119,.25) !important; 24597} 24598.pd-title,.pd-sticky-title,.pd-related-title{color:#380720 !important} 24599.pd-media-card,.pd-info-card,.pd-side-card{border-color:rgba(219,39,119,.22) !important;box-shadow:0 10px 30px rgba(131,24,67,.08) !important} 24600.rtp-machine-v3, 24601.BOYTOTO-container, 24602.tnt-review-mecha, 24603.artikel-glow-BOYTOTO, 24604.BOYTOTO-faq-rainbow, 24605.cyber-mecha{ 24606 background: 24607 radial-gradient(circle at 15% 0%,rgba(244,114,182,.28),transparent 38%), 24608 radial-gradient(circle at 86% 100%,rgba(236,72,153,.22),transparent 42%), 24609 linear-gradient(145deg,#380720,#19020e 62%,#4a0a2b) !important; 24610 border-color:rgba(244,114,182,.72) !important; 24611 box-shadow:0 0 22px rgba(236,72,153,.22),inset 0 0 24px rgba(244,114,182,.08) !important; 24612 color:#fdf2f8 !important; 24613} 24614.rtp-title-v3,.tnt-header h2,.artikel-glow-BOYTOTO h1,.artikel-glow-BOYTOTO h2,.BOYTOTO-faq-top h2,.pd-full-desc-title,.mecha-header h3{ 24615 color:#fbcfe8 !important; 24616 text-shadow:0 0 8px rgba(244,114,182,.45),0 2px 10px rgba(0,0,0,.55) !important; 24617}
24618.rtp-controls-v3,.rtp-main-result-v3,.rtp-alts-v3,.rtp-alt-item{ 24619 background:rgba(35,4,20,.84) !important; 24620 border-color:rgba(244,114,182,.38) !important; 24621 box-shadow:inset 0 0 12px rgba(244,114,182,.08) !important; 24622} 24623.rtp-controls-v3 label,.k,.k-rtp,.k-small,.k-detail,.rtp-alts-title-v3{color:#f472b6 !important} 24624.rtp-controls-v3 select,.rtp-controls-v3 input{background:#19020e !important;color:#fdf2f8 !important;border-color:rgba(244,114,182,.42) !important} 24625#btnRtpRandom{background:linear-gradient(135deg,#ec4899,#f472b6) !important;color:#ffffff !important;box-shadow:0 0 18px rgba(236,72,153,.35) !important} 24626.v-rtp-huge,.rtp-alt-item .r{color:#fbcfe8 !important;text-shadow:0 0 10px rgba(244,114,182,.4) !important} 24627.rtp-tip-v3,.rtp-note-v3,.rtp-alt-item .p,.provider-info{color:#fce7f3 !important} 24628.tnt-name{color:#fbcfe8 !important;text-shadow:none !important} 24629.tnt-comment,.artikel-glow-BOYTOTO p,.artikel-glow-BOYTOTO li,.BOYTOTO-rainbow-answer p,.mecha-content p{color:#fdf2f8 !important} 24630.artikel-glow-BOYTOTO a{color:#fbcfe8 !important;text-shadow:none !important} 24631.BOYTOTO-rainbow-item{border-color:rgba(251,207,232,.32) !important;background:rgba(255,255,255,.08) !important} 24632.BOYTOTO-rainbow-item button{background:linear-gradient(135deg,#831843,#be185d) !important;color:#fdf2f8 !important} 24633.BOYTOTO-rainbow-item button:after{background:linear-gradient(135deg,#fbcfe8,#ec4899) !important;color:#380720 !important;box-shadow:0 0 12px rgba(244,114,182,.4) !important} 24634.BOYTOTO-faq-top span{color:#fbcfe8 !important;border-color:rgba(251,207,232,.5) !important} 24635.qoala-cta,.pey-link,.pd-sticky-btn-buy,.mf-buy{background:linear-gradient(135deg,#ec4899,#db2777) !important;color:#ffffff !important} 24636.sbo-top-fixed-footer{background:linear-gradient(180deg,rgba(40,4,22,.96),rgba(15,2,8,.98)) !important;border-top-color:rgba(244,114,182,.35) !important} 24637.sbo-top-fixed-footer a{color:#fce7f3 !important} 24638.sbo-top-fixed-footer a.is-active{background:rgba(244,114,182,.15) !important} 24639/* Keep unrelated commerce scaffolding from competing with the shio article while preserving DOM structure. */ 24640.pd-price-qr-container,.pd-compare-section,.pd-related-full,.pd-mobile-fab{display:none !important} 24641 24642</style> 24643<div class="sbo-top-fixed-footer"> 24644 <a href="https://mgsco.org/boykita/" target="_blank" rel="nofollow noopener"> 24645 <img src="https://ik.imagekit.io/boytoto/favicon1.png" alt="BONUS BOYTOTO"> 24646 Promosi 24647 </a> 24648 <a href="https://mgsco.org/boykita/" target="_blank" rel="nofollow noopener"> 24649 <img src="https://ik.imagekit.io/boytoto/favicon1.png" alt="LOGIN BOYTOTO"> 24650 Login 24651 </a> 24652 <a href="https://mgsco.org/boykita/" target="_blank" rel="nofollow noopener" class="tada"> 24653 <img src="https://ik.imagekit.io/boytoto/favicon1.png" alt="DAFTAR BOYTOTO"> 24654 Daftar 24655 </a> 24656 <a href="https://mgsco.org/boykita/" target="_blank" rel="nofollow noopener"> 24657 <img src="https://ik.imagekit.io/boytoto/favicon1.png" alt="LINK ALTERNATIF BOYTOTO"> 24658 Alternatif 24659 </a> 24660 <a href="https://mgsco.org/boykita/" target="_blank" rel="nofollow noopener"> 24661 <img src="https://ik.imagekit.io/boytoto/favicon1.png" alt="LIVE CHAT BOYTOTO"> 24662 Live Chat 24663 </a> 24664</div> 24665 24666
24667<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"2e6dd30730da48f49e6dae9c450a9aac","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 24667
24667
24668<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"56dcabd2c1db4f4f85af0d95c8194fc4","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 24668
24668
24669<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"25da37e89362439bb1ac2f51a070a3b5","r":1,"spa":2}' crossorigin="anonymous"></script>
24669 24670</body> 24671</html>
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.