1 2 3 4 5<!DOCTYPE html> 6<html lang="id"> 7<head> 8 <meta charset="UTF-8"> 9 <meta name="google-site-verification" content="2PgBsSFy3dhCu9weu_WV3xa7iIa5ofFabtNF-duIhZQ" /> 10 <meta name="google-site-verification" content="HHAPcPgeS6UggumY_qwn9JzvM_KShp-wCeHh9KK25so" /> 11 <meta name="google-site-verification" content="TiNmFhdOPBxmaC1G7ECewVti7C4-dQaUqhvOJuDIPak" /> 12 <title>JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik</title> 13 <meta name="description" content="JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini."> 14 <meta name="robots" content="index,follow"> 15 <link rel="canonical" href="https://hop.exchange/"> 16 <link rel="amphtml" href="https://mpo5000xjudimpo.blackhostapp.com/"> 17 <link rel="alternate" href="https://mpo5000xjudimpo.blackhostapp.com/" hreflang="id"> 18 <link rel="alternate" href="https://mpo5000xjudimpo.blackhostapp.com/" hreflang="en"> 19 <link rel="alternate" href="https://mpo5000xjudimpo.blackhostapp.com/" hreflang="x-default"> 20 <link rel="alternate" hreflang="id-ID" href="https://mpo5000xjudimpo.blackhostapp.com/" /> 21 <link rel="alternate" hreflang="en-US" href="https://mpo5000xjudimpo.blackhostapp.com/" /> 22 <link rel="alternate" hreflang="en-SG" href="https://mpo5000xjudimpo.blackhostapp.com/" /> 23 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 67 bytes, lines 23-24
23 24 <script async src="https://www.googletagmanager.com/gtag/js?id=
24G-0LSM1YJSPZ
vendor: 48 bytes, lines 24-25
24" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 25
25<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>
25 26 <meta property="og:locale" content="id_ID"> 27 <meta property="og:site_name" content="judimpoofficial"> 28 <meta property="og:type" content="product"> 29 <meta property="og:title" content="JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik"> 30 <meta property="og:description" content="JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini."> 31 <meta property="og:url" content="https://hop.exchange/"> 32 <meta property="og:image" content="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg"> 33 <meta property="og:image:alt" content="JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik"> 34<meta property="product:price:amount" content="5000"> 35<meta property="product:price:currency" content="IDR"> 36<meta property="product:availability" content="out of stock"> 37<meta name="twitter:card" content="summary_large_image"> 38<meta name="twitter:title" content="JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik"> 39<meta name="twitter:description" content="JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini."> 40<meta name="twitter:image" content="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg"> 41
42<script type="application/ld+json"> 43 { 44 "@context": "http:\/\/schema.org", 45 "@type": "BreadcrumbList", 46 "itemListElement": [ 47 { 48 "@type": "ListItem", 49 "item": { 50 "@id": "https://hop.exchange/", 51 "name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik" 52 }, 53 "position": 1 54 } 55 ] 56 } 57 </script>
57 58
58<script type="application/ld+json"> 59 { 60 "@context": "http:\/\/schema.org", 61 "@type": "Product", 62 "name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik", 63 "description": "JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini.", 64 "image": "https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg", 65 "offers": { 66 "@type": "http:\/\/schema.org\/Offer", 67 "price": 9999, 68 "url": "https://hop.exchange/", 69 "priceCurrency": "IDR", 70 "availability": "http:\/\/schema.org\/InStock" 71 } 72 } 73 </script>
73 74
74<script type="application/ld+json"> 75 { 76 "@context": "http:\/\/schema.org\/", 77 "@type": "WebPage", 78 "speakable": { 79 "@type": "SpeakableSpecification", 80 "cssSelector": [".description"], 81 "xpath": ["\/html\/head\/title"] 82 } 83 } 84 </script>
84 85 <!-- ZS --> 86 <!-- Schema JSON untuk SBO -->
87<script type="application/ld+json"> 88{ 89"@context": "https://schema.org/", 90"@type": "Product", 91"name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik", 92"url": "https://hop.exchange/", 93"sku": "Slot1000", 94"image": [ 95 "https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" 96], 97"description": "JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini.", 98"brand": { 99 "@type": "Organization", 100 "name": "Slot1000" 101}, 102"offers": { 103 "@type": "Offer", 104 "name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik", 105 "sku": "Slot1000", 106 "price": "9999", 107 "priceCurrency": "IDR", 108 "availability": "https://schema.org/InStock" 109} 110} 111</script>
111 112
113<script type="application/ld+json"> 114{ 115"@context": "https://schema.org/", 116"@type": "Product", 117"name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik", 118"image": "https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg", 119"description": "JUDIMPO merupakan platform judi mpo slot online yang menghadirkan berbagai permainan Slot 10k hari ini. Dengan sistem cepat dan pilihan game lengkap, pemain memiliki peluang meraih maxwin besar di situs terpercaya ini.", 120"sku": "Slot1000", 121"offers": { 122 "@type": "Offer", 123 "priceCurrency": "IDR", 124 "url": "https://hop.exchange/", 125 "availability": "https://schema.org/InStock", 126 "price": "9999.00" 127} 128} 129</script>
129 130
131<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 132{ 133"@context": "https://schema.org/", 134"@id": "https://hop.exchange/", 135"aggregateRating": { 136 "@type": "AggregateRating", 137 "itemReviewed": { 138 "@type": "IndividualProduct", 139 "name": "Slot1000" 140 }, 141 "ratingValue": "5.0", 142 "reviewCount": 954638, 143 "bestRating": 5 144} 145} 146</script>
146 147
148<script type="application/ld+json"> 149{ 150"@context": "https://schema.org", 151"@type": "BreadcrumbList", 152"itemListElement": [ 153 { 154 "@type": "ListItem", 155 "position": 1, 156 "item": { 157 "@id": "https://hop.exchange/", 158 "name": "Slot1000" 159 } 160 }, 161 { 162 "@type": "ListItem", 163 "position": 2, 164 "item": { 165 "@id": "https://hop.exchange/", 166 "name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik" 167 } 168 }, 169 { 170 "@type": "ListItem", 171 "position": 3, 172 "item": { 173 "@id": "https://hop.exchange/", 174 "name": "Slot1000" 175 } 176 }, 177 { 178 "@type": "ListItem", 179 "position": 4, 180 "item": { 181 "@id": "https://hop.exchange/", 182 "name": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik" 183 } 184 } 185] 186} 187</script>
187 188
189<script type="application/ld+json" id="bv-jsonld-reviews-data"> 190{ 191 "@context": "https://schema.org", 192 "@type": "Organization", 193 "@id": "https://hop.exchange/", 194 "name": "Slot1000", 195 "alternateName": "Slot1000", 196 "url": "https://hop.exchange/", 197 "review": [ 198 { 199 "@type": "Review", 200 "dateCreated": "2026-03-31T12:28:35+00:00", 201 "datePublished": "2026-03-31T12:28:35+00:00", 202 "headline": "Slot1000", 203 "reviewBody": "Tampilan halaman Mpo 5000 cukup sederhana dan mudah dipahami. Informasi mengenai portal digital disusun dengan rapi sehingga saya tidak kesulitan mencari menu yang dibutuhkan.", 204 "reviewRating": { 205 "@type": "Rating", 206 "ratingValue": 5, 207 "bestRating": 5, 208 "worstRating": 1 209 }, 210 "author": { 211 "@type": "Person", 212 "name": "game digital â Surabaya" 213 } 214 }, 215 { 216 "@type": "Review", 217 "dateCreated": "2026-04--02T18:28:21+00:00", 218 "datePublished": "2026-04-03T18:28:21+00:00", 219 "headline": "GAME MPO ONLINE", 220 "reviewBody": "Saya menyukai tampilan yang responsif karena halaman dapat menyesuaikan layar ponsel. Navigasinya juga tidak terlalu rumit sehingga nyaman digunakan untuk membaca informasi.", 221 "reviewRating": { 222 "@type": "Rating", 223 "ratingValue": 5, 224 "bestRating": 5, 225 "worstRating": 1 226 }, 227 "author": { 228 "@type": "Person", 229 "name": "kendaraan yang bisa di-custom total â Semarang" 230 } 231 }, 232 { 233 "@type": "Review", 234 "dateCreated": "2026-04-05T19:28:18+00:00", 235 "datePublished": "2026-04-05T19:28:18+00:00", 236 "headline": "GAME MPO ONLINE", 237 "reviewBody": "Informasi mengenai permainan berbasis peluang disampaikan dengan cukup jelas. Saya juga terbantu dengan penjelasan mengenai keamanan akun dan pentingnya menjaga data pribadi.", 238 "reviewRating": { 239 "@type": "Rating", 240 "ratingValue": 5, 241 "bestRating": 5, 242 "worstRating": 1 243 }, 244 "author": { 245 "@type": "Person", 246 "name": "di-custom total â Makassar" 247 } 248 } 249 ] 250} 251</script>
vendor: 2 bytes, line 251
251
252<script type="application/ld+json"> 253{ 254 "@context": "https://schema.org", 255 "@type": "FAQPage", 256 "mainEntity": [ 257 { 258 "@type": "Question", 259 "name": "What is JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik?", 260 "acceptedAnswer": { 261 "@type": "Answer", 262 "text": "JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik refers to a digital portal providing information about MPO games, available features, and various online entertainment options." 263 } 264}, 265{ 266 "@type": "Question", 267 "name": "What information can users find on JUDIMPO?", 268 "acceptedAnswer": { 269 "@type": "Answer", 270 "text": "Users can find information about available MPO games, platform features, updates, and different categories of digital entertainment." 271 } 272}, 273{ 274 "@type": "Question", 275 "name": "Does gacor guarantee winning results?", 276 "acceptedAnswer": { 277 "@type": "Answer", 278 "text": "No. The term gacor is commonly used by gaming communities to describe games perceived as frequently producing favorable results, but it does not guarantee winnings or specific outcomes." 279 } 280}, 281{ 282 "@type": "Question", 283 "name": "What does Pilihan Game Hoki mean?", 284 "acceptedAnswer": { 285 "@type": "Answer", 286 "text": "Pilihan Game Hoki is a popular term referring to game selections that players consider potentially favorable. However, game outcomes remain uncertain and should not be regarded as guaranteed." 287 } 288}, 289{ 290 "@type": "Question", 291 "name": "Can JUDIMPO be accessed through mobile devices?", 292 "acceptedAnswer": { 293 "@type": "Answer", 294 "text": "A responsive digital platform can generally be accessed through compatible smartphones, tablets, or computers, depending on the services and technical requirements provided by the platform." 295 } 296} 297 ] 298} 299</script>
299 300 <!-- User Session Data --> 301 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 302 <meta name="user-logged-in" content="false"> 303 <meta name="user-id" content="0"> 304 305 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 306 307 <link rel="shortcut icon" href="https://img.tongat.cc/uploads/logo-ri.png" type="image/x-icon"> 308 <style data-inlined="style.css"> 309@font-face { 310 font-family: 'Segoe UI'; 311 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 312 url('/assets/fonts/segoeui.woff') format('woff'); 313 font-weight: 400; 314 font-style: normal; 315} 316 317:root { 318 --brand-50:#D80B6A; 319 --brand-100:#764EAC; 320 --brand-500:#C77DFF; 321 --brand-600:#C77DFF; 322 --gray-900:#D80B6A; 323 --gray-700:#764EAC; 324 --gray-500:#6F737B; 325 --gray-300:#C5C9CF; 326 --gray-100:#764EAC; 327 --border:#E4E7EB; 328 --radius:8px; 329 --transition: .18s ease; 330 /* font-family: "Inter", system-ui, sans-serif; */ 331 font-family: 'Segoe UI', sans-serif; 332} 333 334* { box-sizing: border-box; } 335body { 336 margin:0; 337 padding-bottom: 0; /* Reset for desktop */ 338 color:var(--gray-900); 339 background:#fff; 340} 341img { max-width:100%; display:block; } 342 343.container { 344 width:100%; 345 max-width:1280px; 346 margin:0 auto; 347 padding:10px 0; 348} 349 350.topbar { 351 background:#111; 352 color:#fff; 353 font-size:13px; 354} 355.topbar-inner { 356 display:flex; 357 justify-content:space-between; 358 align-items:center; 359 min-height:36px; 360} 361.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 362 363/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 364.navbar { 365 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 366 backdrop-filter:blur(12px); 367 -webkit-backdrop-filter:blur(12px); 368 border-bottom:1px solid rgba(228,231,235,0.6); 369 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 370 position:sticky; 371 top:0; 372 z-index:500; 373 transition:all .3s ease; 374} 375 376/* Accent line gradient #E3D05A */ 377.navbar::after { 378 content:''; 379 position:absolute; 380 bottom:0; 381 left:0; 382 right:0; 383 height:2px; 384 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 385 opacity:0.5; 386} 387 388.nav-inner { 389 display:flex; 390 align-items:center; 391 gap:24px; 392 min-height:70px; 393} 394.nav-inner .search .m-search-ico { display: none; } 395 396/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 397.login-btn .login-ico { display:none; } 398.navbar-logo { 399 display: none; 400 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 401 backdrop-filter:blur(12px); 402 -webkit-backdrop-filter:blur(12px); 403 border-bottom:1px solid rgba(228,231,235,0.6); 404 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 405 position:sticky; 406 top:0; 407 z-index:500; 408} 409 410/* Hide bottom navigation on desktop - only show on mobile */ 411.bottom-nav { 412 display: none; 413} 414 415/* ============================================================== 416 MOBILE () 417 ============================================================== */ 418@media (max-width:640px){ 419 .container { padding:10px 16px; } 420 421 .topbar { display:none !important; } 422 423 /* Add padding bottom for sticky bottom nav */ 424 body { 425 padding-bottom: 68px; /* Height of bottom nav + safe area */ 426 } 427 428 /* Mobile Top Navbar - Clean & Minimal */ 429 .navbar { 430 background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); 431 backdrop-filter:blur(16px); 432 -webkit-backdrop-filter:blur(16px); 433 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 434 border: 0; 435 } 436 437 /* Simple grid layout - Logo, Search, Category */ 438 .nav-inner { 439 display:grid; 440 grid-template-columns:1fr; 441 gap:10px; 442 padding:12px 16px 0 12px; 443 min-height:auto; 444 } 445 446 /* Logo centered at top */ 447 .nav-inner .logo { 448 justify-self:center; 449 margin:0; 450 display:flex; 451 } 452 .nav-inner .logo img { height:36px; } 453 454 /* Search bar - full width */ 455 .nav-inner .search { 456 position:relative; 457 display:flex;
458 align-items:center; 459 gap:0; 460 width:100%; 461 height:44px; 462 background:#fff; 463 border:1.5px solid #e5e7eb; 464 border-radius:12px; 465 padding:0 12px; 466 box-shadow:0 1px 3px rgba(0,0,0,0.04); 467 transition:all .25s; 468 } 469 .nav-inner .search:focus-within { 470 border-color:#C77DFF; 471 box-shadow:0 0 0 3px rgba(245,202,86,0.1); 472 } 473 474 .nav-inner .search-icon-btn { 475 color:#9ca3af; 476 padding:4px; 477 margin-right:4px; 478 } 479 480 .nav-inner .search-icon-btn svg { 481 width:18px; 482 height:18px; 483 } 484 485 .nav-inner .search input { 486 flex:1; 487 min-width:0; 488 border:none; 489 outline:none; 490 background:transparent; 491 font-size:14px; 492 padding:0 8px; 493 height:100%; 494 } 495 496 .nav-inner .ai-bot-toggle { 497 padding:6px; 498 margin-left:0; 499 } 500 501 .nav-inner .ai-bot-toggle .bot-icon { 502 width:20px; 503 height:20px; 504 } 505 506 .nav-inner .search-suggest { 507 left:0; 508 right:0; 509 width:calc(100vw - 32px); 510 margin-top:6px; 511 border-radius:12px; 512 max-height:70vh; 513 } 514 515 /* Hide desktop nav actions but keep structure for userMenu */ 516 .nav-inner .nav-actions { 517 display:flex !important; /* Keep flex to allow userMenu positioning */ 518 visibility:hidden; /* Hide visually but keep in DOM */ 519 pointer-events:none; /* Disable interactions with hidden elements */ 520 position:absolute; /* Remove from flow */ 521 right:0; 522 top:0; 523 } 524 525 /* Re-enable userMenu for logged-in users */ 526 .nav-inner .nav-actions #userMenu { 527 visibility:visible !important; 528 pointer-events:auto !important; 529 } 530 531 .nav-inner .logo { display: none; } 532 .navbar-logo { 533 display: block; 534 /* background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); */ 535 backdrop-filter:blur(16px); 536 -webkit-backdrop-filter:blur(16px); 537 position: static; /* Remove sticky on mobile */ 538 box-shadow: none; 539 border-bottom: 0; 540 padding-top: .6rem; 541 } 542 .nav-inner .logo-mobile { display: block; } 543 544 /* Category menu wrapper will handle visibility */ 545 546 /* ============================================================== 547 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 548 ============================================================== */ 549 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 550 551 /* Hide desktop elements that now moved to bottom nav */ 552 #wishlistBtn, 553 #notifBtn, 554 #loginBtn { display:none !important; } 555 556 /* Hide desktop user menu on mobile - use bottom sheet instead */ 557 .navbar #userMenu { 558 display:none !important; 559 } 560 561 .nav-actions .cart-indicator, 562 .nav-actions .compare-indicator { 563 display:none !important; 564 } 565 566 /* Hide category button from bottom nav */ 567 #bottomCategoryBtn { 568 display: none !important; 569 } 570 571 /* User menu button styling for mobile - scoped to navbar only */ 572 .navbar #userMenu .login-btn.logged-in { 573 width:40px; 574 height:40px; 575 padding:0; 576 border-radius:50%; 577 display:flex;
578 align-items:center; 579 justify-content:center; 580 background:rgba(255,255,255,0.95); 581 backdrop-filter:blur(12px); 582 -webkit-backdrop-filter:blur(12px); 583 border:1px solid rgba(228,231,235,0.6); 584 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 585 } 586 587 .navbar #userMenu .login-btn.logged-in .login-text { 588 display:none; 589 } 590 591 .navbar #userMenu .login-btn.logged-in .login-ico { 592 display:inline; 593 font-size:20px; 594 line-height:1; 595 } 596 597 /* User dropdown positioning for mobile */ 598 .navbar #userMenu .user-dropdown { 599 position:fixed !important; 600 top:60px !important; 601 right:16px !important; 602 left:auto !important; 603 min-width:200px; 604 background:rgba(255,255,255,0.98); 605 backdrop-filter:blur(20px); 606 -webkit-backdrop-filter:blur(20px); 607 border:1px solid rgba(228,231,235,0.6); 608 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 609 z-index:1002 !important; 610 border-radius:12px; 611 padding:8px 0; 612 } 613 614 /* Force visibility when not hidden attribute */ 615 .navbar #userMenu .user-dropdown:not([hidden]) { 616 display:block !important; 617 visibility:visible !important; 618 opacity:1 !important; 619 } 620 621 /* Container untuk kategori dan tombol kategori sejajar */ 622 .navbar .container:last-child { 623 display:flex !important; 624 align-items:center !important; 625 padding:4px 16px 6px !important; 626 gap:5px; 627 overflow:visible; 628 } 629 630 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 631 .mega-cats-wrapper { 632 position:relative; 633 flex:1; 634 overflow:hidden; 635 min-height:34px; /* Ensure content is visible */ 636 display:flex; /* Ensure wrapper is flex container */ 637 } 638 639 /* Fade gradient indicator di ujung kanan (mobile only) */ 640 .mega-cats-fade { 641 position:absolute; 642 top:0; 643 right:0; 644 bottom:0; 645 width:80px; /* Increased from 60px for better visibility */ 646 background:linear-gradient(to left, 647 rgba(255,245,230,1) 0%, 648 rgba(255,245,230,0.98) 15%, 649 rgba(255,245,230,0.85) 35%, 650 transparent 100%); 651 pointer-events:none; 652 z-index:5; /* Increased z-index */ 653 transition:opacity 0.3s ease; 654 } 655 656 /* Kategori menu di mobile */ 657 .mega-cats.container { 658 display:flex !important; 659 gap:12px; 660 overflow-x:auto; 661 overflow-y:visible; 662 -webkit-overflow-scrolling:touch; 663 scrollbar-width:none; 664 flex:1; 665 padding:0 !important; 666 margin:0 !important; 667 position:relative; /* For z-index stacking */ 668 z-index:1; /* Below fade gradient */ 669 } 670 .mega-cats.container::-webkit-scrollbar { display:none; } 671 672 /* Styling untuk menu items di mobile */ 673 .mega-cats a { 674 font-size:13px; 675 padding:6px 10px; 676 white-space:nowrap; 677 flex-shrink:0; 678 position:relative; /* Ensure proper stacking */ 679 } 680 .mega-cats a.active { 681 background:var(--brand-600); 682 color:#fff; 683 font-weight:600; 684 border-radius:6px; 685 } 686 687 /* Tombol kategori di mobile */ 688 .mm-wp { 689 flex-shrink:0; 690 } 691 .mm2-trigger { 692 display:flex;
693 align-items:center; 694 gap:6px; 695 padding:6px 12px; 696 background:var(--gray-100); 697 color:var(--gray-700); 698 text-decoration:none; 699 border-radius:6px; 700 font-size:13px; 701 white-space:nowrap; 702 } 703 704 .mm2-trigger .label-cat {display: none; } 705 /* Tombol more kategori */ 706 .mega-more-btn { 707 border:1px solid var(--border); 708 background:#fff; 709 border-radius:10px; 710 padding:6px 14px; 711 font-size:18px; 712 cursor:pointer; 713 } 714 .mega-cats-more.active > .mega-more-btn { 715 background:var(--brand-600); 716 color:#fff; 717 border-color:var(--brand-600); 718 } 719 .mega-more-dropdown { 720 top:calc(100% + 10px); 721 right:0; 722 min-width:85vw; 723 max-width:90vw; 724 border-radius:16px; 725 } 726 .mega-more-grid { 727 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 728 } 729 730 /* ============================================================== 731 MOBILE USER BOTTOM SHEET 732 ============================================================== */ 733 .mobile-user-sheet { 734 position:fixed; 735 top:0; 736 left:0; 737 right:0; 738 bottom:0; 739 z-index:10000; 740 pointer-events:none; 741 } 742 743 .mobile-user-sheet.active { 744 pointer-events:auto; 745 } 746 747 .mobile-user-sheet-backdrop { 748 position:absolute; 749 top:0; 750 left:0; 751 right:0; 752 bottom:0; 753 background:rgba(0,0,0,0.5); 754 opacity:0; 755 transition:opacity .3s ease; 756 } 757 758 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 759 opacity:1; 760 } 761 762 .mobile-user-sheet-content { 763 position:absolute; 764 bottom:0; 765 left:0; 766 right:0; 767 background:#fff; 768 border-radius:20px 20px 0 0; 769 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 770 transform:translateY(100%); 771 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 772 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 773 } 774 775 .mobile-user-sheet.active .mobile-user-sheet-content { 776 transform:translateY(0); 777 } 778 779 .mobile-user-sheet-header { 780 padding:16px 20px 12px; 781 border-bottom:1px solid #e5e7eb; 782 } 783 784 .mobile-user-sheet-handle { 785 width:40px; 786 height:4px; 787 background:#d1d5db; 788 border-radius:2px; 789 margin:0 auto 16px; 790 } 791 792 .mobile-user-info { 793 display:flex;
794 align-items:center; 795 gap:12px; 796 } 797 798 .mobile-user-avatar { 799 width:48px; 800 height:48px; 801 border-radius:50%; 802 background:linear-gradient(135deg, #764EAC, #fff1cc); 803 display:flex; 804 align-items:center; 805 justify-content:center; 806 font-size:24px; 807 flex-shrink:0; 808 } 809 810 .mobile-user-details { 811 flex:1; 812 min-width:0; 813 } 814 815 .mobile-user-name { 816 font-size:16px; 817 font-weight:600; 818 color:#111827; 819 margin-bottom:2px; 820 white-space:nowrap; 821 overflow:hidden; 822 text-overflow:ellipsis; 823 } 824 825 .mobile-user-email { 826 font-size:13px; 827 color:#6b7280; 828 white-space:nowrap; 829 overflow:hidden; 830 text-overflow:ellipsis; 831 } 832 833 .mobile-user-sheet-body { 834 padding:8px 0; 835 } 836 837 .mobile-user-sheet-item { 838 display:flex; 839 align-items:center; 840 gap:16px; 841 padding:14px 20px; 842 text-decoration:none; 843 color:#374151; 844 font-size:15px; 845 transition:background .2s; 846 border:none; 847 background:none; 848 width:100%; 849 text-align:left; 850 cursor:pointer; 851 } 852 853 .mobile-user-sheet-item:active { 854 background:#f3f4f6; 855 } 856 857 .mobile-user-sheet-icon { 858 font-size:20px; 859 width:24px; 860 text-align:center; 861 flex-shrink:0; 862 } 863 864 .mobile-user-sheet-label { 865 flex:1; 866 font-weight:500; 867 } 868 869 .mobile-user-sheet-divider { 870 height:1px; 871 background:#e5e7eb; 872 margin:8px 0; 873 } 874} 875 876/* END NAVBAR */ 877 878.logo img { display:block; } 879.search { 880 flex:1; 881 position:relative; 882 display:flex; 883 align-items:center; 884 gap:0; 885 background:#fff; 886 border:1.5px solid #e5e7eb; 887 border-radius:12px; 888 padding:0 16px; 889 height:38px; 890 transition:all 0.3s ease; 891} 892.search:focus-within { 893 border-color:#C77DFF; 894 box-shadow:0 0 0 3px rgba(245,202,86,0.1); 895} 896.search input { 897 flex:1; 898 border:none; 899 outline:none; 900 background:transparent; 901 font-size:15px; 902 padding:0 12px; 903 height:100%; 904 color:#1f2937; 905} 906.search input::placeholder { 907 color:#9ca3af; 908} 909.search-icon-btn { 910 background:transparent; 911 border:none; 912 padding:0; 913 display:flex; 914 align-items:center; 915 justify-content:center; 916 cursor:pointer; 917 color:#6b7280; 918 transition:color 0.2s ease; 919 flex-shrink:0; 920} 921.search-icon-btn:hover { 922 color:#C77DFF; 923} 924.search-suggest { 925 position:absolute; 926 left:0; right:0; top:100%; 927 background:#fff; 928 border:1px solid var(--border); 929 border-radius:var(--radius); 930 padding:8px 0; 931 box-shadow:0 8px 20px rgba(0,0,0,.08); 932 margin-top:4px; 933 font-size:14px; 934 max-height:260px; 935 overflow:auto; 936} 937.search-suggest div { 938 padding:6px 12px; 939 cursor:pointer; 940} 941.search-suggest div:hover { background:var(--gray-100); } 942 943.nav-actions { display:flex; gap:10px; align-items:center; } 944.icon-btn, .login-btn { 945 background:linear-gradient(135deg, rgba(255,255,255,0.95), rgba(249,250,251,0.9)); 946 border:1px solid rgba(228,231,235,0.6); 947 padding:8px 12px; 948 border-radius:var(--radius); 949 cursor:pointer; 950 font-size:14px; 951 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 952 height: 38px; 953 position:relative; 954 box-shadow: 955 0 1px 3px rgba(0,0,0,0.06), 956 0 0 0 1px rgba(255,255,255,0.5) inset; 957 backdrop-filter:blur(8px); 958 -webkit-backdrop-filter:blur(8px); 959} 960.icon-btn:hover, .login-btn:hover { 961 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 962 border-color:rgba(209,213,219,0.8); 963 box-shadow: 964 0 2px 8px rgba(0,0,0,0.08), 965 0 0 0 1px rgba(255,255,255,0.6) inset; 966 transform:translateY(-1px); 967} 968.cart-indicator, .compare-indicator { position:relative; } 969.badge { 970 background:linear-gradient(135deg, #C77DFF, #C77DFF); 971 color:#fff; 972 font-size:10.5px; 973 font-weight:600; 974 padding:2px 6px; 975 border-radius:999px; 976 position:absolute; 977 top:-6px; 978 right:-6px; 979 min-width:18px; 980 height:18px; 981 display:flex; 982 align-items:center; 983 justify-content:center; 984 box-shadow: 985 0 2px 8px rgba(226,166,66,0.4), 986 0 0 0 2px #fff, 987 0 0 12px -2px rgba(226,166,66,0.6); 988 transition:all 0.3s ease; 989} 990.icon-btn:hover .badge { 991 transform:scale(1.08); 992 box-shadow: 993 0 4px 12px rgba(226,166,66,0.5), 994 0 0 0 2px #fff, 995 0 0 16px -2px rgba(226,166,66,0.7); 996} 997.badge.is-empty { display:none; } 998 999/* User Menu Dropdown */ 1000.user-menu { 1001 position:relative; 1002} 1003.user-dropdown { 1004 position:absolute; 1005 top:calc(100% + 8px); 1006 right:0; 1007 background:#fff; 1008 border:1px solid var(--border); 1009 border-radius:12px; 1010 box-shadow:0 10px 40px rgba(0,0,0,0.12);
1011 min-width:200px; 1012 padding:8px 0; 1013 z-index:9999; 1014} 1015 1016/* Ensure dropdown is visible when not hidden */ 1017.user-dropdown:not([hidden]) { 1018 display:block !important; 1019 visibility:visible !important; 1020 opacity:1 !important; 1021} 1022 1023/* Mobile: Force user dropdown to be visible above everything */ 1024@media (max-width:640px) { 1025 .user-dropdown:not([hidden]) { 1026 position:fixed !important; 1027 z-index:9999 !important; 1028 top:60px !important; 1029 right:16px !important; 1030 } 1031} 1032 1033.dropdown-item { 1034 display:flex; 1035 align-items:center; 1036 gap:12px; 1037 padding:10px 16px; 1038 text-decoration:none; 1039 color:var(--gray-700); 1040 font-size:14px; 1041 transition:var(--transition); 1042 border:none; 1043 background:none; 1044 width:100%; 1045 text-align:left; 1046 cursor:pointer; 1047} 1048.dropdown-item:hover { 1049 background:var(--gray-100); 1050 color:var(--gray-900); 1051} 1052.dropdown-icon { 1053 font-size:16px; 1054 opacity:0.7; 1055} 1056.dropdown-separator { 1057 height:1px; 1058 background:var(--border); 1059 margin:8px 0; 1060} 1061.login-btn.logged-in { 1062 background:linear-gradient(135deg, rgba(255,245,230,0.8), rgba(255,231,204,0.6)); 1063 border:1px solid rgba(245,202,86,0.2); 1064 color:var(--brand-700); 1065 max-width:200px; /* Prevent button from getting too wide */ 1066 display:inline-flex; 1067 align-items:center; 1068 gap:10px; 1069 padding:4px 12px 4px 4px; 1070 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1071 position:relative; 1072 overflow:hidden; 1073 box-shadow: 1074 0 1px 3px rgba(245,202,86,0.08), 1075 0 0 0 1px rgba(255,255,255,0.5) inset, 1076 0 0 12px -6px rgba(245,202,86,0.1); 1077 backdrop-filter:blur(8px); 1078 -webkit-backdrop-filter:blur(8px); 1079} 1080/* Subtle background glow effect */ 1081.login-btn.logged-in::before { 1082 content:''; 1083 position:absolute; 1084 top:50%; 1085 left:0; 1086 width:32px; 1087 height:32px; 1088 background:radial-gradient(circle, rgba(245,202,86,0.15), transparent 70%); 1089 transform:translateY(-50%); 1090 opacity:0; 1091 transition:opacity 0.3s ease; 1092} 1093.login-btn.logged-in:hover::before { 1094 opacity:1; 1095} 1096.login-btn.logged-in .user-avatar { 1097 width:32px; 1098 height:32px; 1099 border-radius:50%; 1100 background:linear-gradient(135deg, #764EAC 0%, #fff1cc 50%, #C77DFF 100%); 1101 color:#C77DFF; 1102 font-size:11.5px; 1103 font-weight:700; 1104 display:flex; 1105 align-items:center; 1106 justify-content:center; 1107 flex-shrink:0; 1108 letter-spacing:0.3px; 1109 position:relative; 1110 box-shadow: 1111 0 2px 8px rgba(245,202,86,0.25), 1112 0 1px 3px rgba(0,0,0,0.12), 1113 inset 0 1px 0 rgba(255,255,255,0.5); 1114 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1115} 1116/* Ring gradient border */ 1117.login-btn.logged-in .user-avatar::before { 1118 content:''; 1119 position:absolute; 1120 inset:-2px; 1121 border-radius:50%; 1122 padding:2px; 1123 background:linear-gradient(135deg, #C77DFF, #fff1cc, #C77DFF); 1124 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1125 -webkit-mask-composite:xor; 1126 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1127 mask-composite:exclude; 1128 opacity:0; 1129 transition:opacity 0.3s ease; 1130} 1131.login-btn.logged-in:hover .user-avatar::before { 1132 opacity:1; 1133} 1134.login-btn.logged-in:hover .user-avatar { 1135 transform:scale(1.08) rotate(-3deg); 1136 box-shadow: 1137 0 4px 12px rgba(245,202,86,0.35), 1138 0 2px 6px rgba(0,0,0,0.15), 1139 inset 0 1px 0 rgba(255,255,255,0.6); 1140} 1141.login-btn.logged-in .login-text { 1142 overflow:hidden; 1143 text-overflow:ellipsis; 1144 white-space:nowrap; 1145 flex:1; 1146 min-width:0; 1147 font-weight:500; 1148 font-size:14px; 1149 transition:color 0.2s ease; 1150} 1151.login-btn.logged-in:hover .login-text { 1152 color:var(--brand-600); 1153} 1154.login-btn.logged-in:hover { 1155 background:linear-gradient(135deg, rgba(255,245,230,0.95), rgba(255,231,204,0.85)); 1156 border-color:rgba(245,202,86,0.3); 1157 box-shadow: 1158 0 4px 16px -4px rgba(245,202,86,0.25), 1159 0 0 0 1px rgba(245,202,86,0.1) inset, 1160 0 1px 2px rgba(0,0,0,0.05); 1161 transform:translateY(-1px); 1162} 1163 1164/* Desktop: hide wrapper elements, show menu normally */ 1165@media (min-width: 761px) { 1166 .mega-cats-wrapper { 1167 display:contents; /* Remove wrapper from layout */ 1168 } 1169 .mega-cats-fade { 1170 display:none !important; /* Hide fade on desktop */ 1171 } 1172} 1173 1174.mega-cats { 1175 display:flex; 1176 gap:5px; 1177 /* padding:6px 16px 10px; */ 1178 scrollbar-width:none; 1179 position: relative; 1180 z-index: 510; 1181 overflow-x:auto; 1182 -webkit-overflow-scrolling:touch;
1183 flex:1; 1184} 1185.mega-cats::-webkit-scrollbar { 1186 display:none; 1187} 1188.mega-cats a { 1189 text-decoration:none; 1190 font-size:14px; 1191 color:var(--gray-700); 1192 padding:6px 10px; 1193 border-radius:6px; 1194 white-space:nowrap; 1195 transition:var(--transition); 1196} 1197.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1198/*.mega-cats::after { 1199 content:''; 1200 position:absolute; 1201 right:0; 1202 top:0; 1203 width:40px; 1204 height:100%; 1205 background:linear-gradient(to right,rgba(255,255,255,0),#fff); 1206 pointer-events:none; 1207}*/ 1208 1209/* Tambahan untuk tombol More & dropdown. 1210 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1211 1212.mega-cats-more { 1213 position:relative; 1214 display:flex; 1215 align-items:center; 1216} 1217 1218.mega-cats-more.active > .mega-more-btn { 1219 background:var(--brand-600); 1220 color:#fff; 1221} 1222 1223.mega-more-btn { 1224 cursor:pointer; 1225 border:none; 1226 background:var(--gray-100,#f5f5f5); 1227 color:var(--gray-700,#333); 1228 padding:6px 12px; 1229 font-size:16px; 1230 line-height:1; 1231 border-radius:6px; 1232 display:flex; 1233 align-items:center; 1234 justify-content:center; 1235 transition:var(--transition); 1236} 1237.mega-more-btn:hover { 1238 background:var(--brand-100); 1239 color:var(--brand-600); 1240} 1241 1242.mega-more-dropdown { 1243 position:absolute; 1244 top:100%; 1245 right:0; 1246 margin-top:8px; 1247 background:#fff; 1248 border:1px solid var(--gray-200,#e3e6e9); 1249 border-radius:10px; 1250 padding:12px 14px 14px; 1251 min-width:400px; 1252 max-width:520px; 1253 max-height:420px; 1254 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1255 z-index: 3000; 1256} 1257 1258.mega-more-grid { 1259 display:grid; 1260 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1261 gap:8px 10px; 1262 overflow-x: hidden; 1263 max-height:380px; 1264 overscroll-behavior:contain; 1265 scrollbar-width:thin; 1266} 1267.mega-more-grid::-webkit-scrollbar { 1268 width:6px; 1269} 1270.mega-more-grid::-webkit-scrollbar-thumb { 1271 background:var(--gray-300,#c7ccd1); 1272 border-radius:3px; 1273} 1274 1275/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1276 Kita tambahkan state active agar konsisten. */ 1277.mega-more-grid a.active { 1278 background:var(--brand-600); 1279 color:#fff; 1280 font-weight:600; 1281} 1282.hero { 1283 background:linear-gradient(135deg,var(--brand-50), #fff); 1284} 1285.hero-inner { 1286 display:grid; 1287 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1288 gap:48px; 1289 align-items:center; 1290} 1291.hero-text h1 { 1292 font-size:48px; 1293 line-height:1.1; 1294 margin:0; 1295 font-weight:600; 1296 font-family:"Poppins",sans-serif; 1297} 1298.hero-text .text-brand { color:var(--brand-500); } 1299.lead { font-size:18px; color:var(--gray-700); margin-top:16px; } 1300.hero-cta { margin-top:28px; display:flex; gap:16px; } 1301.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1302.hero-box { 1303 background:#fff; 1304 padding:10px; 1305 border-radius:16px; 1306 box-shadow:0 4px 18px rgba(0,0,0,.08); 1307 /* min-height:320px; */ 1308 display:flex; 1309 align-items:center; 1310 justify-content:center; 1311} 1312.hero-box img {border-radius: 10px; } 1313 1314@media (max-width:680px) { 1315 .hero-inner { gap: 25px; } 1316} 1317 1318.usp-section { 1319 position:relative; 1320 background: 1321 linear-gradient(180deg,#FFFDFB 0%, #FFFFFF 60%) ; 1322 border-top:1px solid var(--border); 1323 border-bottom:1px solid var(--border); 1324 padding:10px 0 10px; 1325} 1326 1327.usp-list { 1328 list-style:none; 1329 margin:0; 1330 padding:0; 1331 display:grid; 1332 gap:26px; 1333} 1334 1335.usp-list.variant-soft { 1336 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1337} 1338 1339.usp-item { 1340 display:flex; 1341 gap:16px; 1342 align-items:flex-start; 1343 background:#fff; 1344 padding:18px 18px 18px 16px; 1345 border:1px solid rgba(226,226,226,0.7); 1346 border-radius:18px; 1347 position:relative; 1348 overflow:hidden; 1349 isolation:isolate; 1350 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1351 transition: .35s cubic-bezier(.16,.8,.32,1); 1352} 1353 1354.usp-item::before { 1355 content:""; 1356 position:absolute; 1357 inset:0; 1358 background:radial-gradient(circle at 18% 22%, rgba(245,202,86,.18), transparent 60%); 1359 opacity:.65; 1360 transition:.5s; 1361 z-index:-1; 1362} 1363 1364.usp-item:hover { 1365 transform:translateY(-6px); 1366 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1367 border-color:rgba(245,202,86,.35); 1368} 1369 1370.usp-item:hover::before { 1371 background:radial-gradient(circle at 70% 40%, rgba(245,202,86,.22), transparent 65%); 1372} 1373 1374.usp-icon { 1375 flex:0 0 56px; 1376 width:56px; 1377 height:56px; 1378 border-radius:16px; 1379 display:flex; 1380 align-items:center; 1381 justify-content:center; 1382 background: 1383 linear-gradient(135deg,#764EAC,#fff1cc); 1384 position:relative; 1385 box-shadow:inset 0 0 0 1px rgba(255,255,255,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1386 transition:.45s; 1387} 1388 1389.usp-item:hover .usp-icon { 1390 transform:rotate(-4deg) scale(1.06); 1391 box-shadow:inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1392} 1393 1394.usp-icon svg { 1395 width:28px; 1396 height:28px; 1397 display:block; 1398 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18));
1399} 1400 1401.usp-text h3 { 1402 font-size:15px; 1403 font-weight:600; 1404 margin:2px 0 6px; 1405 letter-spacing:.2px; 1406} 1407 1408.usp-text p { 1409 font-size:13px; 1410 line-height:1.3; 1411 margin:0; 1412 color:var(--gray-600); 1413} 1414 1415@media (max-width:680px) { 1416 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1417 .usp-item { padding:16px 14px; } 1418 .usp-icon { width:50px;height:50px;border-radius:14px; } 1419 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1420 .usp-text p { font-size:12.5px; } 1421 .usp-list { gap: 10px; } 1422} 1423 1424.flash { padding:50px 0 30px; background:#fff; } 1425.flash-head { 1426 display:flex; align-items:center; 1427 justify-content:space-between; 1428 margin-bottom:20px; 1429} 1430.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1431.flash .countdown { 1432 font-family:monospace; 1433 background:#111; 1434 color:#fff; 1435 padding:8px 14px; 1436 border-radius:8px; 1437 font-size:16px; 1438 letter-spacing:1px; 1439} 1440.flash-scroll { 1441 display:flex; 1442 gap:16px; 1443 overflow-x:auto; 1444 padding-bottom:6px; 1445 scrollbar-width:none; 1446} 1447.flash-scroll::-webkit-scrollbar { display:none; } 1448 1449.products { padding:60px 0; background:#fff; } 1450.products-head { 1451 display:flex; justify-content:space-between; align-items:center; 1452 margin-bottom:24px; 1453 flex-wrap:wrap; 1454 gap:16px; 1455} 1456.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1457.product-filters input { 1458 padding:8px 12px; 1459 border:1px solid var(--border); 1460 border-radius:8px; 1461 font-size:14px; 1462 width:220px; 1463} 1464 1465.product-grid { 1466 display:grid; 1467 gap:24px; 1468 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1469} 1470 1471.prod-card { 1472 background:#fff; 1473 border:1px solid var(--border); 1474 padding:12px; 1475 border-radius:12px; 1476 position:relative; 1477 display:flex; 1478 flex-direction:column; 1479 transition:var(--transition); 1480} 1481.prod-card.small { min-width:160px; } 1482.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1483.prod-card .img-box { 1484 background:var(--gray-100); 1485 border-radius:10px; 1486 aspect-ratio:1/1; 1487 display:flex; 1488 align-items:center; 1489 justify-content:center; 1490 overflow:hidden; 1491} 1492.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1493.prod-card:hover .img-box img { transform:scale(1.04); } 1494.prod-card .name { 1495 font-size:13.5px; 1496 font-weight:500; 1497 margin:10px 0 6px; 1498 min-height:36px; 1499 line-height:1.3; 1500} 1501.price { 1502 display:flex; 1503 gap:6px; 1504 align-items:center; 1505 flex-wrap:wrap; 1506} 1507.price strong { color:var(--brand-600); font-size:14px; } 1508.price .old { 1509 font-size:11px; 1510 color:var(--gray-500); 1511 text-decoration:line-through; 1512} 1513.tag.discount { 1514 position:absolute; 1515 top:8px; left:8px; 1516 background:#C77DFF; 1517 color:#fff; 1518 font-size:11px; 1519 padding:4px 6px; 1520 border-radius:6px; 1521 font-weight:600; 1522} 1523.btn { 1524 border:none; 1525 cursor:pointer; 1526 font-weight:500; 1527 font-size:14px; 1528 padding:12px 22px; 1529 border-radius:10px; 1530 background:var(--gray-900); 1531 color:#fff; 1532 transition:var(--transition); 1533 text-decoration:none; 1534 text-align:center; 1535 display:inline-flex; 1536 align-items:center; 1537 justify-content:center; 1538 gap:6px; 1539} 1540.btn:hover { background:#000; } 1541.btn-primary { background:var(--brand-500); } 1542.btn-primary:hover { background:var(--brand-600); } 1543.btn-outline { 1544 background:#fff; 1545 color:var(--gray-900); 1546 border:1px solid var(--border); 1547} 1548.btn-outline:hover { background:var(--gray-100); } 1549.btn-sm { padding:8px 14px; font-size:13px; } 1550.btn-mini { padding:6px 10px; font-size:12px; } 1551.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1552.add-cart:hover { background:var(--brand-600); } 1553 1554/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1555 1556/* 1. Pastikan grid aktif */ 1557 1558.site-footer.footer-compact .ft-grid { 1559 display:grid; 1560 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1561 gap:30px 40px; 1562 align-items:start; 1563} 1564 1565/* 2. Spacing atas bawah dipadatkan */ 1566.site-footer.footer-compact .ft-top { 1567 padding:42px 0 26px; 1568} 1569 1570/* 3. Judul & teks */ 1571.site-footer.footer-compact .ft-title { 1572 margin:0 0 14px; 1573 font-size:14px; 1574 letter-spacing:.8px; 1575 text-transform:uppercase; 1576 font-weight:600;
1577 color:#82b1c9; 1578} 1579 1580.site-footer.footer-compact .ft-desc, 1581.site-footer.footer-compact .ft-text { 1582 margin:0 0 16px; 1583 font-size:13.5px; 1584 line-height:1.45; 1585 color:#c6d6e0; 1586 max-width:330px; 1587} 1588.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1589 1590/* 4. Reset ikon SVG agar tidak membesar */ 1591.site-footer.footer-compact svg { 1592 width:20px; 1593 height:20px; 1594 flex:0 0 20px; 1595 display:inline-block; 1596} 1597.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1598.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1599 1600/* Jika ada aturan global svg { width:100%; } â paksa override */ 1601svg { max-width:100%; } 1602.site-footer.footer-compact svg { 1603 width:20px !important; 1604 height:20px !important; 1605} 1606 1607/* 5. Link list */ 1608.site-footer.footer-compact .ft-links { 1609 list-style:none; 1610 margin:0 0 12px; 1611 padding:0; 1612 display:flex; 1613 flex-direction:column; 1614 gap:4px; 1615} 1616.site-footer.footer-compact .ft-links li { margin:0; } 1617.site-footer.footer-compact .ft-links a { 1618 display:flex; 1619 align-items:center; 1620 gap:8px; 1621 font-size:13.5px; 1622 padding:5px 2px; 1623 text-decoration:none; 1624 color:#c6d6e0; 1625 border-radius:8px; 1626 transition:.25s; 1627} 1628.site-footer.footer-compact .ft-links a:hover { 1629 background:rgba(255,255,255,.06); 1630 padding-left:6px; 1631 color:#fff; 1632} 1633 1634.site-footer.footer-compact .ico-s { 1635 width:24px; 1636 height:24px; 1637 background:#103346; 1638 color:#89b8cf; 1639 border-radius:6px; 1640 display:flex; 1641 align-items:center; 1642 justify-content:center; 1643 flex:0 0 24px; 1644 font-size:12px; 1645 transition:.3s; 1646} 1647.site-footer.footer-compact .ft-links a:hover .ico-s { 1648 background:#C77DFF; 1649 color:#fff; 1650 transform:rotate(-4deg); 1651} 1652 1653/* 6. WhatsApp button */ 1654.site-footer.footer-compact .ft-wa-btn { 1655 display:inline-flex; 1656 align-items:center; 1657 gap:8px; 1658 background:#fff; 1659 color:#062437; 1660 padding:8px 18px; 1661 font-size:13.5px; 1662 border-radius:22px;
1663 font-weight:500; 1664 text-decoration:none; 1665 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1666 transition:.25s; 1667} 1668.site-footer.footer-compact .ft-wa-btn:hover { 1669 transform:translateY(-2px); 1670 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1671} 1672 1673/* 7. Sosial */ 1674.site-footer.footer-compact .ft-social { 1675 display:flex; 1676 gap:12px; 1677 margin-top:4px; 1678} 1679.site-footer.footer-compact .ft-social a { 1680 width:34px; 1681 height:34px; 1682 background:rgba(255,255,255,.08); 1683 display:flex; 1684 align-items:center; 1685 justify-content:center; 1686 border-radius:10px; 1687 color:#c6d6e0; 1688 transition:.25s; 1689} 1690.site-footer.footer-compact .ft-social a:hover { 1691 background:#C77DFF; 1692 color:#fff; 1693} 1694 1695/* 8. Alamat */ 1696.site-footer.footer-compact .ft-address { 1697 font-size:12.5px; 1698 line-height:1.44; 1699 color:#8fa4b1; 1700 max-width:330px; 1701 margin-top:12px; 1702} 1703.site-footer.footer-compact .ft-address .company { 1704 margin:0 0 4px; 1705 font-weight:600; 1706 color:#e5f2f7; 1707 font-size:12.8px; 1708} 1709 1710/* 9. Payments kecil & rapi */ 1711.site-footer.footer-compact .ft-payments { 1712 margin-top:14px; 1713 display:flex; 1714 flex-direction:column; 1715 gap:8px; 1716 max-width:350px; 1717} 1718.site-footer.footer-compact .ft-payments .pay-row { 1719 display:flex; 1720 gap:8px; 1721 flex-wrap:wrap; 1722} 1723.site-footer.footer-compact .ft-payments img { 1724 width:46px; 1725 height:32px; 1726 padding:3px 5px; 1727 background:#fff; 1728 border-radius:7px; 1729 object-fit:contain; 1730 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1731 transition:.25s; 1732} 1733.site-footer.footer-compact .ft-payments img:hover { 1734 transform:translateY(-3px); 1735 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1736} 1737 1738/* 10. Bottom bar */ 1739.site-footer.footer-compact .ft-bottom { 1740 border-top:1px solid rgba(255,255,255,.08); 1741 background:#041f31; 1742 padding:12px 0 16px; 1743 margin-top:4px; 1744} 1745.site-footer.footer-compact .ft-copy { 1746 font-size:11.5px; 1747 color:#8fa7b5; 1748 display:flex; 1749 flex-wrap:wrap; 1750 gap:12px; 1751 justify-content:center; 1752 line-height:1.3; 1753 margin:0; 1754} 1755.site-footer.footer-compact .ft-copy span { 1756 position:relative; 1757 padding:0 10px; 1758} 1759.site-footer.footer-compact .ft-copy span+span:before { 1760 content:""; 1761 position:absolute; 1762 left:0; 1763 top:50%; 1764 width:1px; 1765 height:12px; 1766 background:rgba(255,255,255,.14); 1767 transform:translateY(-50%); 1768} 1769@media (max-width:580px){ 1770 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1771 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1772 .site-footer.footer-compact .ft-copy span { padding:0; } 1773 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1774} 1775.copy { 1776 margin-top:40px; 1777 border-top:1px solid #222; 1778 text-align:center; 1779 padding:18px 0; 1780 font-size:12px; 1781 color:#666; 1782} 1783 1784@media (max-width:760px) { 1785 .hero-text h1 { font-size:38px; } 1786 .hero { padding:0px 0 30px; } 1787 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1788 .nav-inner { flex-wrap:wrap; } 1789 .mega-cats { gap:10px; } 1790 .product-grid { gap:16px; } 1791 .prod-card { padding:10px; } 1792} 1793 1794/* === PLAZA IT DARK #E3D05A FOOTER THEME === */ 1795.site-footer.footer-compact { 1796 --ft-bg-0:#051722; 1797 --ft-bg-1:#072534; 1798 --ft-bg-2:#0B2F40; 1799 --ft-bg-3:#103A4E; 1800 --ft-orange:#C77DFF; 1801 --ft-orange-dark:#C77DFF; 1802 --ft-text:#E9F2F6; 1803 --ft-text-sec:#B8CAD3; 1804 --ft-text-muted:#8FA4B1; 1805 --ft-divider:rgba(255,255,255,0.08); 1806 --ft-glow:rgba(245,202,86,0.18); 1807 --ft-glow-fade:rgba(245,202,86,0); 1808 color:var(--ft-text); 1809} 1810 1811/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1812.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1813.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1814.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1815.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1816.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1817 1818/* ICON BACKGROUND (kubu gelap) */ 1819.site-footer.footer-compact .ico-s { 1820 background:#0F3647; 1821 color:#9FC7DA; 1822} 1823.site-footer.footer-compact .ft-links a:hover .ico-s { 1824 background:var(--ft-orange); 1825 color:#fff; 1826} 1827 1828/* WhatsApp button biarkan putih agar kontras */ 1829.site-footer.footer-compact .ft-wa-btn { 1830 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(255,255,255,0.05); 1831} 1832.site-footer.footer-compact .ft-wa-btn:hover { 1833 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(245,202,86,0.25); 1834} 1835 1836/* Payment cards border halus */ 1837.site-footer.footer-compact .ft-payments img { 1838 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,0.05); 1839} 1840.site-footer.footer-compact .ft-payments img:hover { 1841 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(245,202,86,0.4); 1842} 1843 1844/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1845.site-footer.footer-compact.variant-ember { 1846 background: 1847 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1848 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1849} 1850.site-footer.footer-compact.variant-ember .ft-bottom { 1851 background:rgba(0,0,0,0.25); 1852 backdrop-filter:blur(2px); 1853} 1854 1855/* ========== VARIANT B: TOP BAR #E3D05A ========== */ 1856.site-footer.footer-compact.variant-bar { 1857 background:var(--ft-bg-1); 1858 position:relative; 1859} 1860.site-footer.footer-compact.variant-bar::before { 1861 content:""; 1862 position:absolute; 1863 left:0;top:0; 1864 width:100%;height:3px; 1865 background:linear-gradient(90deg,var(--ft-orange-dark),var(--ft-orange)); 1866 box-shadow:0 0 12px -2px var(--ft-orange); 1867} 1868.site-footer.footer-compact.variant-bar .ft-bottom { 1869 background:#041c28; 1870} 1871 1872/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1873.site-footer.footer-compact.variant-radial { 1874 background: 1875 radial-gradient(circle at 70% 90%, rgba(245,202,86,0.22) 0%, rgba(245,202,86,0.04) 38%, rgba(245,202,86,0) 65%), 1876 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1877} 1878.site-footer.footer-compact.variant-radial .ft-bottom { 1879 background:#041c29; 1880} 1881
1882/* ========== VARIANT D: LAYERED STRIPES ========== */ 1883.site-footer.footer-compact.variant-stripes { 1884 background: 1885 repeating-linear-gradient(180deg, rgba(255,255,255,0.015) 0 38px, rgba(255,255,255,0) 38px 76px), 1886 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1887 position:relative; 1888} 1889.site-footer.footer-compact.variant-stripes::after { 1890 content:""; 1891 position:absolute; 1892 left:0;bottom:0; 1893 width:100%;height:110px; 1894 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1895 pointer-events:none; 1896} 1897.site-footer.footer-compact.variant-stripes .ft-bottom { 1898 background:#031821; 1899} 1900 1901/* Garis aksen oranye tipis di atas bottom */ 1902.site-footer.footer-compact.variant-stripes .ft-bottom { 1903 position:relative; 1904} 1905.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1906 content:""; 1907 position:absolute; 1908 left:0;top:0; 1909 width:100%;height:2px; 1910 background:linear-gradient(90deg,rgba(245,202,86,0),rgba(245,202,86,.65),rgba(245,202,86,0)); 1911} 1912 1913/* ========== VARIANT E: GLASS PANEL ========== */ 1914.site-footer.footer-compact.variant-glass { 1915 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 100%); 1916} 1917.site-footer.footer-compact.variant-glass .ft-grid { 1918 position:relative; 1919 padding:32px clamp(10px,2vw,24px); 1920 border-radius:22px;
1921 background:linear-gradient(140deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02)); 1922 border:1px solid rgba(255,255,255,0.08); 1923 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04) inset; 1924 backdrop-filter:blur(8px); 1925 -webkit-backdrop-filter:blur(8px); 1926} 1927.site-footer.footer-compact.variant-glass .ft-bottom { 1928 background:#02141d; 1929} 1930 1931/* HOVER ACCENTS */ 1932.site-footer.footer-compact a { 1933 transition:color .25s, background .25s, box-shadow .25s; 1934} 1935.site-footer.footer-compact .ft-links a:hover { 1936 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1937 0 0 0 1px rgba(245,202,86,0.55); 1938} 1939.site-footer.footer-compact .ft-social a { 1940 background:#0E3445; 1941 color:#bcd4dd; 1942} 1943.site-footer.footer-compact .ft-social a:hover { 1944 background:var(--ft-orange); 1945 color:#fff; 1946 box-shadow:0 0 0 3px rgba(245,202,86,0.28); 1947} 1948 1949/* Divider di atas footer (opsional) */ 1950.footer-divider-top { 1951 height:1px; 1952 background:linear-gradient(90deg,transparent,rgba(255,255,255,0.15),transparent); 1953 width:100%; 1954 margin:0 0 4px; 1955} 1956 1957/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1958 1959/* Desktop: wrapper tidak mengganggu grid layout */ 1960.ft-collapsible-content { 1961 display: contents; 1962} 1963 1964@media (max-width: 768px) { 1965 /* Wrapper untuk konten yang bisa collapse */ 1966 .ft-top.ft-collapsible .ft-collapsible-content { 1967 display: block; 1968 max-height: 80px; 1969 overflow: hidden; 1970 position: relative; 1971 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1972 } 1973 1974 /* Fade gradient overlay */ 1975 .ft-top.ft-collapsible .ft-collapsible-content::after { 1976 content: ''; 1977 position: absolute; 1978 bottom: 0; 1979 left: 0; 1980 right: 0; 1981 height: 70px; 1982 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1983 pointer-events: none; 1984 opacity: 1; 1985 transition: opacity 0.3s ease; 1986 z-index: 1; 1987 } 1988 1989 /* State: Expanded */ 1990 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1991 max-height: 1500px; 1992 } 1993 1994 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1995 opacity: 0; 1996 } 1997 1998 /* Expand Button - Di luar collapsible, selalu visible */ 1999 .ft-expand-btn { 2000 display: flex;
2001 align-items: center; 2002 justify-content: center; 2003 gap: 8px; 2004 width: 100%; 2005 margin: -8px 0 0; 2006 padding: 12px 20px; 2007 background: rgba(255,255,255,0.12); 2008 border: 1px solid rgba(255,255,255,0.25); 2009 border-radius: 10px; 2010 color: #e5f2f7; 2011 font-size: 13.5px; 2012 font-weight: 500; 2013 cursor: pointer; 2014 transition: all 0.3s ease; 2015 outline: none; 2016 position: relative; 2017 z-index: 10; 2018 } 2019 2020 .ft-expand-btn:hover { 2021 background: rgba(255,255,255,0.18); 2022 border-color: rgba(255,255,255,0.35); 2023 color: #fff; 2024 transform: translateY(-2px); 2025 } 2026 2027 .ft-expand-btn:active { 2028 transform: scale(0.98); 2029 } 2030 2031 .ft-expand-btn svg { 2032 width: 18px; 2033 height: 18px; 2034 transition: transform 0.3s ease; 2035 fill: currentColor; 2036 } 2037 2038 .ft-top.ft-collapsible.expanded .ft-expand-btn { 2039 margin-top: 15px; 2040 } 2041 2042 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 2043 transform: rotate(180deg); 2044 } 2045} 2046 2047/* Desktop: hide expand button */ 2048@media (min-width: 769px) { 2049 .ft-expand-btn { 2050 display: none !important; 2051 } 2052} 2053 2054/* RESPONSIVE (jika background panel butuh penyesuaian) */ 2055@media (max-width:640px){ 2056 .site-footer.footer-compact.variant-glass .ft-grid { 2057 padding:26px 18px; 2058 } 2059} 2060 2061</style> 2062 <style data-inlined="bottom-nav.css"> 2063/* Mobile Bottom Navigation */ 2064.bottom-nav { 2065 position: fixed; 2066 bottom: 0; 2067 left: 0; 2068 right: 0; 2069 background: #ffffff; 2070 border-top: 1px solid #e5e7eb; 2071 z-index: 9998; /* Below chat widget */ 2072 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 2073 display: none; /* Hidden on desktop */ 2074} 2075 2076.bottom-nav-inner { 2077 display: flex; 2078 justify-content: space-around; 2079 align-items: center; 2080 padding: 2px 0; 2081 max-width: 640px; 2082 margin: 0 auto; 2083} 2084 2085.bottom-nav-item { 2086 flex: 1; 2087 display: flex; 2088 flex-direction: column; 2089 align-items: center; 2090 justify-content: center; 2091 gap: 4px; 2092 padding: 8px 8px; 2093 background: none; 2094 border: none; 2095 cursor: pointer; 2096 text-decoration: none; 2097 color: #6b7280; 2098 transition: all 0.3s ease; 2099 position: relative; 2100} 2101 2102.bottom-nav-item:hover { 2103 color: #C77DFF; 2104} 2105 2106.bottom-nav-item.active { 2107 flex-direction: row; 2108 gap: 8px; 2109 background: linear-gradient(135deg, #C77DFF 0%, #E3D05A 100%); 2110 color: white; 2111 border-radius: 20px; 2112 padding: 8px 16px; 2113 box-shadow: 0 2px 8px rgba(245,202,86,0.3); 2114} 2115 2116.bottom-nav-item-icon { 2117 width: 24px; 2118 height: 24px; 2119 position: relative; 2120} 2121 2122.bottom-nav-item-icon svg { 2123 width: 100%; 2124 height: 100%; 2125} 2126 2127/* Icon variant switching */ 2128.bottom-nav-item .icon-outlined { 2129 display: block; 2130} 2131 2132.bottom-nav-item .icon-filled { 2133 display: none; 2134} 2135 2136.bottom-nav-item.active .icon-outlined { 2137 display: none; 2138} 2139 2140.bottom-nav-item.active .icon-filled { 2141 display: block; 2142} 2143 2144.bottom-nav-item-label { 2145 font-size: 12px; 2146 font-weight: 600; 2147 line-height: 1; 2148 display: none; 2149 white-space: nowrap; 2150} 2151 2152.bottom-nav-item.active .bottom-nav-item-label { 2153 display: block; 2154} 2155 2156/* Avatar styles for bottom-nav */ 2157.bottom-nav-avatar-img, 2158.bottom-nav-avatar-initials { 2159 width: 24px; 2160 height: 24px; 2161 border-radius: 50%; 2162 display: block; 2163} 2164 2165.bottom-nav-avatar-img { 2166 object-fit: cover; 2167} 2168 2169.bottom-nav-avatar-initials { 2170 background: linear-gradient(135deg, #E3D05A 0%, #C77DFF 100%); 2171 color: white; 2172 font-size: 10px; 2173 font-weight: 700; 2174 display: flex; 2175 align-items: center; 2176 justify-content: center; 2177 line-height: 1; 2178 letter-spacing: 0.5px; 2179} 2180 2181/* Hide SVG when avatar is shown */ 2182.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2183.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2184 display: none; 2185} 2186 2187/* Active state adjustments for avatar */ 2188.bottom-nav-item.active .bottom-nav-avatar-initials { 2189 background: white; 2190 color: #C77DFF; 2191 border: 2px solid rgba(255, 255, 255, 0.8); 2192 box-sizing: border-box; 2193} 2194 2195.bottom-nav-item.active .bottom-nav-avatar-img { 2196 border: 2px solid rgba(255, 255, 255, 0.9); 2197 box-sizing: border-box; 2198} 2199 2200.bottom-nav-item-badge { 2201 position: absolute; 2202 top: -4px; 2203 right: -4px; 2204 background: #C77DFF; 2205 color: white; 2206 border-radius: 10px; 2207 padding: 2px 5px; 2208 font-size: 10px; 2209 font-weight: 600; 2210 min-width: 18px;
2211 height: 18px; 2212 display: flex; 2213 align-items: center; 2214 justify-content: center; 2215 line-height: 1; 2216} 2217 2218/* Show on mobile only */ 2219@media (max-width: 640px) { 2220 .bottom-nav { 2221 display: block; 2222 } 2223 2224 /* Add bottom padding to body to prevent content being hidden */ 2225 body { 2226 padding-bottom: 60px; 2227 } 2228} 2229 2230/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2231.mobile-user-sheet { 2232 position: fixed; 2233 bottom: 0; 2234 left: 0; 2235 right: 0; 2236 z-index: 10000; 2237 pointer-events: none; 2238} 2239 2240.mobile-user-sheet:not([hidden]) { 2241 pointer-events: auto; 2242} 2243 2244.mobile-user-sheet-backdrop { 2245 position: fixed; 2246 inset: 0; 2247 background: rgba(0, 0, 0, 0.5); 2248 opacity: 0; 2249 transition: opacity 0.2s ease-out; 2250} 2251 2252.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2253 opacity: 1; 2254} 2255 2256.mobile-user-sheet-content { 2257 position: absolute; 2258 bottom: 0; 2259 left: 0; 2260 right: 0; 2261 background: white; 2262 border-radius: 16px 16px 0 0; 2263 max-height: 80vh; 2264 overflow-y: auto; 2265 transform: translateY(100%); 2266 transition: transform 0.3s ease-out; 2267} 2268 2269.mobile-user-sheet.active .mobile-user-sheet-content { 2270 transform: translateY(0); 2271} 2272 2273.mobile-user-sheet-handle { 2274 width: 40px; 2275 height: 4px; 2276 background: #d1d5db; 2277 border-radius: 2px; 2278 margin: 12px auto 16px; 2279} 2280 2281.mobile-user-sheet-header { 2282 padding: 0 20px 16px; 2283 border-bottom: 1px solid #e5e7eb; 2284} 2285 2286.mobile-user-info { 2287 display: flex; 2288 align-items: center; 2289 gap: 12px; 2290} 2291 2292.mobile-user-avatar { 2293 width: 48px; 2294 height: 48px; 2295 background: linear-gradient(135deg, #764EAC 0%, #fff1cc 50%, #C77DFF 100%); 2296 border-radius: 50%; 2297 display: flex; 2298 align-items: center; 2299 justify-content: center; 2300 font-size: 24px; 2301 color: #C77DFF; 2302 font-weight: 600; 2303} 2304 2305.mobile-user-details { 2306 flex: 1; 2307} 2308 2309.mobile-user-name { 2310 font-weight: 600; 2311 font-size: 16px; 2312 color: #1f2937; 2313 margin-bottom: 4px; 2314} 2315 2316.mobile-user-email { 2317 font-size: 13px; 2318 color: #6b7280; 2319} 2320 2321.mobile-user-sheet-body { 2322 padding: 8px 0; 2323} 2324 2325.mobile-user-sheet-item { 2326 display: flex; 2327 align-items: center; 2328 gap: 12px; 2329 padding: 14px 20px; 2330 background: none; 2331 border: none; 2332 width: 100%; 2333 text-align: left; 2334 cursor: pointer; 2335 text-decoration: none; 2336 color: #374151; 2337 transition: background 0.2s; 2338} 2339 2340.mobile-user-sheet-item:hover { 2341 background: #f9fafb; 2342} 2343 2344.mobile-user-sheet-icon { 2345 font-size: 20px; 2346 width: 24px; 2347 text-align: center; 2348} 2349 2350.mobile-user-sheet-label { 2351 font-size: 15px; 2352 font-weight: 500; 2353} 2354 2355.mobile-user-sheet-divider { 2356 height: 1px; 2357 background: #e5e7eb; 2358 margin: 8px 0; 2359} 2360 2361@keyframes fadeIn { 2362 from { 2363 opacity: 0; 2364 } 2365 to { 2366 opacity: 1; 2367 } 2368} 2369 2370@keyframes slideUp { 2371 from { 2372 transform: translateY(100%); 2373 } 2374 to { 2375 transform: translateY(0); 2376 } 2377} 2378 2379/* Ensure chat widget works well with bottom nav */ 2380@media (max-width: 640px) { 2381 .chat-widget { 2382 bottom: 0 !important; /* Chat opens from bottom */ 2383 } 2384 2385 .chat-popup { 2386 /* Chat popup should be above bottom nav */ 2387 z-index: 9999; 2388 } 2389} 2390 2391</style> 2392 <style data-inlined="search-suggest.css"> 2393.search { 2394 position: relative; 2395} 2396 2397/* AI Bot Toggle Button */ 2398.ai-bot-toggle { 2399 display: flex; 2400 align-items: center; 2401 justify-content: center; 2402 background: transparent; 2403 border: none; 2404 padding: 8px; 2405 cursor: pointer; 2406 border-radius: 8px; 2407 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2408 margin-left: 4px; 2409 flex-shrink: 0; 2410 display: none; 2411} 2412 2413.ai-bot-toggle .bot-icon { 2414 color: #9ca3af; 2415 transition: all 0.3s ease; 2416} 2417 2418.ai-bot-toggle:hover .bot-icon { 2419 color: #6b7280; 2420 transform: scale(1.1); 2421} 2422 2423.ai-bot-toggle.active .bot-icon { 2424 color: #C77DFF; 2425 animation: botPulse 2s ease-in-out infinite; 2426} 2427 2428.ai-bot-toggle.active { 2429 background: rgba(245,202,86,0.1); 2430} 2431 2432.ai-bot-toggle.active:hover .bot-icon { 2433 color: #C77DFF; 2434} 2435 2436@keyframes botPulse { 2437 0%, 100% { transform: scale(1); } 2438 50% { transform: scale(1.12); } 2439} 2440 2441/* Search Suggest Box */ 2442.search-suggest { 2443 position: absolute; 2444 top: 100%; left: 0; right: 0; 2445 background: #fff; 2446 border: 1px solid #e1e4e8; 2447 border-top: none; 2448 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2449 max-height: 400px; 2450 overflow-y: auto; 2451 z-index: 100; 2452 padding: 0; 2453 font-size: 14px; 2454 border-radius: 0 0 8px 8px; 2455} 2456 2457.suggest-item { 2458 display: flex; 2459 align-items: center; 2460 gap: 8px; 2461 padding: 10px 16px; 2462 cursor: pointer; 2463 transition: all 0.2s ease; 2464 border-bottom: 1px solid #f1f5f9; 2465} 2466 2467.suggest-item:last-child { 2468 border-bottom: none; 2469} 2470 2471.suggest-item:hover, 2472.suggest-item.active { 2473 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2474 padding-left: 20px; 2475} 2476 2477.suggest-thumb { 2478 width: 42px; 2479 height: 42px; 2480 object-fit: cover; 2481 border: 1px solid #ddd; 2482 border-radius: 4px; 2483 background: #fafafa; 2484} 2485 2486.suggest-meta { flex: 1; min-width: 0; } 2487 2488.suggest-name { 2489 font-weight: 500;
2490 color: #222; 2491 line-height: 1.2; 2492 white-space: nowrap; 2493 overflow: hidden; 2494 text-overflow: ellipsis; 2495} 2496 2497.suggest-price { 2498 color: #C77DFF; 2499 font-size: 12px; 2500 margin-top: 2px; 2501} 2502 2503/* AI Insights */ 2504.suggest-ai-insight { 2505 display: flex; 2506 align-items: center; 2507 gap: 8px; 2508 padding: 8px 12px; 2509 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 2510 border-bottom: 1px solid #bae6fd; 2511 color: #0369a1; 2512 font-size: 12px; 2513 font-weight: 500; 2514 line-height: 1.4; 2515} 2516 2517.suggest-ai-insight svg { 2518 flex-shrink: 0; 2519 opacity: 0.8; 2520} 2521 2522.suggest-ai-insight span { 2523 flex: 1; 2524} 2525 2526/* AI Progress States */ 2527.ai-progress-container { 2528 padding: 20px 16px; 2529 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2530} 2531 2532.ai-progress-header { 2533 display: flex; 2534 align-items: center; 2535 gap: 10px; 2536 margin-bottom: 16px; 2537} 2538 2539.ai-progress-icon { 2540 width: 36px; 2541 height: 36px; 2542 display: flex; 2543 align-items: center; 2544 justify-content: center; 2545 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 2546 border-radius: 50%; 2547 color: #fff; 2548 font-size: 18px; 2549 animation: aiIconSpin 3s linear infinite; 2550} 2551 2552@keyframes aiIconSpin { 2553 0% { transform: rotate(0deg); } 2554 100% { transform: rotate(360deg); } 2555} 2556 2557.ai-progress-text { 2558 flex: 1; 2559} 2560 2561.ai-progress-title { 2562 font-weight: 600; 2563 color: #1e293b; 2564 font-size: 14px; 2565 margin-bottom: 2px; 2566} 2567 2568.ai-progress-subtitle { 2569 font-size: 12px; 2570 color: #64748b; 2571} 2572 2573/* Progress Steps */ 2574.ai-progress-steps { 2575 display: flex; 2576 flex-direction: column; 2577 gap: 10px; 2578 margin-top: 16px; 2579} 2580 2581.ai-progress-step { 2582 display: flex; 2583 align-items: center; 2584 gap: 10px; 2585 padding: 8px 12px; 2586 background: #fff; 2587 border-radius: 8px; 2588 border: 1px solid #e2e8f0; 2589 transition: all 0.3s ease; 2590} 2591 2592.ai-progress-step.active { 2593 border-color: #667eea; 2594 background: #f8f9ff; 2595 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2596} 2597 2598.ai-progress-step.completed { 2599 border-color: #10b981; 2600 background: #f0fdf4; 2601} 2602 2603.ai-step-icon { 2604 width: 20px; 2605 height: 20px; 2606 display: flex; 2607 align-items: center; 2608 justify-content: center; 2609 border-radius: 50%; 2610 font-size: 11px; 2611 flex-shrink: 0; 2612} 2613 2614.ai-progress-step.active .ai-step-icon { 2615 background: #667eea; 2616 color: #fff; 2617 animation: pulse 1.5s ease-in-out infinite; 2618} 2619 2620.ai-progress-step.completed .ai-step-icon { 2621 background: #10b981; 2622 color: #fff; 2623} 2624 2625.ai-progress-step.pending .ai-step-icon { 2626 background: #e2e8f0; 2627 color: #94a3b8; 2628} 2629 2630.ai-step-text { 2631 flex: 1; 2632 font-size: 13px; 2633 color: #475569; 2634 font-weight: 500; 2635} 2636 2637.ai-progress-step.completed .ai-step-text { 2638 color: #059669; 2639} 2640 2641/* AI Analysis Result Banner */ 2642.ai-result-banner { 2643 padding: 12px 16px; 2644 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2645 border-bottom: 1px solid #86efac; 2646 display: flex; 2647 align-items: center; 2648 gap: 10px; 2649} 2650 2651.ai-result-icon { 2652 font-size: 20px; 2653 animation: bounceIn 0.5s ease; 2654} 2655 2656@keyframes bounceIn { 2657 0% { transform: scale(0); opacity: 0; } 2658 50% { transform: scale(1.1); } 2659 100% { transform: scale(1); opacity: 1; } 2660} 2661 2662.ai-result-text { 2663 flex: 1; 2664} 2665 2666.ai-result-title { 2667 font-weight: 600; 2668 color: #065f46; 2669 font-size: 13px; 2670 margin-bottom: 2px; 2671} 2672 2673.ai-result-description { 2674 font-size: 12px; 2675 color: #047857; 2676 opacity: 0.9; 2677} 2678 2679/* Product Items Section Header */ 2680.suggest-section-header { 2681 padding: 10px 16px; 2682 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2683 border-bottom: 1px solid #e2e8f0; 2684} 2685 2686.suggest-header-main { 2687 display: flex; 2688 align-items: center; 2689 gap: 6px; 2690 margin-bottom: 4px; 2691} 2692 2693.suggest-header-icon { 2694 font-size: 14px; 2695} 2696 2697.suggest-header-text { 2698 font-size: 12px; 2699 font-weight: 600; 2700 color: #64748b; 2701 text-transform: uppercase; 2702 letter-spacing: 0.5px; 2703} 2704 2705.suggest-header-hint { 2706 font-size: 11px; 2707 color: #94a3b8; 2708 font-weight: 400; 2709 text-transform: none; 2710 letter-spacing: 0; 2711 display: flex; 2712 align-items: center; 2713 gap: 4px; 2714} 2715 2716.suggest-header-hint kbd { 2717 display: inline-block; 2718 padding: 2px 6px; 2719 font-size: 10px; 2720 font-weight: 600; 2721 line-height: 1; 2722 color: #475569; 2723 background: #fff; 2724 border: 1px solid #cbd5e1; 2725 border-radius: 4px; 2726 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2727 font-family: 'Segoe UI', monospace; 2728} 2729 2730/* Footer Hint */ 2731.suggest-footer-hint { 2732 display: flex; 2733 align-items: center; 2734 justify-content: center; 2735 padding: 12px 16px; 2736 background: linear-gradient(135deg, #fef3c7 0%, #C77DFF 100%); 2737 border-top: 1px solid #C77DFF; 2738 color: #ae7e21; 2739 font-size: 12px; 2740 font-weight: 500; 2741 text-align: center; 2742 cursor: default; 2743} 2744 2745.suggest-footer-hint svg { 2746 color: #C77DFF; 2747 flex-shrink: 0; 2748} 2749 2750.suggest-footer-hint kbd { 2751 display: inline-block; 2752 padding: 3px 7px; 2753 margin: 0 3px; 2754 font-size: 11px; 2755 font-weight: 600; 2756 line-height: 1; 2757 color: #765212; 2758 background: #fff; 2759 border: 1px solid #C77DFF; 2760 border-radius: 4px; 2761 box-shadow: 0 1px 3px rgba(245,202,86,0.2); 2762 font-family: 'Segoe UI', monospace; 2763} 2764 2765.suggest-footer-link { 2766 color: #C77DFF; 2767 font-weight: 600; 2768 text-decoration: underline; 2769 text-underline-offset: 2px; 2770 text-decoration-thickness: 1px; 2771 cursor: pointer; 2772 transition: all 0.2s ease; 2773} 2774 2775.suggest-footer-link:hover { 2776 color: #E3D05A; 2777 text-decoration-thickness: 2px; 2778 text-shadow: 0 0 8px rgba(245,202,86,0.3); 2779} 2780 2781/* Loading state */ 2782.suggest-loading { 2783 display: flex; 2784 align-items: center; 2785 justify-content: center; 2786 gap: 8px; 2787 padding: 16px 12px; 2788 color: #64748b; 2789 font-size: 13px; 2790} 2791 2792@keyframes pulse { 2793 0%, 100% { opacity: 1; } 2794 50% { opacity: 0.5; } 2795} 2796 2797.suggest-loading { 2798 animation: pulse 1.5s ease-in-out infinite; 2799} 2800 2801/* Responsive Mobile Styles */ 2802@media (max-width: 768px) { 2803 .ai-bot-toggle { 2804 padding: 6px; 2805 margin-left: 0; 2806 } 2807 2808 .ai-bot-toggle .bot-icon {
2809 width: 20px; 2810 height: 20px; 2811 } 2812 2813 .search-icon-btn { 2814 margin-right: 4px; 2815 } 2816 2817 .search-icon-btn svg { 2818 width: 18px; 2819 height: 18px; 2820 } 2821 2822 .search-suggest { 2823 max-height: 350px; 2824 } 2825 2826 /* Mobile hint adjustments */ 2827 .suggest-header-hint { 2828 font-size: 10px; 2829 } 2830 2831 .suggest-header-hint kbd { 2832 padding: 1px 4px; 2833 font-size: 9px; 2834 } 2835 2836 .suggest-footer-hint { 2837 padding: 10px 12px; 2838 font-size: 11px; 2839 } 2840 2841 .suggest-footer-hint svg { 2842 width: 14px; 2843 height: 14px; 2844 } 2845 2846 .suggest-footer-hint kbd { 2847 padding: 2px 5px; 2848 font-size: 10px; 2849 } 2850 2851 .ai-progress-container { 2852 padding: 16px 12px; 2853 } 2854 2855 .ai-progress-icon { 2856 width: 32px; 2857 height: 32px; 2858 font-size: 16px; 2859 } 2860 2861 .ai-progress-title { 2862 font-size: 13px; 2863 } 2864 2865 .ai-progress-subtitle { 2866 font-size: 11px; 2867 } 2868 2869 .ai-progress-step { 2870 padding: 6px 10px; 2871 } 2872 2873 .ai-step-text { 2874 font-size: 12px; 2875 } 2876 2877 .suggest-item { 2878 padding: 8px 12px; 2879 } 2880 2881 .suggest-thumb { 2882 width: 36px; 2883 height: 36px; 2884 } 2885 2886 .suggest-name { 2887 font-size: 13px; 2888 } 2889 2890 .suggest-price { 2891 font-size: 11px; 2892 } 2893} 2894 2895@media (max-width: 480px) { 2896 .ai-bot-toggle { 2897 padding: 4px; 2898 } 2899 2900 .ai-bot-toggle .bot-icon { 2901 width: 18px; 2902 height: 18px; 2903 } 2904 2905 .search-icon-btn svg { 2906 width: 16px; 2907 height: 16px; 2908 } 2909 2910 /* Ultra-compact hints for small screens */ 2911 .suggest-header-hint { 2912 display: none; /* Hide header hint on very small screens */ 2913 } 2914 2915 .suggest-footer-hint { 2916 padding: 8px 10px; 2917 font-size: 10px; 2918 flex-direction: column; 2919 gap: 4px; 2920 } 2921 2922 .suggest-footer-hint svg { 2923 display: none; /* Hide icon to save space */ 2924 } 2925 2926 .suggest-footer-hint kbd { 2927 padding: 2px 4px; 2928 font-size: 9px; 2929 } 2930 2931 .ai-progress-steps { 2932 gap: 8px; 2933 } 2934 2935 .ai-step-text { 2936 font-size: 11px; 2937 } 2938} 2939 2940/* AI Search Prompt Box */ 2941.ai-search-prompt { 2942 padding: 24px; 2943 background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); 2944} 2945 2946.ai-prompt-header { 2947 display: flex; 2948 align-items: center; 2949 gap: 12px; 2950 margin-bottom: 16px; 2951} 2952 2953.ai-prompt-icon { 2954 width: 48px; 2955 height: 48px; 2956 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 2957 border-radius: 12px; 2958 display: flex; 2959 align-items: center; 2960 justify-content: center; 2961 font-size: 24px; 2962 box-shadow: 0 4px 12px rgba(245,202,86,0.3); 2963} 2964 2965.ai-prompt-text { 2966 flex: 1; 2967} 2968 2969.ai-prompt-title { 2970 font-size: 16px; 2971 font-weight: 700; 2972 color: #1f2937; 2973 margin-bottom: 4px; 2974} 2975 2976.ai-prompt-subtitle { 2977 font-size: 13px; 2978 color: #6b7280; 2979} 2980 2981.ai-prompt-query { 2982 background: white; 2983 border: 2px solid #C77DFF; 2984 border-radius: 8px; 2985 padding: 12px 16px; 2986 margin-bottom: 16px; 2987} 2988 2989.ai-query-label { 2990 font-size: 11px; 2991 font-weight: 600; 2992 color: #C77DFF; 2993 text-transform: uppercase; 2994 letter-spacing: 0.5px; 2995 margin-bottom: 4px; 2996} 2997 2998.ai-query-text { 2999 font-size: 15px; 3000 font-weight: 600; 3001 color: #1f2937; 3002} 3003 3004.ai-search-trigger-btn { 3005 width: 100%; 3006 padding: 14px 20px; 3007 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 3008 color: white; 3009 border: none; 3010 border-radius: 10px; 3011 font-size: 15px; 3012 font-weight: 700; 3013 cursor: pointer; 3014 display: flex; 3015 align-items: center; 3016 justify-content: center; 3017 gap: 8px; 3018 transition: all 0.3s ease; 3019 box-shadow: 0 4px 12px rgba(245,202,86,0.4); 3020} 3021 3022.ai-search-trigger-btn:hover { 3023 transform: translateY(-2px); 3024 box-shadow: 0 6px 20px rgba(245,202,86,0.5); 3025} 3026 3027.ai-search-trigger-btn:active { 3028 transform: translateY(0); 3029} 3030 3031.ai-btn-icon { 3032 font-size: 18px; 3033 animation: sparkle 2s ease-in-out infinite; 3034} 3035 3036@keyframes sparkle { 3037 0%, 100% { opacity: 1; transform: scale(1); } 3038 50% { opacity: 0.7; transform: scale(1.2); } 3039} 3040 3041.ai-btn-text { 3042 font-size: 15px; 3043} 3044 3045.ai-prompt-footer { 3046 margin-top: 12px; 3047 text-align: center; 3048 font-size: 12px; 3049 color: #9ca3af; 3050 font-style: italic; 3051} 3052 3053/* ==================== TRENDING SEARCH STYLES ==================== */ 3054 3055.search-suggest.trending-mode { 3056 border-top: 3px solid #C77DFF; 3057 max-width: 900px; 3058 min-width: 600px; 3059} 3060 3061/* Two-column container */ 3062.trending-container { 3063 display: flex; 3064 gap: 0; 3065 padding: 0 !important; 3066} 3067 3068.trending-column { 3069 flex: 1; 3070 min-width: 0; 3071 padding: 0 !important; 3072} 3073 3074.trending-column.trending-searches { 3075 border-right: 2px solid #e2e8f0; padding: 0 !important; 3076} 3077 3078/* Mobile: stack columns vertically */ 3079@media (max-width: 768px) { 3080 .search-suggest.trending-mode { 3081 min-width: auto; 3082 max-width: 100%; 3083 } 3084 3085 .trending-container { 3086 flex-direction: column; 3087 } 3088 3089 .trending-column.trending-searches {
3090 border-right: none; 3091 border-bottom: 2px solid #e2e8f0; 3092 } 3093} 3094 3095.trending-header { 3096 display: flex; 3097 align-items: center; 3098 gap: 8px; 3099 padding: 14px 16px; 3100 border-bottom: 1px solid #e2e8f0; 3101 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 3102 color: white; 3103 position: sticky; 3104 top: 0; 3105 z-index: 10; 3106} 3107 3108.trending-icon { 3109 font-size: 20px; 3110 animation: trendingPulse 2s infinite; 3111} 3112 3113@keyframes trendingPulse { 3114 0%, 100% { 3115 transform: scale(1); 3116 filter: drop-shadow(0 0 0 rgba(245,202,86,0)); 3117 } 3118 50% { 3119 transform: scale(1.15); 3120 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3121 } 3122} 3123 3124.trending-title { 3125 font-weight: 700; 3126 font-size: 14px; 3127 letter-spacing: 0.3px; 3128} 3129 3130.trending-subtitle { 3131 font-size: 11px; 3132 opacity: 0.9; 3133 margin-left: auto; 3134 font-style: italic; 3135} 3136 3137/* Trending searches item */ 3138.trending-item { 3139 display: flex; 3140 align-items: center; 3141 gap: 12px; 3142 padding: 12px 16px; 3143 cursor: pointer; 3144 transition: all 0.2s ease; 3145 border-bottom: 1px solid #f1f5f9; 3146 position: relative; 3147} 3148 3149.trending-item:hover { 3150 background: linear-gradient(90deg, #fff7ed 0%, #ffedd5 100%); 3151 padding-left: 20px; 3152} 3153 3154.trending-item:hover::before { 3155 content: ''; 3156 position: absolute; 3157 left: 0; 3158 top: 0; 3159 bottom: 0; 3160 width: 4px; 3161 background: linear-gradient(180deg, #C77DFF 0%, #C77DFF 100%); 3162} 3163 3164.trending-item:last-child { 3165 border-bottom: none; 3166} 3167 3168.trending-rank { 3169 font-size: 20px; 3170 flex-shrink: 0; 3171 width: 32px; 3172 text-align: center; 3173} 3174 3175.trending-query { 3176 flex: 1; 3177 font-size: 14px; 3178 color: #1f2937; 3179 font-weight: 500; 3180 line-height: 1.4; 3181} 3182 3183.trending-category { 3184 font-size: 11px; 3185 color: #C77DFF; 3186 background: #fff7ed; 3187 padding: 3px 10px; 3188 border-radius: 12px; 3189 white-space: nowrap; 3190 font-weight: 500; 3191 border: 1px solid #fff1cc; 3192} 3193 3194/* Trending products item */ 3195.trending-product-item { 3196 display: flex; 3197 align-items: center; 3198 gap: 12px; 3199 padding: 12px 16px; 3200 cursor: pointer; 3201 transition: all 0.2s ease; 3202 border-bottom: 1px solid #f1f5f9; 3203 position: relative; 3204} 3205 3206.trending-product-item:hover { 3207 background: linear-gradient(90deg, #fef3c7 0%, #C77DFF 100%); 3208 padding-left: 20px; 3209} 3210 3211.trending-product-item:hover::before { 3212 content: ''; 3213 position: absolute; 3214 left: 0; 3215 top: 0; 3216 bottom: 0; 3217 width: 4px; 3218 background: linear-gradient(180deg, #C77DFF 0%, #C77DFF 100%); 3219} 3220 3221.trending-product-item:last-child { 3222 border-bottom: none; 3223} 3224 3225.trending-product-thumb { 3226 width: 48px; 3227 height: 48px; 3228 object-fit: cover; 3229 border-radius: 8px; 3230 border: 1px solid #e5e7eb; 3231 flex-shrink: 0; 3232 background: white; 3233} 3234 3235.trending-product-info { 3236 flex: 1; 3237 min-width: 0; 3238} 3239 3240.trending-product-name { 3241 font-size: 13px; 3242 font-weight: 400; 3243 color: #1f2937; 3244 line-height: 1.4; 3245 margin-bottom: 0; 3246 padding: 0 !important; 3247 overflow: hidden; 3248 text-overflow: ellipsis; 3249 display: -webkit-box; 3250 -webkit-line-clamp: 2; 3251 -webkit-box-orient: vertical; 3252} 3253 3254.trending-product-price { 3255 font-size: 12px; 3256 font-weight: 700; 3257 color: #C77DFF; 3258 padding: 4px 0 !important; 3259} 3260.trending-product { padding: 0;} 3261/* ==================== END TRENDING STYLES ==================== */ 3262 3263</style> 3264 <style data-inlined="product-detail.css"> 3265/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3266 3267/* Root / Container */ 3268.product-single { 3269 --pd-gap: 2rem; 3270 --pd-border: #e2e8f0; 3271 --pd-bg: #ffffff; 3272 --pd-muted: #64748b; 3273 --pd-dark: #0f172a; 3274 --pd-accent: #0d6efd; /* tema utama situs */ 3275 --pd-radius: 12px; 3276 --pd-soft: #f8fafc; 3277 --pd-soft-border: #e5e7eb; 3278 --pd-danger: #C77DFF; 3279 --pd-success-bg: #dcfce7; 3280 --pd-success-text: #166534; 3281 --pd-danger-bg: #fee2e2; 3282 --pd-danger-text: #E3D05A; 3283 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3284 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3285 max-width:1280px; 3286 margin-inline:auto; 3287} 3288 3289/* === Sticky Product Header Bar (muncul saat scroll) === */ 3290.pd-sticky-header { 3291 position: fixed; 3292 top: 0; 3293 left: 0; 3294 right: 0; 3295 z-index: 998; 3296 background: rgba(255, 255, 255, 0.98); 3297 backdrop-filter: blur(12px); 3298 -webkit-backdrop-filter: blur(12px); 3299 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3300 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3301 transform: translateY(-100%); 3302 opacity: 0; 3303 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3304 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3305 pointer-events: none; 3306} 3307 3308.pd-sticky-header.visible { 3309 transform: translateY(0); 3310 opacity: 1;
3311 pointer-events: auto; 3312} 3313 3314.pd-sticky-header-container { 3315 max-width: 1280px; 3316 margin: 0 auto; 3317 padding: 12px 20px; 3318 display: flex; 3319 align-items: center; 3320 gap: 16px; 3321} 3322 3323.pd-sticky-thumb { 3324 width: 48px; 3325 height: 48px; 3326 flex-shrink: 0; 3327 border-radius: 8px; 3328 border: 1px solid #e2e8f0; 3329 overflow: hidden; 3330 background: #f8fafc; 3331} 3332 3333.pd-sticky-thumb img { 3334 width: 100%; 3335 height: 100%; 3336 object-fit: cover; 3337 display: block; 3338} 3339 3340.pd-sticky-info { 3341 flex: 1; 3342 min-width: 0; 3343 display: flex; 3344 flex-direction: column; 3345 gap: 4px; 3346} 3347 3348.pd-sticky-title { 3349 font-size: 0.9rem; 3350 font-weight: 600; 3351 color: #0f172a; 3352 line-height: 1.3; 3353 white-space: nowrap; 3354 overflow: hidden; 3355 text-overflow: ellipsis; 3356 margin: 0; 3357} 3358 3359.pd-sticky-price-row { 3360 display: flex; 3361 align-items: center; 3362 gap: 8px; 3363 flex-wrap: wrap; 3364} 3365 3366.pd-sticky-price { 3367 font-size: 1.1rem; 3368 font-weight: 700; 3369 color: #0d6efd; 3370 line-height: 1; 3371} 3372 3373.pd-sticky-price-original { 3374 font-size: 0.75rem; 3375 color: #94a3b8; 3376 text-decoration: line-through; 3377 line-height: 1; 3378} 3379 3380.pd-sticky-discount { 3381 background: #C77DFF; 3382 color: #fff; 3383 font-size: 0.65rem; 3384 font-weight: 600; 3385 padding: 2px 6px; 3386 border-radius: 4px; 3387 letter-spacing: 0.3px; 3388 line-height: 1; 3389} 3390 3391.pd-sticky-actions { 3392 display: flex; 3393 gap: 8px; 3394 flex-shrink: 0; 3395 align-items: center; 3396} 3397 3398.pd-sticky-share-btn { 3399 padding: 8px 16px; 3400 border-radius: 8px; 3401 border: 1px solid #e2e8f0; 3402 background: #fff; 3403 color: #64748b; 3404 cursor: pointer; 3405 transition: all 0.2s ease; 3406 display: inline-flex; 3407 align-items: center; 3408 justify-content: center; 3409 flex-shrink: 0; 3410} 3411 3412.pd-sticky-share-btn:hover { 3413 background: #f8fafc; 3414 border-color: #cbd5e1; 3415 color: #0f172a; 3416 transform: translateY(-1px); 3417} 3418 3419.pd-sticky-share-btn:active { 3420 transform: translateY(0); 3421} 3422 3423.pd-sticky-share-btn svg { 3424 display: block; 3425 width: 16px; 3426 height: 16px; 3427} 3428 3429.pd-sticky-btn-chat svg { 3430 width: 16px !important; 3431 height: 16px !important; 3432 max-width: none !important; 3433 margin-right: 4px; 3434 flex-shrink: 0; 3435} 3436 3437.pd-sticky-btn { 3438 padding: 8px 16px; 3439 border-radius: 8px; 3440 font-size: 0.85rem; 3441 font-weight: 600; 3442 cursor: pointer; 3443 transition: all 0.2s ease; 3444 border: 0; 3445 display: inline-flex; 3446 align-items: center; 3447 justify-content: center; 3448 white-space: nowrap; 3449} 3450 3451.pd-sticky-btn-cart { 3452 background: #fff; 3453 color: #0d6efd; 3454 border: 1px solid #0d6efd; 3455} 3456 3457.pd-sticky-btn-cart:hover { 3458 background: rgba(13, 110, 253, 0.06); 3459} 3460 3461.pd-sticky-btn-chat { 3462 background: #fff; 3463 color: #64748b; 3464 border: 1px solid #cbd5e1; 3465 flex: 0.8; 3466} 3467 3468.pd-sticky-btn-chat:hover { 3469 background: #f8fafc; 3470 border-color: #94a3b8; 3471 color: #475569; 3472} 3473 3474.pd-sticky-btn-buy { 3475 background: #0d6efd; 3476 color: #fff; 3477 border: 1px solid #0d6efd; 3478 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3479} 3480 3481.pd-sticky-btn-buy:hover { 3482 filter: brightness(0.95); 3483} 3484 3485.pd-sticky-btn:disabled { 3486 opacity: 0.5; 3487 cursor: not-allowed; 3488} 3489 3490/* Hide on mobile (use existing FAB instead) */ 3491@media (max-width: 880px) { 3492 .pd-sticky-header { 3493 display: none; 3494 } 3495} 3496 3497/* === PRINT STYLES (for PDF Export) === */ 3498@media print { 3499 /* Reset & Base */ 3500 * { 3501 -webkit-print-color-adjust: exact !important; 3502 print-color-adjust: exact !important; 3503 } 3504 3505 body { 3506 background: white !important; 3507 margin: 0; 3508 padding: 0; 3509 } 3510 3511 /* Hide all navigation, buttons, and interactive elements */ 3512 nav, header, footer, 3513 nav *, header *, footer *, 3514 .navbar, .navbar *, 3515 .nav-item, .nav-link, 3516 .pd-sticky-header, 3517 .pd-sticky-header.visible, 3518 .pd-breadcrumb, 3519 .pd-sidebar, 3520 .pd-mobile-fab, 3521 .pd-share-btn, 3522 .pd-sticky-share-btn, 3523 #pd-share-btn, 3524 .share-button, 3525 [data-share-btn], 3526 button, 3527 .btn-add-cart, 3528 .btn-buy, 3529 .pd-actions, 3530 .pd-compare-section, 3531 .pd-compare-btn, 3532 .pd-compare-info, 3533 #pd-compare-btn, 3534 .compare-note, 3535 .compare-container, 3536 .compare-dropdown, 3537 .compare-panel, 3538 .compare-wrapper, 3539 [data-compare], 3540 [id*="compare"], 3541 [class*="compare"], 3542 .pd-thumb-gallery, 3543 .pd-installment-card, 3544 .pd-installment-card2, 3545 .inst-alt-banner--compact, 3546 .qoala-banner-card, 3547 .tradein-compact-card, 3548 .pwp-deals-section, 3549 #pwp-deals-section, 3550 .pwp-trigger-badge, 3551 .pd-related-full, 3552 #pd-similar, 3553 .pd-similar-strip, 3554 .pd-scroll-top, 3555 .stock-info-btn, 3556 .stock-popover, 3557 .pd-countdown-modern, 3558 .pd-share-modal-overlay, 3559 [id*="promo-"], 3560 .bottom-nav, 3561 .pd-buy-form, 3562 .pd-buy-card, 3563 .pd-extra-meta, 3564 .pd-reviews-section, 3565 #reviews, 3566 hr, 3567 .divider, 3568 .separator { 3569 display: none !important; 3570 visibility: hidden !important; 3571 opacity: 0 !important; 3572 height: 0 !important; 3573 max-height: 0 !important; 3574 overflow: hidden !important; 3575 margin: 0 !important; 3576 padding: 0 !important; 3577 border: none !important; 3578 } 3579 3580 /* Extra aggressive reset for any remaining borders */ 3581 .pd-info-card *, 3582 .pd-meta-inline *, 3583 .pd-variants * { 3584 border: none !important; 3585 border-top: none !important; 3586 border-bottom: none !important; 3587 } 3588 3589 /* Layout restructure */ 3590 .product-single { 3591 max-width: 100% !important; 3592 margin: 0 !important; 3593 padding: 0 !important; 3594 box-shadow: none !important; 3595 } 3596 3597 /* Reset all shadows and borders for print */ 3598 * { 3599 box-shadow: none !important; 3600 } 3601 3602 .pd-top-layout { 3603 display: grid !important; 3604 grid-template-columns: 280px 1fr !important; 3605 gap: 24px !important; 3606 margin: 0 0 24px 0 !important; 3607 padding: 0 !important; 3608 page-break-inside: avoid;
3609 align-items: start !important; 3610 } 3611 3612 /* Print header with logo - centered and larger */ 3613 .product-single::before { 3614 content: "" !important; 3615 display: block !important; 3616 width: 150px !important; 3617 height: 50px !important; 3618 background: url('/assets/img/logo.png') no-repeat center !important; 3619 background-size: contain !important; 3620 margin: 0 auto 20px !important; 3621 visibility: visible !important; 3622 opacity: 1 !important; 3623 } 3624 3625 /* Media card - left column */ 3626 .pd-media-card { 3627 border: none !important; 3628 padding: 0 !important; 3629 margin: 0 !important; 3630 text-align: center; 3631 page-break-inside: avoid; 3632 } 3633 3634 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3635 .pd-main-image { 3636 display: none !important; 3637 } 3638 3639 /* Mobile slider - show first image only in print */ 3640 .pd-mobile-slider { 3641 display: block !important; 3642 overflow: visible !important; 3643 } 3644 3645 .pd-mslide { 3646 display: none !important; 3647 } 3648 3649 .pd-mslide:first-child { 3650 display: block !important; 3651 border: 1px solid #e2e8f0 !important; 3652 max-width: 280px !important; 3653 margin: 0 auto !important; 3654 border-radius: 8px !important; 3655 } 3656 3657 .pd-mslide:first-child img { 3658 max-height: 280px !important; 3659 width: 100% !important; 3660 height: auto !important; 3661 object-fit: contain !important; 3662 } 3663 3664 /* Info card - right column */ 3665 .pd-info-card { 3666 border: none !important; 3667 border-top: none !important; 3668 border-bottom: none !important; 3669 padding: 0 !important; 3670 margin: 0 !important; 3671 background: white !important; 3672 box-shadow: none !important; 3673 } 3674 3675 /* Title */ 3676 .pd-title-row { 3677 display: block !important; 3678 margin-bottom: 10px !important; 3679 border: none !important; 3680 border-top: none !important; 3681 border-bottom: none !important; 3682 padding: 0 !important; 3683 box-shadow: none !important; 3684 text-decoration: none !important; 3685 } 3686 3687 /* Hide share button in print */ 3688 .pd-title-row .pd-share-btn, 3689 .pd-title-row #pd-share-btn { 3690 display: none !important; 3691 } 3692 3693 .pd-title { 3694 font-size: 13pt !important; 3695 color: #000 !important; 3696 margin: 0 0 10px 0 !important; 3697 page-break-after: avoid; 3698 font-weight: 700 !important; 3699 line-height: 1.3 !important; 3700 border: none !important; 3701 border-top: none !important; 3702 border-bottom: none !important; 3703 padding-bottom: 0 !important; 3704 box-shadow: none !important; 3705 text-decoration: none !important; 3706 } 3707 3708 /* Price and QR container - side by side */ 3709 .pd-price-qr-container { 3710 display: flex !important; 3711 gap: 16px !important; 3712 align-items: flex-start !important; 3713 margin-bottom: 14px !important; 3714 box-shadow: none !important; 3715 border: none !important; 3716 text-decoration: none !important; 3717 } 3718 3719 /* Price block - left side */ 3720 .pd-price-block { 3721 flex: 1 !important; 3722 margin: 0 !important; 3723 padding-bottom: 0 !important; 3724 border-bottom: none !important; 3725 box-shadow: none !important; 3726 } 3727 3728 .pd-price-row { 3729 margin-bottom: 8px !important; 3730 border: none !important; 3731 border-top: none !important; 3732 border-bottom: none !important; 3733 padding: 0 !important; 3734 } 3735 3736 /* Hide all empty or unnecessary elements in price row */ 3737 .pd-price-row > span:empty, 3738 .pd-price-row > .sold-inline, 3739 #pd-sold-inline { 3740 display: none !important; 3741 } 3742 3743 .pd-price-current { 3744 font-size: 18pt !important; 3745 color: #0d6efd !important; 3746 } 3747 3748 /* Show price range for variant products in print */ 3749 .pd-price-current[data-price-range]::after { 3750 content: attr(data-price-range) !important; 3751 display: block !important; 3752 font-size: 11pt !important; 3753 color: #64748b !important; 3754 margin-top: 4px !important; 3755 font-weight: normal !important; 3756 } 3757 3758 /* Hide 'Range:' prefix text */ 3759 .pd-price-current[data-price-range]::after { 3760 content: attr(data-price-range) !important; 3761 } 3762 3763 .pd-price-original { 3764 font-size: 12pt !important; 3765 color: #64748b !important; 3766 } 3767 3768 .pd-discount-badge { 3769 font-size: 10pt !important; 3770 padding: 2px 6px !important; 3771 } 3772 3773 /* QR Code section - right side beside price */ 3774 .pd-qr-print { 3775 display: block !important; 3776 flex: 0 0 100px !important; 3777 margin: 0 !important; 3778 padding: 8px !important; 3779 background: #f8fafc !important; 3780 border-radius: 8px !important; 3781 text-align: center !important; 3782 page-break-inside: avoid !important; 3783 border: 1px solid #cbd5e1 !important; 3784 } 3785 3786 .pd-qr-label { 3787 font-size: 6.5pt !important; 3788 color: #64748b !important; 3789 margin-bottom: 5px !important; 3790 line-height: 1.2 !important; 3791 font-weight: 600 !important; 3792 } 3793 3794 .pd-qr-image { 3795 width: 80px !important; 3796 height: 80px !important; 3797 display: block !important; 3798 margin: 0 auto !important; 3799 } 3800 3801 /* Meta info */ 3802 .pd-meta-inline { 3803 display: flex !important; 3804 gap: 12px !important; 3805 margin-bottom: 0 !important; 3806 padding-bottom: 14px !important; 3807 font-size: 10pt !important; 3808 border: none !important; 3809 border-top: none !important; 3810 border-bottom: none !important; 3811 padding-top: 0 !important; 3812 box-shadow: none !important; 3813 text-decoration: none !important; 3814 background: transparent !important; 3815 } 3816 3817 /* Hide stock badge in print */ 3818 .stock-badge, 3819 .stock-info-btn { 3820 display: none !important; 3821 } 3822 3823 .sku { 3824 color: #64748b !important; 3825 } 3826 3827 /* Hide sold badge if empty/zero */ 3828 .sold-inline { 3829 display: none !important; 3830 } 3831 3832 /* Hide variant summary section in print */ 3833 .pd-variant-combined, 3834 #pd-variant-combined, 3835 .pd-variant-summary, 3836 [data-role="sold-chip"] { 3837 display: none !important; 3838 } 3839 3840 /* Hide compare section completely in print */ 3841 .pd-compare-section, 3842 .pd-compare-btn, 3843 #pd-compare-btn, 3844 .pd-compare-info, 3845 .compare-note, 3846 .compare-text, 3847 .compare-remove { 3848 display: none !important; 3849 visibility: hidden !important; 3850 height: 0 !important; 3851 margin: 0 !important; 3852 padding: 0 !important; 3853 overflow: hidden !important; 3854 } 3855 3856 /* Variants - show all options with selected highlighted */ 3857 .pd-variants { 3858 margin: 0 !important; 3859 padding: 0 !important; 3860 page-break-inside: avoid !important; 3861 border: none !important; 3862 border-top: none !important; 3863 border-bottom: none !important; 3864 box-shadow: none !important; 3865 background: transparent !important; 3866 } 3867 3868 /* Add 'Variant:' label before variant groups */ 3869 .pd-variants::before { 3870 content: "Variant:" !important; 3871 display: block !important; 3872 font-size: 11pt !important; 3873 font-weight: 700 !important; 3874 color: #0f172a !important; 3875 margin-bottom: 10px !important; 3876 border: none !important; 3877 padding: 0 !important; 3878 border-top: none !important; 3879 border-bottom: none !important; 3880 } 3881 3882 .v-group { 3883 display: flex !important;
3884 align-items: center !important; 3885 gap: 8px !important; 3886 margin-bottom: 8px !important; 3887 line-height: 1 !important; 3888 page-break-inside: avoid !important; 3889 border: none !important; 3890 padding: 0 !important; 3891 } 3892 3893 .v-group:last-child { 3894 margin-bottom: 0 !important; 3895 } 3896 3897 .v-label { 3898 font-weight: 700 !important; 3899 margin: 0 !important; 3900 font-size: 9pt !important; 3901 color: #475569 !important; 3902 min-width: 60px !important; 3903 flex-shrink: 0 !important; 3904 border: none !important; 3905 padding: 0 !important; 3906 } 3907 3908 .v-options { 3909 display: flex !important; 3910 gap: 6px !important; 3911 flex-wrap: wrap !important; 3912 border: none !important; 3913 padding: 0 !important; 3914 margin: 0 !important; 3915 } 3916 3917 .v-chip { 3918 border: 1px solid #cbd5e1 !important; 3919 padding: 3px 10px !important; 3920 font-size: 9pt !important; 3921 background: #f8fafc !important; 3922 border-radius: 6px !important; 3923 display: inline-flex !important; 3924 align-items: center !important; 3925 page-break-inside: avoid !important; 3926 } 3927 3928 /* Highlight selected variant with blue background */ 3929 .v-chip.selected { 3930 background: #eff6ff !important; 3931 border-color: #2563eb !important; 3932 font-weight: 600 !important; 3933 } 3934 3935 /* Hide radio inputs */ 3936 .v-chip input[type="radio"] { 3937 display: none !important; 3938 } 3939 3940 /* Hide "Habis" badge in print */ 3941 .v-chip .chip-badge { 3942 display: none !important; 3943 } 3944 3945 /* Short description - HIDDEN in print */ 3946 .pd-short-desc { 3947 display: none !important; 3948 } 3949 3950 /* QR Code section - right side beside price */ 3951 .pd-qr-print { 3952 display: block !important; 3953 flex: 0 0 100px !important; 3954 margin: 0 !important; 3955 padding: 8px !important; 3956 background: #f8fafc !important; 3957 border-radius: 8px !important; 3958 text-align: center !important; 3959 page-break-inside: avoid !important; 3960 border: 1px solid #cbd5e1 !important; 3961 } 3962 3963 .pd-qr-label { 3964 font-size: 6.5pt !important; 3965 color: #64748b !important; 3966 margin-bottom: 5px !important; 3967 line-height: 1.2 !important; 3968 font-weight: 600 !important; 3969 } 3970 3971 .pd-qr-image { 3972 width: 80px !important; 3973 height: 80px !important; 3974 display: block !important; 3975 margin: 0 auto !important; 3976 } 3977 3978 /* Full description - below 2 column layout */ 3979 .pd-full-description { 3980 border: none !important; 3981 padding: 0 !important; 3982 margin: 24px 0 0 0 !important; 3983 page-break-before: avoid; 3984 grid-column: 1 / -1 !important; 3985 } 3986 3987 .pd-full-desc-title { 3988 font-size: 13pt !important; 3989 font-weight: 700 !important; 3990 margin: 0 0 12px 0 !important; 3991 color: #000 !important; 3992 border-top: none !important; 3993 border-bottom: none !important; 3994 padding-top: 16px !important; 3995 } 3996 3997 .pd-full-desc-body { 3998 font-size: 10pt !important; 3999 line-height: 1.6 !important; 4000 color: #334155 !important; 4001 } 4002 4003 .pd-full-desc-body img { 4004 max-height: 200px !important; 4005 width: auto !important; 4006 page-break-inside: avoid; 4007 display: none !important; /* Hide images in print to save space */ 4008 } 4009 4010 /* Also target #deskripsi img if exists */ 4011 #deskripsi img { 4012 max-width: 350px !important; 4013 height: auto !important; 4014 } 4015 4016 .pd-full-desc-body table { 4017 border-collapse: collapse !important; 4018 width: 100% !important; 4019 margin: 12px 0 !important; 4020 font-size: 9pt !important; 4021 } 4022 4023 .pd-full-desc-body th, 4024 .pd-full-desc-body td { 4025 border: 1px solid #cbd5e1 !important; 4026 padding: 6px 8px !important; 4027 } 4028 4029 /* Print footer */ 4030 .product-single::after { 4031 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 4032 display: block; 4033 margin-top: 30px; 4034 padding-top: 16px; 4035 border-top: 1px solid #e2e8f0; 4036 font-size: 9pt; 4037 color: #64748b; 4038 text-align: center; 4039 } 4040 4041 /* Page breaks */ 4042 .pd-media-card, 4043 .pd-price-block, 4044 .pd-full-desc-title { 4045 page-break-after: avoid; 4046 } 4047 4048 .pd-full-description { 4049 page-break-before: auto; 4050 } 4051} 4052 4053/* === Breadcrumb (truncate last item) === */ 4054.pd-breadcrumb{ 4055 display:flex; 4056 flex-wrap:nowrap; 4057 align-items:center; 4058 gap:.5rem; 4059 font-size:.72rem; 4060 margin:.7rem 0 1.2rem .5rem; 4061 color:var(--pd-muted); 4062 overflow:hidden; 4063} 4064.pd-breadcrumb a{color:var(--pd-accent);text-decoration:none} 4065.pd-breadcrumb a:hover{text-decoration:underline} 4066.pd-breadcrumb a, 4067.pd-breadcrumb span{flex:0 0 auto} 4068.pd-breadcrumb .current{ 4069 flex:1 1 auto; 4070 min-width:0; 4071 white-space:nowrap; 4072 overflow:hidden; 4073 text-overflow:ellipsis; 4074 font-weight:600;
4075 color:var(--pd-dark); 4076 padding-right:2px; 4077} 4078 4079/* === Top layout === */ 4080.pd-top-layout{ 4081 display:grid; 4082 gap:var(--pd-gap); 4083 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 4084 align-items:start; 4085 margin-bottom:2.4rem; 4086} 4087 4088/* === Media column === */ 4089.pd-media-card{ 4090 background:#fff; 4091 border:1px solid var(--pd-border); 4092 border-radius:var(--pd-radius); 4093 padding:1rem 1rem 1.2rem; 4094} 4095.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4096.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4097 4098/* Gallery Thumbnails */ 4099.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4100.pd-thumb-btn{ 4101 border:1px solid var(--pd-soft-border); 4102 background:#fff; 4103 cursor:pointer; 4104 width:64px;height:64px;padding:2px; 4105 display:flex;align-items:center;justify-content:center; 4106 overflow:hidden;border-radius:8px; 4107 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4108 position:relative; 4109} 4110.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4111.pd-thumb-btn:hover{border-color:var(--pd-accent)} 4112.pd-thumb-btn.active{border-color:var(--pd-accent);box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4113.pd-thumb-btn:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4114.pd-thumb-btn:active{transform:scale(.96)} 4115 4116/* === Info column === */ 4117.pd-info-card{ 4118 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4119 padding:1.4rem 1.6rem 2rem; 4120} 4121.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4122.pd-price-block{margin:0 0 1rem} 4123.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4124.pd-price-current{font-size:2rem;font-weight:700;color:var(--pd-accent)} 4125.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4126.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} 4127.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} 4128.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} 4129.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4130.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4131.digital-badge{padding:4px 10px;border-radius:24px;background:linear-gradient(135deg,#C77DFF 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} 4132.sku{color:var(--pd-muted)} 4133.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4134.pd-short-desc p{margin:0 0 .75rem} 4135.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4136 4137/* Meta row for sold and rating badges */ 4138.pd-meta-row { 4139 display: flex; 4140 align-items: center; 4141 flex-wrap: wrap; 4142 gap: 5px; 4143 margin-top: 8px; 4144 margin-bottom: 4px; 4145} 4146 4147/* Sales (Terjual) badge inline */ 4148.sold-inline{ 4149 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4150 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4151} 4152 4153/* Price row divider */ 4154.price-divider { 4155 display: inline-block; 4156 margin: 0 .4rem; 4157 color: #cbd5e1; 4158 font-weight: 400; 4159} 4160 4161/* Rating badge inline */ 4162.rating-inline { 4163 display: inline-flex; 4164 align-items: center; 4165 gap: 4px; 4166 padding: 3px 8px 4px; 4167 background: #fff7ed; 4168 border-radius: 6px; 4169 font-size: .65rem; 4170 font-weight: 600; 4171 color: #C77DFF; 4172 text-decoration: none; 4173 transition: all .2s ease; 4174 cursor: pointer; 4175 margin-left: .5rem; 4176 white-space: nowrap; 4177} 4178 4179.rating-inline:hover { 4180 background: #ffedd5; 4181 transform: translateY(-1px); 4182 box-shadow: 0 2px 4px rgba(245,202,86,0.1); 4183} 4184 4185.rating-inline .star-icon { 4186 font-size: .75rem; 4187 color: #C77DFF; 4188 line-height: 1; 4189} 4190 4191.rating-inline .rating-value { 4192 color: #C77DFF; 4193} 4194 4195.rating-inline .rating-badge-count { 4196 color: #E3D05A; 4197 opacity: 0.85; 4198} 4199 4200@media (max-width: 640px) { 4201 .rating-inline { 4202 font-size: .65rem; 4203 padding: 3px 8px 4px; 4204 } 4205 .rating-inline .star-icon { 4206 font-size: .7rem; 4207 } 4208} 4209 4210/* Highlight animation for review section */ 4211@keyframes highlightFlash { 4212 0%, 100% { background-color: transparent; } 4213 50% { background-color: rgba(245,202,86,0.1); } 4214} 4215 4216.highlight-flash { 4217 animation: highlightFlash 1.5s ease; 4218} 4219 4220/* === Variant chips === */ 4221.pd-variants{margin-top:14px} 4222.v-group{margin-bottom:12px} 4223.v-label{font-weight:600;margin-bottom:6px} 4224.v-options{display:flex;flex-wrap:wrap;gap:8px} 4225.v-chip{ 4226 display:inline-flex;align-items:center;gap:8px; 4227 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4228 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4229} 4230.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4231.v-chip.selected{border-color:#2563eb;background:#eff6ff} 4232.v-chip.disabled{opacity:.45;cursor:not-allowed} 4233.v-chip.out{border-style:dashed} 4234.v-chip input{display:none} 4235.v-chip .dot{width:14px;
4235height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4236.v-chip .dot--neutral{background:#9aa3af} 4237.v-chip .txt{font-size:.95rem;white-space:nowrap} 4238.v-chip .chip-badge{margin-left:2px;font-size:.72rem;line-height:1;padding:3px 6px;border-radius:6px;background:#fee2e2;color:#E3D05A;border:1px solid #fecaca} 4239.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4240.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4241 4242/* === Buy form (di sidebar kanan untuk desktop) === */ 4243.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4244.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4245.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4246.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4247 4248/* Tombol senada tema */ 4249.btn-add-cart,.btn-buy,.btn-chat-icon{ 4250 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4251 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4252 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4253} 4254.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4255.btn-add-cart:hover{filter:brightness(.95)} 4256.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4257.btn-buy:hover{background:rgba(13,110,253,.06)} 4258.btn-chat-icon{background:#fff;color:#C77DFF;border:2px solid #C77DFF;padding:0;width:44px;min-width:44px} 4259.btn-chat-icon:hover{background:rgba(245,202,86,0.06);border-color:#C77DFF} 4260.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4261.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4262 4263/* Buy with Voucher Button - Cleaner, more compact design */ 4264.btn-buy-voucher{ 4265 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4266 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4267 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4268 background:linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); 4269 color:#fff; 4270 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4271 position:relative; 4272 overflow:hidden; 4273 flex:1 1 auto 4274} 4275.btn-buy-voucher:hover{ 4276 transform:translateY(-2px); 4277 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4278} 4279.btn-buy-voucher:active{ 4280 transform:translateY(0); 4281 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4282} 4283.btn-buy-voucher .bv-icon{ 4284 font-size:20px; 4285 line-height:1; 4286 flex-shrink:0 4287} 4288.btn-buy-voucher .bv-text{ 4289 display:flex; 4290 flex-direction:column; 4291 align-items:flex-start; 4292 gap:2px; 4293 flex:1; 4294 min-width:0; 4295 font-size:14px; 4296 line-height:1.2 4297} 4298.btn-buy-voucher .bv-text small{ 4299 font-size:10px; 4300 opacity:0.85; 4301 font-weight:500; 4302 letter-spacing:0.3px; 4303 font-family:monospace 4304} 4305.btn-buy-voucher .bv-save{ 4306 font-size:12px; 4307 font-weight:700; 4308 background:rgba(255,255,255,0.25); 4309 padding:4px 8px; 4310 border-radius:6px; 4311 white-space:nowrap; 4312 flex-shrink:0 4313} 4314 4315/* Extra meta (opsional) */ 4316.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} 4317.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4318.pd-extra-meta a{color:var(--pd-accent);text-decoration:none} 4319.pd-extra-meta a:hover{text-decoration:underline} 4320 4321/* === Sidebar === */ 4322.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4323.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4324.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4325.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4326.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4327.pd-side-card li:before{content:'';width:6px;height:6px;background:var(--pd-accent);border-radius:50%;position:absolute;
4327left:0;top:.45rem} 4328.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4329.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} 4330 4331/* Buy card hanya desktop (mobile pakai FAB) */ 4332.pd-buy-card{display:block} 4333@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4334 4335/* === Full Description === */ 4336.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} 4337.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4338.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4339.pd-full-desc-body p{margin:0 0 1rem} 4340.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4341.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4342.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4343.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4344.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4345 4346/* === Related (container) === */ 4347.pd-related-full{ 4348 background:#fff;border:1px solid var(--pd-border); 4349 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4350 margin:0 0 2rem; 4351} 4352.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4353 4354/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4355.pd-related-grid, 4356.pd-related-grid--full{ 4357 display:grid !important; 4358 gap:16px !important; 4359 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4360 align-items:stretch; 4361} 4362 4363/* Fallback jika grid tidak didukung */ 4364@supports not (display:grid){ 4365 .pd-related-grid, 4366 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4367 .pd-related-grid .pd-related-card, 4368 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4369 @media (max-width:1100px){ 4370 .pd-related-grid .pd-related-card, 4371 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4372 } 4373 @media (max-width:820px){ 4374 .pd-related-grid .pd-related-card, 4375 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4376 } 4377 @media (max-width:580px){ 4378 .pd-related-grid .pd-related-card, 4379 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4380 } 4381} 4382 4383/* Related Card */ 4384.pd-related-card{ 4385 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4386 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4387 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4388} 4389.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4390.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4391.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4392.mini-disc{position:absolute;top:6px;left:6px;background:#C77DFF;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4393.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4394.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} 4395.pd-related-card .price{font-size:.7rem;font-weight:600;color:var(--pd-accent)} 4396.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;} 4397.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4398.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4399 4400/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4401.pd-mobile-slider{display:none} 4402.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4403 4404/* Gallery counter badge */ 4405.pd-gallery-counter{ 4406 position:absolute; 4407 bottom:12px; 4408 left:12px; 4409 background:rgba(255,255,255,0.25); 4410 color:#9E9E9E; 4411 padding:4px 10px; 4412 border-radius:10px; 4413 font-size:12px; 4414 font-weight:600; 4415 z-index:100; 4416 backdrop-filter:blur(6px); 4417 -webkit-backdrop-filter:blur(6px); 4418 font-variant-numeric:tabular-nums; 4419 pointer-events:none; 4420 display:none; 4421 box-shadow:0 1px 4px rgba(0,0,0,.15); 4422 line-height:1; 4423 border:1px solid rgba(0,0,0,0.08); 4424} 4425 4426@media (max-width:640px){ 4427 .pd-media-card{border: 0;padding: 0 1rem 0;} 4428 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4429 .pd-side-card {flex: 1 1 !important;} 4430 .pd-top-layout {gap: 0; margin-bottom: 0;} 4431 .pd-variants {margin-bottom: 1.5rem;} 4432 .pd-main-image{display:none} 4433 .pd-gallery-counter{display:block} 4434 .pd-mobile-slider-wrapper{ 4435 position:relative; 4436 margin-bottom:.6rem; 4437 } 4438 .pd-mobile-slider{ 4439 display:flex;gap:8px;overflow-x:auto; 4440 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
4441 } 4442 .pd-mslide{ 4443 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4444 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4445 } 4446 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4447} 4448 4449/* === Mobile Floating Action Bar === */ 4450.pd-mobile-fab{ 4451 position:fixed; 4452 left:0; 4453 right:0; 4454 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4455 display:none; 4456 align-items:center; 4457 gap:.75rem; 4458 padding:12px 16px; 4459 background:rgba(255,255,255,0.95); 4460 backdrop-filter:blur(20px); 4461 -webkit-backdrop-filter:blur(20px); 4462 border-top:1px solid rgba(228,231,235,0.6); 4463 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4464 z-index:999; /* Below bottom-nav (1000) */ 4465 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4466} 4467 4468/* When bottom-nav is hidden, move FAB to bottom */ 4469body:has(.bottom-nav.hidden) .pd-mobile-fab { 4470 bottom: 0; 4471} 4472.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4473.mf-btn{ 4474 border-radius:10px; 4475 padding:.6rem 1rem; 4476 font-weight:600; 4477 font-size:14px; 4478 cursor:pointer; 4479 transition:all .2s; 4480 -webkit-tap-highlight-color:transparent; 4481 display:inline-flex; 4482 align-items:center; 4483 justify-content:center; 4484 gap:4px; 4485} 4486.mf-buy{ 4487 background:#C77DFF; 4488 color:#fff; 4489 border:0; 4490 box-shadow:0 2px 8px rgba(245,202,86,0.3); 4491 flex:1; 4492 flex-direction:column; 4493 padding:.55rem 1rem; 4494 line-height:1; 4495 min-height:44px; 4496 font-family:inherit; 4497} 4498.mf-buy .mf-buy-text{ 4499 font-size:11px; 4500 font-weight:600; 4501 opacity:0.95; 4502 font-family:inherit; 4503} 4504.mf-buy .mf-buy-price{ 4505 font-size:13px; 4506 font-weight:800; 4507 letter-spacing:-0.3px; 4508 font-family:inherit; 4509 line-height:1; 4510} 4511.mf-buy:active{transform:scale(0.97)} 4512.mf-cart{ 4513 background:#fff; 4514 color:#C77DFF; 4515 border:2px solid #C77DFF; 4516 width:44px; 4517 height:44px; 4518 min-height:44px; 4519 padding:0; 4520 flex-shrink:0; 4521 font-family:inherit; 4522} 4523.mf-cart:active{transform:scale(0.95)} 4524.mf-chat{background:#fff;color:#C77DFF;border:2px solid #C77DFF;width:44px;height:44px;min-height:44px;padding:0;flex-shrink:0;font-family:inherit} 4525.mf-chat:active{transform:scale(0.95)} 4526.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4527.mf-share{ 4528 background:#fff; 4529 color:#C77DFF; 4530 border:2px solid #C77DFF; 4531 width:44px; 4532 height:44px; 4533 padding:0; 4534 flex-shrink:0; 4535} 4536.mf-share:active{transform:scale(0.95)} 4537.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4538@media (max-width:640px){ 4539 .pd-mobile-fab{display:flex} 4540 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4541} 4542 4543/* === Installment cards === */ 4544.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} 4545.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4546 4547.inst-tabs{display:flex;gap:.4rem} 4548.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)} 4549.inst-tab.active,.inst-tab:hover{background:var(--pd-accent);color:#fff;border-color:var(--pd-accent)} 4550.inst-tab:focus{outline:2px solid var(--pd-accent);outline-offset:2px} 4551 4552.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4553.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} 4554.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4555 4556.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4557.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} 4558.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4559.inst-line .lbl{opacity:.8} 4560.inst-line .val{font-weight:600;letter-spacing:.3px} 4561.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4562@media (max-width: 860px){ 4563 .pd-installment-card{order:3} 4564 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4565 4566 /* Voucher button responsive - make it simpler */ 4567 .btn-buy-voucher{ 4568 padding:0 12px; 4569 gap:8px 4570 } 4571 .btn-buy-voucher .bv-icon{ 4572 font-size:18px 4573 } 4574 .btn-buy-voucher .bv-text{ 4575 font-size:13px 4576 } 4577 .btn-buy-voucher .bv-text small{ 4578 font-size:9px 4579 } 4580 .btn-buy-voucher .bv-save{ 4581 font-size:11px; 4582 padding:3px 6px 4583 } 4584} 4585 4586/* Installment List v2 (eye-catching) */ 4587.pd-installment-card2{ 4588 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4589 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4590 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4591 position:relative;overflow:hidden 4592} 4593.pd-installment-card2:before{ 4594 content:'';position:absolute;width:140px;height:140px; 4595 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4596 top:-40px;right:-40px;pointer-events:none 4597} 4598.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4599.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4600.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)} 4601 4602.inst-bank-tabs{display:flex;gap:.45rem} 4603.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} 4604.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4605.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4606 4607.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4608.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} 4609.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} 4610.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4611.inst-month-item:hover:before{opacity:1} 4612.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)} 4613.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4614.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4615.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4616 4617.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} 4618.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4619 4620@media (max-width:860px){ 4621 .pd-installment-card2{order:3} 4622 .tenor-badge{min-width:38px;font-size:.75rem} 4623 .amount{font-size:.78rem} 4624} 4625 4626/* Alt Fintech Banner (compact) */ 4627.inst-alt-banner--compact{ 4628 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4629 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4630 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4631} 4632.inst-alt-banner--compact:before, 4633.inst-alt-banner--compact:after{ 4634 content:'';position:absolute;border-radius:50%;background:rgba(255,255,255,.15);filter:blur(2px);pointer-events:none; 4635} 4636.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4637.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4638.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4639.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4640.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4641 4642.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4643.iab-logos-compact .logo-fin{width:30%} 4644.iab-logos-compact .logo{ 4645 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4646 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4647 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(255,255,255,.15); 4648 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4649} 4650.iab-logos-compact .logo:before{ 4651 content:'';position:absolute;inset:0;border-radius:inherit; 4652 background:linear-gradient(140deg,rgba(255,255,255,.35),rgba(255,255,255,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4653} 4654.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4655 4656.iab-cta--compact{ 4657 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; 4658} 4659.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4660.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4661@media (max-width:860px){ 4662 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4663 .iab-logos-compact .logo{width:46px;height:46px} 4664} 4665 4666/* === Scroll To Top (baru) === */ 4667.pd-scroll-top{ 4668 position:fixed; 4669 right:20px; 4670 bottom:90px; 4671 width:48px; 4672 height:48px; 4673 border-radius:50%; 4674 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4675 color:#0f2f54; 4676 border:1px solid rgba(226, 232, 240, 0.8); 4677 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4678 display:flex; 4679 align-items:center; 4680 justify-content:center; 4681 cursor:pointer; 4682 z-index:1105; 4683 opacity:0; 4684 transform:translateY(12px) scale(.92); 4685 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4686 backdrop-filter:blur(8px); 4687 -webkit-backdrop-filter:blur(8px); 4688} 4689.pd-scroll-top:hover{ 4690 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4691 transform:translateY(0) scale(1); 4692 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4693 border-color:rgba(226, 232, 240, 1); 4694} 4695.pd-scroll-top:active{ 4696 transform:translateY(0) scale(.96); 4697} 4698.pd-scroll-top:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4699.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4700@media (max-width:640px){ 4701 /* Dynamic positioning via JavaScript */ 4702 .pd-scroll-top{ 4703 right:14px; 4704 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4705 } 4706} 4707 4708/* === Responsive layout tweaks === */ 4709@media (max-width:1250px){ 4710 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4711 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4712 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4713 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4714 .pd-buy-card{flex:0 0 100%} 4715} 4716@media (max-width:880px){ 4717 .pd-top-layout{grid-template-columns:1fr} 4718 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4719 .pd-title{font-size:1.2rem} 4720 .pd-price-current{font-size:1.5rem} 4721} 4722@media (max-width:640px){ 4723 /* Related: 2 kolom di mobile */ 4724 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4725 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4726} 4727@media (max-width:560px){ 4728 .pd-actions{flex-direction:column} 4729 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4730 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4731} 4732@media (max-width:380px){ 4733 .pd-related-full{padding:.9rem .7rem 1.1rem} 4734 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4735 .pd-related-card .info{padding:.4rem .48rem .55rem} 4736} 4737 4738/* === Modern Countdown (Product Detail) === */ 4739.pd-countdown-modern{ 4740 --cd-bg1:#0ea5e9; /* sky-500 */ 4741 --cd-bg2:#6366f1; /* indigo-500 */ 4742 --cd-text:#0b1220; 4743 --cd-contrast:#ffffff; 4744 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4745 --cd-border: rgba(255,255,255,.25); 4746 --cd-box: rgba(255,255,255,.65); 4747 --cd-box-text:#0f172a; 4748 --cd-danger:#C77DFF; 4749 4750 display:flex; gap:.75rem; align-items:center; 4751 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4752 border:1px solid var(--cd-border); 4753 color:var(--cd-contrast); 4754 border-radius:14px; 4755 padding:.6rem .8rem; 4756 box-shadow:var(--cd-shadow); 4757 position:relative; 4758 overflow:hidden; 4759 margin-bottom: 1rem; 4760} 4761 4762.pd-countdown-modern .pd-cd-icon{ 4763 width:28px;height:28px;flex:0 0 28px; 4764 display:grid;place-items:center; 4765 background:rgba(255,255,255,.22); 4766 border:1px solid var(--cd-border); 4767 border-radius:10px; 4768 box-shadow: inset 0 1px 0 rgba(255,255,255,.35); 4769} 4770.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4771
4772.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4773 4774.pd-countdown-modern .pd-cd-label{ 4775 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4776 text-shadow:0 1px 0 rgba(0,0,0,.08); 4777} 4778 4779.pd-countdown-modern .pd-cd-time{ 4780 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4781 line-height:1; 4782} 4783 4784.pd-countdown-modern .unit{ 4785 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4786 min-width:58px; 4787} 4788.pd-countdown-modern .num{ 4789 display:inline-flex; align-items:center; justify-content:center; 4790 min-width:58px; height:40px; padding:0 .4rem; 4791 background:var(--cd-box); 4792 color:var(--cd-box-text); 4793 border:1px solid rgba(15,23,42,.08); 4794 border-radius:10px; 4795 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4796 letter-spacing:.5px; 4797 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.55); 4798} 4799.pd-countdown-modern .unit-label{ 4800 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(255,255,255,.15); 4801} 4802 4803.pd-countdown-modern .colon{ 4804 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4805 animation: cdBlink 1.2s infinite steps(2, jump-none); 4806} 4807@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4808 4809.pd-countdown-modern .pd-cd-progress{ 4810 display:none; 4811 position:relative; 4812 height:6px; border-radius:999px; overflow:hidden; 4813 background:rgba(255,255,255,.25); 4814 border:1px solid rgba(255,255,255,.28); 4815} 4816.pd-countdown-modern .pd-cd-progress .bar{ 4817 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4818 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4819 transition:width .35s ease; 4820} 4821 4822/* State saat hampir habis */ 4823.pd-countdown-modern.danger{ 4824 --cd-bg1:#C77DFF; /* orange-500 */ 4825 --cd-bg2:#C77DFF; /* red-500 */ 4826} 4827 4828/* Responsive */ 4829@media (max-width:640px){ 4830 .pd-countdown-modern{ 4831 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4832 } 4833 .pd-countdown-modern .unit{min-width:52px} 4834 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4835} 4836 4837/* === Modern Countdown polish (shine + compact) === */ 4838.pd-countdown-modern{ 4839 position:relative; 4840} 4841.pd-countdown-modern::after{ 4842 content:""; 4843 position:absolute; inset:-1px; 4844 background: linear-gradient(120deg, rgba(255,255,255,.12), rgba(255,255,255,0) 35%), 4845 radial-gradient(120% 120% at 0% 0%, rgba(255,255,255,.08), rgba(255,255,255,0) 60%); 4846 border-radius: inherit; 4847 pointer-events:none; 4848 mix-blend-mode: screen; 4849 animation: sheen 4.5s ease-in-out infinite; 4850} 4851@keyframes sheen { 4852 0% { opacity:.35; transform: translateX(-10%) } 4853 50% { opacity:.15; transform: translateX(4%) } 4854 100% { opacity:.35; transform: translateX(-10%) } 4855} 4856 4857/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4858.pd-countdown-modern[data-mode="compact"]{ 4859 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4860} 4861.pd-countdown-modern[data-mode="compact"] .unit{ 4862 min-width:46px; 4863} 4864.pd-countdown-modern[data-mode="compact"] .num{ 4865 min-width:46px; height:34px; font-size:.92rem; 4866} 4867.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4868 font-size:.66rem; 4869} 4870.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4871 width:24px; height:24px; border-radius:8px; 4872} 4873.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4874 height:5px; 4875} 4876 4877/* ============================================================ 4878 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4879 Buy card = form & paylater on the same row. 4880 Alt-fintech logos compact (no over-stretching). 4881 ============================================================ */ 4882@media (min-width:641px) and (max-width:1250px) { 4883 /* Buy card: put qty/buy buttons and paylater promo inline */ 4884 .pd-buy-card { 4885 display: flex !important; 4886 flex-direction: row; 4887 flex-wrap: wrap; 4888 align-items: center; 4889 gap: .6rem .75rem; 4890 } 4891 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4892 .pd-buy-card .paylater-promo { 4893 flex: 0 0 auto;
4894 align-self: center; 4895 margin: 0; 4896 max-width: 44%; 4897 } 4898 4899 /* Alt-fintech banner: tighter logos */ 4900 .iab-logos-compact { 4901 justify-content: center; 4902 gap: 1.25rem; 4903 } 4904 .iab-logos-compact .logo-fin { 4905 width: auto; 4906 max-height: 28px; 4907 object-fit: contain; 4908 } 4909} 4910 4911/* ============================================================ 4912 Tablet layout (641px â 880px) 4913 Restores 2-column grid + horizontal sidebar for mid-size screens. 4914 Overrides the generic â¤880px mobile-collapse rules above. 4915 ============================================================ */ 4916@media (min-width:641px) and (max-width:880px) { 4917 /* 2-column grid: image-card | info-card */ 4918 .pd-top-layout { 4919 grid-template-columns: minmax(260px, 40%) 1fr; 4920 gap: 1rem; 4921 } 4922 4923 /* Sidebar: full-width horizontal flex row below the two main columns */ 4924 .pd-sidebar { 4925 grid-column: 1 / -1; 4926 flex-direction: row; 4927 flex-wrap: wrap; 4928 gap: .75rem; 4929 padding: 0; 4930 } 4931 /* Banner cards fill 2-per-row */ 4932 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4933 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4934 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4935 4936 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4937 .pd-installment-card2, 4938 .inst-alt-banner--compact { display: none !important; } 4939 4940 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4941 .pd-sticky-header { display: flex !important; } 4942 4943 /* Typography â tighten slightly to fit the narrower info column */ 4944 .pd-price-current { font-size: 1.65rem; } 4945 .pd-title { font-size: 1.1rem; } 4946 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4947} 4948</style> 4949 <style data-inlined="product-grid.css"> 4950/* Product Grid (Firebird) */ 4951 4952.product-archive { 4953 max-width: 1320px; 4954 margin-inline: auto; 4955 padding: 0 0 3rem; 4956 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4957} 4958 4959.pa-toolbar { 4960 display: flex; 4961 flex-wrap: wrap; 4962 gap: 1rem; 4963 justify-content: space-between; 4964 align-items: flex-start; 4965} 4966 4967.pa-title { 4968 font-size: 1.4rem; 4969 margin: 0; 4970 font-weight: 600; 4971 line-height: 1.2; 4972 color: #0f172a; 4973} 4974 4975.pa-left { 4976 display: flex; 4977 flex-wrap: wrap; 4978 gap: .75rem; 4979 align-items: center; 4980} 4981 4982.pa-filter-badge { 4983 background: #f1f5f9; 4984 border: 1px solid #e2e8f0; 4985 padding: .45rem .6rem; 4986 border-radius: 8px; 4987 font-size: .7rem; 4988 display: flex; 4989 align-items: center; 4990 gap: .4rem; 4991 line-height: 1; 4992 color: #475569; 4993} 4994.pa-filter-badge strong { color: #0f172a; } 4995.pa-filter-badge .clear { 4996 display:inline-block; 4997 text-decoration:none; 4998 background:#e2e8f0; 4999 color:#475569; 5000 font-weight:600; 5001 padding:0 .35rem .05rem; 5002 border-radius:4px; 5003 transition:.2s; 5004 font-size:.75rem; 5005 line-height:1; 5006} 5007.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 5008 5009.pa-right { 5010 display:flex; 5011 gap:1rem; 5012 flex-wrap:wrap; 5013 align-items:flex-start; 5014 margin-bottom: 0.5rem; 5015} 5016 5017.pa-search { 5018 display:flex; 5019 gap:.5rem; 5020 align-items:center; 5021 background:#fff; 5022 border:1px solid #e2e8f0; 5023 padding:.4rem .5rem; 5024 border-radius:8px; 5025} 5026.pa-search input[type=text] { 5027 border:0; 5028 outline:none; 5029 font-size:.8rem; 5030 padding:.35rem .4rem;
5031 min-width:200px; 5032} 5033.pa-search button { 5034 background:#0d6efd; 5035 border:0; 5036 color:#fff; 5037 font-size:.7rem; 5038 padding:.45rem .75rem; 5039 border-radius:6px; 5040 cursor:pointer; 5041 font-weight:600; 5042 letter-spacing:.3px; 5043 transition:.25s; 5044} 5045.pa-search button:hover { background:#0b5dc4; } 5046 5047.pa-sort { 5048 display:flex; 5049 gap:.4rem; 5050 align-items:center; 5051 font-size:.7rem; 5052 background:#fff; 5053 border:1px solid #e2e8f0; 5054 padding:.4rem .55rem; 5055 border-radius:8px; 5056} 5057.pa-sort select { 5058 font-size:.75rem; 5059 padding:.35rem .5rem; 5060 border:1px solid #cbd5e1; 5061 border-radius:6px; 5062 background:#fff; 5063 outline:none; 5064} 5065 5066.pa-empty { 5067 border:1px dashed #cbd5e1; 5068 padding:3rem 1rem; 5069 text-align:center; 5070 border-radius:12px; 5071 background:#fff; 5072} 5073.pa-empty p { 5074 margin:0 0 1rem; 5075 font-size:.9rem; 5076 color:#475569; 5077} 5078.btn-reset { 5079 display:inline-block; 5080 background:#0d6efd; 5081 color:#fff; 5082 font-size:.7rem; 5083 padding:.55rem 1rem; 5084 border-radius:8px; 5085 text-decoration:none; 5086 font-weight:600; 5087 letter-spacing:.4px; 5088 transition:.25s; 5089} 5090.btn-reset:hover { background:#0a58c7; } 5091 5092/* Grid */ 5093.pa-grid { 5094 display:grid; 5095 gap:1rem; 5096 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5097 align-items:stretch; 5098} 5099 5100.pa-card { 5101 background:#fff; 5102 border:1px solid #e2e8f0; 5103 border-radius:12px; 5104 display:flex; 5105 flex-direction:column; 5106 text-decoration:none; 5107 color:inherit; 5108 position:relative; 5109 overflow:hidden; 5110 transition:.28s cubic-bezier(.4,0,.2,1); 5111 min-width:0; 5112 padding: 0; 5113} 5114.pa-card:hover { 5115 transform:translateY(-4px); 5116 box-shadow:0 8px 22px -8px rgba(0,0,0,.15); 5117 border-color:#dce2ea; 5118} 5119 5120.pa-card .thumb { 5121 aspect-ratio:1/1; 5122 background:#f1f5f9; 5123 display:flex; 5124 align-items:center; 5125 justify-content:center; 5126 overflow:hidden; 5127 position:relative; 5128} 5129.pa-card img { 5130 width:100%; 5131 height:100%; 5132 object-fit:cover; 5133 display:block; 5134} 5135.badge-disc { 5136 position:absolute; 5137 top:8px; 5138 left:8px; 5139 background:#C77DFF; 5140 color:#fff; 5141 font-size:.6rem; 5142 padding:4px 6px 3px; 5143 border-radius:6px; 5144 font-weight:700; 5145 letter-spacing:.4px; 5146} 5147 5148.pa-card .body { 5149 padding:.65rem .7rem .8rem; 5150 display:flex; 5151 flex-direction:column; 5152 gap:.45rem; 5153 min-height:90px; 5154} 5155 5156.pa-card .title { 5157 font-size:.75rem; 5158 line-height:1.25; 5159 margin:0; 5160 font-weight:600;
5161 color:#1e293b; 5162 height:2.4em; 5163 overflow:hidden; 5164} 5165 5166.pa-card .prices { 5167 display:flex; 5168 flex-wrap:wrap; 5169 align-items:baseline; 5170 gap:.4rem; 5171} 5172.pa-card .prices .final { 5173 font-size:.83rem; 5174 font-weight:700; 5175 color:#0d6efd; 5176} 5177.pa-card .prices .ori { 5178 font-size:.63rem; 5179 text-decoration:line-through; 5180 color:#94a3b8; 5181 font-weight:500; 5182} 5183 5184.pa-card .cat { 5185 margin-top:auto; 5186 font-size:.55rem; 5187 letter-spacing:.6px; 5188 text-transform:uppercase; 5189 background:#f1f5f9; 5190 border:1px solid #e2e8f0; 5191 padding:.28rem .45rem .25rem; 5192 border-radius:20px; 5193 font-weight:600; 5194 color:#475569; 5195 align-self:flex-start; 5196} 5197.pa-card .meta-line { 5198 margin-top:4px; 5199 font-size:.65rem; 5200 color:#475569; 5201} 5202.pa-card .meta-line .sold { 5203 display:inline-block; 5204 background:#f1f5f9; 5205 padding:2px 6px 3px; 5206 border-radius:4px; 5207 font-weight:600; 5208 letter-spacing:.3px; 5209 color:#334155; 5210} 5211 5212/* Pagination */ 5213.pa-pagination { 5214 margin-top:2rem; 5215 display:flex; 5216 flex-wrap:wrap; 5217 gap:.4rem; 5218 justify-content:center; 5219 font-size:.7rem; 5220 align-items:center; 5221} 5222.pa-pagination a, 5223.pa-pagination span { 5224 min-width:34px; 5225 height:34px; 5226 display:flex; 5227 align-items:center; 5228 justify-content:center; 5229 border-radius:8px; 5230 text-decoration:none; 5231 font-weight:600; 5232 padding:0 .6rem; 5233 box-sizing:border-box; 5234 border:1px solid #e2e8f0; 5235 background:#fff; 5236 color:#334155; 5237 transition:.25s; 5238} 5239.pa-pagination a:hover { 5240 background:#f1f5f9; 5241 color:#0f172a; 5242} 5243.pa-pagination .current { 5244 background:#0d6efd; 5245 color:#fff; 5246 border-color:#0d6efd; 5247} 5248.pa-pagination .gap { 5249 border:none; 5250 background:transparent; 5251 font-weight:400; 5252 color:#94a3b8; 5253} 5254 5255/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5256.pa-mobile-filterbar{display:none} 5257@media (max-width: 640px){ 5258 /* sembunyikan tombol lama */ 5259 .pa-btn-filter{display:none} 5260 5261 .pa-mobile-filterbar{ 5262 display:block; 5263 position:sticky; 5264 top:0; /* sesuaikan jika ada header fixed di atas */ 5265 z-index:5; 5266 background:#fff; 5267 border-bottom:1px solid #e2e8f0; 5268 padding:.4rem .5rem; 5269 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5270 } 5271 .pa-mobile-filterbar .mf-scroll{ 5272 display:flex;align-items:center;gap:.5rem; 5273 overflow-x:auto; 5274 -webkit-overflow-scrolling:touch; 5275 scrollbar-width:none; 5276 } 5277 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5278 5279 .mf-chip{ 5280 flex:0 0 auto; 5281 background:#fff; 5282 color:#334155; 5283 text-decoration:none; 5284 border:1px solid #e2e8f0; 5285 border-radius:999px; 5286 padding:.44rem .7rem; 5287 font-size:.78rem; 5288 font-weight:600; 5289 letter-spacing:.2px; 5290 white-space:nowrap; 5291 } 5292 .mf-chip:active{transform:scale(.98)} 5293 .mf-chip.active{ 5294 background:#eaf2ff; 5295 border-color:#0d6efd; 5296 color:#0d6efd; 5297 } 5298 5299 .mf-filter-btn{ 5300 position:relative; 5301 flex:0 0 auto; 5302 width:38px;height:38px; 5303 border-radius:50%; 5304 border:1.5px solid #0d6efd; 5305 background:#fff; 5306 display:inline-flex;align-items:center;justify-content:center; 5307 color:#0d6efd; 5308 cursor:pointer; 5309 } 5310 .mf-filter-btn:active{transform:scale(.96)} 5311 .mf-filter-btn svg{width:18px;height:18px;display:block} 5312 .mf-filter-btn .mf-badge{ 5313 position:absolute;top:0;right:-4px; 5314 min-width:18px;height:18px;border-radius:9px; 5315 background:#C77DFF;color:#fff; 5316 font-size:.65rem;font-weight:700; 5317 display:flex;align-items:center;justify-content:center; 5318 padding:0 4px;line-height:1; 5319 box-shadow:0 2px 6px -2px rgba(226,166,66,.6); 5320 } 5321} 5322 5323/* Responsive */ 5324@media (max-width: 640px) { 5325 .product-archive { padding: 0 1rem 3rem; } 5326 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5327 .pa-search input[type=text]{ min-width:140px; } 5328 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5329 .pa-right { display: none; } 5330 .pa-sort { width: 100%; justify-content:center; } 5331 .pa-mobile-filterbar { display: flex !important; } 5332} 5333 5334.pa-mobile-filterbar { 5335 display: none; 5336 align-items: center; 5337 gap: 8px; 5338 padding: 8px 0 8px 12px; 5339 background: #fff; 5340 width: 100vw; 5341} 5342.pa-mobile-filterbar .mf-scroll { 5343 display: flex; 5344 gap: 8px; 5345 overflow-x: auto; 5346 -webkit-overflow-scrolling: touch; 5347 scrollbar-width: none; /* Firefox */ 5348 flex: 1 1 auto; /* takes remaining width */ 5349} 5350.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5351.pa-mobile-filterbar .mf-chip { 5352 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5353 white-space: nowrap; 5354} 5355.pa-mobile-filterbar .mf-filter-btn { 5356 flex: 0 0 auto; /* do not shrink */ 5357 position: relative; /* keep in place, not inside scroller */ 5358 display: inline-flex; 5359 align-items: center; 5360 gap: 6px; 5361} 5362 5363/* Optional: ensure on small screens the button stays visible with min width */ 5364@media (max-width: 768px) { 5365 .pa-mobile-filterbar .mf-filter-btn { 5366 padding: 6px 10px; 5367 } 5368} 5369 5370/* Product Specification Chips */ 5371.pa-card .spec-chips { 5372 font-size: .6rem; 5373 color: #64748b; 5374 font-weight: 500; 5375 letter-spacing: .2px; 5376 margin-top: 2px; 5377 margin-bottom: 4px;
5378 line-height: 1.2; 5379 opacity: 0.9; 5380 max-width: 100%; 5381 overflow: hidden; 5382 text-overflow: ellipsis; 5383 white-space: nowrap; 5384} 5385 5386.pa-card .spec-chips .spec-divider { 5387 color: #94a3b8; 5388 font-weight: 400; 5389 margin: 0 1px; 5390} 5391</style> 5392 <style data-inlined="site-notice.css"> 5393/** 5394 * Site Notice Banner - Reusable Notification System 5395 * Dismissible top bar for announcements, holidays, maintenance, etc. 5396 */ 5397 5398.holiday-notice { 5399 position: fixed; 5400 top: 0; 5401 left: 0; 5402 right: 0; 5403 z-index: 9999; 5404 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 5405 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5406 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 5407 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5408 transform-origin: top; 5409} 5410 5411.holiday-notice.hidden { 5412 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5413} 5414 5415@keyframes slideDown { 5416 from { 5417 transform: translateY(-100%); 5418 opacity: 0; 5419 } 5420 to { 5421 transform: translateY(0); 5422 opacity: 1; 5423 } 5424} 5425 5426@keyframes slideUp { 5427 from { 5428 transform: translateY(0); 5429 opacity: 1; 5430 } 5431 to { 5432 transform: translateY(-100%); 5433 opacity: 0; 5434 } 5435} 5436 5437.holiday-notice-container { 5438 max-width: 1400px; 5439 margin: 0 auto; 5440 padding: 12px 20px; 5441 display: flex;
5442 align-items: center; 5443 justify-content: space-between; 5444 gap: 16px; 5445} 5446 5447.holiday-notice-content { 5448 display: flex; 5449 align-items: center; 5450 gap: 12px; 5451 flex: 1; 5452} 5453 5454.holiday-notice-icon { 5455 font-size: 20px; 5456 line-height: 1; 5457 flex-shrink: 0; 5458} 5459 5460.holiday-notice-text { 5461 color: #ffffff; 5462 font-size: 14px; 5463 font-weight: 500; 5464 line-height: 1.5; 5465 margin: 0; 5466 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 5467} 5468 5469.holiday-notice-emoji { 5470 display: inline-block; 5471 animation: bounce 2s ease-in-out infinite; 5472} 5473 5474@keyframes bounce { 5475 0%, 100% { 5476 transform: translateY(0); 5477 } 5478 50% { 5479 transform: translateY(-4px); 5480 } 5481} 5482 5483.holiday-notice-close { 5484 background: rgba(255, 255, 255, 0.2); 5485 border: 1px solid rgba(255, 255, 255, 0.3); 5486 border-radius: 6px; 5487 padding: 6px; 5488 cursor: pointer; 5489 color: #ffffff; 5490 transition: all 0.2s ease; 5491 flex-shrink: 0; 5492 display: flex; 5493 align-items: center; 5494 justify-content: center; 5495 width: 32px; 5496 height: 32px; 5497} 5498 5499.holiday-notice-close:hover { 5500 background: rgba(255, 255, 255, 0.3); 5501 transform: scale(1.05); 5502} 5503 5504.holiday-notice-close:active { 5505 transform: scale(0.95); 5506} 5507 5508.holiday-notice-close:focus { 5509 outline: 2px solid rgba(255, 255, 255, 0.5); 5510 outline-offset: 2px; 5511} 5512 5513/* Body padding adjustment when banner is visible */ 5514body.has-holiday-notice { 5515 padding-top: 52px; 5516} 5517 5518/* Tablet Styles */ 5519@media (max-width: 1024px) { 5520 .holiday-notice-container { 5521 padding: 10px 16px; 5522 } 5523 5524 .holiday-notice-text { 5525 font-size: 13px; 5526 } 5527 5528 .holiday-notice-icon { 5529 font-size: 18px; 5530 } 5531 5532 body.has-holiday-notice { 5533 padding-top: 48px; 5534 } 5535} 5536 5537/* Mobile Styles */ 5538@media (max-width: 640px) { 5539 .holiday-notice-container { 5540 padding: 10px 12px; 5541 gap: 8px; 5542 } 5543 5544 .holiday-notice-content { 5545 gap: 8px; 5546 } 5547 5548 .holiday-notice-text { 5549 font-size: 12px; 5550 line-height: 1.4; 5551 } 5552 5553 .holiday-notice-icon { 5554 font-size: 16px; 5555 } 5556 5557 .holiday-notice-close { 5558 width: 28px; 5559 height: 28px; 5560 } 5561 5562 .holiday-notice-close svg { 5563 width: 16px; 5564 height: 16px; 5565 } 5566 5567 body.has-holiday-notice { 5568 padding-top: 44px; 5569 } 5570} 5571 5572/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5573/* .header, 5574.navbar, 5575header { 5576 position: relative; 5577 z-index: 100; 5578} */ 5579 5580</style> 5581 <style data-inlined="benchmark.css"> 5582/** 5583 * Benchmark Section Styles 5584 * For product detail page benchmark display 5585 */ 5586 5587.pd-benchmark-section { 5588 background: #fff; 5589 border-radius: 12px; 5590 padding: 24px; 5591 margin: 32px 0; 5592 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5593 border: 1px solid #e5e7eb; 5594} 5595 5596.benchmark-header { 5597 margin-bottom: 20px; 5598} 5599 5600.benchmark-title { 5601 font-size: 22px; 5602 font-weight: 700; 5603 color: #1a1a1a; 5604 margin: 0 0 12px 0; 5605 display: flex;
5606 align-items: center; 5607 gap: 8px; 5608} 5609 5610.benchmark-hint { 5611 font-size: 14px; 5612 color: #6b7280; 5613 margin: 0; 5614} 5615 5616/* Preview Badges */ 5617.benchmark-preview-badges { 5618 display: flex; 5619 flex-wrap: wrap; 5620 gap: 10px; 5621 margin-top: 12px; 5622} 5623 5624.benchmark-badge { 5625 display: inline-flex; 5626 align-items: center; 5627 gap: 6px; 5628 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5629 color: #fff; 5630 padding: 8px 16px; 5631 border-radius: 20px; 5632 font-size: 13px; 5633 font-weight: 500; 5634 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5635 transition: all 0.2s ease; 5636} 5637 5638.benchmark-badge:hover { 5639 transform: translateY(-2px); 5640 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5641} 5642 5643.benchmark-badge strong { 5644 font-weight: 600; 5645 opacity: 0.9; 5646} 5647 5648/* Load Button */ 5649.btn-load-benchmark { 5650 display: flex; 5651 align-items: center; 5652 justify-content: center; 5653 gap: 10px; 5654 width: 100%; 5655 max-width: 420px; 5656 padding: 14px 24px; 5657 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5658 color: #fff; 5659 border: none; 5660 border-radius: 8px; 5661 font-size: 15px; 5662 font-weight: 600; 5663 cursor: pointer; 5664 transition: all 0.3s ease; 5665 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5666 margin: 0 auto; 5667} 5668 5669.btn-load-benchmark:hover:not(:disabled) { 5670 transform: translateY(-2px); 5671 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5672} 5673 5674.btn-load-benchmark:disabled { 5675 opacity: 0.6; 5676 cursor: not-allowed; 5677} 5678 5679.btn-load-benchmark .btn-icon { 5680 font-size: 20px; 5681} 5682 5683.btn-load-benchmark.loading .btn-icon { 5684 animation: spin 1s linear infinite; 5685} 5686 5687@keyframes spin { 5688 from { transform: rotate(0deg); } 5689 to { transform: rotate(360deg); } 5690} 5691 5692/* Benchmark Content */ 5693.benchmark-content { 5694 margin-top: 24px; 5695 padding-top: 24px; 5696 border-top: 2px solid #f3f4f6; 5697} 5698 5699.benchmark-loading { 5700 text-align: center; 5701 padding: 40px 20px; 5702 color: #6b7280; 5703} 5704 5705.benchmark-loading-spinner { 5706 width: 48px; 5707 height: 48px; 5708 margin: 0 auto 16px; 5709 border: 4px solid #f3f4f6; 5710 border-top-color: #667eea; 5711 border-radius: 50%; 5712 animation: spin 1s linear infinite; 5713} 5714 5715.benchmark-error { 5716 text-align: center; 5717 padding: 32px 20px; 5718 color: #C77DFF; 5719 background: #fee2e2; 5720 border-radius: 8px; 5721} 5722 5723/* Benchmark Data Display */ 5724.benchmark-data-grid { 5725 display: grid; 5726 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5727 gap: 16px; 5728 margin-bottom: 24px; 5729} 5730 5731.benchmark-card { 5732 background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); 5733 border: 1px solid #e5e7eb; 5734 border-radius: 10px; 5735 padding: 18px; 5736 text-align: center; 5737 transition: all 0.2s ease; 5738} 5739 5740.benchmark-card:hover { 5741 transform: translateY(-3px); 5742 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5743} 5744 5745.benchmark-card-label { 5746 font-size: 13px; 5747 color: #6b7280; 5748 font-weight: 500; 5749 margin-bottom: 8px; 5750 text-transform: uppercase; 5751 letter-spacing: 0.5px; 5752} 5753 5754.benchmark-card-value { 5755 font-size: 24px; 5756 font-weight: 700; 5757 color: #1a1a1a; 5758 line-height: 1.2; 5759} 5760 5761.benchmark-card-value.large { 5762 font-size: 28px; 5763} 5764 5765.benchmark-card-subtitle { 5766 font-size: 12px; 5767 color: #9ca3af; 5768 margin-top: 4px; 5769} 5770 5771/* Spec Details */ 5772.benchmark-specs { 5773 background: #f9fafb; 5774 border-radius: 10px; 5775 padding: 20px; 5776 margin-top: 20px; 5777} 5778 5779.benchmark-specs-title { 5780 font-size: 16px; 5781 font-weight: 600; 5782 color: #1a1a1a; 5783 margin: 0 0 16px 0; 5784} 5785 5786.benchmark-spec-row { 5787 display: flex; 5788 justify-content: space-between; 5789 padding: 10px 0; 5790 border-bottom: 1px solid #e5e7eb; 5791} 5792 5793.benchmark-spec-row:last-child { 5794 border-bottom: none; 5795} 5796 5797.benchmark-spec-label { 5798 font-size: 14px; 5799 color: #6b7280; 5800 font-weight: 500; 5801} 5802 5803.benchmark-spec-value { 5804 font-size: 14px; 5805 color: #1a1a1a; 5806 font-weight: 600; 5807 text-align: right; 5808} 5809 5810/* Score Badges */ 5811.benchmark-score-badge { 5812 display: inline-block; 5813 padding: 6px 14px; 5814 border-radius: 20px; 5815 font-size: 13px; 5816 font-weight: 600; 5817 margin-top: 8px; 5818} 5819 5820.benchmark-score-badge.excellent { 5821 background: #10b981; 5822 color: #fff; 5823} 5824 5825.benchmark-score-badge.good { 5826 background: #3b82f6; 5827 color: #fff; 5828} 5829 5830.benchmark-score-badge.average { 5831 background: #C77DFF; 5832 color: #fff; 5833} 5834 5835.benchmark-score-badge.poor { 5836 background: #C77DFF; 5837 color: #fff; 5838} 5839 5840/* Attribution */ 5841.benchmark-attribution { 5842 text-align: center; 5843 margin-top: 24px; 5844 padding-top: 20px; 5845 border-top: 1px solid #e5e7eb; 5846 font-size: 13px; 5847 color: #9ca3af; 5848} 5849 5850.benchmark-attribution a { 5851 color: #667eea; 5852 text-decoration: none; 5853 font-weight: 500; 5854} 5855 5856.benchmark-attribution a:hover { 5857 text-decoration: underline; 5858} 5859 5860/* Responsive */ 5861@media (max-width: 768px) { 5862 .pd-benchmark-section { 5863 padding: 16px; 5864 margin: 20px 0; 5865 } 5866 5867 .benchmark-title { 5868 font-size: 18px; 5869 } 5870 5871 .benchmark-preview-badges { 5872 flex-direction: column; 5873 gap: 8px; 5874 } 5875 5876 .benchmark-badge { 5877 justify-content: center; 5878 } 5879 5880 .btn-load-benchmark { 5881 font-size: 14px; 5882 padding: 12px 20px; 5883 } 5884 5885 .benchmark-data-grid { 5886 grid-template-columns: 1fr 1fr; 5887 gap: 12px; 5888 } 5889 5890 .benchmark-card { 5891 padding: 14px; 5892 } 5893 5894 .benchmark-card-value { 5895 font-size: 20px; 5896 } 5897 5898 .benchmark-spec-row { 5899 flex-direction: column; 5900 gap: 4px; 5901 } 5902 5903 .benchmark-spec-value { 5904 text-align: left; 5905 } 5906} 5907 5908@media (max-width: 480px) { 5909 .benchmark-data-grid { 5910 grid-template-columns: 1fr; 5911 } 5912} 5913 5914</style> 5915 <style data-inlined="product-filters.css"> 5916/* ========================================================= 5917 Product Filters (Desktop + Mobile Drawer) 5918 Light Only + FIX: 5919 - Desktop: f-actions selalu tampil (static bottom) 5920 - Mobile: konten filter dapat discroll 5921 ========================================================= */ 5922 5923/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5924.product-archive.with-filters .pa-layout { 5925 display:flex; 5926 gap:24px; 5927 align-items:flex-start; 5928} 5929.pa-main { flex:1; min-width:0; } 5930 5931/* ========================================================= 5932 DESKTOP PANEL 5933 ========================================================= */ 5934.pa-filters { 5935 width:300px; 5936 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5937 border:1px solid #e2e8f0; 5938 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5939 border-radius:18px; 5940 position:sticky; 5941 top:80px; 5942 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5943 font-size:14px; 5944 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5945 backdrop-filter:blur(3px); 5946 -webkit-backdrop-filter:blur(3px);
5947 transition:box-shadow .3s, background .4s; 5948 display:flex; 5949 flex-direction:column; 5950 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5951} 5952.pa-filters::before { 5953 content:""; 5954 position:absolute; 5955 inset:0 auto 0 0; 5956 width:7px; 5957 border-radius:18px 0 0 18px; 5958 background:linear-gradient(180deg,#C77DFF 0%,#C77DFF 35%,#1d6ce0 100%); 5959} 5960.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5961 5962/* Scroll area (desktop/mode shared) */ 5963.filters-scroll { 5964 flex:1; 5965 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5966 overflow:auto; 5967 padding-right:6px; 5968 margin-right:-6px; /* kompensasi scrollbar */ 5969 scrollbar-width:thin; 5970 overscroll-behavior:contain; 5971} 5972.filters-scroll::-webkit-scrollbar { width:8px; } 5973.filters-scroll::-webkit-scrollbar-thumb { 5974 background:#cbd5e1; 5975 border-radius:20px; 5976 border:2px solid #f1f5f9; 5977} 5978.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5979 5980/* Heading */ 5981.pa-filters h2 { 5982 margin:0 0 14px; 5983 font-size:20px; 5984 font-weight:700; 5985 letter-spacing:.4px; 5986 line-height:1.15; 5987 background:linear-gradient(90deg,#0f172a,#1d6ce0 60%,#0f172a); 5988 -webkit-background-clip:text; 5989 color:transparent; 5990 background-size:200% 100%; 5991 animation:titleSheen 8s linear infinite; 5992} 5993@keyframes titleSheen { 5994 0%{background-position:0 0} 5995 50%{background-position:100% 0} 5996 100%{background-position:0 0} 5997} 5998 5999/* Form groups */ 6000.pa-filters .f-group { margin-bottom:18px; position:relative; } 6001.pa-filters label { 6002 display:block; 6003 font-weight:600; 6004 margin:0 0 6px; 6005 font-size:13px; 6006 color:#0f172a; 6007 letter-spacing:.3px; 6008} 6009 6010/* Inputs / selects */ 6011.pa-filters input[type=text], 6012.pa-filters input[type=number], 6013.pa-filters select { 6014 width:100%; 6015 padding:7px 10px; 6016 border:1px solid #cbd5e1; 6017 border-radius:8px; 6018 font-size:13px; 6019 min-height:34px; 6020 background:#ffffff; 6021 box-sizing:border-box; 6022 color:#0f172a; 6023 transition:border-color .25s, box-shadow .25s, background .25s; 6024 -webkit-appearance:none; 6025 -moz-appearance:none; 6026 appearance:none; 6027} 6028.pa-filters select { 6029 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"); 6030 background-repeat:no-repeat; 6031 background-position:right 10px center; 6032 background-size:12px; 6033 padding-right:32px; 6034} 6035.pa-filters input[type=text]:hover, 6036.pa-filters input[type=number]:hover, 6037.pa-filters select:hover { border-color:#94b5ec; } 6038.pa-filters input[type=text]:focus, 6039.pa-filters input[type=number]:focus, 6040.pa-filters select:focus { 6041 outline:none; 6042 border-color:#1d6ce0; 6043 box-shadow:0 0 0 3px rgba(29,108,224,.25); 6044 background:#f8fbff; 6045} 6046.pa-filters .range { display:flex; gap:10px; } 6047.pa-filters .range input { flex:1; } 6048.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 6049 6050/* Spec blocks */ 6051.f-specs { margin-top:4px; } 6052.spec-attr { 6053 margin:0 0 14px; 6054 padding:12px 12px 10px; 6055 border:1px solid #e2e8f0; 6056 border-radius:14px; 6057 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 6058 position:relative; 6059 transition:border-color .3s, box-shadow .3s, background .4s; 6060} 6061.spec-attr.collapsed { padding-bottom:8px; } 6062.spec-attr.has-selected { 6063 border-color:#1d6ce0; 6064 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.35); 6065 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 6066} 6067.spec-attr-title { 6068 font-weight:600; 6069 margin:0 0 8px; 6070 font-size:13px; 6071 display:flex; 6072 justify-content:space-between; 6073 align-items:center; 6074 cursor:pointer; 6075 color:#0f172a; 6076 user-select:none; 6077} 6078.spec-attr-title .chevron { 6079 font-size:10px; 6080 transition:transform .25s, opacity .25s; 6081 opacity:.65; 6082} 6083.spec-attr.collapsed .spec-attr-title .chevron { 6084 transform:rotate(-90deg); 6085 opacity:.5; 6086} 6087 6088/* Spec options */ 6089.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6090.spec-attr-options label { 6091 display:inline-flex; 6092 align-items:center; 6093 gap:4px; 6094 font-weight:500; 6095 border:1px solid #d4dde7; 6096 padding:6px 9px; 6097 border-radius:30px; 6098 cursor:pointer; 6099 background:#f3f6fa; 6100 font-size:12px; 6101 line-height:1.1; 6102 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6103 position:relative; 6104} 6105.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6106.spec-attr-options input { 6107 margin:0; 6108 position:absolute; 6109 inset:0; 6110 opacity:0;
6111 cursor:pointer; 6112} 6113.spec-attr-options label:has(input:checked) { 6114 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6115 border-color:#1d6ce0; 6116 color:#ffffff; 6117 box-shadow:0 2px 6px -1px rgba(29,108,224,.55); 6118} 6119.spec-attr-options label:has(input:checked)::after { 6120 content:""; 6121 position:absolute; 6122 right:6px; 6123 top:6px; 6124 width:6px; 6125 height:6px; 6126 background:#fff; 6127 border-radius:50%; 6128 box-shadow:0 0 0 4px rgba(255,255,255,.25),0 0 0 7px rgba(29,108,224,.5); 6129 animation:pulseDot 1.9s ease-out infinite; 6130} 6131@keyframes pulseDot { 6132 0%{transform:scale(.8);opacity:.9} 6133 60%{transform:scale(1.4);opacity:.15} 6134 100%{transform:scale(.8);opacity:.9} 6135} 6136 6137/* Badges */ 6138.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6139.pa-filter-badge { 6140 display:inline-flex; 6141 gap:6px; 6142 align-items:center; 6143 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6144 border:1px solid #b4cff3; 6145 padding:5px 10px 5px 12px; 6146 border-radius:22px; 6147 margin:2px 4px 0 0; 6148 font-size:11px; 6149 font-weight:500; 6150 line-height:1; 6151 color:#0f172a; 6152 position:relative; 6153 overflow:hidden; 6154} 6155.pa-filter-badge::before { 6156 content:""; 6157 position:absolute; 6158 inset:0; 6159 background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.35),rgba(255,255,255,0)); 6160 transform:translateX(-100%); 6161 animation:badgeSheen 6s linear infinite; 6162} 6163@keyframes badgeSheen { 6164 0%{transform:translateX(-100%)} 6165 55%{transform:translateX(120%)} 6166 100%{transform:translateX(120%)} 6167} 6168.pa-filter-badge a.clear { 6169 text-decoration:none; 6170 color:#E3D05A; 6171 font-weight:700; 6172 padding:0 3px; 6173 display:inline-flex; 6174 align-items:center; 6175 justify-content:center; 6176 border-radius:50%; 6177 line-height:1; 6178 transition:background .25s, color .25s; 6179} 6180.pa-filter-badge a.clear:hover { background:#ffeded; color:#E3D05A; } 6181.pa-btn-filter { width: 100%;} 6182 6183/* ========================================================= 6184 ACTIONS (DESKTOP) - STATIC BOTTOM 6185 ========================================================= */ 6186.f-actions { 6187 margin-top:10px; 6188 display:flex; 6189 gap:12px; 6190 flex-wrap:wrap; 6191 padding:16px 0 4px; 6192 border-top:1px solid #e2e8f0; 6193 background:linear-gradient(180deg,rgba(255,255,255,0) 0%,#f1f5f9 70%); 6194 flex-shrink:0; 6195} 6196.f-actions button { 6197 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6198 color:#fff; 6199 border:none; 6200 padding:10px 20px; 6201 border-radius:12px; 6202 cursor:pointer; 6203 font-size:14px; 6204 font-weight:600; 6205 letter-spacing:.3px; 6206 display:inline-flex; 6207 align-items:center; 6208 gap:6px; 6209 box-shadow:0 4px 14px -2px rgba(29,108,224,.45); 6210 transition:transform .25s, box-shadow .3s; 6211} 6212.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6213.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6214.f-actions .reset { 6215 font-size:12px; 6216 text-decoration:none; 6217 color:#C77DFF; 6218 align-self:center; 6219 font-weight:600; 6220 padding:4px 6px; 6221 border-radius:8px; 6222 transition:background .25s, color .25s; 6223} 6224.f-actions .reset:hover { background:#ffe6e6; color:#E3D05A; text-decoration:underline; } 6225.pa-filters .close-drawer { display: none; } 6226 6227/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6228.filter-form { 6229 flex: 1; 6230 min-height: 0; 6231 display: flex; 6232 flex-direction: column; 6233 overflow: hidden; 6234} 6235 6236/* ========================================================= 6237 FILTER TOGGLE (MOBILE TRIGGER) 6238 ========================================================= */ 6239.filter-toggle { 6240 display:none; 6241 margin:0 0 14px; 6242 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6243 color:#fff; 6244 border:none; 6245 padding:11px 18px; 6246 font-size:14px; 6247 font-weight:600; 6248 border-radius:14px; 6249 cursor:pointer; 6250 letter-spacing:.4px; 6251 box-shadow:0 4px 14px -2px rgba(29,108,224,.55); 6252 position:relative; 6253 z-index:100; 6254 transition:box-shadow .3s, transform .25s; 6255} 6256.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(29,108,224,.6); transform:translateY(-2px); } 6257.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6258.filter-toggle .count-badge { 6259 display:inline-block; 6260 background:linear-gradient(135deg,#C77DFF 0%,#C77DFF 55%,#C77DFF 100%); 6261 color:#1d2b44; 6262 font-size:11px; 6263 font-weight:700; 6264 padding:3px 7px 4px;
6265 border-radius:30px; 6266 margin-left:8px; 6267 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #1d6ce0; 6268} 6269 6270/* Overlay (mobile) */ 6271.pa-filters-overlay { display:none; } 6272 6273/* ========================================================= 6274 MOBILE DRAWER 6275 ========================================================= */ 6276@media (max-width:768px){ 6277 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6278 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6279 6280 .pa-filters-overlay { 6281 position:fixed; 6282 inset:0; 6283 background:rgba(0,20,45,.55); 6284 z-index:998; 6285 opacity:0; 6286 transition:opacity .35s; 6287 display:block; 6288 pointer-events:none; 6289 backdrop-filter:blur(2px); 6290 } 6291 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6292 6293 .pa-filters { 6294 position:fixed; 6295 top:auto; 6296 left:0; right:0; bottom:0; 6297 max-height:92vh; 6298 height:auto; 6299 width:100%; 6300 border-radius:30px 30px 0 0; 6301 border:1px solid #dbe4ef; 6302 padding:26px 22px 0; 6303 z-index:10000; /* di atas bottom-nav (9998) */ 6304 transform:translateY(102%); 6305 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6306 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6307 overflow:hidden; /* outer hidden, inner scroll */ 6308 } 6309 .pa-filters.active { transform:translateY(0); } 6310 6311 /* Drag handle indicator */ 6312 .pa-filters::after { 6313 content:""; 6314 display:block; 6315 position:absolute; 6316 top:10px; 6317 left:50%; 6318 transform:translateX(-50%); 6319 width:36px; 6320 height:4px; 6321 background:#cbd5e1; 6322 border-radius:4px; 6323 } 6324 body.filter-open { overflow:hidden; touch-action:none; } 6325 6326 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6327 .pa-filters input[type=text], 6328 .pa-filters input[type=number], 6329 .pa-filters select { 6330 font-size:16px; 6331 min-height:44px; 6332 padding:10px 12px; 6333 border-radius:10px; 6334 } 6335 .pa-filters select { padding-right:32px; } 6336 6337 .filters-scroll { 6338 flex:1; 6339 overflow:auto; 6340 padding:0 0 120px; /* ruang di atas bar actions */ 6341 margin:0; 6342 -webkit-overflow-scrolling:touch; 6343 } 6344 6345 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6346 6347 .pa-filters .close-drawer { 6348 display: block; 6349 position:absolute; 6350 right:14px; top:12px; 6351 background:linear-gradient(135deg,#C77DFF 0%,#E3D05A 100%); 6352 color:#fff; 6353 padding:7px 14px; 6354 border:none; 6355 border-radius:10px; 6356 font-size:12px; 6357 font-weight:600; 6358 cursor:pointer; 6359 box-shadow:0 3px 10px -2px rgba(226,166,66,.55); 6360 transition:transform .25s, box-shadow .3s; 6361 z-index: 99; 6362 } 6363 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(226,166,66,.6); } 6364 6365 .spec-attr { border-radius:18px; } 6366 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6367 6368 /* Actions fixed di viewport bawah */ 6369 .f-actions { 6370 position:fixed; 6371 left:0; right:0; bottom:0; 6372 margin:0; 6373 border-top:1px solid #d6dee8; 6374 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6375 background:#ffffffea; 6376 backdrop-filter:blur(6px); 6377 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6378 justify-content:space-between; 6379 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6380 } 6381 6382 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6383 body.filter-open .bottom-nav { 6384 display: none !important; 6385 } 6386 .f-actions button { 6387 flex:1 1 auto; 6388 justify-content:center; 6389 padding:14px 14px; 6390 font-size:15px; 6391 border-radius:14px; 6392 } 6393 .f-actions .reset { 6394 order:-1; 6395 font-size:12px; 6396 padding:2px 8px; 6397 } 6398} 6399 6400/* Scrollbar (outer panel desktop only kept for compatibility) */ 6401.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6402 6403/* Utilities */ 6404.hidden { display:none !important; } 6405.gradient-text { 6406 background:linear-gradient(90deg,#1d6ce0,#155ab8,#1d6ce0); 6407 -webkit-background-clip:text; 6408 color:transparent; 6409 animation:titleSheen 10s linear infinite; 6410} 6411.glow-focus:focus {
6412 outline:none !important; 6413 box-shadow:0 0 0 3px rgba(29,108,224,.4) !important; 6414 border-color:#1d6ce0 !important; 6415} 6416 6417/* Simple fade (opsional) */ 6418@keyframes fadeIn { 6419 from { opacity:0; transform:translateY(8px); } 6420 to { opacity:1; transform:translateY(0); } 6421} 6422</style> 6423 <style data-inlined="mega-menu-tiles.css"> 6424/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6425 6426/* Design Tokens */ 6427:root { 6428 --mm2-bg: #ffffff; 6429 --mm2-surface: #ffffff; 6430 --mm2-surface-elevated: rgba(255,255,255,0.98); 6431 --mm2-muted: #f8fafc; 6432 --mm2-border: #e2e8f0; 6433 --mm2-border-strong: #cbd5e1; 6434 --mm2-accent: #6366f1; 6435 --mm2-accent-hover: #4f46e5; 6436 --mm2-accent-10: rgba(99,102,241,.10); 6437 --mm2-accent-20: rgba(99,102,241,.20); 6438 --mm2-fg: #0f172a; 6439 --mm2-fg-muted: #64748b; 6440 --mm2-fg-light: #94a3b8; 6441 6442 /* Enhanced Shadows - Multi-layer elevation */ 6443 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6444 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6445 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6446 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6447 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6448 6449 /* Border Radius */ 6450 --mm2-radius-xl: 20px; 6451 --mm2-radius-lg: 16px; 6452 --mm2-radius-md: 12px; 6453 --mm2-radius-sm: 10px; 6454 6455 --mm2-z: 60; 6456 6457 /* Animation Timing */ 6458 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6459 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6460 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6461} 6462 6463.navbar { position: relative; z-index: var(--mm2-z); } 6464 6465/* Trigger Button - Enhanced */ 6466.mm2-trigger { 6467 display: inline-flex; align-items: center; gap: 8px; 6468 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6469 border: 1.5px solid var(--mm2-border); 6470 background: var(--mm2-surface); color: var(--mm2-fg); 6471 cursor: pointer; user-select: none; 6472 transition: all var(--mm2-transition-fast); 6473 text-decoration: none; 6474 font-weight: 600; 6475 box-shadow: var(--mm2-shadow-sm); 6476} 6477.mm2-trigger:hover { 6478 border-color: var(--mm2-accent); 6479 background: var(--mm2-muted); 6480 box-shadow: var(--mm2-shadow-md); 6481 transform: translateY(-1px); 6482} 6483.mm2-trigger:active { transform: translateY(0); } 6484.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6485.mm2-trigger:hover svg { transform: scale(1.05); } 6486 6487/* Backdrop - Enhanced Glassmorphism */ 6488.mm2-backdrop { 6489 position: fixed; 6490 inset: 0; 6491 top: 0; 6492 left: 0; 6493 right: 0; 6494 bottom: 0; 6495 background: rgba(15,23,42,.40); 6496 backdrop-filter: blur(8px) saturate(1.2); 6497 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6498 opacity: 0; 6499 pointer-events: none; 6500 z-index: 499; 6501 transition: opacity var(--mm2-transition-base); 6502} 6503.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6504 6505/* Panel - Glassmorphism Enhanced */ 6506.mm2-panel { 6507 position: fixed; 6508 left: 50%; 6509 top: 120px; 6510 transform: translateX(-50%) translateY(-16px); 6511 width: min(1200px, 95vw); 6512 background: var(--mm2-surface-elevated); 6513 backdrop-filter: blur(20px) saturate(1.8); 6514 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6515 border: 1.5px solid var(--mm2-border-strong); 6516 border-radius: var(--mm2-radius-xl); 6517 box-shadow: var(--mm2-shadow-xl); 6518 display: none; 6519 overflow: hidden; 6520 opacity: 0; 6521 z-index: 500; 6522 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6523} 6524.mm2-open .mm2-panel { 6525 display: block; 6526 opacity: 1; 6527 transform: translateX(-50%) translateY(0); 6528} 6529 6530/* View switcher */ 6531.mm2-view { display: none; } 6532.mm2-open .mm2-view.is-active { display: block; } 6533 6534/* Top grid (level-1 tiles) - Enhanced */ 6535.mm2-top { 6536 padding: 24px; 6537 max-height: 75vh; overflow: auto; 6538} 6539.mm2-grid { 6540 display: grid; 6541 grid-template-columns: repeat(4, minmax(0,1fr)); 6542 gap: 14px; 6543} 6544@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6545@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6546 6547/* Stagger animation for cards */ 6548.mm2-open .mm2-card { 6549 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6550} 6551.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6552.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6553.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6554.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6555.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6556.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6557.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6558.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6559.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6560.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6561.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6562.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6563.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6564 6565@keyframes slideInUp { 6566 from { 6567 opacity: 0; 6568 transform: translateY(12px); 6569 } 6570 to { 6571 opacity: 1;
6572 transform: translateY(0); 6573 } 6574} 6575 6576.mm2-card { 6577 position: relative; 6578 display: flex; gap: 14px; align-items: center; 6579 padding: 16px; border-radius: var(--mm2-radius-md); 6580 background: var(--mm2-surface); 6581 border: 1.5px solid var(--mm2-border); 6582 text-decoration: none; color: var(--mm2-fg); 6583 transition: all var(--mm2-transition-fast); 6584 min-height: 72px; 6585 box-shadow: var(--mm2-shadow-sm); 6586} 6587.mm2-card:hover { 6588 border-color: var(--mm2-accent); 6589 background: linear-gradient(135deg, rgba(255,255,255,1), rgba(248,250,252,1)); 6590 box-shadow: var(--mm2-shadow-hover); 6591 transform: translateY(-3px); 6592} 6593.mm2-card:active { transform: translateY(-1px); } 6594 6595/* Icon with category-specific gradients */ 6596.mm2-card .mm2-ico { 6597 width: 48px; height: 48px; border-radius: 12px; 6598 display: inline-flex; align-items: center; justify-content: center; 6599 background: linear-gradient(135deg, #e0e7ff, #fff); 6600 border: 1.5px solid #c7d2fe; 6601 color: #4f46e5; 6602 flex-shrink: 0; 6603 transition: all var(--mm2-transition-fast); 6604 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6605} 6606.mm2-card:hover .mm2-ico { 6607 transform: scale(1.08) rotate(2deg); 6608 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6609} 6610.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6611.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6612 6613.mm2-card .mm2-title { 6614 font-weight: 700; 6615 font-size: 15px; 6616 letter-spacing: -0.01em; 6617 line-height: 1.3; 6618} 6619.mm2-card .mm2-sub { 6620 color: var(--mm2-fg-muted); 6621 font-size: 12.5px; 6622 margin-top: 3px; 6623 display: flex; 6624 align-items: center; 6625 gap: 4px; 6626 white-space: nowrap; 6627 overflow: hidden; 6628 text-overflow: ellipsis; 6629} 6630.mm2-card .mm2-count { 6631 display: inline-flex; 6632 padding: 2px 8px; 6633 border-radius: 6px; 6634 background: var(--mm2-accent-10); 6635 color: var(--mm2-accent); 6636 font-weight: 600; 6637 font-size: 11px; 6638 margin-left: 4px; 6639} 6640 6641/* Detail view (level-2 + level-3) - Enhanced */ 6642.mm2-detail { 6643 position: relative; 6644 padding: 16px 24px 24px; 6645 max-height: 70vh; overflow: auto; 6646} 6647.mm2-detail-header { 6648 display: flex; align-items: center; gap: 14px; 6649 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6650} 6651.mm2-back { 6652 display: inline-flex; align-items: center; justify-content: center; 6653 width: 40px; height: 40px; border-radius: 12px; 6654 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6655 cursor: pointer; 6656 transition: all var(--mm2-transition-fast); 6657 box-shadow: var(--mm2-shadow-sm); 6658} 6659.mm2-back:hover { 6660 border-color: var(--mm2-accent); 6661 background: var(--mm2-muted); 6662 transform: translateX(-3px); 6663 box-shadow: var(--mm2-shadow-md); 6664} 6665.mm2-detail-title { 6666 font-weight: 800; 6667 font-size: 18px; 6668 letter-spacing: -0.02em; 6669} 6670 6671/* Two-pane layout (desktop) - Enhanced */ 6672.mm2-detail-layout { 6673 display: grid; 6674 grid-template-columns: 280px 1fr; 6675 gap: 20px; 6676} 6677 6678/* Left: subnav (level-2 list) - Enhanced */ 6679.mm2-subnav { 6680 position: sticky; top: 8px; 6681 align-self: start; 6682 max-height: calc(70vh - 56px); 6683 overflow: auto; 6684 padding-right: 6px; 6685} 6686.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6687.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6688.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6689.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6690 6691.mm2-subitem { 6692 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6693 width: 100%; 6694 padding: 12px 14px; 6695 border-radius: 12px; 6696 border: 1.5px solid transparent; 6697 background: transparent; 6698 color: var(--mm2-fg); 6699 cursor: pointer; 6700 text-align: left; 6701 transition: all var(--mm2-transition-fast); 6702 margin-bottom: 6px; 6703 font-weight: 600; 6704 font-size: 14px; 6705 position: relative; 6706} 6707.mm2-subitem::before { 6708 content: ''; 6709 position: absolute; 6710 left: 0; 6711 top: 50%; 6712 transform: translateY(-50%); 6713 width: 3px; 6714 height: 0; 6715 background: var(--mm2-accent); 6716 border-radius: 0 2px 2px 0; 6717 transition: height var(--mm2-transition-fast); 6718} 6719.mm2-subitem:hover { 6720 background: var(--mm2-muted); 6721 color: var(--mm2-accent-hover); 6722} 6723.mm2-subitem.active { 6724 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6725 border-color: var(--mm2-accent-20); 6726 color: var(--mm2-accent-hover); 6727 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6728} 6729.mm2-subitem.active::before { 6730 height: 24px; 6731} 6732 6733/* Right: subcontent (level-3 grid) - Enhanced */ 6734.mm2-subcontent { min-height: 240px; } 6735.mm2-links { 6736 display: grid; gap: 12px 14px; 6737 grid-template-columns: repeat(3, minmax(0,1fr)); 6738} 6739@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6740 6741.mm2-link { 6742 display: flex; align-items: center; gap: 12px; 6743 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6744 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6745 min-height: 52px; 6746 font-weight: 500; 6747 font-size: 14px; 6748 transition: all var(--mm2-transition-fast); 6749 box-shadow: var(--mm2-shadow-sm); 6750 position: relative; 6751 justify-content: space-between; 6752} 6753.mm2-link:hover { 6754 background: var(--mm2-muted); 6755 border-color: var(--mm2-accent); 6756 transform: translateX(4px); 6757 box-shadow: var(--mm2-shadow-md); 6758} 6759.mm2-link-text { 6760 flex: 1; 6761} 6762 6763/* Product Thumbnails */ 6764.mm2-thumbs { 6765 display: flex; 6766 gap: 6px; 6767 align-items: center; 6768 margin-left: auto; 6769 flex-shrink: 0; 6770} 6771.mm2-thumb { 6772 width: 36px; 6773 height: 36px; 6774 border-radius: 8px; 6775 object-fit: cover; 6776 border: 1.5px solid var(--mm2-border); 6777 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6778 transition: all var(--mm2-transition-fast); 6779 background: var(--mm2-muted); 6780} 6781.mm2-link:hover .mm2-thumb { 6782 border-color: var(--mm2-accent); 6783 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6784 transform: scale(1.08); 6785} 6786
6787/* Thumbnail Skeleton Loader */ 6788.mm2-thumb-skeleton { 6789 width: 36px; 6790 height: 36px; 6791 border-radius: 8px; 6792 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6793 background-size: 200% 100%; 6794 animation: shimmer 1.5s infinite; 6795 border: 1.5px solid var(--mm2-border); 6796} 6797 6798.mm2-badge { 6799 font-size: 11px; padding: 3px 10px; margin-left: auto; 6800 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6801 font-weight: 700; 6802 border: 1px solid var(--mm2-accent-20); 6803} 6804 6805/* "See all" highlight card - Enhanced */ 6806.mm2-seeall { 6807 border: 2px dashed var(--mm2-accent); 6808 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6809 font-weight: 700; 6810} 6811.mm2-seeall:hover { 6812 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6813 border-style: solid; 6814 transform: translateX(6px) scale(1.02); 6815} 6816 6817/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6818@media (max-width: 960px) { 6819 .mm2-panel { 6820 position: fixed; 6821 left: 0; 6822 right: 0; 6823 bottom: 0; 6824 top: auto; 6825 width: 100%; 6826 max-height: 88vh; 6827 transform: translateX(0) translateY(100%); 6828 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6829 border-bottom: none; 6830 } 6831 .mm2-open .mm2-panel { 6832 transform: translateX(0) translateY(0); z-index: 9999; 6833 } 6834 6835 /* Enhanced backdrop for mobile */ 6836 .mm2-backdrop { 6837 backdrop-filter: blur(6px) saturate(1.2); 6838 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6839 } 6840 6841 /* Mobile close button - Soft glassmorphism */ 6842 .mm2-mobile-close { 6843 position: absolute; 6844 top: 4px; 6845 right: 8px; 6846 width: 28px; 6847 height: 28px; 6848 border-radius: 50%; 6849 border: 1px solid rgba(226, 232, 240, 0.6); 6850 background: rgba(255, 255, 255, 0.85); 6851 backdrop-filter: blur(12px) saturate(1.8); 6852 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6853 color: var(--mm2-fg-muted); 6854 display: inline-flex;
6855 align-items: center; 6856 justify-content: center; 6857 cursor: pointer; 6858 transition: all var(--mm2-transition-fast); 6859 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6860 z-index: 20; 6861 } 6862 .mm2-mobile-close:hover { 6863 background: rgba(254, 242, 242, 0.95); 6864 border-color: rgba(226,166,66,0.8); 6865 color: #C77DFF; 6866 box-shadow: 0 4px 12px rgba(226,166,66,0.15); 6867 } 6868 .mm2-mobile-close:active { 6869 transform: scale(0.92); 6870 } 6871 6872 /* Drag handle indicator */ 6873 .mm2-top::before, 6874 .mm2-detail::before { 6875 content: ''; 6876 position: sticky; 6877 top: 0; 6878 left: 50%; 6879 transform: translateX(-50%); 6880 width: 40px; 6881 height: 4px; 6882 background: var(--mm2-border-strong); 6883 border-radius: 999px; 6884 margin: 12px auto 20px; 6885 display: block; 6886 z-index: 10; 6887 transition: background var(--mm2-transition-fast); 6888 } 6889 .mm2-panel:active .mm2-top::before, 6890 .mm2-panel:active .mm2-detail::before { 6891 background: #94a3b8; 6892 } 6893 6894 .mm2-top { 6895 padding: 10px; 6896 } 6897 6898 .mm2-top, .mm2-detail { 6899 max-height: min(82vh, 720px); 6900 padding-top: 8px; 6901 } 6902 6903 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6904 .mm2-detail-layout { display: block; } 6905 .mm2-subnav { 6906 position: static; max-height: none; overflow: visible; 6907 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6908 grid-template-columns: repeat(2, minmax(0,1fr)); 6909 border-bottom: 2px solid var(--mm2-muted); 6910 } 6911 .mm2-subitem { 6912 margin: 0; width: 100%; text-align: center; 6913 border-radius: 14px; 6914 background: #fff; border: 1.5px solid var(--mm2-border); 6915 white-space: normal; line-height: 1.3; min-height: 48px; 6916 justify-content: center; 6917 padding: 12px 10px; 6918 font-size: 13.5px; 6919 box-shadow: var(--mm2-shadow-sm); 6920 } 6921 .mm2-subitem::before { display: none; } 6922 .mm2-subitem:hover { 6923 transform: scale(1.02); 6924 } 6925 .mm2-subitem.active { 6926 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6927 border-color: var(--mm2-accent-hover); 6928 color: white; 6929 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6930 font-weight: 700; 6931 } 6932 6933 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6934 .mm2-links { 6935 grid-template-columns: repeat(2, minmax(0,1fr)); 6936 gap: 10px; 6937 } 6938 6939 /* Mobile: hide thumbnails untuk save space */ 6940 .mm2-thumbs { 6941 display: none; 6942 } 6943 6944 /* Larger touch targets for mobile */ 6945 .mm2-card { 6946 min-height: 80px; 6947 padding: 18px 14px; 6948 } 6949 .mm2-card .mm2-ico { 6950 width: 50px; 6951 height: 50px; 6952 } 6953 .mm2-card .mm2-title { 6954 font-size: 14px; 6955 } 6956 .mm2-card .mm2-sub { 6957 font-size: 11.5px; 6958 } 6959 .mm2-link { 6960 min-height: 56px; 6961 padding: 16px; 6962 } 6963 .mm2-back { 6964 width: 44px; 6965 height: 44px; 6966 } 6967 6968 /* Grid spacing */ 6969 .mm2-grid { 6970 gap: 10px; 6971 } 6972} 6973</style> 6974 <style data-inlined="product-compare.css"> 6975/* Product Compare Feature Styles */ 6976 6977/* Compare Button in Product Detail - Modern Chip Style */ 6978.pd-compare-section { 6979 margin: 0.5rem 0; 6980 display: flex; 6981 align-items: center; 6982 gap: 0.5rem; 6983} 6984 6985.pd-compare-btn { 6986 display: inline-flex; 6987 align-items: center; 6988 gap: 0.4rem; 6989 padding: 0.5rem 0.85rem; 6990 border: 1px solid #cbd5e1; 6991 border-radius: 20px; 6992 background: #fff; 6993 color: #64748b; 6994 font-weight: 600; 6995 font-size: 0.8rem; 6996 cursor: pointer; 6997 transition: all 0.2s ease; 6998 position: relative; 6999 white-space: nowrap; 7000} 7001 7002.pd-compare-btn:hover { 7003 border-color: #3b82f6; 7004 color: #3b82f6; 7005 background: #f0f9ff; 7006 transform: translateY(-1px); 7007 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); 7008} 7009 7010.pd-compare-btn.added { 7011 border-color: #16a34a; 7012 color: #16a34a; 7013 background: #f0fdf4; 7014} 7015 7016.pd-compare-btn .compare-remove { 7017 display: none; 7018} 7019 7020.pd-compare-btn.added .compare-text { 7021 display: none; 7022} 7023 7024.pd-compare-btn.added .compare-remove { 7025 display: flex !important; 7026 align-items: center; 7027 gap: 0.25rem; 7028 font-weight: 600; 7029 font-size: 0.9rem; 7030 color: #C77DFF !important; 7031 opacity: 1 !important; 7032 visibility: visible !important; 7033 z-index: 1; 7034 position: relative; 7035} 7036 7037.pd-compare-btn.added .compare-remove[hidden] { 7038 display: flex !important; 7039} 7040 7041/* Force visibility for added state */ 7042.pd-compare-btn.added .compare-remove { 7043 display: flex !important; 7044} 7045 7046[hidden].compare-remove { 7047 display: none !important; 7048} 7049 7050.pd-compare-btn.added [hidden].compare-remove { 7051 display: flex !important; 7052} 7053 7054/* Ensure the added state text is always visible */ 7055.pd-compare-btn.added { 7056 border-color: #C77DFF !important; 7057 color: #C77DFF !important; 7058 background: #fef2f2 !important; 7059} 7060 7061.pd-compare-btn.added .compare-remove svg { 7062 color: #C77DFF !important; 7063} 7064 7065.pd-compare-btn.added:hover { 7066 border-color: #15803d; 7067 color: #15803d; 7068 background: #dcfce7; 7069 transform: translateY(-1px); 7070 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 7071} 7072 7073.pd-compare-btn .compare-icon { 7074 width: 16px; 7075 height: 16px; 7076 flex-shrink: 0; 7077} 7078 7079.pd-compare-info { 7080 display: inline-flex; 7081 align-items: center; 7082} 7083 7084.compare-note { 7085 color: #94a3b8; 7086 font-size: 0.7rem; 7087 font-weight: 500; 7088 transition: color 0.2s ease; 7089} 7090 7091.pd-compare-btn.added + .pd-compare-info .compare-note { 7092 color: #16a34a; 7093 font-weight: 600; 7094} 7095 7096/* Compare Badge in Navigation */ 7097.compare-indicator { 7098 position: relative; 7099} 7100 7101.compare-indicator .badge { 7102 position: absolute; 7103 top: -8px; 7104 right: -8px; 7105 min-width: 18px;
7106 height: 18px; 7107 padding: 0 5px; 7108 background: #C77DFF; 7109 color: white; 7110 border-radius: 9px; 7111 font-size: 11px; 7112 7113 7114/* Floating Compare Badge (Sticky) */ 7115.compare-floating-badge { 7116 position: fixed; 7117 right: 16px; 7118 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7119 width: auto; 7120 min-width: 56px; 7121 height: 56px; 7122 padding: 0 16px; 7123 border-radius: 28px; 7124 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 7125 color: #fff; 7126 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.5); 7127 cursor: pointer; 7128 z-index: 1104; 7129 opacity: 0; 7130 transform: translateY(8px) scale(0.9); 7131 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7132 display: flex; 7133 align-items: center; 7134 justify-content: center; 7135 gap: 8px; 7136 font-weight: 600; 7137 font-size: 0.85rem; 7138 pointer-events: none; 7139} 7140 7141.compare-floating-badge.visible { 7142 opacity: 1; 7143 transform: translateY(0) scale(1); 7144 pointer-events: auto; 7145} 7146 7147.compare-floating-badge:hover { 7148 box-shadow: 0 12px 32px -10px rgba(59, 130, 246, 0.6); 7149 transform: translateY(-2px) scale(1.02); 7150} 7151 7152.compare-floating-badge:active { 7153 transform: translateY(0) scale(0.98); 7154} 7155 7156.compare-floating-badge .cfb-icon { 7157 width: 20px; 7158 height: 20px; 7159 flex-shrink: 0; 7160} 7161 7162.compare-floating-badge .cfb-count { 7163 font-size: 0.9rem; 7164 font-weight: 700; 7165 min-width: 20px; 7166 text-align: center; 7167 background: rgba(255, 255, 255, 0.25); 7168 padding: 2px 8px; 7169 border-radius: 12px; 7170 line-height: 1; 7171 display: flex; 7172 align-items: center; 7173 justify-content: center; 7174 height: 24px; 7175} 7176 7177.compare-floating-badge .cfb-text { 7178 display: none; 7179} 7180 7181@media (min-width: 641px) { 7182 .compare-floating-badge .cfb-text { 7183 display: block; 7184 white-space: nowrap; 7185 } 7186} 7187 7188@media (max-width: 640px) { 7189 .compare-floating-badge { 7190 right: 14px; 7191 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7192 min-width: 48px; 7193 height: 48px; 7194 padding: 0 12px; 7195 border-radius: 24px; 7196 } 7197 7198 .compare-floating-badge .cfb-icon { 7199 width: 18px; 7200 height: 18px; 7201 } 7202 7203 .compare-floating-badge .cfb-count { 7204 font-size: 0.8rem; 7205 padding: 2px 6px; 7206 height: 20px; 7207 } 7208} font-weight: 600; 7209 display: flex; 7210 align-items: center; 7211 justify-content: center; 7212 transform: scale(0); 7213 transition: transform 0.2s ease; 7214} 7215 7216.compare-indicator .badge.active { 7217 transform: scale(1); 7218} 7219 7220/* Compare Sidebar */ 7221.compare-sidebar { 7222 position: fixed; 7223 top: 0; 7224 right: -400px; 7225 width: 400px; 7226 height: 100vh; 7227 background: #ffffff; 7228 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7229 z-index: 9999; 7230 display: flex; 7231 flex-direction: column; 7232 transition: right 0.3s ease; 7233} 7234 7235.compare-sidebar.open { 7236 right: 0; 7237} 7238 7239.compare-overlay { 7240 position: fixed; 7241 top: 0; 7242 left: 0; 7243 width: 100%; 7244 height: 100%; 7245 background: rgba(0, 0, 0, 0.5); 7246 z-index: 9998; 7247 opacity: 0; 7248 visibility: hidden; 7249 transition: all 0.3s ease; 7250} 7251 7252.compare-overlay.active { 7253 opacity: 1; 7254 visibility: visible; 7255} 7256 7257.compare-header { 7258 display: flex; 7259 align-items: center; 7260 justify-content: space-between; 7261 padding: 1.25rem; 7262 border-bottom: 1px solid #e2e8f0; 7263 background: #f8fafc; 7264} 7265 7266.compare-title { 7267 display: flex; 7268 align-items: center; 7269 gap: 0.5rem; 7270 font-size: 1rem; 7271 font-weight: 600; 7272 color: #1e293b; 7273 margin: 0; 7274} 7275 7276.compare-count { 7277 color: #64748b; 7278 font-weight: 400; 7279} 7280 7281.compare-close { 7282 display: flex; 7283 align-items: center; 7284 justify-content: center; 7285 width: 32px; 7286 height: 32px; 7287 border: none; 7288 border-radius: 6px; 7289 background: transparent; 7290 color: #64748b; 7291 cursor: pointer; 7292 transition: all 0.2s ease; 7293} 7294 7295.compare-close:hover { 7296 background: #e2e8f0; 7297 color: #374151; 7298} 7299 7300.compare-content { 7301 flex: 1; 7302 overflow-y: auto; 7303 padding: 1rem; 7304} 7305 7306.compare-empty { 7307 display: flex; 7308 flex-direction: column; 7309 align-items: center; 7310 justify-content: center; 7311 height: 100%; 7312 text-align: center; 7313 padding: 2rem 1rem; 7314} 7315 7316.compare-empty h5 { 7317 margin: 1rem 0 0.5rem; 7318 color: #374151; 7319} 7320 7321.compare-empty p { 7322 color: #64748b; 7323 font-size: 0.875rem; 7324 line-height: 1.5; 7325} 7326 7327.compare-list { 7328 display: flex; 7329 flex-direction: column; 7330 gap: 0.75rem; 7331} 7332 7333.compare-item { 7334 display: flex; 7335 align-items: center; 7336 gap: 0.75rem; 7337 padding: 0.75rem; 7338 border: 1px solid #e2e8f0; 7339 border-radius: 8px; 7340 background: #ffffff; 7341 transition: all 0.2s ease; 7342} 7343 7344.compare-item:hover { 7345 border-color: #cbd5e1; 7346 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7347} 7348 7349.compare-item-image { 7350 flex-shrink: 0; 7351 width: 48px; 7352 height: 48px; 7353 border-radius: 6px; 7354 overflow: hidden; 7355 border: 1px solid #e2e8f0; 7356} 7357 7358.compare-item-image img { 7359 width: 100%; 7360 height: 100%; 7361 object-fit: cover; 7362} 7363 7364.compare-item-info { 7365 flex: 1; 7366 min-width: 0; 7367} 7368 7369.compare-item-name { 7370 font-weight: 500; 7371 font-size: 0.875rem; 7372 color: #1e293b; 7373 margin-bottom: 0.25rem; 7374 line-height: 1.3; 7375 display: -webkit-box; 7376 -webkit-line-clamp: 2; 7377 -webkit-box-orient: vertical; 7378 overflow: hidden; 7379} 7380 7381.compare-item-price { 7382 font-weight: 600; 7383 font-size: 0.8rem; 7384 color: #059669; 7385} 7386 7387.compare-item-remove { 7388 flex-shrink: 0; 7389 display: flex; 7390 align-items: center; 7391 justify-content: center; 7392 width: 28px; 7393 height: 28px; 7394 border: none; 7395 border-radius: 4px; 7396 background: transparent; 7397 color: #C77DFF; 7398 cursor: pointer; 7399 transition: all 0.2s ease; 7400} 7401 7402.compare-item-remove:hover { 7403 background: #fef2f2; 7404 color: #E3D05A; 7405} 7406 7407.compare-actions { 7408 padding: 1rem; 7409 border-top: 1px solid #e2e8f0; 7410 display: flex; 7411 gap: 0.75rem; 7412} 7413 7414.compare-actions .btn { 7415 flex: 1; 7416 padding: 0.75rem 1rem; 7417 border-radius: 6px; 7418 font-weight: 600; 7419 font-size: 0.875rem; 7420 display: flex; 7421 align-items: center; 7422 justify-content: center; 7423 gap: 0.5rem; 7424 cursor: pointer; 7425 transition: all 0.2s ease; 7426} 7427 7428.btn-outline-danger { 7429 border: 1px solid #C77DFF; 7430 background: transparent; 7431 color: #C77DFF; 7432} 7433 7434.btn-outline-danger:hover { 7435 background: #C77DFF; 7436 color: white; 7437} 7438 7439.btn-primary { 7440 border: 1px solid #3b82f6; 7441 background: #3b82f6; 7442 color: white; 7443} 7444 7445.btn-primary:hover { 7446 background: #2563eb; 7447 border-color: #2563eb; 7448} 7449 7450.btn-primary:disabled { 7451 opacity: 0.5;
7452 cursor: not-allowed; 7453} 7454 7455.btn-primary:disabled:hover { 7456 background: #3b82f6; 7457 border-color: #3b82f6; 7458} 7459 7460/* AI Comparison Modal */ 7461.ai-compare-modal { 7462 position: fixed; 7463 top: 0; 7464 left: 0; 7465 width: 100%; 7466 height: 100%; 7467 z-index: 10000; 7468 display: none; 7469 align-items: center; 7470 justify-content: center; 7471} 7472 7473.ai-compare-modal.open { 7474 display: flex; 7475} 7476 7477.ai-compare-modal .modal-backdrop { 7478 position: absolute; 7479 top: 0; 7480 left: 0; 7481 width: 100%; 7482 height: 100%; 7483 background: rgba(0, 0, 0, 0.6); 7484 backdrop-filter: blur(4px); 7485} 7486 7487.modal-container { 7488 position: relative; 7489 width: 90%; 7490 max-width: 1000px; 7491 max-height: 90vh; 7492 background: #ffffff; 7493 border-radius: 12px; 7494 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7495 display: flex; 7496 flex-direction: column; 7497 overflow: hidden; 7498} 7499 7500.modal-header { 7501 display: flex; 7502 align-items: center; 7503 justify-content: space-between; 7504 padding: 1.5rem 2rem; 7505 border-bottom: 1px solid #e2e8f0; 7506 background: #f8fafc; 7507} 7508 7509.modal-title { 7510 display: flex; 7511 align-items: center; 7512 gap: 0.75rem; 7513 font-size: 1.25rem; 7514 font-weight: 600; 7515 color: #1e293b; 7516 margin: 0; 7517} 7518 7519.modal-close { 7520 display: flex; 7521 align-items: center; 7522 justify-content: center; 7523 width: 36px; 7524 height: 36px; 7525 border: none; 7526 border-radius: 8px; 7527 background: transparent; 7528 color: #64748b; 7529 cursor: pointer; 7530 transition: all 0.2s ease; 7531} 7532 7533.modal-close:hover { 7534 background: #e2e8f0; 7535 color: #374151; 7536} 7537 7538.modal-content { 7539 flex: 1; 7540 overflow-y: auto; 7541 padding: 2rem; 7542} 7543 7544/* AI Loading State */ 7545.ai-loading { 7546 display: flex; 7547 flex-direction: column; 7548 align-items: center; 7549 justify-content: center; 7550 padding: 3rem 2rem; 7551 text-align: center; 7552} 7553 7554.loading-animation { 7555 margin-bottom: 1rem; 7556} 7557 7558.typing-indicator { 7559 display: flex; 7560 align-items: center; 7561 justify-content: center; 7562 gap: 4px; 7563 margin-bottom: 1rem; 7564} 7565 7566.typing-indicator span { 7567 width: 8px; 7568 height: 8px; 7569 background: #3b82f6; 7570 border-radius: 50%; 7571 animation: typing 1.4s infinite ease-in-out; 7572} 7573 7574.typing-indicator span:nth-child(2) { 7575 animation-delay: 0.2s; 7576} 7577 7578.typing-indicator span:nth-child(3) { 7579 animation-delay: 0.4s; 7580} 7581 7582@keyframes typing { 7583 0%, 80%, 100% { 7584 opacity: 0.3; 7585 transform: scale(0.8); 7586 } 7587 40% { 7588 opacity: 1; 7589 transform: scale(1); 7590 } 7591} 7592 7593.ai-loading p { 7594 font-weight: 500; 7595 color: #1e293b; 7596 margin-bottom: 0.5rem; 7597} 7598 7599.ai-loading small { 7600 color: #64748b; 7601} 7602 7603/* AI Error State */ 7604.ai-error { 7605 display: flex; 7606 flex-direction: column; 7607 align-items: center; 7608 justify-content: center; 7609 padding: 3rem 2rem; 7610 text-align: center; 7611} 7612 7613.ai-error h4 { 7614 margin: 1rem 0 0.5rem; 7615 color: #C77DFF; 7616} 7617 7618.ai-error p { 7619 color: #64748b; 7620 margin-bottom: 1.5rem; 7621} 7622 7623/* AI Results */ 7624.ai-results > div { 7625 margin-bottom: 2rem; 7626} 7627 7628.ai-summary-card, 7629.ai-comparison-table, 7630.ai-detailed-analysis, 7631.ai-recommendations, 7632.ai-tips { 7633 background: #f8fafc; 7634 border: 1px solid #e2e8f0; 7635 border-radius: 12px; 7636 padding: 1.5rem; 7637} 7638 7639.ai-results h4 { 7640 display: flex; 7641 align-items: center; 7642 gap: 0.5rem; 7643 font-size: 1.1rem; 7644 font-weight: 600; 7645 color: #1e293b; 7646 margin-bottom: 1rem; 7647} 7648 7649.summary-text { 7650 font-size: 1rem; 7651 line-height: 1.6; 7652 color: #374151; 7653 margin-bottom: 1rem; 7654} 7655 7656.winner-badge { 7657 padding: 1rem; 7658 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7659 color: white; 7660 border-radius: 8px; 7661} 7662 7663.winner-badge strong { 7664 display: block; 7665 margin-bottom: 0.25rem; 7666} 7667 7668.winner-badge small { 7669 opacity: 0.9; 7670 font-size: 0.875rem; 7671} 7672 7673/* Comparison Table */ 7674.comparison-grid { 7675 display: grid; 7676 gap: 1rem; 7677 margin-top: 1rem; 7678} 7679 7680.comparison-category { 7681 background: white; 7682 border: 1px solid #e2e8f0; 7683 border-radius: 8px; 7684 overflow: hidden; 7685} 7686 7687.category-header { 7688 background: #f1f5f9; 7689 padding: 0.75rem 1rem; 7690 font-weight: 600; 7691 color: #1e293b; 7692 border-bottom: 1px solid #e2e8f0; 7693} 7694 7695.category-products { 7696 display: grid; 7697 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7698 gap: 1px; 7699 background: #e2e8f0; 7700} 7701 7702.product-score { 7703 background: white; 7704 padding: 1rem; 7705 text-align: center; 7706} 7707 7708.score-value { 7709 display: inline-flex; 7710 align-items: center; 7711 gap: 0.25rem; 7712 margin-bottom: 0.5rem; 7713} 7714 7715.score-stars { 7716 color: #C77DFF; 7717} 7718 7719.score-note { 7720 font-size: 0.8rem; 7721 color: #64748b; 7722 line-height: 1.3; 7723} 7724 7725/* Product name header in comparison table */ 7726.product-name-header { 7727 margin-bottom: 0.5rem; 7728} 7729 7730.product-name-header strong { 7731 font-size: .8rem; /* h5 size (18px) */ 7732 font-weight: 600; 7733 color: #1e293b; 7734 line-height: 1.3; 7735 display: block; 7736 word-break: break-word; 7737} 7738 7739.product-price { 7740 font-size: 0.8rem; 7741 font-weight: 600; 7742 color: #059669; 7743} 7744 7745/* Detailed Analysis */ 7746.products-analysis { 7747 display: grid; 7748 gap: 1rem; 7749 margin-top: 1rem; 7750} 7751 7752.product-analysis { 7753 background: white; 7754 border: 1px solid #e2e8f0; 7755 border-radius: 8px; 7756 padding: 1rem; 7757} 7758 7759.product-analysis h5 { 7760 color: #1e293b; 7761 margin-bottom: 0.75rem; 7762 font-weight: 600; 7763} 7764 7765.analysis-section { 7766 margin-bottom: 0.75rem; 7767} 7768 7769.analysis-section:last-child { 7770 margin-bottom: 0; 7771} 7772 7773.analysis-section h6 { 7774 font-size: 0.875rem; 7775 font-weight: 600; 7776 margin-bottom: 0.5rem; 7777} 7778 7779.analysis-section h6.strengths { 7780 color: #059669; 7781} 7782 7783.analysis-section h6.weaknesses { 7784 color: #C77DFF; 7785} 7786 7787.analysis-section h6.best-for { 7788 color: #7c3aed; 7789} 7790 7791.analysis-section ul { 7792 list-style: none; 7793 padding: 0; 7794 margin: 0; 7795} 7796 7797.analysis-section li { 7798 display: flex; 7799 align-items: flex-start; 7800 gap: 0.5rem; 7801 margin-bottom: 0.25rem; 7802 font-size: 0.875rem; 7803 line-height: 1.4; 7804} 7805 7806.analysis-section li:before { 7807 content: "â¢"; 7808 color: #64748b; 7809 font-weight: bold; 7810 margin-top: 0.1rem; 7811} 7812 7813.best-for-text { 7814 font-size: 0.875rem; 7815 color: #374151; 7816 font-style: italic; 7817} 7818 7819/* Recommendations */ 7820.recommendations-grid { 7821 display: grid; 7822 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7823 gap: 1rem; 7824 margin-top: 1rem; 7825} 7826 7827.recommendation-card { 7828 background: white; 7829 border: 1px solid #e2e8f0; 7830 border-radius: 8px; 7831 padding: 1rem; 7832 text-align: center; 7833} 7834 7835.recommendation-card h5 { 7836 font-size: 1rem; 7837 margin-bottom: 0.75rem; 7838 color: #1e293b; 7839} 7840 7841.recommendation-card p { 7842 font-size: 0.875rem; 7843 color: #374151; 7844 line-height: 1.5; 7845} 7846 7847/* Tips */ 7848.tips-list { 7849 list-style: none; 7850 padding: 0; 7851 margin: 1rem 0 0 0; 7852} 7853 7854.tips-list li { 7855 display: flex; 7856 align-items: flex-start; 7857 gap: 0.75rem; 7858 margin-bottom: 0.75rem; 7859 font-size: 0.9rem; 7860 line-height: 1.5; 7861 color: #374151; 7862} 7863 7864.tips-list li:before { 7865 content: "ð¡"; 7866 font-size: 1rem; 7867 margin-top: 0.1rem; 7868} 7869 7870/* Modal Footer */ 7871.modal-footer { 7872 display: flex; 7873 justify-content: space-between; 7874 align-items: center; 7875 padding: 1.5rem 2rem; 7876 border-top: 1px solid #e2e8f0; 7877 background: #f8fafc; 7878} 7879 7880.btn-outline-secondary { 7881 border: 1px solid #64748b; 7882 background: transparent; 7883 color: #64748b; 7884 padding: 0.75rem 1.5rem; 7885 border-radius: 6px; 7886 font-weight: 600; 7887 display: flex; 7888 align-items: center; 7889 gap: 0.5rem; 7890 cursor: pointer; 7891 transition: all 0.2s ease; 7892} 7893 7894.btn-outline-secondary:hover { 7895 background: #64748b; 7896 color: white; 7897} 7898 7899/* Responsive for chip button */ 7900@media (max-width: 880px) { 7901 .pd-compare-section { 7902 margin: 1rem 0; 7903 display: block; 7904 } 7905 7906 .pd-compare-btn { 7907 width: 100%; 7908 font-size: 0.85rem; 7909 padding: 0.65rem 1rem; 7910 border-radius: 10px; 7911 justify-content: center; 7912 min-height: 44px;
7913 } 7914 7915 .pd-compare-btn .compare-icon { 7916 width: 16px; 7917 height: 16px; 7918 } 7919 7920 .pd-compare-info { 7921 margin-top: 0.5rem; 7922 display: block; 7923 text-align: center; 7924 } 7925 7926 .compare-note { 7927 font-size: 0.7rem; 7928 display: block; 7929 } 7930} 7931 7932/* Responsive Design */ 7933@media (max-width: 768px) { 7934 .compare-sidebar { 7935 width: 100%; 7936 right: -100%; 7937 /* Fix for mobile viewport height changes */ 7938 height: 100vh; 7939 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7940 } 7941 7942 .compare-actions { 7943 /* Ensure actions are always visible */ 7944 position: sticky; 7945 bottom: 0; 7946 background: white; 7947 border-top: 2px solid #e2e8f0; 7948 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7949 z-index: 10; 7950 } 7951 7952 .compare-actions .btn { 7953 /* Larger touch targets for mobile */ 7954 min-height: 48px; 7955 font-size: 0.9rem; 7956 font-weight: 700; 7957 } 7958 7959 .modal-container { 7960 width: 95%; 7961 max-height: 95vh; 7962 max-height: 95dvh; /* Dynamic viewport height */ 7963 } 7964 7965 .modal-header, 7966 .modal-content, 7967 .modal-footer { 7968 padding: 1rem; 7969 } 7970 7971 .recommendations-grid { 7972 grid-template-columns: 1fr; 7973 } 7974 7975 .category-products { 7976 grid-template-columns: 1fr; 7977 } 7978 7979 .modal-footer { 7980 flex-direction: column; 7981 gap: 0.75rem; 7982 } 7983 7984 .modal-footer .btn { 7985 width: 100%; 7986 } 7987 7988 /* Ensure compare button is visible on mobile */ 7989 .pd-compare-section { 7990 display: block !important; 7991 visibility: visible !important; 7992 opacity: 1 !important; 7993 margin: 1rem 0; 7994 padding: 0; 7995 position: relative !important; 7996 z-index: 1 !important; 7997 max-width: 100%; 7998 overflow: hidden; 7999 } 8000 8001 .pd-compare-btn { 8002 display: flex !important; 8003 visibility: visible !important; 8004 opacity: 1 !important; 8005 width: 100%; 8006 padding: 0.75rem 1rem; 8007 font-size: 0.9rem; 8008 min-height: 48px; /* Minimum touch target for mobile */ 8009 border: 2px solid #e2e8f0 !important; 8010 background: #fff !important; 8011 position: relative !important; 8012 z-index: 2 !important; 8013 box-sizing: border-box; 8014 white-space: normal !important; /* Allow text wrapping on mobile */ 8015 flex-wrap: wrap; 8016 gap: 0.5rem; 8017 } 8018 8019 .pd-compare-btn .compare-text { 8020 display: inline !important; 8021 visibility: visible !important; 8022 font-weight: 600; 8023 color: #475569 !important; 8024 } 8025 8026 .pd-compare-btn .compare-remove { 8027 display: none !important; /* Hidden by default */ 8028 } 8029 8030 .pd-compare-btn.added .compare-remove { 8031 display: flex !important; /* Only show when added */ 8032 flex-wrap: wrap; 8033 align-items: center; 8034 gap: 0.4rem; 8035 white-space: normal !important; 8036 word-break: break-word; 8037 } 8038 8039 .pd-compare-btn .compare-icon { 8040 display: inline-block !important; 8041 width: 18px; 8042 height: 18px; 8043 flex-shrink: 0; 8044 } 8045 8046 .pd-compare-info { 8047 margin-top: 0.5rem; 8048 } 8049 8050 .compare-note { 8051 font-size: 0.85rem !important; 8052 display: block !important; 8053 visibility: visible !important; 8054 color: #64748b !important; 8055 } 8056} 8057 8058/* iOS Safari specific fixes */ 8059@media (max-width: 480px) { 8060 #compareBtn { 8061 width:46px; 8062 height:46px; 8063 } 8064 8065 .pd-compare-section { 8066 -webkit-transform: translateZ(0); 8067 transform: translateZ(0); 8068 } 8069 8070 .pd-compare-btn { 8071 -webkit-appearance: none !important; 8072 appearance: none !important; 8073 -webkit-tap-highlight-color: transparent; 8074 touch-action: manipulation; 8075 } 8076 8077 /* Fix sidebar height for iOS address bar */ 8078 .compare-sidebar { 8079 /* Use fixed positioning that accounts for iOS address bar */ 8080 height: 100vh; 8081 height: -webkit-fill-available; /* iOS Safari compatibility */ 8082 min-height: 100vh; 8083 min-height: -webkit-fill-available; 8084 } 8085 8086 /* Ensure compare actions stay at bottom */ 8087 .compare-actions { 8088 position: sticky; 8089 bottom: 0; 8090 background: white; 8091 border-top: 2px solid #e2e8f0; 8092 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8093 padding: 1.25rem 1rem; 8094 z-index: 20; 8095 } 8096 8097 .compare-actions .btn { 8098 min-height: 52px; /* Larger for easier tapping */ 8099 font-size: 1rem; 8100 font-weight: 700; 8101 border-radius: 8px; 8102 } 8103 8104 /* Adjust content padding to account for sticky actions */ 8105 .compare-content { 8106 padding-bottom: 6rem; /* Extra space for sticky actions */ 8107 } 8108} 8109 8110/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8111@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8112 .pd-compare-section { 8113 display: block !important; 8114 } 8115 8116 .pd-compare-btn { 8117 display: flex !important; 8118 min-height: 44px;
8119 font-size: 16px; /* Prevent zoom on iOS */ 8120 } 8121 8122 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8123 .compare-sidebar { 8124 /* Ensure full height coverage */ 8125 height: 100vh; 8126 height: 100dvh; /* Dynamic viewport height */ 8127 height: -webkit-fill-available; /* iOS fallback */ 8128 max-height: 100vh; 8129 max-height: 100dvh; 8130 max-height: -webkit-fill-available; 8131 overflow: hidden; /* Prevent scroll issues */ 8132 } 8133 8134 .compare-content { 8135 /* Flexible content area */ 8136 flex: 1; 8137 overflow-y: auto; 8138 -webkit-overflow-scrolling: touch; 8139 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8140 } 8141 8142 .compare-actions { 8143 /* Force actions to stay visible */ 8144 position: fixed; 8145 bottom: 0; 8146 left: 0; 8147 right: 0; 8148 width: 100%; 8149 background: white; 8150 border-top: 2px solid #e2e8f0; 8151 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8152 padding: 1rem; 8153 z-index: 9999; 8154 display: flex !important; 8155 gap: 0.75rem; 8156 } 8157 8158 .compare-actions .btn { 8159 flex: 1; 8160 min-height: 30px; /* Extra large for high-DPI screens */ 8161 font-size: .8rem; 8162 font-weight: 600; 8163 border-radius: 12px; 8164 display: flex !important; 8165 align-items: center; 8166 justify-content: center; 8167 gap: 0.5rem; 8168 } 8169 8170 /* Ensure AI analyze button is always visible and prominent */ 8171 #compareAnalyze { 8172 background: #3b82f6 !important; 8173 border-color: #3b82f6 !important; 8174 color: white !important; 8175 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8176 } 8177 8178 #compareAnalyze:not(:disabled) { 8179 background: #2563eb !important; 8180 border-color: #2563eb !important; 8181 transform: scale(1.02); 8182 } 8183 8184 #compareAnalyze:disabled { 8185 opacity: 0.6 !important; 8186 background: #9ca3af !important; 8187 border-color: #9ca3af !important; 8188 } 8189} 8190 8191/* AI Disclaimer Styles */ 8192.ai-disclaimer { 8193 display: flex; 8194 align-items: flex-start; 8195 gap: 0.75rem; 8196 padding: 1rem; 8197 background: #fef3c7; 8198 border: 1px solid #C77DFF; 8199 border-radius: 8px; 8200 margin: 0; 8201} 8202 8203.disclaimer-icon { 8204 font-size: 1.25rem; 8205 flex-shrink: 0; 8206 margin-top: 0.125rem; 8207} 8208 8209.disclaimer-text { 8210 font-size: 0.85rem; 8211 line-height: 1.4; 8212 color: #ae7e21; 8213} 8214 8215.disclaimer-text strong { 8216 color: #765212; 8217 font-weight: 600; 8218} 8219 8220/* Compare Disclaimer in Sidebar */ 8221.compare-disclaimer { 8222 margin-top: 1rem; 8223 padding: 1rem; 8224 background: #f0f9ff; 8225 border: 1px solid #0ea5e9; 8226 border-radius: 8px; 8227} 8228 8229.compare-disclaimer h6 { 8230 display: flex; 8231 align-items: center; 8232 gap: 0.5rem; 8233 font-size: 0.875rem; 8234 font-weight: 600; 8235 color: #0c4a6e; 8236 margin-bottom: 0.5rem; 8237} 8238 8239.compare-disclaimer p { 8240 font-size: 0.8rem; 8241 line-height: 1.4; 8242 color: #075985; 8243 margin-bottom: 0.5rem; 8244} 8245 8246.compare-disclaimer p:last-child { 8247 margin-bottom: 0; 8248} 8249 8250.compare-disclaimer ul { 8251 list-style: none; 8252 padding: 0; 8253 margin: 0.5rem 0 0 0; 8254} 8255 8256.compare-disclaimer li { 8257 display: flex; 8258 align-items: flex-start; 8259 gap: 0.5rem; 8260 margin-bottom: 0.25rem; 8261 font-size: 0.75rem; 8262 line-height: 1.3; 8263 color: #075985; 8264} 8265 8266.compare-disclaimer li:before { 8267 content: "â¢"; 8268 color: #0ea5e9; 8269 font-weight: bold; 8270 margin-top: 0.1rem; 8271 flex-shrink: 0; 8272} 8273 8274/* Mobile Browser Address Bar Fixes */ 8275@supports (height: 100dvh) { 8276 /* Modern browsers with dynamic viewport support */ 8277 @media (max-width: 768px) { 8278 .compare-sidebar { 8279 height: 100dvh !important; 8280 max-height: 100dvh !important; 8281 } 8282 8283 .modal-container { 8284 max-height: 95dvh !important; 8285 } 8286 } 8287} 8288 8289@supports (height: -webkit-fill-available) { 8290 /* iOS Safari specific support */ 8291 @media (max-width: 768px) { 8292 .compare-sidebar { 8293 height: -webkit-fill-available !important; 8294 max-height: -webkit-fill-available !important; 8295 } 8296 } 8297} 8298 8299/* Fallback for older browsers */ 8300@media (max-width: 768px) and (orientation: portrait) { 8301 .compare-sidebar { 8302 /* Fixed height that works across different mobile browsers */ 8303 min-height: 600px; 8304 height: calc(100vh - env(keyboard-inset-height, 0px)); 8305 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8306 } 8307 8308 /* Ensure actions are always accessible */ 8309 .compare-actions { 8310 position: sticky !important; 8311 bottom: env(safe-area-inset-bottom, 0px) !important; 8312 margin-bottom: 0 !important; 8313 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8314 } 8315} 8316 8317/* PWA and fullscreen app support */ 8318@media (display-mode: standalone) { 8319 .compare-sidebar { 8320 height: 100vh !important; 8321 height: 100dvh !important; 8322 } 8323} 8324 8325/* Utilities */
8326.btn-sm { 8327 padding: 0.5rem 0.75rem; 8328 font-size: 0.8rem; 8329} 8330 8331[hidden] { 8332 display: none !important; 8333} 8334 8335/* ===== UX ENHANCEMENTS ===== */ 8336 8337/* Compare Button Highlight Animation */ 8338.pulse-highlight { 8339 animation: pulse-glow 2s ease-in-out 3; 8340 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8341} 8342 8343@keyframes pulse-glow { 8344 0% { 8345 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8346 } 8347 50% { 8348 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8349 } 8350 100% { 8351 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); 8352 } 8353} 8354 8355/* Flying Product Animation */ 8356.flying-product { 8357 position: fixed; 8358 z-index: 9999; 8359 pointer-events: none; 8360} 8361 8362.flying-product img { 8363 border-radius: 8px; 8364 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8365} 8366 8367/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8368 8369/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8370</style> 8371 <style data-inlined="product-card-actions.css"> 8372/** 8373 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8374 * Tokopedia-inspired design 8375 */ 8376 8377/* ===== 3 Dot Button on Product Card ===== */ 8378.prod-card { 8379 position: relative; 8380} 8381 8382.prod-card-actions-btn { 8383 position: absolute; 8384 top: 8px; 8385 right: 8px; 8386 width: 32px; 8387 height: 32px; 8388 border-radius: 50%; 8389 background: rgba(255, 255, 255, 0.95); 8390 border: 1px solid rgba(0, 0, 0, 0.08); 8391 display: inline-flex;
8392 align-items: center; 8393 justify-content: center; 8394 cursor: pointer; 8395 z-index: 3; 8396 transition: all 0.2s ease; 8397 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8398 -webkit-tap-highlight-color: transparent; 8399} 8400 8401.prod-card-actions-btn:hover { 8402 background: #fff; 8403 border-color: rgba(0, 0, 0, 0.15); 8404 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8405 transform: scale(1.05); 8406} 8407 8408.prod-card-actions-btn:active { 8409 transform: scale(0.95); 8410} 8411 8412.prod-card-actions-btn svg { 8413 width: 20px; 8414 height: 20px; 8415 color: #64748b; 8416 display: block; 8417} 8418 8419/* Desktop: Show on hover */ 8420@media (min-width: 641px) { 8421 .prod-card-actions-btn { 8422 opacity: 0; 8423 pointer-events: none; 8424 } 8425 8426 .prod-card:hover .prod-card-actions-btn { 8427 opacity: 1; 8428 pointer-events: auto; 8429 } 8430} 8431 8432/* Mobile: Always visible but smaller */ 8433@media (max-width: 640px) { 8434 .prod-card-actions-btn { 8435 width: 28px; 8436 height: 28px; 8437 top: 6px; 8438 right: 6px; 8439 opacity: 95%; 8440 } 8441 8442 .prod-card-actions-btn svg { 8443 width: 18px; 8444 height: 18px; 8445 } 8446} 8447 8448/* ===== Bottom Sheet Overlay ===== */ 8449.pca-overlay { 8450 position: fixed; 8451 inset: 0; 8452 background: rgba(15, 23, 42, 0.6); 8453 backdrop-filter: blur(4px); 8454 z-index: 99998; 8455 opacity: 0; 8456 visibility: hidden; 8457 transition: all 0.3s ease; 8458} 8459 8460.pca-overlay.active { 8461 opacity: 1; 8462 visibility: visible; 8463} 8464 8465/* ===== Bottom Sheet Container ===== */ 8466.pca-bottom-sheet { 8467 position: fixed; 8468 bottom: 0; 8469 left: 0; 8470 right: 0; 8471 background: #fff; 8472 border-radius: 20px 20px 0 0; 8473 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8474 z-index: 99999; 8475 max-width: 600px; 8476 margin: 0 auto; 8477 transform: translateY(100%); 8478 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8479} 8480 8481.pca-bottom-sheet.active { 8482 transform: translateY(0); 8483} 8484 8485/* Drag Handle */ 8486.pca-sheet-handle { 8487 width: 40px; 8488 height: 4px; 8489 background: #cbd5e1; 8490 border-radius: 999px; 8491 margin: 12px auto 8px; 8492} 8493 8494/* Product Preview */ 8495.pca-sheet-product { 8496 display: flex; 8497 gap: 12px; 8498 padding: 12px 20px; 8499 border-bottom: 1px solid #f1f5f9; 8500} 8501 8502.pca-product-image { 8503 width: 60px; 8504 height: 60px; 8505 border-radius: 8px; 8506 object-fit: cover; 8507 flex-shrink: 0; 8508 border: 1px solid #e2e8f0; 8509} 8510 8511.pca-product-info { 8512 flex: 1; 8513 min-width: 0; 8514} 8515 8516.pca-product-name { 8517 font-size: 14px; 8518 font-weight: 600; 8519 color: #0f172a; 8520 margin: 0 0 4px 0; 8521 display: -webkit-box; 8522 -webkit-line-clamp: 2; 8523 -webkit-box-orient: vertical; 8524 overflow: hidden; 8525 line-height: 1.3; 8526} 8527 8528.pca-product-price { 8529 font-size: 15px; 8530 font-weight: 700; 8531 color: #0ea5e9; 8532 margin: 0; 8533} 8534 8535.pca-product-stock { 8536 font-size: 12px; 8537 color: #64748b; 8538 margin: 2px 0 0 0; 8539} 8540 8541.pca-product-stock.in { 8542 color: #16a34a; 8543} 8544 8545.pca-product-stock.low { 8546 color: #C77DFF; 8547} 8548 8549.pca-product-stock.out { 8550 color: #C77DFF; 8551} 8552 8553/* Action Menu */ 8554.pca-sheet-actions { 8555 padding: 8px 0 20px; 8556} 8557 8558.pca-action-item { 8559 display: flex;
8560 align-items: center; 8561 gap: 16px; 8562 padding: 16px 20px; 8563 background: transparent; 8564 border: 0; 8565 width: 100%; 8566 cursor: pointer; 8567 transition: background-color 0.15s ease; 8568 -webkit-tap-highlight-color: transparent; 8569} 8570 8571.pca-action-item:hover { 8572 background: #f8fafc; 8573} 8574 8575.pca-action-item:active { 8576 background: #f1f5f9; 8577} 8578 8579.pca-action-item:disabled { 8580 opacity: 0.4; 8581 cursor: not-allowed; 8582} 8583 8584.pca-action-item:disabled:hover { 8585 background: transparent; 8586} 8587 8588.pca-action-icon { 8589 width: 24px; 8590 height: 24px; 8591 color: #475569; 8592 flex-shrink: 0; 8593} 8594 8595.pca-action-content { 8596 flex: 1; 8597 text-align: left; 8598} 8599 8600.pca-action-label { 8601 font-size: 15px; 8602 font-weight: 600; 8603 color: #0f172a; 8604 margin: 0 0 2px 0; 8605} 8606 8607.pca-action-desc { 8608 font-size: 12px; 8609 color: #64748b; 8610 margin: 0; 8611} 8612 8613.pca-action-badge { 8614 display: inline-flex; 8615 align-items: center; 8616 justify-content: center; 8617 min-width: 20px; 8618 height: 20px; 8619 padding: 0 6px; 8620 background: #0ea5e9; 8621 color: #fff; 8622 font-size: 11px; 8623 font-weight: 700; 8624 border-radius: 999px; 8625 margin-left: auto; 8626} 8627 8628.pca-action-badge.compare { 8629 background: #8b5cf6; 8630} 8631 8632/* Action Item Colors */ 8633.pca-action-item.cart .pca-action-icon { 8634 color: #0ea5e9; 8635} 8636 8637.pca-action-item.compare .pca-action-icon { 8638 color: #8b5cf6; 8639} 8640 8641.pca-action-item.compare.active { 8642 background: rgba(139, 92, 246, 0.05); 8643} 8644 8645.pca-action-item.compare.active .pca-action-label { 8646 color: #8b5cf6; 8647} 8648 8649.pca-action-item.compare.active .pca-action-icon { 8650 color: #8b5cf6; 8651} 8652 8653.pca-action-item.share .pca-action-icon { 8654 color: #64748b; 8655} 8656 8657/* Loading State */ 8658.pca-action-item.loading { 8659 pointer-events: none; 8660} 8661 8662.pca-action-item.loading .pca-action-icon { 8663 animation: spin 1s linear infinite; 8664} 8665 8666@keyframes spin { 8667 from { transform: rotate(0deg); } 8668 to { transform: rotate(360deg); } 8669} 8670 8671/* Success State Animation */ 8672.pca-action-item.success .pca-action-icon { 8673 animation: checkmark 0.5s ease; 8674} 8675 8676@keyframes checkmark { 8677 0% { transform: scale(0); } 8678 50% { transform: scale(1.2); } 8679 100% { transform: scale(1); } 8680} 8681 8682/* ===== Safe Area for Mobile Devices ===== */ 8683@supports (padding-bottom: env(safe-area-inset-bottom)) { 8684 .pca-sheet-actions { 8685 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8686 } 8687} 8688 8689/* ===== Responsive Tweaks ===== */ 8690@media (max-width: 640px) { 8691 .pca-bottom-sheet { 8692 border-radius: 16px 16px 0 0; 8693 } 8694 8695 .pca-sheet-product { 8696 padding: 10px 16px; 8697 } 8698 8699 .pca-product-image { 8700 width: 50px; 8701 height: 50px; 8702 } 8703 8704 .pca-product-name { 8705 font-size: 13px; 8706 } 8707 8708 .pca-product-price { 8709 font-size: 14px; 8710 } 8711 8712 .pca-action-item { 8713 padding: 14px 16px; 8714 gap: 12px; 8715 } 8716 8717 .pca-action-icon { 8718 width: 22px; 8719 height: 22px; 8720 } 8721 8722 .pca-action-label { 8723 font-size: 14px; 8724 } 8725} 8726 8727/* ===== Animation for Flying Product ===== */ 8728.flying-product { 8729 position: fixed; 8730 pointer-events: none; 8731 z-index: 99999; 8732 animation: fadeOut 0.8s ease forwards; 8733} 8734 8735@keyframes fadeOut { 8736 to { 8737 opacity: 0; 8738 transform: scale(0.5); 8739 } 8740} 8741 8742/* ===== Desktop Dropdown (Alternative) ===== */ 8743@media (min-width: 1024px) { 8744 /* Optional: Could add desktop dropdown menu here */ 8745 /* For now, using bottom sheet for consistency */ 8746} 8747 8748/* ===== Print Styles ===== */ 8749@media print { 8750 .prod-card-actions-btn, 8751 .pca-overlay, 8752 .pca-bottom-sheet { 8753 display: none !important; 8754 } 8755} 8756 8757</style> 8758 <style data-inlined="modal.css"> 8759/* Modern Modal Dialog for Cart Operations */ 8760.modal-overlay { 8761 position: fixed; 8762 top: 0; 8763 left: 0; 8764 right: 0; 8765 bottom: 0; 8766 background: rgba(0, 0, 0, 0.5); 8767 backdrop-filter: blur(4px); 8768 display: flex; 8769 align-items: center; 8770 justify-content: center; 8771 z-index: 9999; 8772 opacity: 0; 8773 visibility: hidden; 8774 transition: all 0.2s ease; 8775} 8776 8777.modal-overlay.show { 8778 opacity: 1;
8779 visibility: visible; 8780} 8781 8782.modal-dialog { 8783 background: white; 8784 border-radius: 16px; 8785 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8786 max-width: 400px; 8787 width: 90%; 8788 max-height: 90vh; 8789 overflow-y: auto; 8790 transform: scale(0.95); 8791 transition: transform 0.2s ease; 8792} 8793 8794.modal-overlay.show .modal-dialog { 8795 transform: scale(1); 8796} 8797 8798.modal-header { 8799 padding: 24px 24px 16px; 8800 border-bottom: 1px solid #f1f5f9; 8801} 8802 8803.modal-title { 8804 font-size: 20px; 8805 font-weight: 700; 8806 color: #0f172a; 8807 margin: 0; 8808 display: flex; 8809 align-items: center; 8810 gap: 12px; 8811} 8812 8813.modal-body { 8814 padding: 16px 24px 24px; 8815} 8816 8817.modal-message { 8818 font-size: 16px; 8819 line-height: 1.5; 8820 color: #475569; 8821 margin: 0; 8822} 8823 8824.modal-actions { 8825 display: flex; 8826 gap: 12px; 8827 margin-top: 24px; 8828} 8829 8830.modal-btn { 8831 flex: 1; 8832 padding: 12px 16px; 8833 border: none; 8834 border-radius: 8px; 8835 font-size: 14px; 8836 font-weight: 600; 8837 cursor: pointer; 8838 transition: all 0.2s ease; 8839 text-align: center; 8840} 8841 8842.modal-btn-secondary { 8843 background: #f8fafc; 8844 color: #64748b; 8845 border: 1px solid #e2e8f0; 8846} 8847 8848.modal-btn-secondary:hover { 8849 background: #f1f5f9; 8850 border-color: #cbd5e1; 8851} 8852 8853.modal-btn-danger { 8854 background: #C77DFF; 8855 color: white; 8856} 8857 8858.modal-btn-danger:hover { 8859 background: #C77DFF; 8860} 8861 8862.modal-btn-primary { 8863 background: #3b82f6; 8864 color: white; 8865} 8866 8867.modal-btn-primary:hover { 8868 background: #2563eb; 8869} 8870 8871.modal-icon { 8872 font-size: 24px; 8873 width: 32px; 8874 height: 32px; 8875 display: flex; 8876 align-items: center; 8877 justify-content: center; 8878 border-radius: 8px; 8879} 8880 8881.modal-icon.danger { 8882 background: #fef2f2; 8883 color: #C77DFF; 8884} 8885 8886.modal-icon.warning { 8887 background: #fffbeb; 8888 color: #C77DFF; 8889} 8890 8891.modal-icon.success { 8892 background: #f0fdf4; 8893 color: #22c55e; 8894} 8895 8896.modal-icon.info { 8897 background: #eff6ff; 8898 color: #3b82f6; 8899} 8900 8901/* Mobile responsive */ 8902@media (max-width: 640px) { 8903 .modal-dialog { 8904 margin: 16px; 8905 max-width: none; 8906 width: auto; 8907 } 8908 8909 .modal-header, 8910 .modal-body { 8911 padding-left: 20px; 8912 padding-right: 20px; 8913 } 8914 8915 .modal-actions { 8916 flex-direction: column; 8917 } 8918 8919 .modal-btn { 8920 flex: none; 8921 } 8922} 8923</style> 8924 8925 <style data-inlined="reviews-points.css"> 8926/** 8927 * Reviews & Points System - Styles 8928 * Product reviews, rating display, and point redemption UI 8929 */ 8930 8931/* =================================== 8932 PRODUCT REVIEWS 8933 =================================== */ 8934 8935.product-tabs { 8936 display: flex; 8937 border-bottom: 2px solid #e5e7eb; 8938 margin: 24px 0 16px; 8939 gap: 8px; 8940} 8941 8942.tab-btn { 8943 padding: 12px 20px; 8944 border: none; 8945 background: none; 8946 cursor: pointer; 8947 font-size: 15px; 8948 font-weight: 500; 8949 color: #6b7280; 8950 border-bottom: 3px solid transparent; 8951 transition: all 0.2s ease; 8952 position: relative; 8953 top: 2px; 8954} 8955 8956.tab-btn:hover { 8957 color: #111827; 8958} 8959 8960.tab-btn.active { 8961 color: #2563eb; 8962 border-bottom-color: #2563eb; 8963} 8964 8965.tab-content { 8966 padding: 20px 0; 8967} 8968 8969/* Review Statistics */ 8970.review-stats { 8971 display: grid; 8972 grid-template-columns: 200px 1fr; 8973 gap: 32px; 8974 padding: 24px; 8975 background: #f9fafb; 8976 border-radius: 12px; 8977 margin-bottom: 24px; 8978} 8979 8980.rating-summary { 8981 text-align: center; 8982} 8983 8984.avg-rating { 8985 display: block; 8986 font-size: 48px; 8987 font-weight: 700; 8988 color: #111827; 8989 line-height: 1; 8990} 8991 8992.rating-summary .stars { 8993 font-size: 24px; 8994 color: #C77DFF; 8995 margin: 8px 0; 8996} 8997 8998.total-reviews { 8999 display: block; 9000 font-size: 14px; 9001 color: #6b7280; 9002 margin-top: 4px; 9003} 9004 9005.rating-breakdown { 9006 display: flex; 9007 flex-direction: column; 9008 gap: 8px; 9009} 9010 9011.rating-bar { 9012 display: flex; 9013 align-items: center; 9014 gap: 12px; 9015} 9016 9017.rating-label { 9018 font-size: 14px; 9019 color: #374151; 9020 width: 32px; 9021 text-align: right; 9022} 9023 9024.bar-container { 9025 flex: 1; 9026 height: 8px; 9027 background: #e5e7eb; 9028 border-radius: 4px; 9029 overflow: hidden; 9030} 9031 9032.bar-fill { 9033 height: 100%; 9034 background: linear-gradient(90deg, #C77DFF 0%, #C77DFF 100%); 9035 transition: width 0.3s ease; 9036} 9037 9038.rating-count { 9039 font-size: 14px; 9040 color: #6b7280; 9041 width: 40px; 9042 text-align: right; 9043} 9044 9045/* Review Filters */ 9046.review-filters { 9047 display: flex; 9048 justify-content: space-between; 9049 align-items: center; 9050 margin-bottom: 20px; 9051} 9052 9053.review-sort { 9054 padding: 8px 12px; 9055 border: 1px solid #d1d5db; 9056 border-radius: 8px; 9057 font-size: 14px; 9058 background: white; 9059} 9060 9061.write-review-btn { 9062 padding: 10px 20px; 9063 background: #2563eb; 9064 color: white; 9065 border: none; 9066 border-radius: 8px; 9067 font-weight: 500;
9068 cursor: pointer; 9069 transition: background 0.2s; 9070} 9071 9072.write-review-btn:hover { 9073 background: #1d4ed8; 9074} 9075 9076/* Review List */ 9077.reviews-list { 9078 display: flex; 9079 flex-direction: column; 9080 gap: 16px; 9081} 9082 9083.review-item { 9084 padding: 20px; 9085 background: white; 9086 border: 1px solid #e5e7eb; 9087 border-radius: 12px; 9088 transition: box-shadow 0.2s; 9089} 9090 9091.review-item:hover { 9092 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9093} 9094 9095.review-header { 9096 display: flex; 9097 justify-content: space-between; 9098 align-items: flex-start; 9099 margin-bottom: 8px; 9100} 9101 9102.review-author { 9103 display: flex; 9104 align-items: center; 9105 gap: 8px; 9106} 9107 9108.review-author strong { 9109 font-size: 15px; 9110 color: #111827; 9111} 9112 9113.verified-badge { 9114 display: inline-flex; 9115 align-items: center; 9116 gap: 4px; 9117 font-size: 12px; 9118 color: #059669; 9119 background: #d1fae5; 9120 padding: 2px 8px; 9121 border-radius: 4px; 9122 font-weight: 500; 9123} 9124 9125.anonymous-badge { 9126 display: inline-flex; 9127 align-items: center; 9128 font-size: 12px; 9129 color: #6b7280; 9130 background: #f3f4f6; 9131 padding: 2px 6px; 9132 border-radius: 4px; 9133 margin-left: 4px; 9134 cursor: help; 9135} 9136 9137.review-rating .stars { 9138 font-size: 16px; 9139 color: #C77DFF; 9140} 9141 9142.review-date { 9143 font-size: 13px; 9144 color: #9ca3af; 9145 margin-bottom: 12px; 9146} 9147 9148.review-text { 9149 font-size: 15px; 9150 line-height: 1.6; 9151 color: #374151; 9152 margin-bottom: 12px; 9153} 9154 9155.review-images { 9156 display: flex; 9157 gap: 8px; 9158 margin-bottom: 12px; 9159 flex-wrap: wrap; 9160} 9161 9162.review-img { 9163 width: 80px; 9164 height: 80px; 9165 object-fit: cover; 9166 border-radius: 8px; 9167 cursor: pointer; 9168 transition: transform 0.2s; 9169} 9170 9171.review-img:hover { 9172 transform: scale(1.05); 9173} 9174 9175.review-actions { 9176 display: flex; 9177 gap: 12px; 9178 padding-top: 12px; 9179 border-top: 1px solid #f3f4f6; 9180} 9181 9182.btn-helpful { 9183 display: flex; 9184 align-items: center; 9185 gap: 6px; 9186 padding: 6px 12px; 9187 background: #f9fafb; 9188 border: 1px solid #e5e7eb; 9189 border-radius: 6px; 9190 font-size: 13px; 9191 color: #6b7280; 9192 cursor: pointer; 9193 transition: all 0.2s; 9194} 9195 9196.btn-helpful:hover { 9197 background: #f3f4f6; 9198 color: #111827; 9199 border-color: #d1d5db; 9200} 9201 9202.btn-helpful.voted { 9203 background: #dbeafe; 9204 color: #1d4ed8; 9205 border-color: #93c5fd; 9206} 9207 9208.helpful-icon { 9209 font-size: 14px; 9210} 9211 9212.no-reviews { 9213 text-align: center; 9214 padding: 60px 20px; 9215 color: #9ca3af; 9216 font-size: 15px; 9217} 9218 9219.load-more-btn { 9220 display: block; 9221 margin: 24px auto 0; 9222 padding: 12px 32px; 9223 background: white; 9224 border: 1px solid #d1d5db; 9225 border-radius: 8px; 9226 font-size: 14px; 9227 font-weight: 500; 9228 color: #374151; 9229 cursor: pointer; 9230 transition: all 0.2s; 9231} 9232 9233.load-more-btn:hover { 9234 background: #f9fafb; 9235 border-color: #9ca3af; 9236} 9237 9238/* =================================== 9239 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9240 =================================== */ 9241 9242.point-balance-card { 9243 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 9244 color: white; 9245 padding: 24px; 9246 border-radius: 16px; 9247 margin-bottom: 24px; 9248 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9249} 9250 9251.point-header { 9252 display: flex; 9253 justify-content: space-between; 9254 align-items: center; 9255 margin-bottom: 12px; 9256} 9257 9258.point-header h3 { 9259 font-size: 18px; 9260 font-weight: 600; 9261 margin: 0; 9262 display: flex; 9263 align-items: center; 9264 gap: 8px; 9265} 9266 9267.point-header .balance { 9268 font-size: 32px; 9269 font-weight: 700; 9270} 9271 9272.point-value { 9273 font-size: 14px; 9274 opacity: 0.9; 9275 margin: 8px 0 16px; 9276} 9277 9278.point-stats { 9279 display: flex; 9280 gap: 20px; 9281 font-size: 13px; 9282 opacity: 0.85; 9283 margin-bottom: 16px; 9284} 9285 9286.btn-view-history { 9287 display: inline-block; 9288 padding: 10px 20px; 9289 background: rgba(255, 255, 255, 0.2); 9290 color: white; 9291 text-decoration: none; 9292 border-radius: 8px; 9293 font-size: 14px; 9294 font-weight: 500; 9295 transition: background 0.2s; 9296} 9297 9298.btn-view-history:hover { 9299 background: rgba(255, 255, 255, 0.3); 9300} 9301 9302/* =================================== 9303 CHECKOUT POINT REDEMPTION 9304 =================================== */ 9305 9306.checkout-points { 9307 padding: 16px; 9308 background: #fef3c7; 9309 border: 2px solid #C77DFF; 9310 border-radius: 12px; 9311 margin-bottom: 16px; 9312} 9313 9314.points-header { 9315 display: flex; 9316 align-items: center; 9317 gap: 10px; 9318 margin-bottom: 12px; 9319} 9320 9321.points-header input[type="checkbox"] { 9322 width: 18px;
9323 height: 18px; 9324 cursor: pointer; 9325} 9326 9327.points-header label { 9328 font-size: 15px; 9329 color: #765212; 9330 cursor: pointer; 9331 margin: 0; 9332} 9333 9334#points-slider { 9335 padding: 16px; 9336 background: white; 9337 border-radius: 8px; 9338 margin-top: 12px; 9339} 9340 9341#points-slider label { 9342 display: block; 9343 font-size: 14px; 9344 color: #374151; 9345 margin-bottom: 8px; 9346 font-weight: 500; 9347} 9348 9349#points-amount { 9350 width: 100%; 9351 margin-bottom: 8px; 9352} 9353 9354#points-display { 9355 display: flex; 9356 justify-content: space-between; 9357 font-size: 14px; 9358 color: #111827; 9359 margin-top: 8px; 9360} 9361 9362#points-display .discount-amount { 9363 color: #059669; 9364 font-weight: 600; 9365} 9366 9367.price-row.point-discount { 9368 color: #059669; 9369 font-weight: 500; 9370} 9371 9372/* =================================== 9373 REVIEWABLE ORDERS 9374 =================================== */ 9375 9376.reviewable-orders { 9377 background: white; 9378 border: 1px solid #e5e7eb; 9379 border-radius: 12px; 9380 padding: 20px; 9381 margin-bottom: 24px; 9382} 9383 9384.reviewable-orders h3 { 9385 font-size: 18px; 9386 margin-bottom: 16px; 9387 color: #111827; 9388} 9389 9390.order-card { 9391 display: flex; 9392 gap: 16px; 9393 padding: 16px; 9394 background: #f9fafb; 9395 border-radius: 8px; 9396 margin-bottom: 12px; 9397} 9398 9399.order-card:last-child { 9400 margin-bottom: 0; 9401} 9402 9403.order-card img { 9404 width: 80px; 9405 height: 80px; 9406 object-fit: cover; 9407 border-radius: 8px; 9408} 9409 9410.order-info { 9411 flex: 1; 9412} 9413 9414.order-info h4 { 9415 font-size: 15px; 9416 margin: 0 0 4px; 9417 color: #111827; 9418} 9419 9420.order-info p { 9421 font-size: 13px; 9422 color: #6b7280; 9423 margin: 0 0 12px; 9424} 9425 9426.btn-review { 9427 padding: 8px 16px; 9428 background: #059669; 9429 color: white; 9430 border: none; 9431 border-radius: 6px; 9432 font-size: 14px; 9433 font-weight: 500; 9434 cursor: pointer; 9435 transition: background 0.2s; 9436} 9437 9438.btn-review:hover { 9439 background: #047857; 9440} 9441 9442/* =================================== 9443 TOAST NOTIFICATIONS 9444 =================================== */ 9445 9446.review-toast { 9447 position: fixed; 9448 bottom: 24px; 9449 right: 24px; 9450 padding: 14px 20px; 9451 background: #111827; 9452 color: white; 9453 border-radius: 8px; 9454 font-size: 14px; 9455 line-height: 1.4; 9456 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9457 transform: translateY(100px); 9458 opacity: 0; 9459 transition: all 0.3s ease; 9460 z-index: 10000; 9461 max-width: 400px; 9462 word-wrap: break-word; 9463} 9464 9465.review-toast.show { 9466 transform: translateY(0); 9467 opacity: 1; 9468} 9469 9470/* Mobile: Position above bottom-nav */ 9471@media (max-width: 768px) { 9472 .review-toast { 9473 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9474 right: 16px; 9475 left: 16px; 9476 max-width: calc(100% - 32px); 9477 } 9478} 9479 9480.review-toast.toast-success { 9481 background: #059669; 9482} 9483 9484.review-toast.toast-error { 9485 background: #C77DFF; 9486} 9487 9488.review-toast.toast-info { 9489 background: #2563eb; 9490} 9491 9492/* =================================== 9493 RESPONSIVE 9494 =================================== */ 9495 9496@media (max-width: 768px) { 9497 .point-balance-card { 9498 border-radius: 0; 9499 } 9500 9501 .review-stats { 9502 grid-template-columns: 70px 1fr; 9503 gap: 20px; 9504 padding: 16px; 9505 } 9506 9507 .rating-summary { 9508 text-align: center; 9509 } 9510 9511 .rating-summary .avg-rating { 9512 font-size: 32px; 9513 } 9514 9515 .rating-summary .stars { 9516 font-size: 18px; 9517 margin: 4px 0; 9518 } 9519 9520 .total-reviews { 9521 font-size: 11px; 9522 } 9523 9524 /* Rating breakdown - more compact */ 9525 .rating-breakdown { 9526 gap: 2px; 9527 } 9528 9529 .rating-breakdown .rating-bar { 9530 gap: 4px; 9531 } 9532 9533 .rating-breakdown .rating-label { 9534 font-size: 11px; 9535 width: 16px; 9536 min-width: 16px; 9537 text-align: left; 9538 } 9539 9540 .rating-breakdown .bar-container { 9541 height: 6px; 9542 } 9543 9544 .rating-breakdown .rating-count { 9545 font-size: 11px; 9546 width: 16px; 9547 min-width: 16px; 9548 } 9549 9550 .review-header { 9551 flex-direction: column; 9552 gap: 8px; 9553 align-items: flex-start; 9554 } 9555 9556 .review-rating { 9557 order: -1; 9558 margin-bottom: 4px; 9559 } 9560 9561 .review-rating .stars { 9562 font-size: 14px; 9563 } 9564 9565 .review-filters { 9566 flex-direction: column; 9567 gap: 12px;
9568 align-items: stretch; 9569 } 9570 9571 .review-sort { 9572 width: 100%; 9573 } 9574 9575 .point-header .balance { 9576 font-size: 24px; 9577 } 9578 9579 .point-stats { 9580 flex-direction: column; 9581 gap: 8px; 9582 } 9583 9584 .order-card { 9585 flex-direction: column; 9586 } 9587 9588 .order-card img { 9589 width: 100%; 9590 height: 200px; 9591 } 9592} 9593 9594/* =================================== 9595 LOADING & ERROR STATES 9596 =================================== */ 9597 9598.review-loading { 9599 text-align: center; 9600 padding: 40px; 9601 color: #9ca3af; 9602 font-size: 14px; 9603} 9604 9605.review-error { 9606 text-align: center; 9607 padding: 40px; 9608 color: #C77DFF; 9609 font-size: 14px; 9610 background: #fee2e2; 9611 border-radius: 8px; 9612} 9613 9614.review-empty { 9615 text-align: center; 9616 padding: 10px; 9617 color: #6b7280; 9618 font-size: 14px; 9619} 9620 9621/* =================================== 9622 REVIEWABLE ORDERS LIST 9623 =================================== */ 9624 9625.reviewable-orders-list { 9626 display: flex; 9627 flex-direction: column; 9628 gap: 16px; 9629} 9630 9631.reviewable-order-item { 9632 display: grid; 9633 grid-template-columns: 80px 1fr auto; 9634 gap: 16px; 9635 padding: 16px; 9636 background: #fff; 9637 border: 1px solid #e5e7eb; 9638 border-radius: 12px; 9639 align-items: center; 9640 transition: box-shadow 0.2s; 9641} 9642 9643.reviewable-order-item:hover { 9644 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9645} 9646 9647.reviewable-order-item .roi-image { 9648 width: 80px; 9649 height: 80px; 9650 object-fit: cover; 9651 border-radius: 8px; 9652 border: 1px solid #e5e7eb; 9653} 9654 9655.reviewable-order-item .roi-info { 9656 display: flex; 9657 flex-direction: column; 9658 gap: 4px; 9659} 9660 9661.reviewable-order-item .roi-info h4 { 9662 margin: 0; 9663 font-size: 15px; 9664 font-weight: 600; 9665 color: #111827; 9666 line-height: 1.4; 9667} 9668 9669.reviewable-order-item .roi-meta { 9670 margin: 0; 9671 font-size: 13px; 9672 color: #6b7280; 9673} 9674 9675.reviewable-order-item .btn-write-review { 9676 white-space: nowrap; 9677 padding: 10px 20px; 9678 font-size: 14px; 9679} 9680 9681@media (max-width: 640px) { 9682 .reviewable-order-item { 9683 grid-template-columns: 60px 1fr; 9684 gap: 12px; 9685 } 9686 9687 .reviewable-order-item .roi-image { 9688 width: 60px; 9689 height: 60px; 9690 } 9691 9692 .reviewable-order-item .btn-write-review { 9693 grid-column: 1 / -1; 9694 width: 100%; 9695 } 9696} 9697 9698/* =================================== 9699 REVIEW SUBMISSION MODAL 9700 =================================== */ 9701 9702/* Modal Overlay Background */ 9703#review-modal-overlay { 9704 position: fixed; 9705 top: 0; 9706 left: 0; 9707 right: 0; 9708 bottom: 0; 9709 background: rgba(0, 0, 0, 0.6) !important; 9710 backdrop-filter: blur(8px); 9711 display: flex; 9712 align-items: center; 9713 justify-content: center; 9714 z-index: 99999 !important; 9715 padding: 20px; 9716} 9717 9718.review-modal { 9719 max-width: 600px !important; 9720 width: 90% !important; 9721 background: white; 9722 border-radius: 16px !important; 9723 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9724 max-height: 90vh; 9725 display: flex; 9726 flex-direction: column; 9727 overflow: hidden !important; 9728} 9729 9730#review-modal-overlay .modal-content, 9731.review-modal .modal-content { 9732 padding: 0 !important; 9733 flex: initial !important; 9734 overflow-y: visible !important; 9735} 9736 9737.review-modal .modal-header { 9738 display: flex; 9739 justify-content: space-between; 9740 align-items: center; 9741 padding: 20px 24px; 9742 border-bottom: 1px solid #e5e7eb; 9743} 9744 9745.review-modal .modal-title { 9746 margin: 0; 9747 font-size: 20px; 9748 font-weight: 700; 9749 color: #111827; 9750} 9751 9752.review-modal .modal-close { 9753 background: none; 9754 border: none; 9755 cursor: pointer; 9756 padding: 4px; 9757 color: #6b7280; 9758 transition: color 0.2s; 9759 display: flex; 9760 align-items: center; 9761 justify-content: center; 9762} 9763
9764.review-modal .modal-close:hover { 9765 color: #111827; 9766} 9767 9768.review-modal .modal-body { 9769 padding: 24px; 9770 max-height: calc(90vh - 140px); 9771 overflow-y: auto; 9772} 9773 9774/* Custom Scrollbar for Modal */ 9775.review-modal .modal-body::-webkit-scrollbar { 9776 width: 8px; 9777} 9778 9779.review-modal .modal-body::-webkit-scrollbar-track { 9780 background: #f1f5f9; 9781 border-radius: 4px; 9782} 9783 9784.review-modal .modal-body::-webkit-scrollbar-thumb { 9785 background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); 9786 border-radius: 4px; 9787} 9788 9789.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9790 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9791} 9792 9793/* Firefox Scrollbar */ 9794.review-modal .modal-body { 9795 scrollbar-width: thin; 9796 scrollbar-color: #3b82f6 #f1f5f9; 9797} 9798 9799.review-modal .modal-footer { 9800 display: flex; 9801 gap: 12px; 9802 margin-top: 24px; 9803 padding-top: 20px; 9804 border-top: 1px solid #f1f5f9; 9805} 9806 9807.review-product-info { 9808 display: flex; 9809 gap: 16px; 9810 padding: 16px; 9811 background: #f9fafb; 9812 border-radius: 8px; 9813 margin-bottom: 24px; 9814} 9815 9816.review-product-thumb { 9817 width: 80px; 9818 height: 80px; 9819 object-fit: cover; 9820 border-radius: 6px; 9821 border: 1px solid #e5e7eb; 9822} 9823 9824.review-product-details h4 { 9825 margin: 0 0 8px 0; 9826 font-size: 16px; 9827 font-weight: 600; 9828 color: #111827; 9829} 9830 9831.review-product-details p { 9832 margin: 0; 9833 font-size: 13px; 9834 color: #6b7280; 9835} 9836 9837/* Star Rating Input */ 9838.star-rating-input { 9839 display: inline-flex; 9840 gap: 8px; 9841 margin-right: 12px; 9842 vertical-align: middle; 9843} 9844 9845.star-btn { 9846 background: none; 9847 border: none; 9848 font-size: 32px; 9849 color: #d1d5db; 9850 cursor: pointer; 9851 transition: all 0.2s; 9852 padding: 0; 9853 line-height: 1; 9854} 9855 9856.star-btn:hover, 9857.star-btn.active { 9858 color: #C77DFF; 9859 transform: scale(1.1); 9860} 9861 9862.rating-label { 9863 display: inline-block; 9864 font-size: 15px; 9865 color: #6b7280; 9866 margin: 0; 9867 vertical-align: middle; 9868 min-width: 150px; 9869} 9870 9871/* Form Elements */ 9872.review-form .form-group { 9873 margin-bottom: 20px; 9874} 9875 9876.review-form .form-label { 9877 display: block; 9878 font-size: 14px; 9879 font-weight: 600; 9880 color: #374151; 9881 margin-bottom: 8px; 9882} 9883 9884.review-form .form-control { 9885 width: 100%; 9886 padding: 12px; 9887 border: 1px solid #d1d5db; 9888 border-radius: 8px; 9889 font-size: 14px; 9890 font-family: inherit; 9891 transition: border-color 0.2s; 9892} 9893 9894.review-form .form-control:focus { 9895 outline: none; 9896 border-color: #2563eb; 9897 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 9898} 9899 9900.review-form .form-hint { 9901 font-size: 13px; 9902 color: #6b7280; 9903 margin-top: 8px; 9904} 9905 9906/* Image Upload */ 9907.btn-upload-trigger { 9908 display: inline-flex; 9909 align-items: center; 9910 gap: 8px; 9911 padding: 10px 16px; 9912 background: #f9fafb; 9913 border: 2px dashed #d1d5db; 9914 border-radius: 8px; 9915 color: #374151; 9916 cursor: pointer; 9917 font-size: 14px; 9918 font-weight: 500; 9919 transition: all 0.2s; 9920} 9921 9922.btn-upload-trigger:hover { 9923 background: #f3f4f6; 9924 border-color: #2563eb; 9925 color: #2563eb; 9926} 9927 9928.image-preview-grid { 9929 display: grid; 9930 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9931 gap: 12px; 9932 margin-top: 16px; 9933} 9934 9935.image-preview-item { 9936 position: relative; 9937 aspect-ratio: 1; 9938 border-radius: 8px; 9939 overflow: hidden; 9940 border: 1px solid #e5e7eb; 9941} 9942 9943.image-preview-item img { 9944 width: 100%; 9945 height: 100%; 9946 object-fit: cover; 9947} 9948 9949.remove-image { 9950 position: absolute; 9951 top: 4px; 9952 right: 4px; 9953 background: rgba(0, 0, 0, 0.7); 9954 border: none; 9955 color: white; 9956 width: 24px; 9957 height: 24px; 9958 border-radius: 50%; 9959 cursor: pointer; 9960 display: flex; 9961 align-items: center; 9962 justify-content: center; 9963 transition: background 0.2s; 9964} 9965 9966.remove-image:hover { 9967 background: rgba(0, 0, 0, 0.9); 9968} 9969 9970/* Point Reward Info */ 9971.point-reward-info { 9972 background: linear-gradient(135deg, #fef3c7 0%, #C77DFF 100%); 9973 border: 2px solid #C77DFF; 9974 border-radius: 12px; 9975 padding: 16px; 9976 margin: 20px 0; 9977} 9978 9979.pri-header { 9980 display: flex; 9981 align-items: center; 9982 gap: 8px; 9983 font-size: 16px; 9984 font-weight: 700;
9985 color: #ae7e21; 9986 margin-bottom: 12px; 9987} 9988 9989.pri-list { 9990 list-style: none; 9991 padding: 0; 9992 margin: 0 0 12px 0; 9993} 9994 9995.pri-list li { 9996 padding: 6px 0; 9997 font-size: 14px; 9998 color: #765212; 9999 transition: opacity 0.2s; 10000} 10001 10002.pri-total { 10003 padding-top: 12px; 10004 border-top: 2px solid rgba(245,202,86,0.3); 10005 font-size: 16px; 10006 color: #ae7e21; 10007 font-weight: 600; 10008} 10009 10010.form-error { 10011 background: #fee2e2; 10012 border: 1px solid #C77DFF; 10013 color: #E3D05A; 10014 padding: 12px; 10015 border-radius: 8px; 10016 font-size: 14px; 10017 margin-top: 16px; 10018} 10019 10020/* Buttons */ 10021.review-form .btn { 10022 padding: 12px 24px; 10023 border: none; 10024 border-radius: 8px; 10025 font-size: 15px; 10026 font-weight: 600; 10027 cursor: pointer; 10028 transition: all 0.2s; 10029} 10030 10031.review-form .btn-primary { 10032 background: #2563eb; 10033 color: white; 10034} 10035 10036.review-form .btn-primary:hover { 10037 background: #1d4ed8; 10038} 10039 10040.review-form .btn-primary:disabled { 10041 background: #94a3b8; 10042 cursor: not-allowed; 10043} 10044 10045.review-form .btn-secondary { 10046 background: #f1f5f9; 10047 color: #64748b; 10048} 10049 10050.review-form .btn-secondary:hover { 10051 background: #e2e8f0; 10052 color: #475569; 10053} 10054 10055@media (max-width: 768px) { 10056 #review-modal-overlay { padding: 0; } 10057 .review-modal { max-width: 100%; width: 100% !important; } 10058} 10059 10060/* =================================== 10061 POINT SUMMARY GRID (Profile Page) 10062 =================================== */ 10063 10064.point-summary-grid { 10065 display: grid; 10066 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 10067 gap: 12px; 10068 margin-bottom: 32px; 10069} 10070 10071.point-summary-card { 10072 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 10073 border: 1px solid #e5e7eb; 10074 border-radius: 12px; 10075 padding: 16px 20px; 10076 display: flex; 10077 align-items: center; 10078 justify-content: space-between; 10079 gap: 12px; 10080 transition: all 0.2s; 10081} 10082 10083.point-summary-card:hover { 10084 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10085 transform: translateY(-2px); 10086} 10087 10088.point-summary-card .psc-left { 10089 display: flex; 10090 flex-direction: column; 10091 gap: 2px; 10092} 10093 10094.point-summary-card .psc-icon { 10095 font-size: 24px; 10096 margin-bottom: 4px; 10097} 10098 10099.point-summary-card .psc-label { 10100 font-size: 13px; 10101 color: #6b7280; 10102 font-weight: 500; 10103 text-transform: uppercase; 10104 letter-spacing: 0.5px; 10105} 10106 10107.point-summary-card .psc-value { 10108 font-size: 26px; 10109 font-weight: 700; 10110 color: #111827; 10111 flex-shrink: 0; 10112} 10113 10114/* Point History Section */ 10115.point-history-section { 10116 margin-top: 32px; 10117} 10118 10119.point-history-section h3 { 10120 font-size: 18px; 10121 font-weight: 600; 10122 color: #111827; 10123 margin-bottom: 16px; 10124} 10125 10126.point-history-list { 10127 display: flex; 10128 flex-direction: column; 10129 gap: 12px; 10130} 10131 10132.point-history-item { 10133 display: grid; 10134 grid-template-columns: 40px 1fr auto; 10135 gap: 16px; 10136 padding: 16px; 10137 background: white; 10138 border: 1px solid #e5e7eb; 10139 border-radius: 10px; 10140 align-items: center; 10141 transition: all 0.2s; 10142} 10143 10144.point-history-item:hover { 10145 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10146} 10147 10148.point-history-item.credit { 10149 border-left: 3px solid #10b981; 10150} 10151 10152.point-history-item.debit { 10153 border-left: 3px solid #C77DFF; 10154} 10155 10156.point-history-item .phi-icon { 10157 width: 40px; 10158 height: 40px; 10159 display: flex; 10160 align-items: center; 10161 justify-content: center; 10162 font-size: 20px; 10163 border-radius: 50%; 10164 background: #f9fafb; 10165} 10166 10167.point-history-item.credit .phi-icon { 10168 background: #d1fae5; 10169} 10170 10171.point-history-item.debit .phi-icon { 10172 background: #fef3c7; 10173} 10174
10175.point-history-item .phi-details { 10176 display: flex; 10177 flex-direction: column; 10178 gap: 4px; 10179} 10180 10181.point-history-item .phi-desc { 10182 font-size: 15px; 10183 font-weight: 500; 10184 color: #111827; 10185} 10186 10187.point-history-item .phi-date { 10188 font-size: 13px; 10189 color: #9ca3af; 10190} 10191 10192.point-history-item .phi-amount { 10193 font-size: 18px; 10194 font-weight: 700; 10195} 10196 10197.point-history-item .phi-amount.credit { 10198 color: #10b981; 10199} 10200 10201.point-history-item .phi-amount.debit { 10202 color: #C77DFF; 10203} 10204 10205@media (max-width: 640px) { 10206 .point-summary-grid { 10207 grid-template-columns: 1fr; 10208 } 10209 10210 .point-summary-card .psc-value { 10211 font-size: 22px; 10212 } 10213 10214 .point-history-item { 10215 grid-template-columns: 36px 1fr; 10216 gap: 12px; 10217 } 10218 10219 .point-history-item .phi-amount { 10220 grid-column: 2; 10221 text-align: right; 10222 margin-top: 8px; 10223 } 10224} 10225 10226/* ââ Pending points: compact widget indicator ââ */ 10227.pbc-pending { 10228 font-size: 12px; 10229 opacity: 0.85; 10230 margin-top: 4px; 10231 color: #C77DFF; 10232} 10233 10234/* ââ Pending points: summary card ââ */ 10235.point-summary-card.psc-pending-card { 10236 background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); 10237 border-color: #C77DFF; 10238} 10239 10240.point-summary-card.psc-pending-card .psc-value { 10241 color: #daa837; 10242} 10243 10244.point-summary-card .psc-sub { 10245 font-size: 11px; 10246 color: #9ca3af; 10247 margin-top: 4px; 10248} 10249 10250/* ââ Pending points: history item ââ */ 10251.point-history-item.phi-pending { 10252 opacity: 0.75; 10253 border-left-color: #C77DFF; 10254 background: #fffbeb; 10255} 10256 10257.point-history-item.phi-pending .phi-icon { 10258 background: #fef3c7; 10259} 10260 10261.phi-pending-badge { 10262 display: inline-block; 10263 font-size: 11px; 10264 font-weight: 600; 10265 padding: 2px 7px; 10266 border-radius: 10px; 10267 background: #fef3c7; 10268 color: #ae7e21; 10269 border: 1px solid #C77DFF; 10270 margin-left: 8px; 10271 vertical-align: middle; 10272} 10273 10274/* ââ Reversed points: history item ââ */ 10275.point-history-item.phi-reversed { 10276 opacity: 0.55; 10277 border-left-color: #d1d5db; 10278 background: #f9fafb; 10279} 10280 10281.point-history-item.phi-reversed .phi-icon { 10282 background: #f3f4f6; 10283} 10284 10285.point-history-item.phi-reversed .phi-amount { 10286 text-decoration: line-through; 10287 color: #9ca3af !important; 10288} 10289 10290.phi-reversed-badge { 10291 display: inline-block; 10292 font-size: 11px; 10293 font-weight: 600; 10294 padding: 2px 7px; 10295 border-radius: 10px; 10296 background: #fee2e2; 10297 color: #E3D05A; 10298 border: 1px solid #C77DFF; 10299 margin-left: 8px; 10300 vertical-align: middle; 10301} 10302 10303</style> 10304 10305</head> 10306<body> 10307 10308<header class="topbar"> 10309 <div class="container topbar-inner"> 10310 <div class="topbar-left"> 10311 JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik 10312 </div> 10313 <div class="topbar-right"> 10314 <a href="https://hop.exchange/">Apk Slot1000</a> 10315 <a href="https://hop.exchange/">Slot1000</a> 10316 <a href="https://hop.exchange/">Idn Slot777</a> 10317 <a href="https://hop.exchange/">Game Slot1000</a> 10318 </div> 10319 </div> 10320</header> 10321<nav class="navbar"> 10322 <nav class="navbar-logo" style="width: 100%;"> 10323 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10324 <a href="https://hop.exchange/" class="logo-mobile" title="Logo APK Slot1000"> 10325 <img src="https://img.tongat.cc/uploads/nick-judimpo.png" alt="Logo APK Slot1000" height="40"> 10326 </a> 10327 </div> 10328</nav> 10329 <div class="container nav-inner"> 10330 <a href="https://hop.exchange/" class="logo" title="Logo APK Slot1000"> 10331 <img src="https://img.tongat.cc/uploads/nick-judimpo.png" alt="Logo APK Slot1000" height="40"> 10332 </a> 10333 10334 10335 <form class="search" method="get" action="#"> 10336 <input type="hidden" name="page" value="produk"> 10337 10338 <!-- Search Icon (Desktop) --> 10339 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10340 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10341 <circle cx="11" cy="11" r="8"/> 10342 <path d="m21 21-4.35-4.35"/> 10343 </svg> 10344 </button> 10345 10346 <input type="text" name="q" id="searchInput" placeholder="PENCARIAN GAME MPO" 10347 value="" autocomplete="off"> 10348 10349 <div class="search-suggest" id="searchSuggest" hidden></div> 10350 </form> 10351 10352 <div class="nav-actions"> 10353 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10354 <button class="icon-btn cart-indicator" id="cartBtn" title="Keranjang"> 10355 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10356 </button> 10357 10358 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10359 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10360 </button> 10361
10361<script src="/https://www.wood.com/etc.clientlibs/samsung/clientlibs/consumer/global/clientlib-common/global/js/crypto-js.min.6fa2276cf659f30cabde72a6fc452183.js"></script>
10361 10362 <!-- Dynamic login/logout button --> 10363 <button class="login-btn" id="loginBtn" aria-label="Masuk"> 10364 <span class="login-text">Masuk</span> 10365 <span class="login-ico" aria-hidden="true">ð</span> 10366 </button> 10367 </div> 10368 </div> 10369 10370 <div class="container" style="display: flex; align-items: center;"> 10371 10372 10373 <div class="mega-cats-wrapper"> 10374 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10375 <a class="" href="https://hop.exchange/"> 10376 PLATFORM SLOT1000</a> 10377 <a class="" href="https://hop.exchange/"> 10378 SERVER JEPANG 2026</a> 10379 <a class="" href="https://hop.exchange/"> 10380 LOGIN SLOT1000</a> 10381 <a class="" href="https://hop.exchange/"> 10382 AGEN MPO 1000</a> 10383 <a class="" href="https://hop.exchange/"> 10384 MPO1000</a> 10385 <a class="" href="https://hop.exchange/"> 10386 APK SLOT777 SERVER JEPANG</a> 10387 </div> 10388 <div class="mega-cats-fade" id="megaCatsFade"></div> 10389 </div> 10390 </div> 10391</nav> 10392 10393<!-- Panel Mega Menu - Outside navbar for full-screen --> 10394<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10395 <div class="mm2-view is-active" data-mm2="top"></div> 10396 <div class="mm2-view" data-mm2="detail"></div> 10397</div> 10398 10399<!-- Backdrop - Outside navbar for full-screen --> 10400<div class="mm2-backdrop" data-mm2="backdrop"></div> 10401 10402<!-- Mobile Bottom Navigation --> 10403<nav class="bottom-nav"> 10404 <div class="bottom-nav-inner"> 10405 <!-- Home --> 10406 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10407 <div class="bottom-nav-item-icon"> 10408 <!-- Outlined icon (default) --> 10409 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10410 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10411 <polyline points="9 22 9 12 15 12 15 22"/> 10412 </svg> 10413 <!-- Filled icon (active) --> 10414 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10415 <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"/> 10416 <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"/> 10417 </svg> 10418 </div> 10419 <span class="bottom-nav-item-label">Home</span> 10420 </a> 10421 10422 <!-- Kategori --> 10423 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10424 <div class="bottom-nav-item-icon"> 10425 <!-- Outlined icon (default) --> 10426 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10427 <line x1="3" y1="12" x2="21" y2="12"/> 10428 <line x1="3" y1="6" x2="21" y2="6"/> 10429 <line x1="3" y1="18" x2="21" y2="18"/> 10430 </svg> 10431 <!-- Filled icon (active) --> 10432 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10433 <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"/> 10434 </svg> 10435 </div> 10436 <span class="bottom-nav-item-label">Kategori</span> 10437 </button> 10438 10439 <!-- Keranjang --> 10440 <button class="bottom-nav-item" id="bottomCartBtn"> 10441 <div class="bottom-nav-item-icon"> 10442 <!-- Outlined icon (default) --> 10443 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10444 <circle cx="9" cy="21" r="1"/> 10445 <circle cx="20" cy="21" r="1"/> 10446 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10447 </svg> 10448 <!-- Filled icon (active) -->
10449 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10450 <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"/> 10451 </svg> 10452 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10453 </div> 10454 <span class="bottom-nav-item-label">Keranjang</span> 10455 </button> 10456 10457 <!-- Compare --> 10458 <button class="bottom-nav-item" id="bottomCompareBtn"> 10459 <div class="bottom-nav-item-icon"> 10460 <!-- Outlined icon (default) --> 10461 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10462 <polyline points="17 11 21 7 17 3"/> 10463 <path d="M21 7H9"/> 10464 <polyline points="7 21 3 17 7 13"/> 10465 <path d="M15 17H3"/> 10466 </svg> 10467 <!-- Filled icon (active) --> 10468 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10469 <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"/> 10470 </svg> 10471 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10472 </div> 10473 <span class="bottom-nav-item-label">Compare</span> 10474 </button> 10475 10476 <!-- Chat - Always show, check login in JavaScript --> 10477 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10478 <div class="bottom-nav-item-icon"> 10479 <!-- Outlined icon (default) --> 10480 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10481 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10482 </svg> 10483 <!-- Filled icon (active) --> 10484 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10485 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10486 </svg> 10487 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10488 </div> 10489 <span class="bottom-nav-item-label">Chat</span> 10490 </button> 10491 10492 <!-- Akun --> 10493 <button class="bottom-nav-item" id="bottomLoginBtn"> 10494 <div class="bottom-nav-item-icon"> 10495 <!-- Outlined icon (default) --> 10496 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10497 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10498 <circle cx="12" cy="7" r="4"/> 10499 </svg> 10500 <!-- Filled icon (active) --> 10501 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10502 <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"/> 10503 </svg> 10504 </div> 10505 <span class="bottom-nav-item-label">Login</span> 10506 </button> 10507 </div> 10508</nav> 10509 10510<!-- Mobile User Menu Bottom Sheet --> 10511
10512<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10513(function(){ 10514 const btn = document.getElementById('megaMoreBtn'); 10515 const dd = document.getElementById('megaMoreDropdown'); 10516 if(!btn || !dd) return; 10517 10518 function openDD(){ 10519 dd.hidden = false; 10520 btn.setAttribute('aria-expanded','true'); 10521 document.addEventListener('click', outside, {capture:true}); 10522 document.addEventListener('keydown', esc); 10523 } 10524 function closeDD(){ 10525 if(dd.hidden) return; 10526 dd.hidden = true; 10527 btn.setAttribute('aria-expanded','false'); 10528 document.removeEventListener('click', outside, {capture:true}); 10529 document.removeEventListener('keydown', esc); 10530 } 10531 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10532 function outside(e){ 10533 if(!dd.contains(e.target) && e.target !== btn){ 10534 closeDD(); 10535 } 10536 } 10537 function esc(e){ 10538 if(e.key === 'Escape'){ 10539 closeDD(); 10540 btn.focus(); 10541 } 10542 } 10543 btn.addEventListener('click', toggle); 10544})(); 10545 10546// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10547(function(){ 10548 const megaCats = document.getElementById('megaCatsScroll'); 10549 const fade = document.getElementById('megaCatsFade'); 10550 if (!megaCats || !fade) return; 10551 10552 function updateFade() { 10553 const scrollLeft = megaCats.scrollLeft; 10554 const scrollWidth = megaCats.scrollWidth; 10555 const clientWidth = megaCats.clientWidth; 10556 const maxScroll = scrollWidth - clientWidth; 10557 10558 // Hide fade when scrolled to end (within 10px tolerance) 10559 if (maxScroll - scrollLeft <= 10) { 10560 fade.style.opacity = '0'; 10561 } else { 10562 fade.style.opacity = '1'; 10563 } 10564 } 10565 10566 // Update on scroll 10567 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10568 10569 // Update on resize 10570 window.addEventListener('resize', updateFade, { passive: true }); 10571 10572 // Initial check 10573 setTimeout(updateFade, 100); 10574})(); 10575 10576document.addEventListener('DOMContentLoaded', function () { 10577 var input = document.getElementById('searchInput'); 10578 if (!input) return; 10579 input.addEventListener('keydown', function (e) { 10580 if (e.key === 'Enter') { 10581 e.preventDefault(); 10582 var form = input.form; 10583 if (!form) return; 10584 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10585 else form.submit(); 10586 } 10587 }); 10588}); 10589 10590 10591(function(){ 10592 const cartBtn = document.getElementById('cartBtn'); 10593 if (cartBtn) { 10594 cartBtn.addEventListener('click', function(e) { 10595 e.preventDefault(); 10596 10597 const checkLogin = () => { 10598 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10599 }; 10600 10601 if (checkLogin()) { 10602 window.location.href = '/keranjang'; 10603 } else { 10604 showCartLoginModal(); 10605 } 10606 }); 10607 } 10608 10609 const loginBtn = document.getElementById('loginBtn'); 10610 if (loginBtn) { 10611 loginBtn.addEventListener('click', function(e) { 10612 e.preventDefault(); 10613 window.location.href = 'https://hop.exchange/'; 10614 }); 10615 } 10616 10617 const userMenuBtn = document.getElementById('userMenuBtn'); 10618 const userDropdown = document.getElementById('userDropdown'); 10619 10620 if (userMenuBtn && userDropdown) { 10621 function openDropdown() { 10622 userDropdown.hidden = false; 10623 userMenuBtn.setAttribute('aria-expanded', 'true'); 10624 document.addEventListener('click', closeOnOutside, { capture: true }); 10625 document.addEventListener('keydown', closeOnEscape); 10626 } 10627 10628 function closeDropdown() { 10629 if (userDropdown.hidden) return; 10630 userDropdown.hidden = true; 10631 userMenuBtn.setAttribute('aria-expanded', 'false'); 10632 document.removeEventListener('click', closeOnOutside, { capture: true }); 10633 document.removeEventListener('keydown', closeOnEscape); 10634 } 10635 10636 function closeOnOutside(e) { 10637 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10638 closeDropdown(); 10639 } 10640 } 10641 10642 function closeOnEscape(e) { 10643 if (e.key === 'Escape') { 10644 closeDropdown(); 10645 userMenuBtn.focus(); 10646 } 10647 } 10648 10649 userMenuBtn.addEventListener('click', function(e) { 10650 e.preventDefault(); 10651 userDropdown.hidden ? openDropdown() : closeDropdown(); 10652 });
10653 10654 // Close dropdown when clicking profile links 10655 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10656 link.addEventListener('click', function() { 10657 closeDropdown(); 10658 }); 10659 }); 10660 } 10661 10662 // Bottom nav profile button - toggle mobile bottom sheet 10663 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10664 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10665 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10666 10667 if (bottomProfileBtn && mobileUserSheet) { 10668 function openMobileSheet() { 10669 mobileUserSheet.hidden = false; 10670 document.body.style.overflow = 'hidden'; 10671 // Trigger animation 10672 requestAnimationFrame(() => { 10673 mobileUserSheet.classList.add('active'); 10674 }); 10675 } 10676 10677 function closeMobileSheet() { 10678 mobileUserSheet.classList.remove('active'); 10679 document.body.style.overflow = ''; 10680 setTimeout(() => { 10681 mobileUserSheet.hidden = true; 10682 }, 300); 10683 } 10684 10685 bottomProfileBtn.addEventListener('click', function(e) { 10686 e.preventDefault(); 10687 openMobileSheet(); 10688 }); 10689 10690 if (mobileUserSheetBackdrop) { 10691 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10692 } 10693 10694 // Close on escape key 10695 document.addEventListener('keydown', function(e) { 10696 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10697 closeMobileSheet(); 10698 } 10699 }); 10700 10701 // Close sheet when clicking profile links 10702 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10703 link.addEventListener('click', function() { 10704 closeMobileSheet(); 10705 }); 10706 }); 10707 } 10708 10709 // Handle Google avatar error in bottom-nav (fallback to initials) 10710 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10711 if (bottomNavAvatar) { 10712 bottomNavAvatar.addEventListener('error', function() { 10713 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10714 const initialsDiv = document.createElement('div'); 10715 initialsDiv.className = 'bottom-nav-avatar-initials'; 10716 initialsDiv.textContent = initials; 10717 this.parentNode.replaceChild(initialsDiv, this); 10718 }); 10719 } 10720 10721 // Mobile logout button 10722 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10723 if (mobileLogoutBtn) { 10724 mobileLogoutBtn.addEventListener('click', async function(e) { 10725 e.preventDefault(); 10726 10727 // Check push notification before logout 10728 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10729 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10730 if (!confirmLogout) { 10731 return; // User cancelled logout 10732 } 10733 } 10734 10735 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10736 10737 try { 10738 const response = await fetch('/auth/logout', { 10739 method: 'POST', 10740 credentials: 'same-origin', 10741 headers: { 10742 'Content-Type': 'application/json', 10743 'X-CSRF-Token': getCsrfToken() 10744 }, 10745 body: JSON.stringify({ 10746 csrf_token: getCsrfToken() 10747 }) 10748 }); 10749 10750 if (response.ok) { 10751 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10752 window.guestCart.clear(); 10753 } 10754 window.location.href = '/login?logged_out=1'; 10755 } else { 10756 console.error('Logout failed'); 10757 window.location.href = '/logout'; 10758 } 10759 } catch (error) { 10760 console.error('Logout error:', error); 10761 window.location.href = '/logout'; 10762 } 10763 }); 10764 } 10765 10766 const logoutBtn = document.getElementById('logoutBtn'); 10767 if (logoutBtn) { 10768 logoutBtn.addEventListener('click', async function(e) { 10769 e.preventDefault(); 10770 10771 // Check push notification before logout 10772 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10773 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10774 if (!confirmLogout) { 10775 return; // User cancelled logout 10776 } 10777 } 10778 10779 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10780
10781 try { 10782 const response = await fetch('/auth/logout', { 10783 method: 'POST', 10784 credentials: 'same-origin', 10785 headers: { 10786 'Content-Type': 'application/json', 10787 'X-CSRF-Token': getCsrfToken() 10788 }, 10789 body: JSON.stringify({ 10790 csrf_token: getCsrfToken() 10791 }) 10792 }); 10793 10794 if (response.ok) { 10795 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10796 window.guestCart.clear(); 10797 } 10798 10799 window.location.href = '/login?logged_out=1'; 10800 } else { 10801 console.error('Logout failed'); 10802 window.location.href = '/logout'; 10803 } 10804 } catch (error) { 10805 console.error('Logout error:', error); 10806 window.location.href = '/logout'; 10807 } 10808 }); 10809 } 10810 10811 const wishlistBtn = document.getElementById('wishlistBtn'); 10812 const notifBtn = document.getElementById('notifBtn'); 10813 10814 if (wishlistBtn) { 10815 wishlistBtn.addEventListener('click', function(e) { 10816 e.preventDefault(); 10817 alert('Fitur wishlist akan segera tersedia!'); 10818 }); 10819 } 10820 10821 if (notifBtn) { 10822 notifBtn.addEventListener('click', function(e) { 10823 e.preventDefault(); 10824 alert('Fitur notifikasi akan segera tersedia!'); 10825 }); 10826 } 10827})(); 10828 10829// ========== MOBILE BOTTOM NAVIGATION ========== 10830(function initBottomNav() { 10831 // Sync cart badge with bottom nav 10832 function syncBottomCartBadge() { 10833 const mainBadge = document.getElementById('cartCount'); 10834 const bottomBadge = document.getElementById('bottomCartCount'); 10835 if (mainBadge && bottomBadge) { 10836 const count = mainBadge.textContent; 10837 bottomBadge.textContent = count; 10838 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10839 } 10840 } 10841 10842 // Sync compare badge with bottom nav 10843 function syncBottomCompareBadge() { 10844 const mainBadge = document.getElementById('compareCount'); 10845 const bottomBadge = document.getElementById('bottomCompareCount'); 10846 if (mainBadge && bottomBadge) { 10847 const count = mainBadge.textContent; 10848 bottomBadge.textContent = count; 10849 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10850 } 10851 } 10852 10853 // Initial sync 10854 syncBottomCartBadge(); 10855 syncBottomCompareBadge(); 10856 10857 // Observe badge changes 10858 const observer = new MutationObserver(function() { 10859 syncBottomCartBadge(); 10860 syncBottomCompareBadge(); 10861 }); 10862 10863 const cartBadge = document.getElementById('cartCount'); 10864 const compareBadge = document.getElementById('compareCount'); 10865 10866 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10867 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10868 10869 // Bottom Cart Button 10870 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10871 if (bottomCartBtn) { 10872 bottomCartBtn.addEventListener('click', function(e) { 10873 e.preventDefault(); 10874 10875 const checkLogin = () => { 10876 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10877 }; 10878 10879 if (checkLogin()) { 10880 window.location.href = '/keranjang'; 10881 } else { 10882 showCartLoginModal(); 10883 } 10884 }); 10885 } 10886 10887 // Bottom Compare Button 10888 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10889 if (bottomCompareBtn) { 10890 bottomCompareBtn.addEventListener('click', function(e) { 10891 e.preventDefault(); 10892 10893 // Close chat widget if open 10894 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10895 window.plazaChat.closeWidget(); 10896 } 10897 10898 const mainCompareBtn = document.getElementById('compareBtn'); 10899 if (mainCompareBtn) { 10900 mainCompareBtn.click(); 10901 } 10902 }); 10903 } 10904 10905 // Bottom Category Button - triggers mega menu 10906 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10907 if (bottomCategoryBtn) { 10908 bottomCategoryBtn.addEventListener('click', function(e) { 10909 e.preventDefault(); 10910 // Trigger desktop mega menu 10911 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10912 if (desktopTrigger) { 10913 desktopTrigger.click(); 10914 } 10915 });
10916 } 10917 10918 // Bottom Login Button 10919 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10920 if (bottomLoginBtn) { 10921 bottomLoginBtn.addEventListener('click', function(e) { 10922 e.preventDefault(); 10923 window.location.href = '/login'; 10924 }); 10925 } 10926 10927 // Bottom Chat Button 10928 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10929 if (bottomChatBtn) { 10930 bottomChatBtn.addEventListener('click', function(e) { 10931 e.preventDefault(); 10932 10933 // Trigger chat widget (try multiple times if needed) 10934 const triggerChat = () => { 10935 const chatToggle = document.getElementById('chat-toggle-btn'); 10936 if (chatToggle) { 10937 chatToggle.click(); 10938 return true; 10939 } 10940 return false; 10941 }; 10942 10943 // Immediate attempt 10944 if (!triggerChat()) { 10945 // Retry after 100ms if not found 10946 setTimeout(() => { 10947 if (!triggerChat()) { 10948 // Last resort: manually show popup 10949 const popup = document.getElementById('chat-popup'); 10950 if (popup) { 10951 popup.style.display = 'block'; 10952 } 10953 } 10954 }, 100); 10955 } 10956 }); 10957 } 10958 10959 // Sync chat unread badge 10960 function syncBottomChatBadge() { 10961 const chatBadge = document.getElementById('chat-unread-badge'); 10962 const bottomChatBadge = document.getElementById('bottomChatCount'); 10963 if (chatBadge && bottomChatBadge) { 10964 const count = chatBadge.textContent; 10965 bottomChatBadge.textContent = count; 10966 bottomChatBadge.style.display = chatBadge.style.display; 10967 } 10968 } 10969 10970 // Observe chat badge changes 10971 const chatBadge = document.getElementById('chat-unread-badge'); 10972 if (chatBadge) { 10973 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10974 syncBottomChatBadge(); 10975 } 10976 10977 // Active state management based on current page 10978 function updateActiveState() { 10979 const currentPath = window.location.pathname; 10980 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10981 10982 bottomNavItems.forEach(item => { 10983 item.classList.remove('active'); 10984 10985 const itemId = item.id; 10986 const page = item.getAttribute('data-page'); 10987 const href = item.getAttribute('href'); 10988 10989 // Check by ID for specific pages 10990 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10991 item.classList.add('active'); 10992 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10993 item.classList.add('active'); 10994 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10995 item.classList.add('active'); 10996 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10997 item.classList.add('active'); 10998 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10999 item.classList.add('active'); 11000 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 11001 item.classList.add('active'); 11002 } 11003 // Fallback: check by data-page attribute 11004 else if (page === 'home' && currentPath === '/') { 11005 item.classList.add('active'); 11006 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 11007 item.classList.add('active'); 11008 } 11009 // Fallback: check by href 11010 else if (href && currentPath === href) { 11011 item.classList.add('active'); 11012 } 11013 }); 11014 } 11015 11016 // Update active state on load 11017 updateActiveState(); 11018 11019 // Bottom nav always visible - auto-hide disabled for better UX 11020 // const bottomNav = document.querySelector('.bottom-nav'); 11021 // if (bottomNav && window.innerWidth <= 640) { 11022 // // Auto-hide feature disabled 11023 // } 11024})(); 11025// Cart Login Modal Handler (Global scope) 11026function showCartLoginModal() { 11027 const modal = document.getElementById('cartLoginModal'); 11028 if (!modal) return; 11029 11030 modal.classList.add('show'); 11031 11032 const confirmBtn = document.getElementById('cartLoginConfirm'); 11033 const registerBtn = document.getElementById('cartRegisterBtn');
11034 const cancelBtn = document.getElementById('cartLoginCancel'); 11035 11036 const handleLogin = () => { 11037 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 11038 }; 11039 11040 const handleRegister = () => { 11041 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 11042 }; 11043 11044 const handleCancel = () => { 11045 modal.classList.remove('show'); 11046 confirmBtn.removeEventListener('click', handleLogin); 11047 registerBtn.removeEventListener('click', handleRegister); 11048 cancelBtn.removeEventListener('click', handleCancel); 11049 }; 11050 11051 confirmBtn.removeEventListener('click', handleLogin); 11052 registerBtn.removeEventListener('click', handleRegister); 11053 cancelBtn.removeEventListener('click', handleCancel); 11054 11055 confirmBtn.addEventListener('click', handleLogin); 11056 registerBtn.addEventListener('click', handleRegister); 11057 cancelBtn.addEventListener('click', handleCancel); 11058 11059 modal.addEventListener('click', (e) => { 11060 if (e.target === modal) { 11061 handleCancel(); 11062 } 11063 }); 11064} 11065</script>
vendor: 2 bytes, line 11065
11065
11066<script data-inlined="cart-badge.js"> 11067(function(){ 11068 function selBadge(){ 11069 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 11070 } 11071 function setBadge(n){ 11072 const el = selBadge(); 11073 if (!el) return; 11074 const count = Math.max(0, parseInt(n||0,10) || 0); 11075 el.textContent = String(count); 11076 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 11077 if (count <= 0) { el.classList.add('is-empty'); } 11078 else { el.classList.remove('is-empty'); } 11079 } 11080 async function refresh(){ 11081 try{ 11082 // Primary: Use server-side endpoint (handles both logged and guest users) 11083 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11084 if (res.ok) { 11085 const data = await res.json(); 11086 if (data && data.ok) { 11087 setBadge(data.items_count || 0); 11088 return; 11089 } 11090 } 11091 11092 // Fallback hanya untuk guest user: check localStorage 11093 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11094 document.body.classList.contains('logged-in') || 11095 window.userLoggedIn === true; 11096 11097 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11098 setBadge(window.guestCart.getItemsCount()); 11099 } 11100 }catch(e){ 11101 // Final fallback hanya untuk guest 11102 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11103 if (!isLoggedIn) { 11104 try { 11105 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11106 const items = guestCartData.items || {}; 11107 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11108 setBadge(count); 11109 } catch(e2) { 11110 setBadge(0); 11111 } 11112 } else { 11113 setBadge(0); 11114 } 11115 } 11116 } 11117 11118 // Ekspos untuk dipanggil dari skrip lain (opsional) 11119 window.CartBadge = { set: setBadge, refresh }; 11120 window.updateCartCounter = refresh; // Alias for compatibility 11121 11122 // Update saat halaman load 11123 if (document.readyState === 'loading') { 11124 document.addEventListener('DOMContentLoaded', refresh); 11125 } else { 11126 refresh(); 11127 } 11128 11129 // Dengarkan event global dari add/remove/update 11130 document.addEventListener('cart:updated', function(ev){ 11131 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11132 ? ev.detail.items_count : null; 11133 if (c !== null) setBadge(c); 11134 else refresh(); 11135 }); 11136 11137 // Dengarkan event khusus cart remove untuk update badge 11138 document.addEventListener('cart:removed', function(ev){ 11139 // Langsung refresh untuk memastikan data terbaru 11140 refresh(); 11141 }); 11142 11143 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11144 const prevAdd = window.addToCart; 11145 if (typeof prevAdd === 'function'){ 11146 window.addToCart = async function(pid, qty, vid, meta){ 11147 const res = await prevAdd(pid, qty, vid, meta); 11148 try{ 11149 const cnt = res && res.cart ? res.cart.items_count : null; 11150 if (cnt !== null) setBadge(cnt); 11151 // tetap siarkan event agar listener lain ikut sync 11152 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11153 }catch(_){} 11154 return res; 11155 }; 11156 } 11157 11158 // Helper function to check if user is logged in 11159 function isUserLoggedIn() { 11160 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11161 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11162 } 11163 11164 // Make login check globally accessible 11165 window.isUserLoggedIn = isUserLoggedIn; 11166})(); 11167</script>
11167 11168 11169<!-- Product Compare Sidebar --> 11170<div class="compare-sidebar" id="compareSidebar"> 11171 <div class="compare-header"> 11172 <h4 class="compare-title"> 11173 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11174 <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"/> 11175 </svg> 11176 Bandingkan Produk 11177 <span class="compare-count" id="compareCountSidebar">(0)</span> 11178 </h4> 11179 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11180 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11181 <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"/> 11182 </svg> 11183 </button> 11184 </div> 11185 11186 <div class="compare-content"> 11187 <div class="compare-empty" id="compareEmpty"> 11188 <div class="empty-icon"> 11189 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11190 <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"/> 11191 </svg> 11192 </div> 11193 <h5>Belum ada produk dipilih</h5> 11194 <p>Pilih produk yang ingin dibandingkan dengan klik tombol "Bandingkan Produk" di halaman produk.</p> 11195 </div> 11196 11197 <div class="compare-list" id="compareList"> 11198 <!-- Products will be populated here --> 11199 </div> 11200 </div> 11201 11202 <div class="compare-actions" id="compareActions"> 11203 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11204 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11205 <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"/> 11206 </svg> 11207 Hapus Semua 11208 </button> 11209 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11210 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11211 <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"/> 11212 </svg> 11213 Analisis AI 11214 </button> 11215 </div> 11216 11217 <!-- AI Disclaimer in Sidebar --> 11218 <div class="compare-disclaimer"> 11219 <div class="ai-disclaimer"> 11220 <div class="disclaimer-text"> 11221 <strong>ð¡ Catatan:</strong> Analisis ini dihasilkan secara otomatis dan dapat memiliki keterbatasan. 11222 Gunakan sebagai referensi tambahan dengan tetap mempertimbangkan kebutuhan Anda. 11223 Untuk informasi lebih lanjut, silakan hubungi tim GAME MPO. 11224 </div> 11225 </div> 11226 </div> 11227</div><style data-inlined="pd-lightbox.css"> 11228/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11229.pd-lb { 11230 position: fixed; inset: 0; z-index: 99999; 11231 background: rgba(0,0,0,.95); 11232 display: none; 11233 color: #fff; 11234 /* Transition untuk swipe-down feedback */ 11235 transition: transform .18s ease, opacity .18s ease; 11236} 11237.pd-lb.show { display: block; } 11238 11239/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11240.pd-lb-canvas { 11241 position: absolute; inset: 56px 0 56px 0; 11242 display: flex; align-items: center; justify-content: center; 11243 overflow: hidden; 11244 touch-action: none; 11245 z-index: 1; 11246} 11247 11248.pd-lb-img { 11249 max-width: 96vw; max-height: 86vh; 11250 will-change: transform; 11251 user-select: none; 11252 pointer-events: none; 11253 transform-origin: 50% 50%; 11254} 11255 11256.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11257 position: absolute; 11258 z-index: 2; 11259} 11260 11261.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11262 background: rgba(17,24,39,.7); 11263 color: #fff; border: 0; cursor: pointer; 11264 width: 40px; height: 40px; border-radius: 999px; 11265 display: inline-flex; align-items: center; justify-content: center; 11266 transition: background-color .15s ease, transform .05s ease; 11267 -webkit-tap-highlight-color: transparent; 11268} 11269.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11270.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11271 11272.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11273.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11274.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11275 11276.pd-lb-counter { 11277 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11278 transform: translateX(-50%); 11279 font-weight: 700; font-size: 13px; letter-spacing: .4px;
11280 background: rgba(17,24,39,.65); 11281 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11282} 11283 11284@media (max-width: 640px){ 11285 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11286 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11287 .pd-lb-close { width: 38px; height: 38px; } 11288} 11289</style> 11290<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(245,202,86,.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(245,202,86,.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:#C77DFF;margin-bottom:2px}
11290.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,#C77DFF,#C77DFF);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(245,202,86,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(245,202,86,.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> 11291<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11292.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} 11293 11294.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)}} 11295 11296.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} 11297 11298.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} 11299 11300.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)} 11301 11302.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,#C77DFF 0%,#E3D05A 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#C77DFF 0%,#C77DFF 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#C77DFF 0%,#C77DFF 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11303 11304.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)}
11304.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} 11305 11306@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}} 11307 11308.mf-share{background:#fff;color:#C77DFF;border:2px solid #C77DFF;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11309 11310@media(max-width:640px){.pd-share-btn{display:none!important}} 11311 11312.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#C77DFF;color:#14b1d7;border-radius:999px;padding:4px 10px;font-weight:700} 11313 11314.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} 11315 11316.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} 11317 11318.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} 11319 11320 11321.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} 11322 11323.sp-wa{ 11324 grid-column:3; justify-self:end; 11325 width:32px; height:32px; min-width:32px; min-height:32px; 11326 box-sizing:border-box; 11327 padding:0; line-height:0; text-decoration:none; outline:0; 11328 display:inline-flex; align-items:center; justify-content:center; 11329 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11330 background:rgba(37,211,102,.08); color:#25D366; 11331 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11332 -webkit-tap-highlight-color:transparent; 11333} 11334.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11335.sp-wa:active{transform:translateY(.5px) scale(.98)} 11336 11337.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11338.stock-popover a{text-decoration:none} 11339 11340#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}} 11341 11342.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:#C77DFF}.stock-dot.out{background:#C77DFF}.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}} 11343 11344.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}} 11345 11346.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#C77DFF 0%,#C77DFF 100%);color:#765212;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11347.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#fef3c7 0%,#C77DFF 100%);border-radius:12px;border:2px solid #C77DFF}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#C77DFF;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#765212}
11347.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#ae7e21;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#ae7e21}.pwp-spinner{width:40px;height:40px;border:3px solid #C77DFF;border-top-color:#C77DFF;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11348.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #C77DFF;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:#C77DFF;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:#C77DFF;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} 11349@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}} 11350 11351.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:#C77DFF;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #C77DFF;font-size:12px;color:#765212;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}} 11352</style> 11353 11354<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik</h2><div class="pd-sticky-price-row"><span class="pd-sticky-price" id="pd-sticky-price">Rp 20.000,-</span><span class="pd-sticky-price-original" id="pd-sticky-price-original">Rp 1.000.000</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 produk" title="Bagikan"><svg viewBox="0 0 24 24" width="16" height="16" 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
11354-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>+ Keranjang</button><button type="button" class="pd-sticky-btn pd-sticky-btn-buy" id="pd-sticky-buy" disabled>Beli Sekarang</button></div></div></div> 11355 11356<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://hop.exchange/">MPO5000</a><span>-</span><a href="https://hop.exchange/">LOGIN GAME MPO</a><span>-</span><a href="https://hop.exchange/">APK JUDIMPO</a><span>-</span><a href="https://hop.exchange/">SLOT BCA 5000</a><span>-</span><a href="https://hop.exchange/">LOGIN MPO SLOT</a><span>-</span><span class="current">JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik 11357 11358</span></nav> 11359 11360<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://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="di-custom total GAME MPO" data-slug="" loading="eager"> 11361</figure> 11362 11363<div class="pd-mobile-slider-wrapper"> 11364<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="eager"></div><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11365<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://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt="GAME MPO ONLINE" data-slug="" aria-label="Gambar 1"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt="GAME MPO ONLINE" data-slug="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" aria-label="Gambar 2"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt="GAME MPO ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp" aria-label="Gambar 3"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt
11365="GAME MPO ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp" aria-label="Gambar 4"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt="GAME MPO ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp" aria-label="Gambar 5"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" data-alt="GAME MPO ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp" aria-label="Gambar 6"><img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE" 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"> 11366 <h1 class="pd-title" id="pd-title">JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik</h1><button type="button" class="pd-share-btn" id="pd-share-btn" aria-label="Bagikan produk" 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> 11367<div id="pd-price-anchor"> 11368</div> 11369<div class="pd-price-qr-container"> 11370 <div class="pd-price-block" id="pd-price-block"> 11371 <div class="pd-price-row"> 11372 <span class="pd-price-current" id="pd-price-current" >Rp 5.000</span> 11373 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11374 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11375 <div class="pd-meta-row"> 11376 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11377 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11378 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11379 <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> 11380 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">GAME MPO</span> 11381 <span class="sku">SKU: <span id="pd-sku">GAME MPO SITUS</span></span></div> 11382<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11383 <span class="paylater-icon">ð³</span> 11384 <span class="paylater-text"> 11385 <strong>GAME MPO</strong> 11386 <small>ONLINE</small> 11387 </span> 11388</div> 11389 11390<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">Stok Cabang</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"> 11391<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%2Flenovo-legion-go-8as
11391p2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" target="_blank" rel="noopener" aria-label="WhatsApp Asia Mega Mas"><svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><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> 11392 11393 <li class="sp-item"> 11394 <div class="sp-left"> 11395 <span class="sp-dot" style="--dot:#34d399;"></span> 11396 <span class="sp-label">Medan Fair</span> 11397 </div> 11398 <span class="sp-qty" id="sp-qty-mdf">0</span> 11399 <a class="sp-wa" 11400 href="https://wa.me/6285388992009?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Medan%20Fair%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" 11401 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11402 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11403 <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"/> 11404 </svg> 11405 </a> 11406 </li> 11407 11408 <li class="sp-item"> 11409 <div class="sp-left"> 11410 <span class="sp-dot" style="--dot:#C77DFF;"></span> 11411 <span class="sp-label">Banda Aceh</span> 11412 </div> 11413 <span class="sp-qty" id="sp-qty-bda">0</span> 11414 <a class="sp-wa" 11415 href="https://wa.me/628116833411?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Banda%20Aceh%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" 11416 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11417 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
11418 <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"/> 11419 </svg> 11420 </a> 11421 </li> 11422 </ul> 11423 </div> 11424 11425 </div> 11426 <div class="pd-qr-print" style="display:none;"><div class="pd-qr-label">Scan untuk lihat produk:</div><img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" alt="QR Code" class="pd-qr-image"></div> 11427 </div> 11428 <div id="pd-variants-anchor"></div> 11429 11430 11431 11432 <div class="pd-compare-section"> 11433 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11434 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11435 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11436 </svg> 11437 <span class="compare-text">Bandingkan Produk</span> 11438 <span class="compare-remove" hidden> 11439 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11440 <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"/> 11441 </svg> 11442 Hapus dari Perbandingan 11443 </span> 11444 </button> 11445 <div class="pd-compare-info"> 11446 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11447 </div> 11448 </div> 11449 11450 <div class="pd-short-desc"><p><strong>INFORMASI GAME MPO 5000:</strong></p><ul><li>Minimal Deposit: Rp.5.000,-</li><li>Jenis Permainan: Mpo Slot Bca</li><li>Metode Pembayaran: Bank, QRIS, E-Wallet</li><li>Mata Uang: IDR (Indonesian Rupiah)</li><li>Jam Operasional: 24 Jam Nonstop</li><li>Total Rating: 399.999.999</li><li>Rating: âââââ</li><li>Daftar Buruan: <a href="https://mpo5000xjudimpo.blackhostapp.com/">Klik Disini</a></li></ul></div><div class="capzrao-v1 mini"> 11451<div class="capzrao"> 11452 11453 <!-- HEADER --> 11454<div class="capzrao-v3 mini"> 11455 <div class="Masa-Kini-yang-Selalu-v3"> 11456 <h2 class="capzrao-title-v3">ð¯ PREDIKSI MPO SLOT 1000 RESMI (SIMULASI) ð¯</h2> 11457 11458 <div class="capzrao-controls-v3"> 11459 <label> 11460 PASARAN 11461 <select id="marketFilter"> 11462 <option value="ALL">ALL PASARAN</option> 11463 </select> 11464 </label> 11465 11466 <label> 11467 TIPE 11468 <select id="typeFilter"> 11469 <option value="4D">4D</option> 11470 <option value="3D">3D</option> 11471 <option value="2D">2D</option> 11472 </select> 11473 </label> 11474 11475 <label> 11476 MIN CONF 11477 <input id="minConf" type="number" min="50" max="99" step="1" value="70"> 11478 </label> 11479 11480 <button id="btncapzraoRandom" type="button">
11481 <span class="icon">ð²</span> GENERATE 11482 </button> 11483 </div> 11484 11485 <div class="capzrao-main-result-v3" id="capzraoResult"> 11486 <div class="capzrao-status-v3" id="capzraoStatus">â READY â</div> 11487 11488 <div class="capzrao-highlight-box"> 11489 <div class="capzrao-col-left"> 11490 <div class="k">Pasaran Rekomendasi</div> 11491 <div class="v-game" id="rMarket">-</div> 11492 11493 <div class="provider-info"> 11494 <span class="k-small">Tipe:</span> 11495 <span class="v-small" id="rType">-</span> 11496 <span class="k-small" style="margin-left:10px;">Conf:</span> 11497 <span class="v-small" id="rConf">-</span> 11498 </div> 11499 </div> 11500 11501 <div class="capzrao-col-right"> 11502 <div class="k-rtp">Angka Utama</div> 11503 <div class="v-rtp-huge" id="rAngka">-</div> 11504 </div> 11505 </div> 11506 11507 <div class="capzrao-details-v3"> 11508 <div class="capzrao-detail-item"> 11509 <div class="k-detail">BBFS</div> 11510 <div class="v-detail" id="rBBFS">-</div> 11511 </div> 11512 <div class="capzrao-detail-item"> 11513 <div class="k-detail">Angka Ikut</div> 11514 <div class="v-detail" id="rIkut">-</div> 11515 </div> 11516 <div class="capzrao-detail-item"> 11517 <div class="k-detail">Shio</div> 11518 <div class="v-detail" id="rShio">-</div> 11519 </div> 11520 </div> 11521 11522 <div class="capzrao-tip-v3" id="rTip"> 11523 Tips: Ini simulasi hiburan. Tetap bijak & disiplin. 11524 </div> 11525 </div> 11526 11527 <div class="capzrao-alts-v3"> 11528 <div class="capzrao-alts-title-v3">3 Alternatif Angka</div> 11529 <div class="capzrao-alt-list-v3" id="capzraoAltList"></div> 11530 </div> 11531 11532 <div class="capzrao-note-v3"> 11533 Disclaimer: Hasil di halaman ini random (simulasi), bukan acuan hasil resmi. 11534 </div> 11535 </div> 11536 </div> 11537 11538 <style> 11539 @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11540 11541 /* =============================== 11542 WRAPPER 11543================================ */ 11544 .capzrao-v3 { 11545 display: flex; 11546 justify-content: center; 11547 padding: 16px 10px; 11548 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 11549 } 11550 11551 /* =============================== 11552 MACHINE CORE (#E3D05A ELEGANT) 11553================================ */ 11554 .Masa-Kini-yang-Selalu-v3 { 11555 position: relative; 11556 width: 100%; 11557 max-width: 520px; 11558 padding: 16px; 11559 border-radius: 16px; 11560 overflow: hidden; 11561 isolation: isolate; 11562 background: radial-gradient(circle at 15% 0%, rgba(163,100,34,1), transparent 48%), radial-gradient(circle at 85% 100%, rgba(201,131,42,0.18), transparent 55%), linear-gradient(123deg, #000000, #000000); 11563 border: 1px solid rgba(201,131,42,1); 11564 box-shadow: inset 0 0 18px rgba(201,131,42,1), 0 0 18px rgba(201,131,42,1); 11565 color: #dbdbdb; 11566 } 11567 11568 /* HUD SWEEP */ 11569 .Masa-Kini-yang-Selalu-v3::before { 11570 content: ""; 11571 position: absolute; 11572 inset: 0; 11573 background: linear-gradient(120deg, transparent 40%, rgba(226,166,66,.10), transparent 65%); 11574 animation: hudSweep 7s linear infinite; 11575 pointer-events: none; 11576 opacity: .8; 11577 } 11578 11579 /* =============================== 11580 TITLE 11581================================ */ 11582 .capzrao-title-v3 { 11583 text-align: center; 11584 margin: 0 0 12px; 11585 font-size: 15px; 11586 font-weight: 900; 11587 letter-spacing: 1px; 11588 text-shadow: 0 0 6px rgba(163,100,34,90%);
11589 } 11590 11591 /* =============================== 11592 CONTROLS 11593================================ */ 11594 .capzrao-controls-v3 { 11595 display: grid; 11596 grid-template-columns: 1fr 1fr; 11597 gap: 10px; 11598 padding: 12px; 11599 border-radius: 14px; 11600 11601 background: rgba(0, 0, 0, 0.4); 11602 border: 1px solid rgba(226,166,66,0.45); 11603 box-shadow: inset 0 0 14px rgba(201,131,42,22%); 11604 } 11605 11606 .capzrao-controls-v3 label { 11607 display: grid; 11608 gap: 6px; 11609 font-size: 10px; 11610 letter-spacing: .8px; 11611 text-transform: uppercase; 11612 color: #E3D05A; 11613 } 11614 11615 .capzrao-controls-v3 select, 11616 .capzrao-controls-v3 input { 11617 background: rgba(0 0 0 / 92%); 11618 color: #c5c5c5; 11619 border: 1px solid rgba(201,131,42,1); 11620 border-radius: 10px; 11621 padding: 7px 9px; 11622 font-weight: 700; 11623 font-size: 12px; 11624 outline: none; 11625 box-shadow: inset 0 0 10px rgba(201,131,42,1); 11626 } 11627 11628 /* BUTTON */ 11629 #btncapzraoRandom { 11630 grid-column: span 2; 11631 display: flex; 11632 align-items: center; 11633 justify-content: center; 11634 gap: 8px; 11635 11636 background: linear-gradient(145deg, #E3D05A, #E3D05A); 11637 border: none; 11638 color: #000000; 11639 font-weight: 900; 11640 letter-spacing: .8px; 11641 border-radius: 14px; 11642 cursor: pointer; 11643 padding: 10px 12px; 11644 font-size: 12px; 11645 11646 box-shadow: 0 0 16px rgba(226,166,66,0.75); 11647 transition: .2s ease; 11648 } 11649 11650 #btncapzraoRandom:active { 11651 transform: scale(.98); 11652 } 11653 11654 #btncapzraoRandom:disabled { 11655 opacity: .75; 11656 cursor: not-allowed; 11657 } 11658 11659 /* =============================== 11660 MAIN RESULT 11661================================ */ 11662 .capzrao-main-result-v3 { 11663 margin-top: 12px; 11664 padding: 12px; 11665 border-radius: 14px; 11666 11667 background: rgba(18,12,8,0.78); 11668 border: 1px solid rgba(226,166,66,1); 11669 box-shadow: inset 0 0 16px rgba(226,166,66,1); 11670 } 11671 11672 .capzrao-status-v3 { 11673 text-align: center; 11674 font-weight: 800; 11675 letter-spacing: .8px; 11676 color: #E3D05A; 11677 margin-bottom: 10px; 11678 font-size: 11px; 11679 } 11680 11681 .capzrao-highlight-box { 11682 display: flex; 11683 justify-content: space-between; 11684 gap: 12px; 11685 padding-bottom: 10px; 11686 margin-bottom: 10px; 11687 border-bottom: 1px dashed rgba(226,166,66,0.3); 11688 } 11689 11690 .k, 11691 .k-rtp { 11692 font-size: 10px; 11693 color: #c7c7c7; 11694 text-transform: uppercase; 11695 } 11696 11697 .v-game { 11698 font-size: 14px; 11699 font-weight: 900; 11700 color: #ffffff; 11701 margin-top: 6px; 11702 line-height: 1.2; 11703 } 11704 11705 .provider-info { 11706 margin-top: 8px; 11707 font-size: 11px; 11708 color: #d9d9d9; 11709 opacity: .95; 11710 } 11711 11712 .k-small { 11713 color: #E3D05A; 11714 font-size: 10px; 11715 } 11716 11717 .v-small { 11718 color: #fff0f0; 11719 font-weight: 900; 11720 } 11721 11722 .v-rtp-huge { 11723 font-size: 2.4em; 11724 font-weight: 900; 11725 color: #C77DFF; 11726 text-shadow: 0 0 10px rgba(0 0 0); 11727 line-height: 1; 11728 letter-spacing: 1px; 11729 } 11730 11731 /* DETAILS */ 11732 .capzrao-details-v3 { 11733 display: grid; 11734 grid-template-columns: repeat(3, 1fr); 11735 gap: 8px; 11736 background: rgba(0, 0, 0, .30); 11737 padding: 10px; 11738 border-radius: 12px; 11739 } 11740 11741 .k-detail { 11742 font-size: 9px; 11743 color: #E3D05A; 11744 text-transform: uppercase; 11745 } 11746 11747 .v-detail { 11748 font-size: 12px; 11749 font-weight: 800; 11750 color: #ffffff; 11751 margin-top: 4px; 11752 letter-spacing: .8px; 11753 } 11754 11755 .capzrao-tip-v3 { 11756 margin-top: 10px; 11757 font-size: 10px; 11758 color: #d9d9d9; 11759 opacity: .9; 11760 text-align: center; 11761 } 11762 11763 /* ALTS */ 11764 .capzrao-alts-v3 { 11765 margin-top: 12px; 11766 padding: 12px; 11767 border-radius: 14px; 11768 background: rgba(0, 0, 0, .40); 11769 border: 1px solid rgba(226,166,66,1); 11770 } 11771 11772 .capzrao-alts-title-v3 { 11773 font-weight: 900; 11774 color: #E3D05A; 11775 margin-bottom: 10px; 11776 font-size: 12px; 11777 } 11778 11779 .capzrao-alt-list-v3 { 11780 display: grid; 11781 grid-template-columns: 1fr; 11782 gap: 8px; 11783 } 11784 11785 .capzrao-alt-item { 11786 background: rgba(18,12,8,.86); 11787 border-radius: 12px; 11788 padding: 10px; 11789 border-left: 3px solid rgba(163,100,34,90%);
11790 } 11791 11792 .capzrao-alt-item .g { 11793 font-weight: 900; 11794 color: #c5c5c5; 11795 font-size: 12px; 11796 } 11797 11798 .capzrao-alt-item .p { 11799 font-size: 10px; 11800 color: #E3D05A; 11801 margin-top: 4px; 11802 } 11803 11804 .capzrao-alt-item .r { 11805 margin-top: 6px; 11806 font-weight: 900; 11807 color: #C77DFF; 11808 letter-spacing: .8px; 11809 font-size: 12px; 11810 } 11811 11812 .capzrao-note-v3 { 11813 margin-top: 10px; 11814 font-size: 10px; 11815 color: #d9d9d9; 11816 opacity: .85; 11817 text-align: center; 11818 } 11819 11820 /* MOBILE */ 11821 @media(max-width:420px) { 11822 .capzrao-highlight-box { 11823 flex-direction: column 11824 } 11825 11826 .capzrao-details-v3 { 11827 grid-template-columns: 1fr 11828 } 11829 } 11830 11831 /* ANIM */ 11832 @keyframes hudSweep { 11833 0% { 11834 transform: translateX(-110%) 11835 } 11836 11837 100% { 11838 transform: translateX(110%) 11839 } 11840 } 11841 11842 @keyframes pulse { 11843 0% { 11844 opacity: .55 11845 } 11846 11847 50% { 11848 opacity: 1 11849 } 11850 11851 100% { 11852 opacity: .55 11853 } 11854 } 11855 </style> 11856 11857
11857<script> 11858 /* =============================== 11859 DATASET GAME MPO (SIMULASI) 11860 ================================ */ 11861 const capzrao_MARKETS = [ 11862 { market: "Singapore", code: "SGP", open: "17:45", zone: "WIB" }, 11863 { market: "Hongkong", code: "HK", open: "22:45", zone: "WIB" }, 11864 { market: "Sydney", code: "SDY", open: "13:45", zone: "WIB" }, 11865 { market: "Macau", code: "MC", open: "16:00", zone: "WIB" }, 11866 { market: "Cambodia", code: "KHM", open: "10:45", zone: "WIB" }, 11867 { market: "Japan", code: "JP", open: "11:30", zone: "WIB" } 11868 ]; 11869 11870 const SHIO = ["Tikus", "Kerbau", "Macan", "Kelinci", "Naga", "Ular", "Kuda", "Kambing", "Monyet", "Ayam", "Anjing", "Babi"]; 11871 11872 const $ = (q) => document.querySelector(q); 11873 const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 11874 11875 function uniqueMarkets() { 11876 return capzrao_MARKETS.map(m => m.market); 11877 } 11878 11879 function genDigits(len) { 11880 let s = ""; 11881 for (let i = 0; i < len; i++) s += String(randInt(0, 9)); 11882 return s; 11883 } 11884 11885 function genBBFS() { 11886 const pool = [...Array(10)].map((_, i) => i); 11887 const out = []; 11888 for (let i = 0; i < 7; i++) { 11889 const idx = randInt(0, pool.length - 1); 11890 out.push(pool.splice(idx, 1)[0]); 11891 } 11892 return out.join(""); 11893 } 11894 11895 function genIkut() { 11896 const pool = [...Array(10)].map((_, i) => i); 11897 const out = []; 11898 for (let i = 0; i < 4; i++) { 11899 const idx = randInt(0, pool.length - 1); 11900 out.push(pool.splice(idx, 1)[0]); 11901 } 11902 return out.join(" ⢠"); 11903 } 11904 11905 function simulateConfidence() { 11906 return randInt(60, 96); 11907 } 11908 11909 function applyFilters() { 11910 const m = $("#marketFilter").value; 11911 const minC = parseFloat($("#minConf").value || "0"); 11912 11913 return capzrao_MARKETS 11914 .filter(x => (m === "ALL" || x.market === m)) 11915 .map(x => ({ ...x, conf: simulateConfidence() })) 11916 .filter(x => x.conf >= minC); 11917 } 11918 11919 function pickRandom(arr, n = 1) { 11920 const copy = [...arr]; 11921 const out = []; 11922 for (let i = 0; i < n && copy.length; i++) { 11923 const idx = randInt(0, copy.length - 1); 11924 out.push(copy.splice(idx, 1)[0]); 11925 } 11926 return out; 11927 } 11928 11929 function formatAngka(type) { 11930 if (type === "4D") return genDigits(4); 11931 if (type === "3D") return genDigits(3); 11932 return genDigits(2); 11933 } 11934 11935 function renderMain(item, type) { 11936 $("#rMarket").textContent = `${item.market} (${item.code}) ⢠Open ${item.open} ${item.zone}`; 11937 $("#rType").textContent = type; 11938 $("#rConf").textContent = `${item.conf}%`; 11939 11940 $("#rAngka").textContent = formatAngka(type); 11941 $("#rBBFS").textContent = genBBFS(); 11942 $("#rIkut").textContent = genIkut(); 11943 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 11944 $("#rTip").textContent = "Tips: Ini simulasi hiburan. Tetap bijak & disiplin."; 11945 } 11946 11947 function renderAlts(list, type) { 11948 const wrap = $("#capzraoAltList"); 11949 wrap.innerHTML = ""; 11950 list.forEach(x => { 11951 const el = document.createElement("div"); 11952 el.className = "capzrao-alt-item"; 11953 el.innerHTML = ` 11954 <div class="g">${x.market} (${x.code})</div> 11955 <div class="p">Tipe ${type} ⢠Conf <strong>${x.conf}%</strong></div> 11956 <div class="r">Angka: <strong>${formatAngka(type)}</strong></div> 11957 `; 11958 wrap.appendChild(el); 11959 }); 11960 } 11961 11962 function spinButton(btn, on) { 11963 if (on) { 11964 btn.disabled = true; 11965 btn.dataset.text = btn.innerHTML; 11966 btn.innerHTML = `<span class="icon">ð</span> PROCESS...`; 11967 } else { 11968 btn.disabled = false; 11969 btn.innerHTML = btn.dataset.text || `<span class="icon">ð²</span> GENERATE`; 11970 } 11971 } 11972 11973 /* =============================== 11974 INIT 11975 ================================ */
11976 (function init() { 11977 const sel = $("#marketFilter"); 11978 uniqueMarkets().forEach(m => { 11979 const o = document.createElement("option"); 11980 o.value = m; o.textContent = m; 11981 sel.appendChild(o); 11982 }); 11983 11984 $("#btncapzraoRandom").addEventListener("click", (e) => { 11985 const btn = e.currentTarget; 11986 const type = $("#typeFilter").value; 11987 const filtered = applyFilters(); 11988 const st = $("#capzraoStatus"); 11989 11990 if (!filtered.length) { 11991 st.textContent = "â NO MATCH â"; 11992 st.style.color = "white"; 11993 renderAlts([], type); 11994 $("#rMarket").textContent = "-"; 11995 $("#rType").textContent = type; 11996 $("#rConf").textContent = "-"; 11997 $("#rAngka").textContent = "-"; 11998 $("#rBBFS").textContent = "-"; 11999 $("#rIkut").textContent = "-"; 12000 $("#rShio").textContent = "-"; 12001 return; 12002 } 12003 12004 spinButton(btn, true); 12005 st.textContent = "â GENERATING â"; 12006 st.style.animation = "pulse .6s linear infinite"; 12007 st.style.color = "white"; 12008 12009 const t0 = setInterval(() => { 12010 const temp = pickRandom(filtered, 1)[0]; 12011 $("#rMarket").textContent = `${temp.market} (${temp.code}) ⢠Open ${temp.open} ${temp.zone}`; 12012 $("#rType").textContent = type; 12013 $("#rConf").textContent = `${temp.conf}%`; 12014 $("#rAngka").textContent = formatAngka(type); 12015 $("#rBBFS").textContent = genBBFS(); 12016 $("#rIkut").textContent = genIkut(); 12017 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 12018 }, 85); 12019 12020 setTimeout(() => { 12021 clearInterval(t0); 12022 12023 const picks = pickRandom(filtered, 4); 12024 renderMain(picks[0], type); 12025 renderAlts(picks.slice(1), type); 12026 12027 st.textContent = "â READY â"; 12028 st.style.animation = ""; 12029 st.style.color = "gold"; 12030 spinButton(btn, false); 12031 }, 1200); 12032 }); 12033 12034 renderAlts([], "4D"); 12035 })(); 12036 </script>
12036 12037
12038<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516" integrity="sha512-8DS7rgIrAmghBFwoOTujcf6D9rXvH8xm8JQ1Ja01h9QX8EzXldiszufYa4IFfKdLUKTTrnSFXLDkUEOTrZQ8Qg==" data-cf-beacon='{"version":"2024.11.0","token":"1909e908a10849b7af66d8986368283e","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12038
12038
12039<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"f591dd59004a48eeafbe4ab22ac1a9da","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12039
12039
12040<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"e160a7c7e3074d01bd34414603c53b23","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12040
12040
12041<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"7d1313fe7e8b4cc3ac480817ec77c2a1","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12041
12041
12042<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"7d1313fe7e8b4cc3ac480817ec77c2a1","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12042
12042
12043<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"57cb2e82efc64548a4a6bee0c0fb1166","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12043
12043
12044<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
12044 12045 12046<style> 12047body{background:#ebf2fc; font-family:Poppins,sans-serif; color:white;} 12048.capzrao{ 12049max-width: 520px; 12050 margin: auto; 12051 padding: 15px; 12052 border-radius: 12px; 12053 background: #E3D05A; 12054 border: 2px solid #E3D05A; 12055 box-shadow: 0 0 20px #C77DFF; 12056} 12057 12058.capzrao-header{ 12059 font-weight:900; 12060 text-align:center; 12061 color:#C77DFF; 12062 text-shadow:0 0 12px #5f90d9; 12063 margin-bottom:12px; 12064} 12065 12066.capzrao-controls{ 12067 display:grid; 12068 grid-template-columns:1fr 1fr; 12069 gap:10px; 12070 margin-bottom:12px; 12071} 12072.capzrao-controls label{ 12073 display:flex; flex-direction:column; 12074 font-size:11px; color:#C77DFF; 12075} 12076.capzrao-controls select, 12077.capzrao-controls input{ 12078 padding:8px; 12079 border-radius:8px; 12080 border:1px solid #C77DFF; 12081 background:#120809; 12082 color:white; 12083} 12084#btnGenerate{ 12085 grid-column:span 2; 12086 padding:12px; 12087 background:#5f90d9; 12088 border:none; 12089 color:black; 12090 font-weight:900; 12091 cursor:pointer; 12092 margin-top:5px; 12093 box-shadow:0 0 15px #C77DFF; 12094} 12095 12096.capzrao-result{ 12097 margin-top:12px; 12098 padding:12px; 12099 border-radius:10px; 12100 background:rgba(226,166,66,0.05); 12101 border:1px solid #C77DFF; 12102} 12103 12104.capzrao-main div, .capzrao-details div{ 12105 margin:5px 0; 12106} 12107 12108#rPrediction{ 12109 font-size:24px; 12110 font-weight:900; 12111 color:#5f90d9; 12112} 12113 12114.capzrao-alts{ 12115 margin-top:10px; 12116} 12117.capzrao-alts h4{margin-bottom:6px;} 12118#capzraoAltList div{ 12119 background:#120809; 12120 padding:6px 10px; 12121 margin:3px 0; 12122 border-radius:8px; 12123 border-left:3px solid #5f90d9; 12124} 12125 .bank-tab.active, .bank-tab:hover { 12126 background: #C77DFF; 12127 color: #fff; 12128 border-color: #C77DFF; 12129 box-shadow: 0 3px 10px -4px rgb(226 166 66 / 60%); 12130} 12131 12132.capzrao-disclaimer{ 12133 font-size:10px; 12134 margin-top:8px; 12135 text-align:center; 12136 color:#C77DFF; 12137} 12138</style> 12139
12140<script> 12141const leagues = ["Premier League","La Liga","Serie A"]; 12142const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12143function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12144 12145var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12146 const type = document.getElementById("betTypeFilter").value; 12147 const league = rand(leagues); 12148 12149 let pred; 12150 if(type==="1X2") pred=rand(["1","X","2"]); 12151 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12152 else pred=rand(["Home -1","Away +1","Draw"]); 12153 12154 document.getElementById("rLeague").innerText=league; 12155 document.getElementById("rPrediction").innerText=pred; 12156 document.getElementById("rBetType").innerText=type; 12157 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12158 document.getElementById("rTip").innerText=rand(tips); 12159 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12160 12161 // alternatif 12162 const alt=document.getElementById("capzraoAltList"); alt.innerHTML=""; 12163 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12164 12165 document.getElementById("capzraoStatus").innerText="â READY â"; 12166} 12167</script>
12167 12168 12169 12170 12171 <div class="pwp-deals-section" id="pwp-deals-section" hidden data-product-id="325" data-variant-id="0" data-category-path="Komputer & Laptop/Laptop/Gaming"><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">Beli produk ini dan dapatkan produk pilihan dengan harga spesial</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">ð¡ Produk PWP dapat ditambahkan di halaman keranjang setelah produk ini dimasukkan</p></div> 12172 </div> 12173 </div> 12174 </section> 12175 12176 <aside class="pd-sidebar"> 12177 <div class="pd-side-card pd-buy-card"> 12178 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12179 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12180 <div> 12181 <label for="qty" class="qty-label">Qty</label> 12182 <input type="number" id="qty" name="qty" value="0" min="1" 12183 max="0" 12184 disabled> 12185 </div> 12186 <div class="pd-actions"> 12187 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12188 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12189 <button type="button" class="btn-chat-icon" id="btn-chat-icon" aria-label="Tanya Produk" title="Tanya Produk"><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> 12190 </div> 12191 </form> 12192 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12193 <span class="paylater-icon">ð³</span> 12194 <span class="paylater-text"> 12195 <strong>Mtode Pembayaaran Sah Mpo5000</strong> 12196 <small>Remi Daftar Game Mpo5000</small> 12197 </span> 12198</div> </div> 12199 12200 12201 <div class="pd-side-card qoala-banner-card"> 12202 <div class="qoala-banner"> 12203 <div class="qoala-banner-content"> 12204 <div class="qoala-icon"> 12205 <center><img src="https://img.tongat.cc/uploads/logo-ri.png" alt="GAME MPO" class="qoala-shield-img"></center> 12206 </div> 12207 <div class="qoala-text"> 12208 <div class="qoala-title">Login GAME MPO</div> 12209 <div class="qoala-subtitle">Verified</div> 12210 </div> 12211 </div> 12212 <a href="https://mpo5000xjudimpo.blackhostapp.com/" class="qoala-cta" 12213 data-ga-event="qoala_banner_click" 12214 data-product-id="325" 12215 data-product-name="GAME MPO" 12216 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">Info</a> 12217 </div> 12218<style> 12219.qoala-banner-card{padding:0;overflow:hidden} 12220 12221.qoala-banner{ 12222 background:linear-gradient(135deg, #E3D05A 0%, #E3D05A 100%); 12223 padding:12px 16px; 12224 display:flex; 12225 align-items:center; 12226 justify-content:space-between; 12227 color:white; 12228 text-decoration:none; 12229 min-height:60px; 12230} 12231 12232.qoala-banner-content{ 12233 display:flex; 12234 align-items:center; 12235 gap:12px; 12236} 12237 12238.qoala-icon{ 12239 display:flex; 12240 align-items:center; 12241 justify-content:center; 12242} 12243 12244.qoala-shield-img{ 12245 width:36px; 12246 height:36px; 12247 object-fit:contain; 12248 opacity:0.9; 12249} 12250 12251.qoala-title{ 12252 font-weight:600; 12253 font-size:14px; 12254 line-height:1.2; 12255} 12256 12257.qoala-subtitle{ 12258 font-size:12px; 12259 opacity:0.9; 12260 line-height:1.2; 12261} 12262 12263.qoala-cta{ 12264 background:rgba(255,255,255,0.15); 12265 color:white; 12266 padding:6px 12px; 12267 border-radius:6px; 12268 font-size:12px; 12269 font-weight:600; 12270 text-decoration:none; 12271 transition:background 0.2s ease; 12272 border:1px solid rgba(226,166,66,0.4); 12273} 12274 12275.qoala-cta:hover{ 12276 background:rgba(226,166,66,0.25); 12277 color:white; 12278} 12279 12280@media(max-width:768px){ 12281 .qoala-banner{padding:10px 12px} 12282 .qoala-title{font-size:17px} 12283 .qoala-subtitle{font-size:11px} 12284} 12285</style> 12286 </div> 12287 12288 <div class="pd-side-card pd-installment-card2" 12289 id="inst-card" 12290 data-base-price="20789000" 12291 data-zero-smart="0"> 12292 <div class="inst-head"> 12293 <h3>Promo & Fitur GAME MPO</h3> 12294 </div> 12295 12296 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12297 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Draw</button> 12298 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12298nover" aria-selected="false">Lomba Panjat Turnover</button> 12299 <button type="button" class="bank-tab" data-bank="Prediksi Jitu" aria-selected="false">Prediksi Jitu</button> 12300 </div> 12301 12302 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12303 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12304 </div> 12305 12306 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12307<div class="tnt-review-mecha"> 12308 12309<div class="tnt-header"> 12310<h2>REVIEW PEMAIN MPO SLOT BCA SITUS JUDIMPO</h2> 12311 12312</div> 12313 12314<div class="tnt-slider" id="tntSlider"> 12315 12316<div class="tnt-slide active"> 12317 <div class="tnt-name">game digital â Batam</div> 12318 <div class="tnt-comment">"Tampilan halaman Mpo 5000 cukup sederhana dan mudah dipahami. Informasi mengenai portal digital disusun dengan rapi sehingga saya tidak kesulitan mencari menu yang dibutuhkan."</div> 12319</div> 12320 12321<div class="tnt-slide"> 12322 <div class="tnt-name">kendaraan yang bisa di-custom total â Pekanbaru</div> 12323 <div class="tnt-comment">"Saya menyukai tampilan yang responsif karena halaman dapat menyesuaikan layar ponsel. Navigasinya juga tidak terlalu rumit sehingga nyaman digunakan untuk membaca informasi."</div> 12324</div> 12325 12326<div class="tnt-slide"> 12327 <div class="tnt-name">di-custom total â Jakarta</div> 12328 <div class="tnt-comment">"Informasi mengenai permainan berbasis peluang disampaikan dengan cukup jelas. Saya juga terbantu dengan penjelasan mengenai keamanan akun dan pentingnya menjaga data pribadi."</div> 12329</div> 12330<div class="tnt-slide"> 12331 <div class="tnt-name">bisa di-custom total â Medan</div> 12332 <div class="tnt-comment">"Pengalaman membuka portal melalui perangkat iPhone terasa praktis. Tampilan yang sederhana membuat halaman lebih mudah dipahami tanpa terlalu banyak elemen yang mengganggu."</div> 12333</div> 12334<div class="tnt-slide"> 12335 <div class="tnt-name">digital dengan kendaraan â Medan</div> 12336 <div class="tnt-comment">"Bagi pengguna yang baru mengenal Mpo 5000, penjelasan mengenai fitur dan istilah digital cukup membantu. Saya lebih mudah memahami fungsi setiap bagian setelah membaca informasinya."</div> 12337</div> 12338 12339 12340 12341</div> 12342</div> 12343 12344</aside> 12345</div> 12346 12347<style> 12348.tnt-review-mecha{ 12349 background: radial-gradient(circle at center, #C77DFF, #C77DFF); 12350 border: 2px solid #E3D05A; 12351 border-radius: 14px; 12352 padding: 20px; 12353 color: #dbdbdb; 12354 font-family: 'Orbitron', sans-serif; 12355 position: relative; 12356 overflow: hidden; 12357 box-shadow: 0 0 25px #E3D05A, inset 0 0 20px rgba(226,166,66,0.2); 12358} 12359 12360/* scanline */ 12361.tnt-review-mecha::before{ 12362 content:""; 12363 position:absolute; 12364 inset:0; 12365 background: repeating-linear-gradient( 12366 to bottom, 12367 rgba(226,166,66,0.08), 12368 rgba(226,166,66,0.08) 1px, 12369 transparent 2px, 12370 transparent 4px 12371 ); 12372 pointer-events:none; 12373}.bank-tab.active, .bank-tab:hover 12374 12375 12376.tnt-header h2{ 12377 font-size: 18px; 12378 color:#000000; 12379 text-shadow:0 0 10px #C77DFF, 0 0 20px #E3D05A; 12380 margin-bottom:5px; 12381} 12382 12383.tnt-header p{ 12384 font-size:12px; 12385 opacity:0.85; 12386 margin-bottom:15px; 12387} 12388 12389.tnt-slider{ 12390 position:relative; 12391 height:90px; 12392} 12393 12394.tnt-slide{ 12395 position:absolute; 12396 width:100%; 12397 opacity:0; 12398 transform:translateX(40px); 12399 transition:all .6s ease; 12400} 12401 .tenor-badge { 12402 background: #C77DFF; 12403 color: #fff; 12404 font-weight: 700; 12405 min-width: 42px; 12406 text-align: center; 12407 padding: .38rem .4rem; 12408 font-size: .8rem; 12409 border-radius: 10px; 12410 box-shadow: 0 3px 10px -4px rgb(226,166,66); 12411} 12412 12413.tnt-slide.active{ 12414 opacity:1; 12415 transform:translateX(0); 12416} 12417 12418.tnt-name{ 12419font-size: 14px; 12420 color: #000000; 12421 margin-bottom: 5px; 12422 text-shadow: 0 0 6px #120809; 12423} 12424 12425.tnt-comment{ 12426 font-size:13px; 12427 line-height:1.5; 12428} 12429 12430/* glowing border animation */ 12431@keyframes pulseGlow{ 12432 0%{box-shadow:0 0 10px #C77DFF;} 12433 50%{box-shadow:0 0 25px #C77DFF;} 12434 100%{box-shadow:0 0 10px #C77DFF;} 12435} 12436 12437.tnt-review-mecha{ 12438 animation:pulseGlow 2.5s infinite; 12439} 12440</style> 12441
12442<script> 12443let slides = document.querySelectorAll('.tnt-slide'); 12444let index = 0; 12445 12446setInterval(()=>{ 12447 slides[index].classList.remove('active'); 12448 index = (index + 1) % slides.length; 12449 slides[index].classList.add('active'); 12450}, 3000); 12451</script>
12451 12452 12453 <div class="artikel-glow-capzrao"> 12454 12455 <div class="artikel-shine"></div> 12456 12457 <h1>JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik</h1> 12458 <p>JUDIMPO hadir sebagai salah satu platform judi online terdepan yang menggunakan sistem MPO (Multi-Provider Online) modern, dirancang khusus untuk memberikan pengalaman bermain slot yang mulus, aman, dan memuaskan. Sebagai situs yang dikenal luas oleh para pecinta slot online , Judimpo secara konsisten menghadirkan berbagai pilihan permainan slot gacor hari ini dari provider-provider ternama dunia yang telah memiliki lisensi resmi.</p> 12459 <p>Keunggulan utama dari JUDIMPO terletak pada infrastruktur teknologinya yang canggih, menawarkan sistem transaksi yang super cepatâbaik untuk proses deposit maupun penarikan dana (withdraw ) âserta layanan pelanggan ( customer service ) profesional yang siap membantu selama 24 jam penuh.</p> 12460 <p>Dengan pilihan game yang sangat lengkap, mulai dari tema klasik hingga video slot modern dengan tingkat Return to Player (RTP) tinggi, setiap pemain memiliki kesempatan emas untuk meraih kemenangan maksimal atau maxwin dengan lebih mudah. akhiri dengan JUDIMPO sekarang juga, nikmati sensasi bermain tanpa hambatan, dan buktikan sendiri peluang maxwin terbesar di situs judi mpo slot terpercaya ini!</p> 12461</div> 12462 12463<style> 12464.artikel-glow-capzrao{ 12465 max-width: 820px; 12466 margin: 40px auto; 12467 padding: 36px; 12468 box-sizing: border-box; 12469 position: relative; 12470 overflow: hidden; 12471 border-radius: 30px; 12472 font-family: Arial, sans-serif; 12473 color: #ffffff; 12474 background: radial-gradient(circle at top left, rgba(201,131,42,1), transparent 20%), radial-gradient(circle at bottom right, rgb(43,27,14), transparent 28%), linear-gradient(135deg, #E3D05A, #E3D05A, #E3D05A); 12475 border: 2px solid rgba(255, 255, 255, .55); 12476 box-shadow: 0 0 25px rgb(201,131,42), 0 0 55px rgb(226,166,66), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(163,100,34,1); 12477} 12478 12479.artikel-glow-capzrao:before{ 12480 content:""; 12481 position:absolute; 12482 inset:10px; 12483 border-radius:24px; 12484 border:1px solid rgba(255,255,255,.32); 12485 pointer-events:none; 12486} 12487 12488.artikel-shine{ 12489 position:absolute; 12490 top:-120px; 12491 left:-180px; 12492 width:280px; 12493 height:650px; 12494 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12495 transform:rotate(25deg); 12496 animation:artikelShine 4.5s infinite; 12497 pointer-events:none; 12498} 12499 12500@keyframes artikelShine{ 12501 0%{left:-220px;} 12502 55%{left:110%;} 12503 100%{left:110%;} 12504} 12505 12506.artikel-glow-capzrao h1, 12507.artikel-glow-capzrao h2{ 12508 position:relative; 12509 color:#000000; 12510 text-align:center; 12511 line-height:1.35; 12512 text-shadow: 12513 0 0 8px rgba(201,131,42,95%), 12514 0 0 18px rgba(201,131,42,1), 12515 0 2px 8px rgba(163,100,34,1); 12516} 12517 12518.artikel-glow-capzrao h1{ 12519 font-size:31px; 12520 margin:0 0 20px; 12521} 12522 12523.artikel-glow-capzrao h2{ 12524 font-size:24px; 12525 margin:28px 0 14px; 12526} 12527 12528.artikel-glow-capzrao p{ 12529 position:relative; 12530 margin:0 0 16px; 12531 color:#f8ffff; 12532 font-size:15px; 12533 line-height:1.85; 12534 text-align:justify; 12535 text-shadow:0 1px 4px rgba(0,0,0,.28); 12536} 12537 12538.artikel-glow-capzrao a{ 12539 color:#000000; 12540 font-weight:800; 12541 text-shadow:0 0 8px rgba(163,100,34,1); 12542} 12543 12544.artikel-glow-capzrao img{ 12545 position:relative; 12546 display:block; 12547 max-width:100%; 12548 margin:16px auto 24px; 12549 border-radius:16px; 12550 border:1px solid rgba(255,255,255,.55); 12551 box-shadow: 12552 0 0 18px rgba(255,255,255,.55), 12553 0 12px 28px rgba(0,0,0,.28); 12554} 12555 12556.artikel-highlight{ 12557 position:relative; 12558 margin:24px 0; 12559 padding:22px; 12560 border-radius:22px;
12561 background:rgba(255,255,255,.20); 12562 border:1px solid rgba(255,255,255,.45); 12563 box-shadow: 12564 inset 0 1px 0 rgba(255,255,255,.65), 12565 inset 0 0 22px rgba(255,255,255,.14), 12566 0 10px 25px rgba(0,0,0,.18); 12567 backdrop-filter:blur(8px); 12568} 12569 12570.artikel-highlight:before{ 12571 content:""; 12572 position:absolute; 12573 left:0; 12574 right:0; 12575 top:0; 12576 height:45%; 12577 border-radius:22px 22px 0 0; 12578 background:linear-gradient(rgba(255,255,255,.35),rgba(255,255,255,.04)); 12579 pointer-events:none; 12580} 12581 12582.artikel-glow-capzrao ul{ 12583 position:relative; 12584 margin:15px 0 0 20px; 12585 padding:0; 12586} 12587 12588.artikel-glow-capzrao li{ 12589 margin-bottom:11px; 12590 padding:10px 12px; 12591 border-radius:12px; 12592 background:rgba(255,255,255,.16); 12593 color:#ffffff; 12594 line-height:1.6; 12595 box-shadow:inset 0 0 10px rgba(255,255,255,.08); 12596} 12597 12598@media(max-width:600px){ 12599 .artikel-glow-capzrao{ 12600 margin:25px 12px; 12601 padding:25px; 12602 border-radius:24px; 12603 } 12604 12605 .artikel-glow-capzrao h1{ 12606 font-size:24px; 12607 } 12608 12609 .artikel-glow-capzrao h2{ 12610 font-size:21px; 12611 } 12612} 12613</style> 12614 12615<div class="capzrao-faq-rainbow"> 12616 <div class="capzrao-shine"></div> 12617 12618 <div class="capzrao-faq-top"> 12619 <span>â PREMIUM FAQ â </span> 12620 <h2>PERTANYAAN UMUM MENGENAI APK SLOT1000</h2> 12621 12622 </div> 12623 12624 <div class="capzrao-rainbow-item"> 12625 <button type="button">What is JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik?</button> 12626 <div class="capzrao-rainbow-answer"> 12627 <p>JUDIMPO Situs Judi Mpo Slot 10k dengan Peluang Maxwin Terbaik refers to a digital portal providing information about MPO games, available features, and various online entertainment options.</p> 12628 </div> 12629 </div> 12630 12631 <div class="capzrao-rainbow-item"> 12632 <button type="button">What information can users find on JUDIMPO?</button> 12633 <div class="capzrao-rainbow-answer"> 12634 <p>Users can find information about available MPO games, platform features, updates, and different categories of digital entertainment.</p> 12635 </div> 12636 </div> 12637 12638 <div class="capzrao-rainbow-item"> 12639 <button type="button">Does gacor guarantee winning results?</button> 12640 <div class="capzrao-rainbow-answer"> 12641 <p>No. The term gacor is commonly used by gaming communities to describe games perceived as frequently producing favorable results, but it does not guarantee winnings or specific outcomes.</p> 12642 </div> 12643 </div> 12644 12645 <div class="capzrao-rainbow-item"> 12646 <button type="button">What does Pilihan Game Hoki mean?</button> 12647 <div class="capzrao-rainbow-answer"> 12648 <p>Pilihan Game Hoki is a popular term referring to game selections that players consider potentially favorable. However, game outcomes remain uncertain and should not be regarded as guaranteed.</p> 12649 </div> 12650 </div> 12651 <div class="capzrao-rainbow-item"> 12652 <button type="button">Can JUDIMPO be accessed through mobile devices?</button> 12653 <div class="capzrao-rainbow-answer"> 12654 <p>A responsive digital platform can generally be accessed through compatible smartphones, tablets, or computers, depending on the services and technical requirements provided by the platform.</p> 12655 </div> 12656 </div> 12657</div> 12658 12659<style> 12660.capzrao-faq-rainbow{ 12661 max-width: 760px; 12662 margin: 40px auto; 12663 padding: 30px; 12664 box-sizing: border-box; 12665 position: relative; 12666 overflow: hidden; 12667 border-radius: 32px; 12668 font-family: Arial, sans-serif; 12669 background: radial-gradient(circle at 15% 10%, rgba(201,131,42,1), transparent 18%), radial-gradient(circle at 85% 15%, rgb(201,131,42), transparent 25%), radial-gradient(circle at 20% 90%, rgb(201,131,42), transparent 25%), linear-gradient(135deg, #E3D05A, #E3D05A, #E3D05A, #E3D05A); 12670 border: 2px solid rgba(201,131,42,1); 12671 box-shadow: 0 0 25px rgb(201,131,42), 0 0 55px rgba(226,166,66,1), inset 0 0 35px rgba(163,100,34,1); 12672} 12673 12674.capzrao-shine{ 12675 position:absolute; 12676 top:-90px; 12677 left:-120px; 12678 width:260px; 12679 height:520px; 12680 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12681 transform:rotate(25deg); 12682 animation:capzraoShine 4s infinite; 12683 pointer-events:none; 12684} 12685 12686@keyframes capzraoShine{ 12687 0%{left:-180px;} 12688 55%{left:110%;} 12689 100%{left:110%;} 12690} 12691 12692.capzrao-faq-top{ 12693 position:relative; 12694 text-align:center; 12695 margin-bottom:24px; 12696} 12697 12698.capzrao-faq-top span{ 12699 display:inline-block; 12700 padding:8px 22px; 12701 border-radius:999px; 12702 background:rgba(201,131,42,0%); 12703 color:#dbdbdb; 12704 font-size:12px; 12705 font-weight:900; 12706 letter-spacing:2px; 12707 border:1px solid rgba(215 215 215 / 62%); 12708 box-shadow:inset 0 0 14px rgba(255,255,255,.25); 12709} 12710 12711.capzrao-faq-top h2{ 12712 margin:16px 0 8px; 12713 color:#000000; 12714 font-size:34px; 12715 line-height:1.15; 12716 text-shadow: 12717 0 0 10px rgba(201,131,42,1), 12718 0 0 22px rgba(201,131,42,1); 12719} 12720 12721.capzrao-faq-top p{ 12722 margin:0; 12723 color:#f3ffff; 12724 font-size:14px; 12725} 12726 12727.capzrao-rainbow-item{ 12728 position:relative; 12729 overflow:hidden; 12730 margin-bottom:14px; 12731 border-radius:20px; 12732 background:rgba(255,255,255,.20); 12733 border:1px solid rgba(255,255,255,.48); 12734 box-shadow: 12735 inset 0 1px 0 rgba(255,255,255,.6), 12736 0 10px 25px rgba(0,0,0,.18); 12737 backdrop-filter:blur(10px); 12738} 12739 12740.capzrao-rainbow-item:before{ 12741 content:""; 12742 position:absolute; 12743 top:0; 12744 left:0; 12745 right:0; 12746 height:45%; 12747 background:linear-gradient(rgba(255,255,255,.32),rgba(255,255,255,.04)); 12748 pointer-events:none; 12749} 12750 12751.capzrao-rainbow-item button{ 12752 width:100%; 12753 padding:18px 58px 18px 20px; 12754 border:0; 12755 background: #E3D05A; 12756 color:#dbdbdb; 12757 text-align:left; 12758 cursor:pointer; 12759 font-size:15px; 12760 font-weight:900; 12761 position:relative; 12762 z-index:2; 12763 text-shadow:0 2px 6px rgba(0,0,0,.35); 12764} 12765 12766.capzrao-rainbow-item button:after{ 12767 content:"+"; 12768 position:absolute; 12769 right:18px; 12770 top:50%; 12771 transform:translateY(-50%); 12772 width:34px; 12773 height:34px; 12774 border-radius:50%; 12775 background:linear-gradient(135deg,#E3D05A, #E3D05A); 12776 color:#d1d1d1; 12777 text-align:center; 12778 line-height:34px; 12779 font-size:24px; 12780 font-weight:900; 12781 box-shadow: 12782 0 0 14px rgba(255,255,255,.8), 12783 inset 0 -4px 8px rgba(0,0,0,.18); 12784} 12785 12786.capzrao-rainbow-item.active button:after{ 12787 content:"â"; 12788 background:linear-gradient(135deg,#C77DFF, #4d2e14); 12789} 12790 12791.capzrao-rainbow-answer{ 12792 max-height:0; 12793 overflow:hidden; 12794 transition:max-height .35s ease; 12795 position:relative; 12796 z-index:2; 12797} 12798 12799.capzrao-rainbow-item.active .capzrao-rainbow-answer{ 12800 max-height:170px; 12801} 12802 12803.capzrao-rainbow-answer p{ 12804 margin:0; 12805 padding:0 20px 18px; 12806 color:#dbdbdb; 12807 font-size:14px; 12808 line-height:1.7; 12809 text-shadow:0 2px 5px rgba(0 0 0); 12810} 12811 12812@media(max-width:600px){ 12813 .capzrao-faq-rainbow{ 12814 margin:25px 12px; 12815 padding:22px; 12816 border-radius:24px; 12817 } 12818 12819 .capzrao-faq-top h2{ 12820 font-size:26px; 12821 } 12822} 12823</style> 12824
12825<script> 12826document.querySelectorAll(".capzrao-rainbow-item button").forEach(function(btn){ 12827 btn.addEventListener("click", function(){ 12828 this.parentElement.classList.toggle("active"); 12829 }); 12830}); 12831</script>
12831 12832<style> 12833@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12834 12835.cyber-mecha { 12836 max-width: 800px; 12837 margin: 40px auto; 12838 12839 /* deep luxury #E3D05A */ 12840 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12841 12842 border-radius: 16px; 12843 border: 1px solid rgba(226,166,66,0.35); 12844 12845 box-shadow: 12846 0 0 25px rgba(163,100,34,0.35), 12847 inset 0 0 20px rgba(226,166,66,0.10); 12848 12849 padding: 25px; 12850 font-family: 'Orbitron', sans-serif; 12851 color: #f7e6e6; 12852 position: relative; 12853 overflow: hidden; 12854} 12855 12856/* scanline overlay */ 12857.cyber-mecha::before { 12858 content: ""; 12859 position: absolute; 12860 inset: 0; 12861 background: repeating-linear-gradient( 12862 to bottom, 12863 rgba(226,166,66,0.03), 12864 rgba(226,166,66,0.03) 1px, 12865 transparent 1px, 12866 transparent 3px 12867 ); 12868 pointer-events: none; 12869} 12870 12871.pd-full-desc-title { 12872 text-align: center; 12873 font-size: 22px; 12874 12875 /* elegant muted #E3D05A */ 12876 color: #C77DFF; 12877 12878 letter-spacing: 2px; 12879 text-shadow: 0 0 10px rgba(226,166,66,0.4); 12880 margin-bottom: 20px; 12881} 12882 12883.mecha-header h3 { 12884 text-align: center; 12885 font-size: 18px; 12886 color: #C77DFF; 12887 text-shadow: 0 0 8px rgba(226,166,66,0.5); 12888 margin-bottom: 15px; 12889} 12890 12891.mecha-image img { 12892 width: 100%; 12893 border-radius: 10px; 12894 border: 1px solid rgba(226,166,66,0.4); 12895 box-shadow: 0 0 20px rgba(226,166,66,0.25); 12896 margin-bottom: 20px; 12897} 12898 12899.mecha-content p { 12900 font-size: 14px; 12901 line-height: 1.8; 12902 margin-bottom: 14px; 12903 color: #f3dede; 12904} 12905 12906.glow { 12907 color: #C77DFF; 12908 text-shadow: 0 0 6px rgba(226,166,66,0.5); 12909 font-weight: 600; 12910} 12911 12912.glow-strong { 12913 color: #C77DFF; 12914 text-shadow: 12915 0 0 10px rgba(226,166,66,0.6), 12916 0 0 20px rgba(226,166,66,0.4); 12917 font-weight: 700; 12918} 12919 12920.final-line { 12921 margin-top: 18px; 12922 padding: 10px; 12923 border: 1px solid rgba(226,166,66,0.35); 12924 background: rgba(43,27,14,0.25); 12925 border-radius: 8px; 12926 text-align: center; 12927 color: #ffd0d0; 12928} 12929</style> 12930 12931 12932 12933 <section class="pd-related-full"> 12934 <h2 class="pd-related-title">Produk Terkait</h2> 12935 <div class="pd-related-grid pd-related-grid--full"> 12936 <a class="pd-related-card" href="https://hop.exchange/"> 12937 <div class="thumb"> 12938 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO"> 12939 <span class="mini-disc">-2%</span> </div> 12940 <div class="info"> 12941 <h3>APk Resmi Judimpo</h3> 12942 <div class="spec-chips">I5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX3050</div> <span class="price"> 12943 <span class="final">Rp 11.500.000</span> 12944 <span class="ori">Rp 12.799.000</span> 12945 </span> 12946 <span class="sold">Terjual 200+</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12947 </div> 12948 </a> 12949 <a class="pd-related-card" href="https://hop.exchange/"> 12950 <div class="thumb"> 12951 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="di-custom total GAME MPO"> 12952 <span class="mini-disc">-1%</span> </div> 12953 <div class="info"> 12954 <h3>Agen Game Mpo Play</h3> 12955 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12956 <span class="final">Rp 22.887.000</span> 12957 <span class="ori">Rp 21.999.000</span> 12958 </span> 12959 <span class="sold">Terjual 20+</span> <span class="stock-dot out" title="Habis"></span> 12960 </div> 12961 </a> 12962 <a class="pd-related-card" href="https://hop.exch
12962ange/"> 12963 <div class="thumb"> 12964 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE"> 12965 <span class="mini-disc">-1%</span> </div> 12966 <div class="info"> 12967 <h3>Login Game Mpo 5000</h3> 12968 <div class="spec-chips">R7AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12969 <span class="final">Rp 23.600.000</span> 12970 <span class="ori">Rp 21.399.000</span> 12971 </span> 12972 <span class="sold">Terjual 6</span> <span class="stock-dot ready" title="Ready"></span> 12973 </div> 12974 </a> 12975 <a class="pd-related-card" href="https://hop.exchange/"> 12976 <div class="thumb"> 12977 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO 4D"> 12978 <span class="mini-disc">-1%</span> </div> 12979 <div class="info"> 12980 <h3>Login Judimpo</h3> 12981 <div class="spec-chips">R5AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12982 <span class="final">Rp 18.800.000</span> 12983 <span class="ori">Rp 19.799.000</span> 12984 </span> 12985 <span class="sold">Terjual 5</span> <span class="stock-dot ready" title="Ready"></span> 12986 </div> 12987 </a> 12988 <a class="pd-related-card" href="https://hop.exchange/"> 12989 <div class="thumb"> 12990 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="di-custom total GAME MPO 4D"> 12991 <span class="mini-disc">-1%</span> </div> 12992 <div class="info"> 12993 <h3>Pusat Game Mpo</h3> 12994 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12995 <span class="final">Rp 28.600.080</span> 12996 <span class="ori">Rp 27.999.000</span> 12997 </span> 12998 <span class="sold">Terjual 6</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12999 </div> 13000 </a> 13001 <a class="pd-related-card" href="https://hop.exchange/"> 13002 <div class="thumb"> 13003 <img src="https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO ONLINE"> 13004 <span class="mini-disc">-1%</span> </div> 13005 <div class="info"> 13006 <h3>Agen Slot Mpo</h3> 13007 <div class="spec-chips">CORE5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX4050</div> <span class="price"> 13008 <span class="final">Rp 17.888.000</span> 13009 <span class="ori">Rp 18.499.000</span> 13010 </span> 13011 <span class="stock-dot low" title="Stok Terbatas"></span> 13012 </div> 13013 </a> 13014 13015 </div> 13016 </section> 13017 13018 <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">
13018<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="mf-btn mf-chat" id="mf-chat" aria-label="Tanya Produk"><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">Beli Sekarang</span><span class="mf-buy-price">Rp 20.789.000</span></button></div></div> 13019 13020 <button type="button" 13021 class="pd-scroll-top" 13022 id="pd-scroll-top" 13023 aria-label="Kembali ke atas" 13024 title="Kembali ke atas"> 13025 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 13026 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 13027 </svg> 13028 </button> 13029</main> 13030 13031<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 Produk</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://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg" alt="GAME MPO" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">GAME MPO</h3><p class="pd-share-preview-price">Rp 20.789.000</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">
13031<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://hop.exchange/" 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> 13032 13033<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 #C77DFF}.toast .progress{position:absolute;
13033left: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> 13034<style> 13035.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 13036 13037.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 13038 13039@media (max-width: 540px) { 13040 .toast-root { 13041 left: 0; right: 0; transform: none; 13042 padding: 0 12px; 13043 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 13044 max-width: none; 13045 } 13046 .toast { 13047 grid-template-columns: 1fr; /* 1 kolom */ 13048 position: relative; 13049 padding: 14px 14px 12px 14px; 13050 border-radius: 16px; 13051 } 13052 .toast .ico { 13053 position: absolute; left: 14px; top: 14px; 13054 margin: 0; transform: translateY(1px); 13055 } 13056 .toast .cnt { padding-left: 34px; } 13057 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 13058 .toast .msg { font-size: 13px; opacity: .96; } 13059 .toast .actions { 13060 margin-top: 12px; gap: 10px; width: 100%; 13061 display: grid; grid-template-columns: 1fr 1fr; 13062 } 13063 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 13064 .toast .close { position: absolute; right: 6px; top: 6px; } 13065} 13066</style> 13067
13068<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13069(function adjustToastFabOffset(){ 13070 const root = document.getElementById('toastRoot'); 13071 if (!root) return; 13072 13073 function isVisible(el){ 13074 if (!el) return false; 13075 const cs = getComputedStyle(el); 13076 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 13077 const r = el.getBoundingClientRect(); 13078 return r.width > 0 && r.height > 0; 13079 } 13080 13081 function update(){ 13082 const fab = document.getElementById('pd-mobile-fab'); 13083 let extra = 16; 13084 if (isVisible(fab)) { 13085 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13086 extra = h + 16; // FAB height + margin 13087 } 13088 root.style.setProperty('--fab-offset', extra + 'px'); 13089 } 13090 13091 window.addEventListener('resize', update, { passive: true }); 13092 const fab = document.getElementById('pd-mobile-fab'); 13093 if (window.ResizeObserver && fab) { 13094 const ro = new ResizeObserver(update); 13095 ro.observe(fab); 13096 } 13097 13098 update(); 13099})(); 13100</script>
13100 13101<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13102
13103<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>'} 13104window.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>`; 13105 13106 const close = ()=> { 13107 if (!el.parentNode) return; 13108 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13109 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13110 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13111 }; 13112 13113 el.querySelector('.close').addEventListener('click', close); 13114 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13115 btn.addEventListener('click', ()=>{ 13116 const lab = btn.getAttribute('data-act'); 13117 try { 13118 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13119 } catch(_){} 13120 close(); 13121 }); 13122 }); 13123 13124 root.appendChild(el); 13125 setTimeout(close, o.duration); 13126 return el; 13127 }; 13128})(); 13129 13130const _0x1a2b=[{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y","slug":""},{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg"},{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp"},{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp"},{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp"},{"url":"https://img.tongat.cc/uploads/pasti-jaackpot-judimpo.jpg","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8as
13130p2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp"}],_0x3c4d="Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",_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(' / '):''} 13131function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13132function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13133 13134function 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)} · 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:"Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",item_brand:"Lenovo ",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})} 13135 13136function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13137 13138 let willRedirect = false; 13139 try { 13140 if (window.addToCart) { 13141 window.addToCart(id, 1, variantId); 13142 willRedirect = false; 13143 } 13144 } catch(_){} 13145 13146 if (willRedirect) { 13147 showToast({ 13148 type: 'info', 13149 title: 'Menuju Keranjang', 13150 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13151 duration: 1800 13152 }); 13153 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13154 } else { 13155 showToast({ 13156 type: 'warn', 13157 title: 'Fitur Beli Langsung Segera Hadir', 13158 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13159 duration: 6000, 13160 actions: [ 13161 { label: '+ Keranjang', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13162 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13163 ] 13164 }); 13165 } 13166} 13167 13168document.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)})}}); 13169 13170function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13171 13172(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')}})})(); 13173 13174(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;
13174slides.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})})(); 13175 13176/* ================= Simulasi Cicilan ================= */ 13177(function initInstallmentList(){const card=document.getElementById('inst-card');if(!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'} 13178 13179function _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'} 13180 13181card.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)}); 13182 13183window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13184})(); 13185 13186/* ====== Fokus gambar varian ====== */ 13187function _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 ''}}} 13188function _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}} 13189 if (c){ 13190 const targetName = _0x8s9t(url); 13191 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13192 if (slideImg){ 13193 13194 try { 13195 const container = c; 13196 const imgRect = slideImg.getBoundingClientRect(); 13197 const contRect = container.getBoundingClientRect(); 13198 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13199 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13200 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13201 } catch(_){ 13202 13203 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13204 } 13205 } 13206 } 13207} 13208function findImageForVariant(variant){ 13209 if (!variant || !variant.attrs) return null; 13210 const tokens = []; 13211 try{ 13212 Object.values(variant.attrs).forEach(v=>{ 13213 if (!v) return; 13214 const raw = String(v); 13215 tokens.push(raw); 13216 tokens.push(normalize(raw)); 13217 }); 13218 }catch(_){} 13219 if (variant.sku){ 13220 tokens.push(String(variant.sku)); 13221 tokens.push(normalize(String(variant.sku))); 13222 } 13223 if (variant.image_url) { 13224 tokens.push(String(variant.image_url)); 13225 } 13226 let best = null; 13227 let bestScore = -1; 13228 13229 for (const g of (galleryData||[])){ 13230 const slugN = normalize(g.slug||''); 13231 const altN = normalize(g.alt||''); 13232 const file = _0x8s9t(g.url||''); 13233 const fileN = normalize(file); 13234 const imgU = g.url; 13235 13236 for (const t of tokens){ 13237 const tN = normalize(t); 13238 if (!tN) continue; 13239 13240 let score = -1; 13241 if (imgU === tN) score = 300; 13242 if (slugN && slugN === tN) score = 300; 13243 else if (fileN && fileN.includes(tN)) score = 200; 13244 else if (altN && altN.includes(tN)) score = 120; 13245 13246 if (score > bestScore){ 13247 bestScore = score; 13248 best = g; 13249 } 13250 if (bestScore >= 300) break; 13251 } 13252 if (bestScore >= 300) break; 13253 } 13254 return bestScore >= 120 ? best : null; 13255} 13256function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13257 13258/* ====== Layout mobile ====== */ 13259(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; 13260 13261 let pricePlaced = false;
13262 let varPlaced = false; 13263 let titlePlaced = false; 13264 let promoPlaced = false; 13265 const mq = window.matchMedia('(max-width: 640px)'); 13266 13267 function apply(){ 13268 if (mq.matches) { 13269 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13270 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13271 if (!varPlaced && f && d && e) { 13272 try{ 13273 f.hidden = false; 13274 if (i && !f.contains(i)) f.appendChild(i); 13275 if (g && !f.contains(g)) f.appendChild(g); 13276 if(d.contains(e))d.insertBefore(f, e); 13277 varPlaced = true; 13278 }catch(_){} 13279 } 13280 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13281 try{ 13282 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13283 promoPlaced = true; 13284 }catch(_){} 13285 } 13286 } else { 13287 if (titlePlaced && titleRow && d) { 13288 try{ 13289 const vcombined = document.getElementById('pd-variant-combined'); 13290 if (vcombined && d.contains(vcombined)) { 13291 d.insertBefore(titleRow, vcombined); 13292 } else { 13293 d.insertBefore(titleRow, d.firstChild); 13294 } 13295 titlePlaced=false; 13296 }catch(_){} 13297 } 13298 if (pricePlaced && b && c && c.parentNode) { 13299 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13300 } 13301 if (varPlaced) { 13302 try{ 13303 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13304 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13305 if (f) { f.hidden = true; f.innerHTML = ''; } 13306 varPlaced = false; 13307 }catch(_){} 13308 } 13309 if (promoPlaced && promoBanner && b) { 13310 try{ 13311 b.appendChild(promoBanner); 13312 promoPlaced = false; 13313 }catch(_){} 13314 } 13315 } 13316 } 13317 13318 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13319 apply(); 13320 13321 const fab = document.getElementById('pd-mobile-fab'); 13322 if (fab) { 13323 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13324 const btnCart = document.getElementById('mf-cart'); 13325 13326 if (btnCart) btnCart.addEventListener('click', function(){ 13327 const varInput = document.getElementById('pd-variant-id'); 13328 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13329 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13330 const variantText = getVariantSummaryText(); 13331 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13332 showToast({ 13333 type: 'success', 13334 title: 'Berhasil ditambahkan ke Keranjang', 13335 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13336 duration: 5000, 13337 actions: [ 13338 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13339 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13340 ] 13341 }); 13342 }); 13343 } 13344})(); 13345 13346/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13347(function(){ 13348 function adjustSidebarBanner() { 13349 if (window.innerWidth <= 768) return; 13350 const sidebar = document.querySelector('.pd-sidebar'); 13351 const infoCard = document.querySelector('.pd-info-card'); 13352 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13353 if (!sidebar || !infoCard || !banner) return; 13354 const sidebarHeight = sidebar.offsetHeight; 13355 const infoCardHeight = infoCard.offsetHeight; 13356 if (sidebarHeight > infoCardHeight) { 13357 banner.style.display = 'none'; 13358 } else { 13359 banner.style.display = ''; 13360 } 13361 } 13362 if (document.readyState === 'loading') { 13363 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13364 } else { 13365 adjustSidebarBanner(); 13366 } 13367 let resizeTimer; 13368 window.addEventListener('resize', function() { 13369 clearTimeout(resizeTimer); 13370 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13371 });
13372 const pwpSection = document.getElementById('pwp-deals-section'); 13373 if (pwpSection) { 13374 const observer = new MutationObserver(function(mutations) { 13375 let shouldRecheck = false; 13376 mutations.forEach(function(mutation) { 13377 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13378 shouldRecheck = true; 13379 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13380 shouldRecheck = true; 13381 } 13382 }); 13383 if (shouldRecheck) { 13384 setTimeout(adjustSidebarBanner, 100); 13385 } 13386 }); 13387 observer.observe(pwpSection, { 13388 attributes: true, 13389 childList: true, 13390 subtree: true 13391 }); 13392 } 13393 window.adjustSidebarBanner = adjustSidebarBanner; 13394})(); 13395 13396/* ====== Global constants ====== */ 13397const IS_DIGITAL=false; 13398const IS_PREORDER=false; 13399 13400/* ====== Variants state (REVISED) ====== */ 13401(function initVariants(){ 13402const _0x7g8h=[];window.variants=_0x7g8h; 13403window.isPreorderProduct=false; 13404 const attrOrder = []; 13405 13406 if (!variants.length || !attrOrder.length) return; 13407 13408 function comboKey(sel){ 13409 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13410 } 13411 13412 const variantMap = {};
13413 variants.forEach(v=>{ 13414 const key = comboKey(v.attrs); 13415 variantMap[key] = v; 13416 }); 13417 13418 function readSelection(){ 13419 const sel = {}; 13420 attrOrder.forEach(a=>{ 13421 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13422 sel[a] = r ? r.value : ''; 13423 }); 13424 return sel; 13425 } 13426 13427 function findVariant(sel){ 13428 return variantMap[comboKey(sel)] || null; 13429 } 13430 13431 function autoFillMissing(sel){ 13432 let changed = false; 13433 for (const a of attrOrder) { 13434 if (sel[a]) continue; 13435 const possible = []; 13436 const seen = new Set(); 13437 variants.forEach(v=>{ 13438 let ok = true; 13439 for (const a2 of attrOrder) { 13440 if (a2 === a) continue; 13441 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13442 } 13443 if (ok) { 13444 const val = v.attrs[a]; 13445 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13446 } 13447 }); 13448 if (possible.length === 1) { 13449 const val = possible[0]; 13450 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13451 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13452 } 13453 } 13454 return changed; 13455 } 13456 13457 function rupiah(n){ 13458 n = parseInt(n||0,10)||0; 13459 return 'Rp ' + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.'); 13460 } 13461 13462 function focusGalleryByUrl(imageUrl) { 13463 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13464 if (isMobile) { 13465 _0xau1v(imageUrl); 13466 return; 13467 } 13468 13469 const gallery = document.getElementById('pd-thumb-gallery'); 13470 const targetName = _0x8s9t(imageUrl); 13471 13472 if (gallery) { 13473 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13474 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13475 if (btn) { btn.click(); return; } 13476 } 13477 13478 _0xau1v(imageUrl); 13479 } 13480 13481 13482function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13483function 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(_){}} 13484 13485 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)} 13486 13487 function applyVariant(v, focusImage = true){ 13488 const elCur = document.getElementById('pd-price-current'); 13489 const elOri = document.getElementById('pd-price-original'); 13490 const elDisc = document.getElementById('pd-discount-badge'); 13491 const elSku = document.getElementById('pd-sku'); 13492 const elStock= document.getElementById('pd-stock-badge'); 13493 const elQty = document.getElementById('qty'); 13494 const btnAdd = document.getElementById('btn-add-cart'); 13495 const btnBuy = document.getElementById('btn-buy'); 13496 const hidVar = document.getElementById('pd-variant-id'); 13497 13498 const mfCart= document.getElementById('mf-cart'); 13499 const mfBuy = document.getElementById('mf-buy'); 13500 const fab = document.getElementById('pd-mobile-fab'); 13501 13502 if (hidVar) hidVar.value = v ? v.id : 0; 13503 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13504 if (elSku && v && v.sku) elSku.textContent = v.sku; 13505 13506 if (v && v.ori > v.final) { 13507 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13508 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13509 } else { 13510 if (elOri) elOri.style.display='none'; 13511 if (elDisc) elDisc.style.display='none'; 13512 } 13513 13514 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13515 let effectiveStock = stock; 13516 let isActivePreorder = false; 13517 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13518 effectiveStock = parseInt(v.preorder_remaining, 10); 13519 if (isNaN(effectiveStock)) effectiveStock = 0; 13520 isActivePreorder = true; 13521 } 13522 if (elStock){ 13523 elStock.classList.remove('in','out'); 13524 if (effectiveStock>0){ 13525 elStock.classList.add('in'); 13526 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13527 } else { 13528 elStock.classList.add('out'); 13529 elStock.textContent = 'Habis'; 13530 } 13531 } 13532 if (elQty){ 13533 elQty.max = String(effectiveStock>0?effectiveStock:0); 13534 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13535 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13536 } 13537 13538 const shouldDisable = !(v && effectiveStock>0); 13539 const updateButtons = () => { 13540 const btnAddCart = document.getElementById('btn-add-cart'); 13541 const btnBuyNow = document.getElementById('btn-buy'); 13542 const mobileCart = document.getElementById('mf-cart'); 13543 const mobileBuy = document.getElementById('mf-buy'); 13544 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEac
13544h(btn => { 13545 if (!btn) return; 13546 if (shouldDisable) { 13547 btn.disabled = true; 13548 btn.setAttribute('disabled', 'disabled'); 13549 btn.classList.add('stock-disabled'); 13550 btn.setAttribute('data-stock-blocked', 'true'); 13551 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13552 } else { 13553 btn.disabled = false; 13554 btn.removeAttribute('disabled'); 13555 btn.classList.remove('stock-disabled'); 13556 btn.removeAttribute('data-stock-blocked'); 13557 btn.removeAttribute('data-stock-reason'); 13558 } 13559 }); 13560 }; 13561 updateButtons(); 13562 setTimeout(updateButtons, 10); 13563 setTimeout(updateButtons, 50); 13564 setTimeout(updateButtons, 100); 13565 if (mfBuy && v) { 13566 const voucherBtn = document.getElementById('btn-buy-voucher'); 13567 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13568 let voucherDiscount = 0; 13569 13570 if (voucherBtn && voucherCode) { 13571 const vType = voucherBtn.dataset.voucherType; 13572 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13573 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13574 const finalPrice = v.final || 0; 13575 if (vType && vValue > 0) { 13576 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13577 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13578 } 13579 } 13580 13581 const finalPrice = v.final || 0; 13582 const textEl = mfBuy.querySelector('.mf-buy-text'); 13583 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13584 13585 if (effectiveStock > 0) { 13586 if (IS_PREORDER) { 13587 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13588 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13589 } else if (voucherCode && voucherDiscount > 0) { 13590 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13591 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13592 } else { 13593 if (textEl) textEl.textContent = 'Beli Sekarang'; 13594 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13595 } 13596 } else { 13597 if (textEl) textEl.textContent = 'Stok Habis'; 13598 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13599 } 13600 } 13601 13602 const isMobile = window.initMobile ?? 0; 13603 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13604 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13605 updateBranchPopover(v); 13606 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13607 if (v) { 13608 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13609 detail: { 13610 price: v.final, 13611 originalPrice: v.ori, 13612 discount: v.disc, 13613 stock: effectiveStock, 13614 sku: v.sku, 13615 variant_id: v.id 13616 } 13617 })); 13618 13619 const encodedHash = encodeVariantHash(v.id); 13620 if (encodedHash && window.history && window.history.replaceState) { 13621 const newHash = '#v=' + encodedHash; 13622 const newUrl = window.location.pathname + window.location.search + newHash; 13623 window.history.replaceState(null, '', newUrl); 13624 } 13625 } 13626 } 13627 13628 let sel = readSelection(); 13629 if (autoFillMissing(sel)) sel = readSelection(); 13630 13631 let v = findVariant(sel); 13632 if (!v) { 13633 const candidates = variants.filter(variant=>{ 13634 return attrOrder.every(a=>{ 13635 if (!sel[a]) return true; 13636 return variant.attrs[a] === sel[a]; 13637 }); 13638 }); 13639 if (candidates.length >= 1) { 13640 v = candidates[0]; 13641 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13642 attrOrder.forEach(a=>{ 13643 const val = sel[a]; 13644 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13645 if (input) input.checked = true; 13646 }); 13647 } 13648 } 13649 13650 const initSel = readSelection(); 13651 const initVar = findVariant(initSel) || variants[0] || null; 13652 if (document.readyState === 'loading') { 13653 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13654 } else { 13655 applyVariant(initVar, false); 13656 } 13657 13658function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13659 function refreshOptions(sel){ 13660 attrOrder.forEach(a=>{ 13661 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13662 if (!group) return; 13663 const chips = group.querySelectorAll('.v-chip'); 13664 chips.forEach(ch=>{ 13665 const val = ch.getAttribute('data-value'); 13666 const possible = variants.some(v=>{ 13667 if (v.attrs[a] !== val) return false; 13668 for (const a2 of attrOrder) { 13669 if (a2 === a) continue; 13670 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13671 } 13672 return true; 13673 }); 13674 const input = ch.querySelector('input[type=radio]'); 13675 const disabled = !possible; 13676 if (input) input.disabled = disabled; 13677 ch.classList.toggle('disabled', disabled); 13678 13679 let candidateVariant = null; 13680 if (possible) { 13681 candidateVariant = variants.find(v=>{ 13682 if (v.attrs[a] !== val) return false; 13683 for (const a2 of attrOrder) { 13684 if (a2 === a) continue; 13685 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13686 } 13687 return true; 13688 }); 13689 } 13690 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13691 ch.classList.toggle('out', !disabled && outOfStock); 13692 const badge = ch.querySelector('.chip-badge'); 13693 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13694 }); 13695 group.querySelectorAll('.v-chip').forEach(ch=>{ 13696 const input = ch.querySelector('input[type=radio]'); 13697 ch.classList.toggle('selected', input && input.checked); 13698 }); 13699 }); 13700 } 13701 updateSummary(sel); 13702 refreshOptions(sel); 13703 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13704 13705 const container = document.getElementById('pd-variants'); 13706 container && container.addEventListener('change', function(ev){ 13707 const radio = ev.target.closest('input[type=radio]'); 13708 if (!radio) return; 13709 sel = readSelection(); 13710 if (autoFillMissing(sel)) sel = readSelection(); 13711 const chosen = findVariant(sel); 13712 applyVariant(chosen || null, true); 13713 13714 updateSummary(sel); 13715 refreshOptions(sel); 13716 });
13717 13718window.applyVariant=applyVariant; 13719})(); 13720 13721(function cleanBrokenDescImages(){ 13722 const containers = [ 13723 document.querySelector('.pd-full-desc-body'), 13724 document.querySelector('.pd-short-desc') 13725 ].filter(Boolean); 13726 13727 containers.forEach(root => { 13728 const imgs = root.querySelectorAll('img'); 13729 imgs.forEach(img => { 13730 if (img.complete && img.naturalWidth === 0) { 13731 img.remove(); 13732 return; 13733 } 13734 img.addEventListener('error', () => { 13735 img.remove(); 13736 }, { once:true }); 13737 }); 13738 }); 13739})(); 13740 13741/* ====== Scroll To Top ====== */ 13742(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'})})})(); 13743 13744/* Format stok 10+ */ 13745function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13746 13747/* Toggle popover stok per cabang */ 13748(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=''}} 13749function 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()} 13750a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13751 13752/* ===== Share Product Modal ===== */ 13753(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://hop.exchange/",g="Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",h="Rp 20.789.000";const discountPercent=1;let i='';if(discountPercent>0){i=`ð¥ Lagi Promo Nih! Diskon ${discountPercent}%\n\n⨠${g}\nð° Cuma ${h} aja!\n\nð Buruan cek sebelum kehabisan!\nð `}else{i=`⨠Ada barang bagus nih!\n\nð¦ ${g}\nð¸ Harga: ${h}\n\nðï¸ 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
13753ncodeURIComponent(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)}})(); 13754 13755/* ===== Embedding variants (server â client) ===== */ 13756(function(){ 13757 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13758})(); 13759 13760/* ===== Helper: update angka popover ===== */ 13761function updateStockPopoverForVariant(v){ 13762 try{ 13763 const elMeg = document.getElementById('sp-qty-meg'); 13764 const elMdf = document.getElementById('sp-qty-mdf'); 13765 const elBda = document.getElementById('sp-qty-bda'); 13766 13767 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13768 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13769 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13770 }catch(_){} 13771} 13772 13773/* ===== applyVariant: 10+ dan update popover ===== */ 13774(function patchApplyVariant(){ 13775 if (typeof applyVariant !== 'function') return; 13776 const original = applyVariant; 13777 window.applyVariant = function(v){ 13778 original(v); 13779 13780 try { 13781 const elStock = document.getElementById('pd-stock-badge'); 13782 if (elStock){ 13783 let txt = elStock.textContent || ''; 13784 let m = txt.match(/\d+/); 13785 if (m) { 13786 const n = parseInt(m[0]||'0',10)||0; 13787 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13788 } 13789 } 13790 } catch(_){} 13791if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13792 }; 13793})(); 13794 13795(function initCurrentVariantBranch(){ 13796 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13797 if (!list.length) return; 13798 const hid = document.getElementById('pd-variant-id'); 13799 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13800 const current = list.find(x => x.id === vid) || list[0] || null; 13801 if (current) updateStockPopoverForVariant(current); 13802})(); 13803 13804(function(){ 13805 if (!window.ga4Ecomm) return; 13806 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13807 window.ga4Ecomm.viewItem({ 13808 currency: 'IDR', 13809 value: 20789000, 13810 item_id: "SB0B3T", 13811 item_name: "Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y", 13812 item_brand: "Lenovo ",
13813 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13814 }); 13815 })(); 13816 13817(function patchApplyVariantFocus(){ 13818 if (typeof applyVariant !== 'function') return; 13819 const original = applyVariant; 13820 window.applyVariant = function(v, focusImage = true){ 13821 13822 original(v, focusImage); 13823 13824 if (focusImage && v && v.image_url) { 13825 _0xau1v(v.image_url); 13826 } 13827 }; 13828})(); 13829 13830(function() { 13831 var soldMap = []; 13832 var soldTotalLabel = "Terjual 2"; 13833 13834 function formatSoldLabel(qty) { 13835 if (qty >= 1000000) { 13836 var v = Math.floor((qty / 100000) ) / 10; 13837 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13838 } 13839 if (qty >= 1000) { 13840 var v2 = Math.floor((qty / 100) ) / 10; 13841 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13842 } 13843 if (qty >= 100) { 13844 var v2 = Math.floor((qty / 100) ) * 100; 13845 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13846 } 13847 if (qty >= 10) { 13848 var v2 = Math.floor((qty / 10) ) * 10; 13849 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13850 } 13851 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13852 } 13853 13854 function updateSoldLabel(labelText) { 13855 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13856 if (el) el.textContent = labelText; 13857 } 13858 13859 window.updateSoldBySku = function(sku) { 13860 if (!sku || typeof sku !== 'string') { 13861 updateSoldLabel(soldTotalLabel); 13862 return; 13863 } 13864 var qty = soldMap[sku] || 0; 13865 updateSoldLabel(formatSoldLabel(qty)); 13866 }; 13867 13868 updateSoldLabel(soldTotalLabel); 13869 })(); 13870 13871(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)}})} 13872function 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():'Rp 20.789.000';const pd={id:325,name:"Lenovo Legion Go 8ASP2 - 83N0000RID \/AMD Ryzen Z2 Extreme\/32GB\/1TB SSD\/AMD Radeon Graphics\/8.8\" WUXGA OLED Touch\/Win 11 Home\/Eclipse Black\/3Y",price:currentPrice,image:"\/uploads\/products\/lenovo-legion-go-8asp2-83n0000rid.webp",url:window.location.href};if(variantId>0){pd.variant_id=variantId}if(variantSummary){pd.variant_summary=variantSummary}return pd} 13873const 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}});
13873function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13874 13875(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')}})(); 13876</script>
vendor: 2 bytes, line 13876
13876
13877<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13878window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13879window.IS_DIGITAL=IS_DIGITAL; 13880window.isPreorderProduct=false; 13881window.PD_PREORDER_DATA=null; 13882</script>
13882 13883
13884<script data-inlined="pd-lightbox.js"> 13885// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13886(function(){ 13887 const state = { 13888 items: [], 13889 open: false, 13890 idx: 0, 13891 scale: 1, 13892 minScale: 1, 13893 maxScale: 4, 13894 tx: 0, ty: 0, 13895 isDragging: false, 13896 dragStartX: 0, dragStartY: 0, 13897 baseTx: 0, baseTy: 0, 13898 touchStartX: 0, touchStartY: 0, 13899 lastTap: 0, 13900 swipeMode: 'none', // 'none' | 'h' | 'v' 13901 totalDx: 0, totalDy: 0 13902 }; 13903 13904 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13905 13906 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13907 13908 function absUrl(u){ 13909 if (!u) return ''; 13910 try { 13911 if (/^https?:\/\//i.test(u)) return u; 13912 return new URL(u, ORIGIN).href; 13913 } catch(_) { return u; } 13914 } 13915 function fileName(u){ 13916 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13917 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13918 } 13919 13920 function buildItems(){ 13921 const gd = (window.galleryData || []).filter(Boolean); 13922 if (gd.length){ 13923 state.items = gd.map(g => ({ 13924 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13925 })).filter(it => !!it.src); 13926 return; 13927 } 13928 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13929 state.items = Array.from(thumbs).map(btn => { 13930 const raw = btn.getAttribute('data-full') || ''; 13931 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13932 }).filter(it => !!it.src); 13933 } 13934 13935 function ensureUI(){ 13936 if (root) return; 13937 root = document.createElement('div'); 13938 root.className = 'pd-lb'; 13939 root.id = 'pd-lightbox'; 13940 root.setAttribute('role','dialog'); 13941 root.setAttribute('aria-modal','true'); 13942 root.setAttribute('aria-hidden','true'); 13943 root.setAttribute('tabindex','-1'); 13944 13945 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13946 root.innerHTML = ` 13947 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13948 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13949 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13950 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13951 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13952 `; 13953 document.body.appendChild(root); 13954 13955 imgEl = root.querySelector('#pd-lb-img'); 13956 counterEl = root.querySelector('#pd-lb-counter'); 13957 btnPrev = root.querySelector('.pd-lb-prev'); 13958 btnNext = root.querySelector('.pd-lb-next'); 13959 btnClose = root.querySelector('.pd-lb-close'); 13960 canvas = root.querySelector('.pd-lb-canvas'); 13961 13962 // Button clicks 13963 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13964 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13965 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13966 13967 // Klik backdrop untuk tutup 13968 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13969 13970 // Keyboard global 13971 document.addEventListener('keydown', (e)=>{ 13972 if (!state.open) return; 13973 if (e.key === 'Escape') { e.preventDefault(); close(); } 13974 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13975 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13976 }); 13977 13978 // Wheel zoom (desktop) 13979 root.addEventListener('wheel', (e)=>{ 13980 if (!state.open) return; 13981 e.preventDefault(); 13982 const delta = -Math.sign(e.deltaY) * 0.2; 13983 zoomAt(delta, e.clientX, e.clientY); 13984 }, { passive: false }); 13985 13986 // Drag pan (mouse) 13987 canvas.addEventListener('mousedown', (e)=>{ 13988 if (state.scale <= 1) return; 13989 state.isDragging = true; 13990 state.dragStartX = e.clientX; 13991 state.dragStartY = e.clientY; 13992 state.baseTx = state.tx; 13993 state.baseTy = state.ty; 13994 e.preventDefault(); 13995 });
13996 window.addEventListener('mousemove', (e)=>{ 13997 if (!state.isDragging) return; 13998 const dx = e.clientX - state.dragStartX; 13999 const dy = e.clientY - state.dragStartY; 14000 setTranslate(state.baseTx + dx, state.baseTy + dy); 14001 }); 14002 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 14003 14004 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 14005 canvas.addEventListener('touchstart', (e)=>{ 14006 if (!state.open) return; 14007 if (e.touches.length === 1) { 14008 const t = e.touches[0]; 14009 state.touchStartX = t.clientX; 14010 state.touchStartY = t.clientY; 14011 state.totalDx = 0; 14012 state.totalDy = 0; 14013 state.swipeMode = 'none'; 14014 14015 state.isDragging = state.scale > 1; 14016 state.dragStartX = t.clientX; 14017 state.dragStartY = t.clientY; 14018 state.baseTx = state.tx; 14019 state.baseTy = state.ty; 14020 14021 // Double-tap toggle zoom 14022 const now = Date.now(); 14023 if (now - state.lastTap < 300) { 14024 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 14025 state.lastTap = 0; 14026 e.preventDefault(); 14027 return; 14028 } 14029 state.lastTap = now; 14030 } 14031 }, { passive: true }); 14032 14033 canvas.addEventListener('touchmove', (e)=>{ 14034 if (!state.open) return; 14035 14036 // Drag pan saat zoom 14037 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 14038 e.preventDefault(); 14039 const t = e.touches[0]; 14040 const dx = t.clientX - state.dragStartX; 14041 const dy = t.clientY - state.dragStartY; 14042 setTranslate(state.baseTx + dx, state.baseTy + dy); 14043 return; 14044 } 14045 14046 // Gesture saat scale <= 1 14047 if (state.scale <= 1 && e.touches.length === 1) { 14048 const t = e.touches[0]; 14049 const dx = t.clientX - state.touchStartX; 14050 const dy = t.clientY - state.touchStartY; 14051 state.totalDx = dx; state.totalDy = dy; 14052 14053 // Tentukan mode swipe di awal gerakan 14054 if (state.swipeMode === 'none') { 14055 const absX = Math.abs(dx), absY = Math.abs(dy); 14056 if (absY > 12 && absY > absX * 1.3) { 14057 state.swipeMode = 'v'; // swipe vertical (untuk close) 14058 } else if (absX > 12 && absX > absY * 1.3) { 14059 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 14060 } 14061 } 14062 14063 if (state.swipeMode === 'v') { 14064 // Ikuti jari, kurangi opacity untuk feedback 14065 e.preventDefault(); 14066 setRootDrag(dy); 14067 } else if (state.swipeMode === 'h') { 14068 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 14069 } 14070 } 14071 }, { passive: false }); 14072 14073 canvas.addEventListener('touchend', (e)=>{ 14074 if (!state.open) return; 14075 14076 if (state.scale <= 1) { 14077 const dx = state.totalDx; 14078 const dy = state.totalDy; 14079 14080 if (state.swipeMode === 'v') { 14081 const TH = 80; // threshold jarak piksel untuk menutup 14082 if (Math.abs(dy) > TH) { 14083 // Animasi keluar arah swipe 14084 animateClose(dy); 14085 } else { 14086 // Snap back 14087 resetRootDrag(true); 14088 } 14089 } else if (state.swipeMode === 'h') { 14090 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14091 } 14092 } 14093 14094 state.isDragging = false; 14095 state.swipeMode = 'none'; 14096 state.totalDx = 0; 14097 state.totalDy = 0; 14098 }); 14099 } 14100 14101 function setImage(idx){ 14102 if (!state.items.length) return; 14103 state.idx = (idx + state.items.length) % state.items.length; 14104 const it = state.items[state.idx]; 14105 imgEl.src = it.src; 14106 imgEl.alt = it.alt || ''; 14107 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14108 resetTransform(); 14109 preloadNeighbors(); 14110 } 14111 14112 function resetTransform(){ 14113 state.scale = 1; 14114 state.tx = 0; state.ty = 0; 14115 applyTransform(); 14116 } 14117 14118 function setTranslate(tx, ty){ 14119 state.tx = tx; state.ty = ty; 14120 applyTransform(); 14121 } 14122 14123 function applyTransform(){ 14124 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14125 } 14126 14127 function zoomAt(delta, clientX, clientY, snapTo){ 14128 const old = state.scale; 14129 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14130 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14131 14132 if (next !== old) { 14133 const rect = imgEl.getBoundingClientRect(); 14134 const cx = clientX - rect.left - rect.width/2; 14135 const cy = clientY - rect.top - rect.height/2; 14136 state.tx -= cx * (next/old - 1); 14137 state.ty -= cy * (next/old - 1); 14138 state.scale = next; 14139 applyTransform(); 14140 } 14141 } 14142 14143 function preloadNeighbors(){ 14144 if (state.items.length < 2) return; 14145 const nextIdx = (state.idx + 1) % state.items.length; 14146 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14147 [nextIdx, prevIdx].forEach(i=>{ 14148 const img = new Image(); img.src = state.items[i].src; 14149 }); 14150 } 14151 14152 // ----- Swipe-down visual helpers ----- 14153 function setRootDrag(dy){ 14154 const damped = dy * 0.6; // redam gerakan agar lembut 14155 const abs = Math.abs(dy); 14156 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14157 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14158 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14159 } 14160 function resetRootDrag(withAnim){ 14161 if (withAnim) { 14162 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14163 root.style.transform = 'translate3d(0,0,0)'; 14164 root.style.opacity = '1'; 14165 setTimeout(()=>{ root.style.transition = ''; }, 190); 14166 } else { 14167 root.style.transition = ''; 14168 root.style.transform = ''; 14169 root.style.opacity = ''; 14170 } 14171 } 14172 function animateClose(dy){ 14173 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14174 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14175 root.style.transform = `translate3d(0, ${off}px, 0)`; 14176 root.style.opacity = '0'; 14177 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14178 } 14179 14180 function setSiblingsInert(makeInert){ 14181 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14182 const nodes = Array.from(document.body.children).filter(el => el !== root); 14183 nodes.forEach(el=>{ 14184 if (makeInert) { 14185 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14186 } else { 14187 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14188 } 14189 }); 14190 } 14191 14192 function open(idx){ 14193 if (!state.items.length) buildItems();
14194 if (!state.items.length) return; 14195 ensureUI(); 14196 14197 // Reset efek drag visual 14198 resetRootDrag(false); 14199 14200 document.body.style.overflow = 'hidden'; 14201 root.classList.add('show'); 14202 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14203 setSiblingsInert(true); 14204 14205 state.open = true; 14206 setImage(typeof idx === 'number' ? idx : 0); 14207 14208 // Fokuskan dialog agar keyboard pasti tertangkap 14209 try { root.focus(); } catch(_){} 14210 } 14211 14212 function close(){ 14213 state.open = false; 14214 root.classList.remove('show'); 14215 root.setAttribute('aria-hidden','true'); 14216 setSiblingsInert(false); 14217 document.body.style.overflow = ''; 14218 14219 // Bersihkan transform/opacity 14220 resetRootDrag(false); 14221 } 14222 14223 function next(){ setImage(state.idx + 1); } 14224 function prev(){ setImage(state.idx - 1); } 14225 14226 function findIndexByUrl(url){ 14227 const target = fileName(absUrl(url)); 14228 const idx = state.items.findIndex(it => fileName(it.src) === target); 14229 return idx >= 0 ? idx : 0; 14230 } 14231 14232 function initPdLightbox(){ 14233 buildItems();
14234 ensureUI(); 14235 14236 // Klik gambar utama â buka LB di index yang sama 14237 const mainImg = document.getElementById('pd-main-img'); 14238 if (mainImg) { 14239 mainImg.style.cursor = 'zoom-in'; 14240 mainImg.addEventListener('click', ()=>{ 14241 open(findIndexByUrl(mainImg.src)); 14242 }); 14243 } 14244 14245 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14246 const slider = document.getElementById('pd-mobile-slider'); 14247 if (slider) { 14248 slider.addEventListener('click', (e)=>{ 14249 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14250 if (!im) return; 14251 open(findIndexByUrl(im.src)); 14252 }); 14253 } 14254 14255 // (Opsional) double-click pada thumbnail untuk buka langsung 14256 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14257 thumbs.forEach((btn)=>{ 14258 let last = 0; 14259 btn.addEventListener('click', ()=>{ 14260 const now = Date.now(); 14261 if (now - last < 300) { 14262 const src = btn.getAttribute('data-full') || ''; 14263 open(findIndexByUrl(absUrl(src))); 14264 last = 0; 14265 } else { 14266 last = now; 14267 } 14268 }); 14269 }); 14270 14271 // Ekspos API 14272 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14273 } 14274 14275 if (document.readyState === 'loading') { 14276 document.addEventListener('DOMContentLoaded', initPdLightbox); 14277 } else { 14278 initPdLightbox(); 14279 } 14280})(); 14281</script>
vendor: 2 bytes, line 14281
14281
14282<script data-inlined="cart.js"> 14283(function(){ 14284 function $(s,ctx=document){ return ctx.querySelector(s); } 14285 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14286 14287 // CSRF token helper 14288 function getCsrfToken() { 14289 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14290 return metaTag ? metaTag.getAttribute('content') : ''; 14291 } 14292 14293 function guessPdMeta(){ 14294 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14295 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14296 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14297 const imgEl = $('#pd-main-img'); 14298 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14299 return { name, sku, image_url }; // price removed for security 14300 } 14301 14302 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14303 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14304 const payload = { 14305 product_id: parseInt(productId||0,10) || 0, 14306 qty: parseInt(qty||1,10) || 1, 14307 variant_id: parseInt(variantId||0,10) || 0, 14308 meta 14309 }; 14310 14311 // Check if user is logged in 14312 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14313 document.body.classList.contains('logged-in') || 14314 window.userLoggedIn === true || 14315 document.body.dataset.userLoggedIn === 'true'; 14316 14317 // Skip localStorage untuk logged user - mereka pakai database 14318 if (!isLoggedIn && window.guestCart) { 14319 try { 14320 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14321 } catch (e) { 14322 console.warn('Failed to add to localStorage cart:', e); 14323 } 14324 } 14325 14326 const res = await fetch('/api/cart/add.php', { 14327 method: 'POST', 14328 headers: { 14329 'Content-Type':'application/json', 14330 'X-CSRF-Token': getCsrfToken() 14331 }, 14332 credentials: 'same-origin', 14333 body: JSON.stringify({ 14334 ...payload, 14335 csrf_token: getCsrfToken() 14336 }), 14337 }); 14338 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14339 if (!res.ok || !data.ok) { 14340 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14341 throw new Error(msg); 14342 } 14343 14344 // Update badge di navbar dengan data dari response 14345 try { 14346 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14347 if (el && data.cart && typeof data.cart.items_count === 'number') { 14348 el.textContent = String(data.cart.items_count); 14349 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14350 } 14351 } catch(_){} 14352 14353 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14354 try { 14355 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14356 } catch(_){} 14357 14358 // Update CartBadge dengan data dari response (lebih reliable) 14359 try { 14360 if (window.CartBadge && window.CartBadge.set) { 14361 window.CartBadge.set(data.cart.items_count||0); 14362 } 14363 } catch(_){} 14364 14365 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14366 setTimeout(() => { 14367 if (window.updateCartCounter) { 14368 window.updateCartCounter(); 14369 } 14370 }, 100); 14371 14372 return data; 14373 } 14374 14375 // Function to update cart counter from server 14376 async function updateCartCounter(){ 14377 try { 14378 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14379 const data = await res.json().catch(()=>({ok:false})); 14380 if (data.ok && typeof data.items_count === 'number') { 14381 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14382 if (el) { 14383 el.textContent = String(data.items_count); 14384 el.classList.toggle('is-empty', data.items_count <= 0); 14385 } 14386 // Dispatch event 14387 try { 14388 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14389 } catch(_){} 14390 } 14391 } catch (err) { 14392 console.log('Could not update cart counter:', err); 14393 } 14394 } 14395 14396 // Function to manually merge guest cart (if needed) 14397 async function mergeGuestCart(){ 14398 try { 14399 const res = await fetch('/api/cart/merge-guest.php', { 14400 method: 'POST', 14401 headers: { 14402 'Content-Type': 'application/json', 14403 'X-CSRF-Token': getCsrfToken() 14404 }, 14405 credentials: 'same-origin', 14406 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14407 }); 14408 const data = await res.json().catch(()=>({ok:false})); 14409 14410 if (data.ok && data.merged_items > 0) { 14411 console.log(`Guest cart merged: ${data.merged_items} items`); 14412 // Update cart counter after merge 14413 await updateCartCounter(); 14414 return data; 14415 } 14416 return data; 14417 } catch (err) { 14418 console.log('Could not merge guest cart:', err); 14419 return { ok: false, error: err.message }; 14420 } 14421 } 14422 14423 window.addToCart = addToCart; 14424 window.updateCartCounter = updateCartCounter; 14425 window.mergeGuestCart = mergeGuestCart; 14426 14427 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14428 document.addEventListener('click', async (e)=>{ 14429 const btn = e.target.closest('[data-addtocart]'); 14430 if (!btn) return; 14431 e.preventDefault(); 14432 if (btn.disabled) return; 14433 14434 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14435 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14436 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14437 14438 try { 14439 const data = await addToCart(pid, qty, vid); 14440 if (window.showToast) { 14441 const meta = guessPdMeta(); 14442 window.showToast({ 14443 type:'success', 14444 title:'Berhasil ditambahkan', 14445 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14446 duration: 3800, 14447 actions: [ 14448 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14449 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14450 ] 14451 }); 14452 } 14453 } catch (err) { 14454 if (window.showToast) { 14455 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14456 } else { 14457 console.error(err); 14458 } 14459 } 14460 }); 14461})(); 14462</script>
vendor: 2 bytes, line 14462
14462
14463<script data-inlined="stock-validator-vanilla.js"> 14464/** 14465 * Stock Validation - Vanilla JS Version 14466 * 14467 * This script provides client-side stock validation without jQuery dependency 14468 * - Works with native JavaScript and fetch API 14469 * - Graceful fallback for older browsers 14470 * - Same functionality as jQuery version 14471 */ 14472 14473window.StockValidator = (function() { 14474 'use strict'; 14475 14476 const config = { 14477 apiEndpoint: '/api/stock/check.php', 14478 modalSelector: '#stock-modal-overlay', // We create our own modal 14479 loadingClass: 'loading-stock', 14480 disabledClass: 'stock-disabled', 14481 debug: true // Enable debug logging 14482 }; 14483 14484 /** 14485 * Cross-browser event listener helper 14486 */ 14487 function addEventListener(element, event, handler) { 14488 if (element.addEventListener) { 14489 element.addEventListener(event, handler); 14490 } else if (element.attachEvent) { 14491 element.attachEvent('on' + event, handler); 14492 } 14493 } 14494 14495 /** 14496 * Get stock information from embedded variant data (instant, no AJAX needed) 14497 */ 14498 function getStockFromEmbeddedData(variantId) { 14499 // Check if embedded variant data exists 14500 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14501 return null; 14502 } 14503 14504 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14505 14506 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14507 if (variants.length === 0) { 14508 // For simple products, we cannot determine stock from embedded data 14509 // Return null to force API validation 14510 return null; 14511 } 14512 14513 // If no variant specified, check base product stock (all variants) 14514 if (!variantId || variantId <= 0) { 14515 // For base product, check if ANY variant has stock 14516 const isPreorder = window.isPreorderProduct || false; 14517 let hasAnyStock = false; 14518 let totalStock = 0; 14519 14520 if (isPreorder) { 14521 // For pre-order, check preorder_remaining 14522 hasAnyStock = variants.some(v => { 14523 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14524 return v.preorder_remaining > 0; 14525 } 14526 return v.stock > 0; 14527 }); 14528 totalStock = variants.reduce((sum, v) => { 14529 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14530 return sum + (v.preorder_remaining || 0); 14531 } 14532 return sum + (v.stock || 0); 14533 }, 0); 14534 } else { 14535 hasAnyStock = variants.some(v => v.stock > 0); 14536 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14537 } 14538 14539 return { 14540 hasStock: hasAnyStock, 14541 stock: totalStock, 14542 sku: 'Base Product', 14543 variant: null 14544 }; 14545 } 14546 14547 // Find specific variant 14548 const variant = variants.find(v => v.id === variantId); 14549 14550 if (!variant) { 14551 return { 14552 hasStock: false, 14553 stock: 0, 14554 sku: 'Unknown Variant', 14555 variant: null 14556 }; 14557 } 14558 14559 // For pre-order products, check preorder_remaining instead of stock 14560 const isPreorder = window.isPreorderProduct || false; 14561 let hasStock = false; 14562 let availableStock = 0; 14563 14564 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14565 availableStock = parseInt(variant.preorder_remaining, 10); 14566 if (isNaN(availableStock)) availableStock = 0; 14567 hasStock = availableStock > 0; 14568 } else { 14569 availableStock = variant.stock || 0; 14570 hasStock = availableStock > 0;
14571 } 14572 14573 return { 14574 hasStock: hasStock, 14575 stock: availableStock, 14576 sku: variant.sku, 14577 variant: variant 14578 }; 14579 } 14580 14581 /** 14582 * Cross-browser AJAX helper 14583 */ 14584 function makeRequest(url, options) { 14585 return new Promise((resolve, reject) => { 14586 // Try fetch first (modern browsers) 14587 if (typeof fetch !== 'undefined') { 14588 const fetchOptions = { 14589 method: options.method || 'POST', 14590 headers: { 14591 'Content-Type': 'application/x-www-form-urlencoded', 14592 }, 14593 body: options.data ? new URLSearchParams(options.data).toString() : null 14594 }; 14595 14596 fetch(url, fetchOptions) 14597 .then(response => { 14598 if (!response.ok) { 14599 throw new Error(`HTTP ${response.status}`); 14600 } 14601 return response.json(); 14602 }) 14603 .then(resolve) 14604 .catch(reject); 14605 } else { 14606 // Fallback to XMLHttpRequest 14607 const xhr = new XMLHttpRequest(); 14608 xhr.open(options.method || 'POST', url); 14609 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14610 14611 xhr.onload = function() { 14612 if (xhr.status >= 200 && xhr.status < 300) { 14613 try { 14614 resolve(JSON.parse(xhr.responseText)); 14615 } catch (e) { 14616 reject(new Error('Invalid JSON response')); 14617 } 14618 } else { 14619 reject(new Error(`HTTP ${xhr.status}`)); 14620 } 14621 }; 14622 14623 xhr.onerror = function() { 14624 reject(new Error('Network error')); 14625 }; 14626 14627 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14628 xhr.send(data); 14629 } 14630 }); 14631 } 14632 14633 /** 14634 * Check stock via AJAX 14635 */ 14636 function checkStock(productId, variantId, qty) { 14637 return makeRequest(config.apiEndpoint, { 14638 method: 'POST', 14639 data: { 14640 product_id: productId, 14641 variant_id: variantId || 0, 14642 qty: qty || 1 14643 } 14644 }); 14645 } 14646 14647 /** 14648 * Create and show stock alert modal using modal.css structure 14649 */ 14650 function showStockModal(stockInfo, action = 'add to cart') { 14651 let title, message, type, iconClass; 14652 14653 // Handle both string and object formats 14654 if (typeof stockInfo === 'string') { 14655 title = 'Stok Tidak Tersedia'; 14656 message = stockInfo; 14657 type = 'warning'; 14658 iconClass = 'warning'; 14659 } else if (stockInfo && typeof stockInfo === 'object') { 14660 if (!stockInfo.is_active) { 14661 title = 'Produk Tidak Tersedia'; 14662 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14663 type = 'danger'; 14664 iconClass = 'danger'; 14665 } else if (!stockInfo.is_in_stock) { 14666 title = 'Stok Habis'; 14667 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14668 if (stockInfo.sku) { 14669 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14670 } 14671 type = 'warning'; 14672 iconClass = 'warning'; 14673 } else if (!stockInfo.can_add_to_cart) { 14674 title = 'Stok Tidak Mencukupi'; 14675 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14676 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14677 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14678 type = 'warning'; 14679 iconClass = 'warning'; 14680 } else { 14681 title = 'Stok Tidak Tersedia'; 14682 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14683 type = 'warning'; 14684 iconClass = 'warning'; 14685 } 14686 } else { 14687 title = 'Stok Tidak Tersedia'; 14688 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14689 type = 'warning'; 14690 iconClass = 'warning'; 14691 } 14692 14693 // Create modal HTML using modal.css structure 14694 const modalHTML = ` 14695 <div class="modal-overlay" id="stock-modal-overlay"> 14696 <div class="modal-dialog"> 14697 <div class="modal-header"> 14698 <div class="modal-icon ${iconClass}"> 14699 ${getIconSVG(iconClass)} 14700 </div> 14701 <h3 class="modal-title">${title}</h3> 14702 </div> 14703 <div class="modal-body"> 14704 <p class="modal-message">${message}</p> 14705 <div class="modal-actions"> 14706 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14707 Tutup 14708 </button> 14709 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14710 Muat Ulang 14711 </button> 14712 </div> 14713 </div> 14714 </div> 14715 </div> 14716 `; 14717 14718 // Remove existing modal if any 14719 const existingModal = document.getElementById('stock-modal-overlay'); 14720 if (existingModal) { 14721 existingModal.remove(); 14722 } 14723 14724 // Add modal to body 14725 document.body.insertAdjacentHTML('beforeend', modalHTML); 14726 14727 // Show modal with animation 14728 const modal = document.getElementById('stock-modal-overlay'); 14729 setTimeout(() => { 14730 modal.classList.add('show'); 14731 }, 10); 14732 14733 // Add event listeners to modal buttons 14734 const closeBtn = modal.querySelector('[data-action="close"]'); 14735 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14736 14737 if (closeBtn) { 14738 addEventListener(closeBtn, 'click', closeStockModal); 14739 } 14740 14741 if (reloadBtn) { 14742 addEventListener(reloadBtn, 'click', function() { 14743 closeStockModal(); 14744 window.location.reload(); 14745 }); 14746 } 14747 14748 // Close modal when clicking overlay 14749 addEventListener(modal, 'click', function(event) { 14750 if (event.target === modal) { 14751 closeStockModal(); 14752 } 14753 }); 14754 14755 // Auto-close after 8 seconds 14756 setTimeout(() => { 14757 closeStockModal(); 14758 }, 8000); 14759 } 14760 14761 /** 14762 * Get icon SVG based on type 14763 */ 14764 function getIconSVG(type) { 14765 const icons = { 14766 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>', 14767 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14767<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14768 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>', 14769 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>' 14770 }; 14771 return icons[type] || icons.info; 14772 } 14773 14774 /** 14775 * Close stock modal 14776 */ 14777 function closeStockModal() { 14778 const modal = document.getElementById('stock-modal-overlay'); 14779 if (modal) { 14780 modal.classList.remove('show'); 14781 setTimeout(() => { 14782 modal.remove(); 14783 }, 200); 14784 } 14785 } 14786 14787 // Make closeStockModal globally available 14788 if (typeof window !== 'undefined') { 14789 window.closeStockModal = closeStockModal; 14790 } 14791 14792 /** 14793 * Get element data attribute with fallback 14794 */ 14795 function getDataAttribute(element, attr) { 14796 if (element.dataset) { 14797 return element.dataset[attr]; 14798 } else { 14799 return element.getAttribute('data-' + attr); 14800 } 14801 } 14802 14803 /** 14804 * Validate stock before cart action 14805 */ 14806 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14807 try { 14808 // SMART VALIDATION: For simple products, check if we already have cached stock info 14809 // If buttons are enabled and cached info shows stock available, skip API validation 14810 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14811 14812 return true; 14813 } 14814 14815 // For variant products or when cached info is not available, do API validation 14816 const stockInfo = await checkStock(productId, variantId, qty); 14817 14818 if (!stockInfo.ok) { 14819 showStockModal({ 14820 message: stockInfo.error || 'Gagal mengecek stok', 14821 is_active: false 14822 }, action); 14823 return false; 14824 } 14825 14826 if (!stockInfo.can_add_to_cart) { 14827 showStockModal(stockInfo, action); 14828 return false; 14829 } 14830 14831 return true; // Stock available, proceed 14832 14833 } catch (error) { 14834 console.error('Stock validation error:', error); 14835 showStockModal({ 14836 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14837 is_active: false 14838 }, action); 14839 return false; 14840 } 14841 } 14842 14843 /** 14844 * Handle button click with stock validation 14845 */ 14846 function handleButtonClick(event, action) { 14847 const button = event.target; 14848 14849 14850 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14851 if (button.hasAttribute('data-stock-blocked')) { 14852 event.preventDefault(); 14853 event.stopImmediatePropagation(); 14854 14855 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14856 showStockModal(reason); 14857 14858 // Show security warning 14859 if (window.showToast) { 14860 window.showToast({ 14861 type: 'warning', 14862 title: 'Aksi Tidak Diizinkan', 14863 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14864 duration: 3000 14865 }); 14866 } 14867 return false; 14868 } 14869 14870 // SECOND CHECK: Real-time stock validation against cached stock info 14871 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14872 event.preventDefault(); 14873 event.stopImmediatePropagation(); 14874 14875 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14876 14877 // Re-enforce button disabling 14878 button.disabled = true; 14879 button.style.opacity = '0.5'; 14880 button.style.cursor = 'not-allowed'; 14881 button.setAttribute('data-stock-blocked', 'true'); 14882 14883 return false; 14884 } 14885 14886 // Skip if already processing 14887 if (button.classList.contains(config.loadingClass)) { 14888 event.preventDefault(); 14889 return false; 14890 } 14891 14892 // Get product data from button attributes or form 14893 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14894 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14895 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14896 14897 // Try to get from global variables (set by product detail page) 14898 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14899 productId = parseInt(window.PD_ID || 0); 14900 } 14901 14902 // Try to get from parent form if not in button 14903 if (productId <= 0) { 14904 const form = button.closest ? button.closest('form') : null; 14905 if (form) { 14906 const pidAttr = form.getAttribute('data-pid'); 14907 const pidInput = form.querySelector('[name="product_id"]'); 14908 const vidInput = form.querySelector('[name="variant_id"]');
14909 const qtyInput = form.querySelector('[name="qty"]'); 14910 14911 if (pidAttr) productId = parseInt(pidAttr); 14912 if (pidInput) productId = parseInt(pidInput.value || 0); 14913 if (vidInput) variantId = parseInt(vidInput.value || 0); 14914 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14915 } 14916 } 14917 14918 // CRITICAL: Always get current variant from selector (overrides button attributes) 14919 // This ensures we validate against the currently selected variant, not cached button data 14920 const variantSelector = document.getElementById('pd-variant-id'); 14921 if (variantSelector) { 14922 const currentVariantId = parseInt(variantSelector.value || 0); 14923 if (currentVariantId > 0) { 14924 variantId = currentVariantId; 14925 } 14926 } 14927 14928 14929 if (productId <= 0) { 14930 return true; // Let default handler deal with it 14931 } 14932 14933 // Prevent default and validate stock 14934 event.preventDefault(); 14935 event.stopPropagation(); 14936 14937 // Show loading state 14938 button.classList.add(config.loadingClass); 14939 const originalDisabled = button.disabled; 14940 button.disabled = true; 14941 14942 14943 validateBeforeCart(productId, variantId, qty, action) 14944 .then(isValid => { 14945 if (isValid) { 14946 // Stock OK, proceed with original action 14947 if (action === 'cart' && typeof window.addToCart === 'function') { 14948 window.addToCart(productId, qty, variantId); 14949 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14950 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14951 window.buyNow(); 14952 } else { 14953 // Add fallback cart/buy logic here if needed 14954 } 14955 } 14956 }) 14957 .catch(error => { 14958 }) 14959 .finally(() => { 14960 // Remove loading state 14961 button.classList.remove(config.loadingClass); 14962 button.disabled = originalDisabled; 14963 }); 14964 14965 return false; 14966 } 14967 14968 /** 14969 * Handle mobile button click with stock validation 14970 */ 14971 function handleMobileButtonClick(event, action) { 14972 const button = event.target; 14973 14974 14975 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14976 if (button.hasAttribute('data-stock-blocked')) { 14977 event.preventDefault(); 14978 event.stopImmediatePropagation(); 14979 14980 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14981 showStockModal(reason); 14982 14983 // Show security warning 14984 if (window.showToast) { 14985 window.showToast({ 14986 type: 'warning', 14987 title: 'Aksi Tidak Diizinkan', 14988 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14989 duration: 3000 14990 }); 14991 } 14992 return false; 14993 } 14994 14995 // SECOND CHECK: Real-time stock validation against cached stock info 14996 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14997 event.preventDefault(); 14998 event.stopImmediatePropagation(); 14999 15000 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15001 15002 // Re-enforce button disabling 15003 button.disabled = true; 15004 button.style.opacity = '0.5'; 15005 button.style.cursor = 'not-allowed'; 15006 button.setAttribute('data-stock-blocked', 'true'); 15007 15008 return false; 15009 } 15010 15011 // Skip if already processing 15012 if (button.classList.contains(config.loadingClass)) { 15013 event.preventDefault(); 15014 return false; 15015 } 15016 15017 // Get product data from mobile FAB container 15018 let productId = 0; 15019 let variantId = 0; 15020 let qty = 1; 15021
15022 // Try to get from mobile FAB data-pid 15023 const mobileFab = document.getElementById('pd-mobile-fab'); 15024 if (mobileFab) { 15025 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15026 if (pidAttr) productId = parseInt(pidAttr); 15027 } 15028 15029 // Try to get from global variables (set by product detail page) 15030 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 15031 productId = parseInt(window.PD_ID || 0); 15032 } 15033 15034 // CRITICAL: Always get current variant from selector (real-time) 15035 // This ensures we validate against the currently selected variant 15036 const variantSelector = document.getElementById('pd-variant-id'); 15037 if (variantSelector) { 15038 variantId = parseInt(variantSelector.value || 0); 15039 } 15040 15041 // Try to get quantity from quantity input 15042 const qtySelector = document.querySelector('[name="qty"]'); 15043 if (qtySelector) { 15044 qty = parseInt(qtySelector.value || 1); 15045 } 15046 15047 // Ensure minimum quantity is 1 15048 qty = Math.max(1, qty); 15049 15050 15051 if (productId <= 0) { 15052 return true; // Let default handler deal with it 15053 } 15054 15055 // Event already prevented at interceptor level, just proceed with validation 15056 15057 // Show loading state 15058 button.classList.add(config.loadingClass); 15059 const originalDisabled = button.disabled; 15060 const originalText = button.textContent; 15061 button.disabled = true; 15062 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 15063 15064 15065 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 15066 if (window.addToCart && typeof window.addToCart === 'function') { 15067 window.addToCart._stockValidatorBlocked = true; 15068 } 15069 15070 // Also preemptively block other cart functions 15071 const originalAddToCartSubmit = window.addToCartSubmit; 15072 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 15073 15074 // Temporarily block ALL cart functions during validation 15075 window.addToCartSubmit = (...args) => { 15076 return Promise.resolve(false); 15077 }; 15078 15079 if (originalPdAddToCartSubmit) { 15080 window._pdAddToCartSubmit = (...args) => { 15081 return Promise.resolve(false); 15082 }; 15083 } 15084 15085 validateBeforeCart(productId, variantId, qty, action) 15086 .then(isValid => { 15087 if (isValid) { 15088 // Stock OK, proceed with original action 15089 15090 if (action === 'cart') { 15091 // For mobile cart, we need to recreate the original behavior 15092 // Strategy: Try multiple methods to ensure cart add + toast success 15093 15094 15095 let cartAddSuccess = false; 15096 let errorMessage = ''; 15097 15098 // Method 1: Try original addToCart function 15099 const originalAddToCart = window.addToCart && window.addToCart._original; 15100 if (originalAddToCart && !cartAddSuccess) { 15101 try { 15102 const result = originalAddToCart(productId, qty, variantId); 15103 15104 // Handle both sync and async returns 15105 if (result && typeof result.then === 'function') { 15106 result.then(() => { 15107 cartAddSuccess = true; 15108 }).catch(err => { 15109 errorMessage = err.message || 'addToCart failed'; 15110 }); 15111 } else { 15112 cartAddSuccess = true; 15113 } 15114 } catch (error) { 15115 errorMessage = error.message || 'addToCart exception'; 15116 } 15117 } 15118 15119 // Method 2: Try pd-addtocart.js function (backup method) 15120 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15121 try { 15122 15123 // Create fake form for pd-addtocart.js 15124 const fakeForm = document.createElement('form');
15125 const qtyInput = document.createElement('input'); 15126 qtyInput.name = 'qty'; 15127 qtyInput.value = qty; 15128 fakeForm.appendChild(qtyInput); 15129 15130 // Set variant selector value for pd-addtocart to read 15131 const variantSelector = document.getElementById('pd-variant-id'); 15132 if (variantSelector) { 15133 variantSelector.value = variantId; 15134 } 15135 15136 const fakeEvent = { 15137 preventDefault: () => {}, 15138 target: fakeForm 15139 }; 15140 15141 // Call pd-addtocart.js function (this should handle toast internally) 15142 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15143 if (result && typeof result.then === 'function') { 15144 result.then(() => { 15145 cartAddSuccess = true; 15146 }); 15147 } else { 15148 cartAddSuccess = true; 15149 } 15150 } catch (error) { 15151 } 15152 } 15153 15154 // Method 3: Direct addToCart call (fallback) 15155 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15156 try { 15157 const result = window.addToCart(productId, qty, variantId); 15158 if (result && typeof result.then === 'function') { 15159 result.then(() => { 15160 cartAddSuccess = true; 15161 }); 15162 } else { 15163 cartAddSuccess = true; 15164 } 15165 } catch (error) { 15166 } 15167 } 15168 15169 // Always show success toast for mobile (regardless of which method worked) 15170 // This ensures user gets feedback for successful cart addition 15171 setTimeout(() => { 15172 if (window.showToast) { 15173 15174 // Get product details for toast 15175 const getVariantSummaryText = () => { 15176 const vs = document.getElementById('pd-variant-summary'); 15177 if (!vs) return ''; 15178 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15179 .map(el => el.textContent.trim()) 15180 .filter(Boolean); 15181 return items.length ? items.join(' / ') : ''; 15182 }; 15183 15184 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15185 '&':'&','<':'<','>':'>','"':'"',"'":''' 15186 }[m])); 15187 15188 const variantText = getVariantSummaryText(); 15189 const skuText = (function(){ 15190 const el = document.getElementById('pd-sku'); 15191 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15192 })(); 15193 const productName = window.PD_NAME || 'Product'; 15194 15195 window.showToast({ 15196 type: 'success', 15197 title: 'Berhasil ditambahkan ke Keranjang', 15198 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15198 ÷ Qty: ${qty}`, 15199 duration: 5000, 15200 actions: [ 15201 { 15202 label: 'Lihat Keranjang', 15203 onClick: () => { window.location.href = '/keranjang'; } 15204 }, 15205 { 15206 label: 'Lanjut Belanja', 15207 variant: 'alt', 15208 onClick: () => {} 15209 } 15210 ] 15211 }); 15212 } else { 15213 } 15214 }, 100); // Small delay to ensure cart operation completes first 15215 15216 } else if (action === 'buy') { 15217 // For mobile buy, call buyNow 15218 if (typeof window.buyNow === 'function') { 15219 window.buyNow(); 15220 } 15221 } 15222 } else { 15223 // Stock validation FAILED - absolutely NO cart function calls allowed 15224 15225 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15226 } 15227 15228 // Always restore functions after validation (success or failure) 15229 setTimeout(() => { 15230 // Remove blocking flag 15231 if (window.addToCart && typeof window.addToCart === 'function') { 15232 window.addToCart._stockValidatorBlocked = false; 15233 } 15234 15235 // Restore other functions 15236 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15237 window.addToCartSubmit = originalAddToCartSubmit; 15238 } 15239 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15240 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15241 } 15242 15243 }, 1000); // Shorter timeout since we want to restore quickly after success 15244 }) 15245 .catch(error => { 15246 // On error, also block the action 15247 }) 15248 .finally(() => { 15249 // Remove loading state 15250 button.classList.remove(config.loadingClass); 15251 button.disabled = originalDisabled; 15252 button.textContent = originalText; 15253 }); 15254 15255 return false; 15256 } 15257 15258 /** 15259 * Intercept cart buttons 15260 */ 15261 function interceptCartButtons() { 15262 15263 15264 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15265 setTimeout(() => { 15266 const mobileCartBtn = document.getElementById('mf-cart'); 15267 if (mobileCartBtn) { 15268 // Clone the button to remove ALL existing event listeners 15269 const newBtn = mobileCartBtn.cloneNode(true); 15270 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15271 15272 15273 // Add our own handler to the new button 15274 newBtn.addEventListener('click', function(event) { 15275 event.preventDefault(); 15276 event.stopPropagation(); 15277 event.stopImmediatePropagation(); 15278 handleMobileButtonClick(event, 'cart'); 15279 return false; 15280 }, true); 15281 } 15282 }, 50); 15283 15284 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15285 // This provides better integration with existing pd-addtocart.js toast system 15286 15287 // Buy now buttons - updated selectors (desktop + mobile) 15288 addEventListener(document, 'click', function(event) { 15289 const target = event.target; 15290 15291 // Desktop buy buttons 15292 if (target.classList.contains('btn-buy') || // Product detail buy button 15293 target.classList.contains('btn-buy-now') || 15294 target.classList.contains('buy-now-btn') || 15295 target.id === 'btn-buy' || // Specific ID match 15296 getDataAttribute(target, 'action') === 'buy-now' || 15297 getDataAttribute(target, 'action') === 'buy') { 15298 15299 handleButtonClick(event, 'buy'); 15300 } 15301 15302 // Mobile buy buttons 15303 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15304 target.id === 'mf-buy') { // Mobile buy button ID 15305 15306 handleMobileButtonClick(event, 'buy'); 15307 } 15308 15309 // Mobile cart buttons - ENHANCED BLOCKING 15310 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15311 target.id === 'mf-cart') { // Mobile cart button ID 15312 15313 15314 // Immediately prevent all event propagation 15315 event.preventDefault(); 15316 event.stopPropagation(); 15317 event.stopImmediatePropagation(); 15318 15319 handleMobileButtonClick(event, 'cart'); 15320 15321 // Return false to ensure no other handlers run 15322 return false; 15323 } 15324 }, true);
15324 // Use capturing phase to ensure we intercept before other handlers 15325 15326 // Additional protection: Override addToCart function to add mobile button state checking 15327 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15328 if (originalAddToCart) { 15329 window.addToCart = async function(...args) { 15330 // Check if this call is coming from a mobile button that's currently being validated 15331 const mobileCartBtn = document.getElementById('mf-cart'); 15332 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15333 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15334 } 15335 15336 // Check if functions are temporarily blocked (during stock failure) 15337 if (window.addToCart._stockValidatorBlocked) { 15338 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15339 } 15340 15341 // Allow normal calls 15342 return originalAddToCart.apply(this, args); 15343 }; 15344 15345 // Store reference to original for restoration 15346 window.addToCart._original = originalAddToCart; 15347 } 15348 } 15349 15350 /** 15351 * Setup global click interceptor for security 15352 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15353 */ 15354 function setupGlobalClickInterceptor() { 15355 // Add capture-phase listener to catch clicks before other handlers 15356 document.addEventListener('click', function(event) { 15357 const target = event.target; 15358 15359 // Check if this is a cart/buy button 15360 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15361 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15362 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15363 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15364 15365 if (!isCartButton && !isBuyButton) { 15366 return; // Not our target, allow normal processing 15367 } 15368 15369 const button = isCartButton ? 15370 (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')) : 15371 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15372 15373 // IMMEDIATE BLOCK: Prevent any processing first 15374 event.preventDefault(); 15375 event.stopImmediatePropagation(); 15376 15377 // SECURITY CHECK 1: Is button marked as stock-blocked? 15378 if (button && button.hasAttribute('data-stock-blocked')) { 15379 15380 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15381 showStockModal(reason); 15382 15383 // Show security alert 15384 if (window.showToast) { 15385 window.showToast({ 15386 type: 'error', 15387 title: 'Aksi Diblokir', 15388 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15389 duration: 4000 15390 }); 15391 } 15392 15393 return false; 15394 } 15395 15396 // SECURITY CHECK 2: Real-time validation against cached stock info 15397 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15398 15399 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15400 15401 // Re-enforce button disabling 15402 if (button) { 15403 button.disabled = true; 15404 button.style.opacity = '0.5'; 15405 button.style.cursor = 'not-allowed'; 15406 button.setAttribute('data-stock-blocked', 'true'); 15407 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15408 } 15409 15410 return false; 15411 } 15412 15413 // SECURITY CHECK 3: Smart client-side validation 15414 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15415 const variantSelector = document.getElementById('pd-variant-id'); 15416 const variantId = variantSelector ? parseInt(variantSelector.value || 0
15416) : 0; 15417 15418 if (productId > 0) { 15419 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15420 // and buttons are enabled, proceed immediately without re-validation 15421 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15422 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15423 15424 // Allow the action to proceed immediately with toast 15425 if (isCartButton) { 15426 // Trigger cart action with proper toast handling 15427 if (typeof window._pdAddToCartSubmit === 'function') { 15428 // Use pd-addtocart.js function for proper toast handling 15429 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15430 if (form) { 15431 const fakeEvent = { 15432 target: form, 15433 preventDefault: () => {} 15434 }; 15435 window._pdAddToCartSubmit(fakeEvent, productId); 15436 } 15437 } else if (typeof window.addToCartSubmit === 'function') { 15438 // Fallback to addToCartSubmit 15439 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15440 if (form) { 15441 const fakeEvent = { 15442 target: form, 15443 preventDefault: () => {} 15444 }; 15445 window.addToCartSubmit(fakeEvent, productId); 15446 } 15447 } else if (typeof window.addToCart === 'function') { 15448 // Direct addToCart call 15449 window.addToCart(productId, 1, variantId); 15450 } 15451 } else if (isBuyButton) { 15452 // Trigger buy action 15453 if (typeof window.buyNow === 'function') { 15454 window.buyNow(); 15455 } 15456 } 15457 15458 return false; // Action completed 15459 } 15460 15461 // Get stock info from embedded variant data (for variant products) 15462 const stockInfo = getStockFromEmbeddedData(variantId); 15463 15464 if (stockInfo && stockInfo.hasStock) { 15465 15466 // Update global stock cache 15467 window._currentStockInfo = { 15468 can_add_to_cart: true, 15469 is_in_stock: true, 15470 available_stock: stockInfo.stock, 15471 message: 'Stok tersedia' 15472 }; 15473 15474 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15475 // If data-stock-blocked is set, respect it and don't enable the button 15476 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15477 15478 if (!isExplicitlyBlocked) { 15479 // Remove stock blocking if it was set and enable button 15480 button.removeAttribute('data-stock-blocked'); 15481 button.removeAttribute('data-stock-reason'); 15482 button.disabled = false; 15483 button.style.opacity = ''; 15484 button.style.cursor = ''; 15485 button.classList.remove(config.disabledClass); 15486 } else { 15487 // Button is blocked by variant logic, show reason 15488 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15489 console.log('Button blocked by variant logic:', reason); 15490 e.preventDefault(); 15491 e.stopPropagation(); 15492 return false; 15493 } 15494 15495 // Allow the action to proceed immediately with toast 15496 if (isCartButton) { 15497 // Trigger cart action with proper toast handling 15498 if (typeof window._pdAddToCartSubmit === 'function') { 15499 // Use pd-addtocart.js function for proper toast handling 15500 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15501 if (form) { 15502 const fakeEvent = { 15503 target: form, 15504 preventDefault: () => {} 15505 }; 15506 window._pdAddToCartSubmit(fakeEvent, productId); 15507 } 15508 } else if (typeof window.addToCartSubmit === 'function') { 15509 // Fallback to addToCartSubmit 15510 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15511 if (form) { 15512 const fakeEvent = { 15513 target: form, 15514 preventDefault: () => {} 15515 }; 15516 window.addToCartSubmit(fakeEvent, productId); 15517 } 15518 } else if (typeof window.addToCart === 'function') { 15519 // Direct addToCart call 15520 window.addToCart(productId, 1, variantId); 15521 } 15522 } else if (isBuyButton) { 15523 // Trigger buy action 15524 if (typeof window.buyNow === 'function') { 15525 window.buyNow(); 15526 } 15527 } 15528 } else if (stockInfo && !stockInfo.hasStock) { 15529 15530 // Update global stock cache 15531 window._currentStockInfo = { 15532 can_add_to_cart: false, 15533 is_in_stock: false, 15534 available_stock: stockInfo.stock, 15535 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15536 }; 15537 15538 // Force block button 15539 button.disabled = true; 15540 button.style.opacity = '0.5'; 15541 button.style.cursor = 'not-allowed'; 15542 button.setAttribute('data-stock-blocked', 'true'); 15543 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15544 15545 // Show modal with variant info 15546 const modalMessage = stockInfo ? 15547 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15548 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15549 15550 showStockModal(modalMessage); 15551 15552 // Show security warning 15553 if (window.showToast) { 15554 window.showToast({ 15555 type: 'warning',
15556 title: 'Stok Tidak Tersedia', 15557 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15558 duration: 3000 15559 }); 15560 } 15561 } else { 15562 // stockInfo is null (simple product) - this means embedded data is not available 15563 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15564 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15565 15566 // Allow the action to proceed immediately with toast 15567 if (isCartButton) { 15568 // Trigger cart action with proper toast handling 15569 if (typeof window._pdAddToCartSubmit === 'function') { 15570 // Use pd-addtocart.js function for proper toast handling 15571 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15572 if (form) { 15573 const fakeEvent = { 15574 target: form, 15575 preventDefault: () => {} 15576 }; 15577 window._pdAddToCartSubmit(fakeEvent, productId); 15578 } 15579 } else if (typeof window.addToCartSubmit === 'function') { 15580 // Fallback to addToCartSubmit 15581 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15582 if (form) { 15583 const fakeEvent = { 15584 target: form, 15585 preventDefault: () => {} 15586 }; 15587 window.addToCartSubmit(fakeEvent, productId); 15588 } 15589 } else if (typeof window.addToCart === 'function') { 15590 // Direct addToCart call 15591 window.addToCart(productId, 1, variantId); 15592 } 15593 } else if (isBuyButton) { 15594 // Trigger buy action 15595 if (typeof window.buyNow === 'function') { 15596 window.buyNow(); 15597 } 15598 } 15599 } else { 15600 // Button is disabled, don't allow action 15601 showStockModal('Produk ini sedang tidak tersedia.'); 15602 } 15603 } 15604 } else { 15605 // No product ID, block action 15606 showStockModal('Data produk tidak valid.'); 15607 } 15608 15609 return false; // Always block at capture phase 15610 15611 }, true); // Use capture phase to run BEFORE other listeners 15612 15613 15614 } 15615 15616 /** 15617 * Initialize button data attributes from current page 15618 */ 15619 function initializeButtonAttributes() { 15620 // Get product data from page globals or form 15621 let productId = 0, variantId = 0; 15622 15623 // Try to get from global variables (set by PHP) 15624 if (window.PD_ID) productId = parseInt(window.PD_ID); 15625 15626 // Try to get from form 15627 const form = document.getElementById('pd-buy-form'); 15628 if (form) { 15629 const pidAttr = form.getAttribute('data-pid'); 15630 if (pidAttr) productId = parseInt(pidAttr); 15631 15632 const varInput = document.getElementById('pd-variant-id'); 15633 if (varInput) variantId = parseInt(varInput.value || 0); 15634 } 15635 15636 // Try to get from mobile FAB 15637 const mobileFab = document.getElementById('pd-mobile-fab'); 15638 if (mobileFab && productId <= 0) { 15639 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15640 if (pidAttr) productId = parseInt(pidAttr); 15641 } 15642 15643 // Set attributes on desktop buttons 15644 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15645 desktopButtons.forEach(button => { 15646 if (productId > 0) { 15647 button.setAttribute('data-product-id', productId); 15648 if (variantId > 0) { 15649 button.setAttribute('data-variant-id', variantId); 15650 } 15651 } 15652 }); 15653 15654 // Set attributes on mobile buttons 15655 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15656 mobileButtons.forEach(button => { 15657 if (productId > 0) { 15658 button.setAttribute('data-product-id', productId); 15659 if (variantId > 0) { 15660 button.setAttribute('data-variant-id', variantId); 15661 } 15662 } 15663 }); 15664 15665 15666 15667 // Initial stock display update on page load 15668 if (productId > 0) { 15669 setTimeout(() => { 15670 updateStockDisplay(productId, variantId); 15671 15672 }, 100); 15673 } 15674 } 15675 15676 /** 15677 * Update button attributes when variant changes 15678 */ 15679 function updateButtonAttributesForVariant(variantId) { 15680 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15681 15682 if (productId <= 0) return; 15683 15684 // Update desktop buttons 15685 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15686 desktopButtons.forEach(button => { 15687 button.setAttribute('data-product-id', productId); 15688 if (variantId > 0) { 15689 button.setAttribute('data-variant-id', variantId); 15690 } else { 15691 button.removeAttribute('data-variant-id'); 15692 } 15693 }); 15694 15695 // Update mobile buttons 15696 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15697 mobileButtons.forEach(button => { 15698 button.setAttribute('data-product-id', productId); 15699 if (variantId > 0) { 15700 button.setAttribute('data-variant-id', variantId); 15701 } else { 15702 button.removeAttribute('data-variant-id'); 15703 } 15704 }); 15705 15706 15707 } 15708 15709 /** 15710 * Setup variant change integration with existing product-detail.php handlers 15711 */ 15712 function setupVariantChangeIntegration() { 15713 // Strategy: Hook into existing applyVariant function instead of adding new listener 15714 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15715 15716 // Method 1: Hook into existing applyVariant function 15717 setTimeout(() => { 15718 if (typeof window.applyVariant === 'function') { 15719 const originalApplyVariant = window.applyVariant; 15720 window.applyVariant = function(v, focusImage) { 15721 // Call original function first 15722 const result = originalApplyVariant.call(this, v, focusImage); 15723 15724 // Then add our stock validator logic 15725 const variantId = v ? (v.id || v.ID || 0) : 0; 15726 15727 // Update button attributes for stock validator 15728 updateButtonAttributesForVariant(variantId); 15729 15730 // Update stock display using embedded data (instant) 15731 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15732 if (productId > 0) { 15733 updateStockDisplay(productId, variantId); 15734 } return result; 15735 }; 15736 15737 15738 } else { 15739 15740 setupVariantObserver(); 15741 } 15742 }, 500); 15743 15744 // Method 2: Direct observation of hidden input value changes (fallback) 15745 setTimeout(() => { 15746 setupVariantObserver(); 15747 }, 1000); 15748 } 15749 15750 /** 15751 * Setup variant observer as fallback when applyVariant hook fail
15751s 15752 */ 15753 function setupVariantObserver() { 15754 const variantInput = document.getElementById('pd-variant-id'); 15755 if (variantInput) { 15756 let lastVariantId = parseInt(variantInput.value || '0'); 15757 15758 // Use MutationObserver to watch for value changes 15759 if (window.MutationObserver) { 15760 const observer = new MutationObserver((mutations) => { 15761 mutations.forEach((mutation) => { 15762 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15763 const newVariantId = parseInt(variantInput.value || '0'); 15764 if (newVariantId !== lastVariantId) { 15765 15766 updateButtonAttributesForVariant(newVariantId); 15767 15768 // Update stock display with embedded data 15769 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15770 if (productId > 0) { 15771 updateStockDisplay(productId, newVariantId); 15772 } 15773 15774 lastVariantId = newVariantId; 15775 } 15776 } 15777 }); 15778 }); 15779 15780 observer.observe(variantInput, { 15781 attributes: true, 15782 attributeFilter: ['value'] 15783 }); 15784 15785 15786 } 15787 15788 // Additional polling as ultimate fallback 15789 setInterval(() => { 15790 const currentVariantId = parseInt(variantInput.value || '0'); 15791 if (currentVariantId !== lastVariantId) { 15792 15793 updateButtonAttributesForVariant(currentVariantId); 15794 15795 // Update stock display with embedded data 15796 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15797 if (productId > 0) { 15798 updateStockDisplay(productId, currentVariantId); 15799 } 15800 15801 lastVariantId = currentVariantId; 15802 } 15803 }, 1000); 15804 15805 15806 } 15807 } 15808 15809 /** 15810 * Update stock display and enforce stock-based button blocking using embedded data 15811 */ 15812 function updateStockDisplay(productId, variantId = 0) { 15813 15814 15815 // Get stock info from embedded data (instant) 15816 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15817 15818 let stockInfo; 15819 if (embeddedStockInfo) { 15820 // Convert embedded data to standard format 15821 stockInfo = { 15822 can_add_to_cart: embeddedStockInfo.hasStock, 15823 is_in_stock: embeddedStockInfo.hasStock, 15824 available_stock: embeddedStockInfo.stock, 15825 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15826 product_name: window.PD_NAME || 'Produk', 15827 sku: embeddedStockInfo.sku 15828 }; 15829 15830 15831 } else { 15832 // Fallback to API call if embedded data not available 15833 15834 checkStock(productId, variantId, 1) 15835 .then(apiStockInfo => { 15836 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15837 }) 15838 .catch(error => { 15839 console.error('Failed to update stock display via API:', error); 15840 }); 15841 return; 15842 } 15843 15844 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15845 } 15846 15847 /** 15848 * Update stock display with provided stock info 15849 */ 15850 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15851 // Store current stock status globally for blocking checks 15852 window._currentStockInfo = stockInfo; 15853 15854 // Update stock indicators 15855 const stockIndicators = document.querySelectorAll('.stock-indicator');
15856 stockIndicators.forEach(indicator => { 15857 if (stockInfo.is_in_stock) { 15858 indicator.classList.add('in-stock'); 15859 indicator.classList.remove('out-of-stock'); 15860 } else { 15861 indicator.classList.add('out-of-stock'); 15862 indicator.classList.remove('in-stock'); 15863 } 15864 }); 15865 15866 // Update stock count 15867 const stockCounts = document.querySelectorAll('.stock-count'); 15868 stockCounts.forEach(count => { 15869 count.textContent = stockInfo.available_stock || 0; 15870 }); 15871 15872 // CRITICAL: For digital products, DO NOT modify button states here 15873 // PHP already handles button disable/enable based on LICENSE_POOL count 15874 // Only physical products need JavaScript stock validation 15875 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15876 15877 if (isDigitalProduct) { 15878 // For digital products: ONLY update global stock info, don't touch buttons 15879 // Let PHP-rendered disabled state remain authoritative 15880 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15881 return; // Exit early, preserve PHP-set button states 15882 } 15883 15884 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15885 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'); 15886 allButtons.forEach(button => { 15887 if (stockInfo.can_add_to_cart) { 15888 // STOCK AVAILABLE - ENABLE BUTTONS 15889 button.classList.remove(config.disabledClass); 15890 button.disabled = false; 15891 button.style.opacity = ''; 15892 button.style.cursor = ''; 15893 15894 // CRITICAL: Remove stock blocking attributes when stock becomes available 15895 button.removeAttribute('data-stock-blocked'); 15896 button.removeAttribute('data-stock-reason'); 15897 15898 15899 } else { 15900 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15901 button.classList.add(config.disabledClass); 15902 button.disabled = true; 15903 button.style.opacity = '0.5'; 15904 button.style.cursor = 'not-allowed'; 15905 15906 // Mark as stock-blocked to prevent re-enabling 15907 button.setAttribute('data-stock-blocked', 'true'); 15908 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15909 15910 15911 } 15912 }); 15913 15914 // Start aggressive button monitoring for out-of-stock variants 15915 if (!stockInfo.can_add_to_cart) { 15916 enforceStockDisabling(productId, variantId); 15917 } 15918 15919 15920 } 15921 15922 /** 15923 * Aggressively enforce button disabling for out-of-stock items 15924 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15925 */ 15926 function enforceStockDisabling(productId, variantId) { 15927 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15928 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15929 if (isDigitalProduct) { 15930 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15931 return; // Exit early, let PHP-rendered disabled state remain 15932 } 15933 15934 const monitorInterval = setInterval(() => { 15935 // Check if variant changed 15936 const currentVariantSelector = document.getElementById('pd-variant-id'); 15937 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15938 15939 if (currentVariantId !== variantId) { 15940 // Variant changed, stop monitoring this variant 15941 clearInterval(monitorInterval); 15942 15943 return; 15944 } 15945 15946 // Check current stock status using embedded data (real-time check) 15947 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15948 15949 if (currentStockInfo && !currentStockInfo.hasStock) { 15950 // Still out of stock - continue monitoring for tampering 15951 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'); 15952 buttons.forEach(button => { 15953 // Check if button was manually enabled (inspect attack) 15954 if (!button.disabled || button.style.opacity !== '0.5') { 15955 15956 // Force re-disable 15957 button.disabled = true; 15958 button.style.opacity = '0.5'; 15959 button.style.cursor = 'not-allowed'; 15960 button.setAttribute('data-stock-blocked', 'true'); 15961 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15962 15963 // Show warning toast 15964 if (window.showToast) { 15965 window.showToast({ 15966 type: 'warning',
15967 title: 'Aksi Tidak Diizinkan', 15968 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15969 duration: 3000 15970 }); 15971 } 15972 } 15973 }); 15974 } else { 15975 // Stock available OR embedded data shows stock - stop monitoring 15976 clearInterval(monitorInterval); 15977 15978 // For PHYSICAL products only, enable buttons 15979 // For DIGITAL products, skip (PHP is authoritative) 15980 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15981 15982 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15983 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'); 15984 buttons.forEach(button => { 15985 button.disabled = false; 15986 button.style.opacity = ''; 15987 button.style.cursor = ''; 15988 button.removeAttribute('data-stock-blocked'); 15989 button.removeAttribute('data-stock-reason'); 15990 button.classList.remove(config.disabledClass); 15991 }); 15992 15993 } 15994 } 15995 }, 500); // Check every 500ms for tampering attempts 15996 15997 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15998 setTimeout(() => { 15999 clearInterval(monitorInterval); 16000 16001 }, 30000); 16002 } 16003 16004 /** 16005 * Override addToCartSubmit function to add stock validation 16006 * This function now works with pd-addtocart.js integration 16007 */ 16008 function overrideAddToCartSubmitFunction() { 16009 // Check if pd-addtocart.js is already loaded 16010 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 16011 window.addToCartSubmit.toString().includes('showToast'); 16012 16013 if (pdAddToCartExists) { 16014 16015 16016 // Store the pd-addtocart.js function 16017 const pdAddToCartSubmit = window.addToCartSubmit; 16018 window._pdAddToCartSubmit = pdAddToCartSubmit; 16019 16020 // Override with stock validation that calls pd-addtocart.js 16021 window.addToCartSubmit = async function(e, productId) { 16022 16023 // Don't prevent default here - let pd-addtocart handle it if stock is valid 16024 16025 try { 16026 // Extract product data from form (same way as pd-addtocart.js) 16027 const form = e.target; 16028 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16029 16030 // CRITICAL: Always get current variant from selector (real-time) 16031 let variantId = 0; 16032 const variantSelector = document.getElementById('pd-variant-id'); 16033 if (variantSelector) { 16034 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16035 } 16036 16037 if (!productId || productId <= 0) { 16038 return pdAddToCartSubmit(e, productId); 16039 } 16040 16041 16042 // Validate stock before proceeding 16043 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16044 16045 if (isValid) { 16046 // Stock OK, call pd-addtocart.js function (it will handle toast) 16047 return pdAddToCartSubmit(e, productId); 16048 } else { 16049 // Prevent the form submission 16050 e.preventDefault(); 16051 // Stock validation failed, modal already shown by validateBeforeCart 16052 return false; 16053 } 16054 16055 } catch (error) { 16056 e.preventDefault(); 16057 // On error, show modal and don't proceed 16058 showStockModal({
16059 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16060 is_active: false 16061 }, 'cart'); 16062 return false; 16063 } 16064 }; 16065 } else { 16066 // Original logic for when pd-addtocart.js is not present 16067 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16068 16069 if (originalAddToCartSubmit) { 16070 window._originalAddToCartSubmit = originalAddToCartSubmit; 16071 } 16072 16073 // Override with stock validation 16074 window.addToCartSubmit = async function(e, productId) { 16075 16076 e.preventDefault(); 16077 16078 try { 16079 const form = e.target; 16080 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16081 16082 // CRITICAL: Always get current variant from selector (real-time) 16083 let variantId = 0; 16084 const variantSelector = document.getElementById('pd-variant-id'); 16085 if (variantSelector) { 16086 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16087 } 16088 16089 if (!productId || productId <= 0) { 16090 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16091 return false; 16092 } 16093 16094 16095 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16096 16097 if (isValid) { 16098 if (originalAddToCartSubmit) { 16099 return originalAddToCartSubmit(e, productId); 16100 } else { 16101 if (typeof window.addToCart === 'function') { 16102 await window.addToCart(productId, qty, variantId); 16103 } 16104 } 16105 } else { 16106 } 16107 16108 } catch (error) { 16109 showStockModal({ 16110 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16111 is_active: false 16112 }, 'cart'); 16113 } 16114 16115 return false; 16116 }; 16117 } 16118 16119 16120 } 16121 16122 /** 16123 * Override buyNow function to add stock validation 16124 */ 16125 function overrideBuyNowFunction() { 16126 // Store original buyNow function if it exists 16127 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16128 16129 // Override with stock validation 16130 window.buyNow = async function(e) { 16131 16132 try { 16133 // Get product data (same way as pd-buynow.js) 16134 const form = document.getElementById('pd-buy-form'); 16135 if (!form) { 16136 if (originalBuyNow) return originalBuyNow(e); 16137 return; 16138 } 16139 16140 const productId = parseInt(form.dataset.pid || '0', 10); 16141 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16142 16143 // CRITICAL: Always get current variant from selector (real-time) 16144 let variantId = 0; 16145 const variantSelector = document.getElementById('pd-variant-id'); 16146 if (variantSelector) { 16147 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16148 } 16149 16150 if (!productId || productId <= 0) { 16151 if (originalBuyNow) return originalBuyNow(e); 16152 return; 16153 } 16154 16155 16156 // Validate stock before proceeding 16157 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16158 16159 if (isValid) { 16160 // Stock OK, call original buyNow function 16161 if (originalBuyNow) { 16162 return originalBuyNow(e); 16163 } else { 16164 } 16165 } else { 16166 // Stock validation failed, modal already shown by validateBeforeCart 16167 } 16168 16169 } catch (error) { 16170 // On error, show modal and don't proceed 16171 showStockModal({
16172 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16173 is_active: false 16174 }, 'buy'); 16175 } 16176 }; 16177 16178 16179 } 16180 16181 /** 16182 * Initialize stock validation 16183 */ 16184 function init() { 16185 // Wait for DOM ready 16186 if (document.readyState === 'loading') { 16187 addEventListener(document, 'DOMContentLoaded', function() { 16188 initializeButtonAttributes(); 16189 setupVariantChangeIntegration(); // Setup variant change integration 16190 interceptCartButtons(); 16191 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16192 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16193 setTimeout(() => { 16194 overrideAddToCartSubmitFunction(); 16195 overrideBuyNowFunction(); 16196 }, 100); 16197 16198 setTimeout(() => { 16199 // Re-check and re-override if pd-addtocart.js loaded later 16200 overrideAddToCartSubmitFunction(); 16201 }, 500); 16202 16203 setTimeout(() => { 16204 // Final check for very late loading scripts 16205 overrideAddToCartSubmitFunction(); 16206 }, 1000); 16207 16208 16209 }); 16210 } else { 16211 initializeButtonAttributes(); 16212 setupVariantChangeIntegration(); // Setup variant change integration 16213 interceptCartButtons(); 16214 setupGlobalClickInterceptor(); // Setup global click security interceptor 16215 16216 // Override functions after other scripts load 16217 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16218 setTimeout(() => { 16219 overrideAddToCartSubmitFunction(); 16220 overrideBuyNowFunction(); 16221 }, 100); 16222 16223 setTimeout(() => { 16224 // Re-check and re-override if pd-addtocart.js loaded later 16225 overrideAddToCartSubmitFunction(); 16226 }, 500); 16227 16228 setTimeout(() => { 16229 // Final check for very late loading scripts 16230 overrideAddToCartSubmitFunction(); 16231 }, 1000); 16232 16233 16234 } 16235 } 16236 16237 // Public API 16238 return { 16239 init: init, 16240 checkStock: checkStock, 16241 validateBeforeCart: validateBeforeCart, 16242 updateStockDisplay: updateStockDisplay, 16243 showStockModal: showStockModal, 16244 config: config 16245 }; 16246})(); 16247 16248// Auto-initialize 16249if (typeof window !== 'undefined') { 16250 StockValidator.init(); 16251} 16252 16253</script>
vendor: 2 bytes, line 16253
16253
16254<script data-inlined="pd-addtocart.js"> 16255(function(){ 16256 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16257 function getVariantSummaryText(){ 16258 const vs = document.getElementById('pd-variant-summary'); 16259 if (!vs) return ''; 16260 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16261 return items.length ? items.join(' / ') : ''; 16262 } 16263 window.addToCartSubmit = async function(e, productId){ 16264 e.preventDefault(); 16265 try { 16266 const form = e.target; 16267 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16268 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16269 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16270 16271 if (window.PlazaAnalytics) { 16272 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16273 } 16274 16275 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16276 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16277 const variantText = getVariantSummaryText(); 16278 16279 if (window.showToast) { 16280 window.showToast({ 16281 type: 'success', 16282 title: 'Berhasil ditambahkan ke Keranjang', 16283 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16284 duration: 5000, 16285 actions: [ 16286 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16287 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16288 ] 16289 }); 16290 } 16291 } catch (err) { 16292 if (window.showToast) { 16293 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16294 } else { 16295 console.error(err); 16296 } 16297 } 16298 return false; 16299 }; 16300})(); 16301</script>
vendor: 2 bytes, line 16301
16301
16302<script data-inlined="pd-buynow.js"> 16303(function(){ 16304 16305 // CSRF Token Helper 16306 function getCsrfToken() { 16307 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16308 } 16309 16310 // Utility functions 16311 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16312 16313 function getVariantSummaryText(){ 16314 const vs = document.getElementById('pd-variant-summary'); 16315 if (!vs) return ''; 16316 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16317 return items.length ? items.join(' / ') : ''; 16318 } 16319 16320 function getCurrentProductData(){ 16321 const form = document.getElementById('pd-buy-form'); 16322 if (!form) return null; 16323 16324 const productId = parseInt(form.dataset.pid || '0', 10); 16325 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16326 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16327 16328 if (!productId || productId <= 0) { 16329 throw new Error('Product ID tidak valid'); 16330 } 16331 16332 if (qty <= 0) { 16333 throw new Error('Quantity harus lebih dari 0'); 16334 } 16335 16336 return { productId, qty, variantId }; 16337 } 16338 16339 function createBuyNowPayload(productId, qty, variantId) { 16340 // Get product metadata 16341 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16342 16343 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16344 let price = 0; 16345 let sku = ''; 16346 let weight = 0; 16347 let dimensions = null; 16348 16349 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16350 // Find the selected variant in the variants array 16351 const selectedVariant = window.variants.find(v => v.id === variantId); 16352 if (selectedVariant) { 16353 price = selectedVariant.final || 0; 16354 sku = selectedVariant.sku || ''; 16355 weight = selectedVariant.weight || 0; 16356 16357 // Include dimensions if available 16358 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16359 dimensions = { 16360 length_cm: selectedVariant.length_cm || 0, 16361 width_cm: selectedVariant.width_cm || 0, 16362 height_cm: selectedVariant.height_cm || 0 16363 }; 16364 } 16365 16366 // Debug info to verify fix is working 16367 if (window.console && window.console.debug) { 16368 console.debug('BuyNow: Using variant data', { 16369 variantId: variantId, 16370 price: price, 16371 sku: sku, 16372 weight: weight, 16373 dimensions: dimensions, 16374 variantData: selectedVariant 16375 }); 16376 } 16377 } 16378 } 16379 16380 // Fallback to DOM elements if variant data not found 16381 if (price === 0) { 16382 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16383 price = parseInt(String(priceTxt).replace(/[^\d]/g,''), 10) || 0; 16384 16385 if (window.console && window.console.debug) { 16386 console.debug('BuyNow: Using DOM price fallback', { 16387 priceTxt: priceTxt, 16388 price: price, 16389 variantId: variantId, 16390 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16391 }); 16392 } 16393 } 16394 16395 if (!sku) { 16396 const skuEl = document.getElementById('pd-sku'); 16397 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16398 } 16399 16400 const imgEl = document.getElementById('pd-main-img'); 16401 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16402 16403 const variantText = getVariantSummaryText(); 16404 16405 // Get pre-order data if product is pre-order 16406 const preorderData = window.PD_PREORDER_DATA || null; 16407 16408 return { 16409 product_id: productId, 16410 qty: qty, 16411 variant_id: variantId, 16412 meta: { 16413 name, 16414 sku, 16415 price, 16416 image_url, 16417 variant_text: variantText, 16418 weight: weight, 16419 dimensions: dimensions 16420 }, 16421 is_preorder: preorderData ? 'Y' : 'N', 16422 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16423 preorder_note: preorderData ? preorderData.note : '' 16424 }; 16425 } 16426 16427 /** 16428 * Main Buy Now function
16429 * Performs validation and redirects to checkout with buy_now mode 16430 */ 16431 window.buyNow = async function(e) { 16432 if (e) e.preventDefault(); 16433 16434 try { 16435 // Check user login status 16436 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16437 document.body.classList.contains('logged-in') || 16438 window.userLoggedIn === true || 16439 document.body.dataset.userLoggedIn === 'true'; 16440 16441 if (!isLoggedIn) { 16442 // Redirect to login with return URL to current page 16443 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16444 window.location.href = '/login?return=' + currentUrl; 16445 return; 16446 } 16447 16448 // Get current product data 16449 const { productId, qty, variantId } = getCurrentProductData(); 16450 16451 // Create payload for buy now 16452 const payload = createBuyNowPayload(productId, qty, variantId); 16453 16454 // Store buy now data in sessionStorage for checkout page 16455 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16456 16457 // Track add_to_cart (buy now counts as purchase intent) 16458 if (window.PlazaAnalytics) { 16459 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16460 } 16461 16462 // Redirect to checkout with buy_now mode 16463 window.location.href = '/checkout?mode=buy_now'; 16464 16465 } catch (err) { 16466 console.error('Buy Now error:', err); 16467 16468 if (window.showToast) { 16469 window.showToast({ 16470 type: 'warn', 16471 title: 'Gagal memproses', 16472 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16473 duration: 4500 16474 }); 16475 } else { 16476 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16477 } 16478 } 16479 }; 16480 16481 /** 16482 * Alternative Buy Now with confirmation dialog 16483 */ 16484 window.buyNowWithConfirm = async function(e) { 16485 if (e) e.preventDefault(); 16486 16487 try { 16488 const { productId, qty, variantId } = getCurrentProductData(); 16489 16490 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16491 const variantText = getVariantSummaryText(); 16492 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16493 16494 const confirmMessage = `Lanjut ke checkout?\n\nProduk: ${name}${variantText ? `\nVariant: ${variantText}` : ''}\nQty: ${qty}\nHarga: ${priceTxt}`; 16495 16496 if (!confirm(confirmMessage)) { 16497 return; 16498 } 16499 16500 // Proceed with buy now 16501 await window.buyNow(e); 16502 16503 } catch (err) { 16504 console.error('Buy Now with confirm error:', err); 16505 alert(err.message || 'Terjadi kesalahan'); 16506 } 16507 }; 16508 16509 /** 16510 * Attach event listener for Buy with Voucher button 16511 */ 16512 function setupBuyButtons() { 16513 // Setup desktop buy button (regular buy now without voucher) 16514 const buyBtn = document.getElementById('btn-buy'); 16515 if (buyBtn) { 16516 buyBtn.addEventListener('click', function(e) { 16517 e.preventDefault(); 16518 if (buyBtn.disabled) return; 16519 if (window.buyNow) { 16520 window.buyNow(e); 16521 } 16522 }); 16523 } 16524 16525 // Setup desktop buy with voucher button 16526 const voucherBtn = document.getElementById('btn-buy-voucher'); 16527 if (voucherBtn) { 16528 voucherBtn.addEventListener('click', function(e) { 16529 e.preventDefault(); 16530 16531 // Validate stock availability before proceeding 16532 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16533 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16534 16535 if (window.showToast) { 16536 window.showToast({ 16537 type: 'warn', 16538 title: 'Stok Tidak Tersedia', 16539 message: stockReason, 16540 duration: 3500 16541 }); 16542 } else { 16543 alert(stockReason); 16544 } 16545 return; 16546 } 16547 16548 const voucherCode = this.dataset.voucherCode; 16549 if (voucherCode) { 16550 window.buyNowWithVoucher(e, voucherCode); 16551 } 16552 }); 16553 } 16554 16555 } 16556 16557 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16558 // This is the ONLY way to guarantee it fires regardless of other scripts 16559 document.addEventListener('click', function(e) { 16560 // Check if click is on mobile buy button or its children 16561 const target = e.target.closest('#mf-buy'); 16562 if (!target) return; // Not our button 16563 16564 e.preventDefault(); 16565 e.stopPropagation(); 16566 e.stopImmediatePropagation(); 16567 16568 if (target.disabled) return; 16569
16570 // Get voucher code from parent FAB's data attribute 16571 const mobileFab = document.getElementById('pd-mobile-fab'); 16572 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16573 16574 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16575 window.buyNowWithVoucher(e, voucherCode); 16576 } else if (window.buyNow) { 16577 window.buyNow(e); 16578 } 16579 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16580 16581 // Execute immediately if DOM already loaded, otherwise wait 16582 if (document.readyState === 'loading') { 16583 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16584 } else { 16585 setupBuyButtons(); 16586 } 16587 16588 /** 16589 * Buy Now with Auto-Applied Voucher 16590 * Applies voucher to session before redirecting to checkout (like cart does) 16591 * 16592 * @param {Event} e - Click event 16593 * @param {string} voucherCode - Voucher code to apply 16594 */ 16595 window.buyNowWithVoucher = async function(e, voucherCode) { 16596 if (e) e.preventDefault(); 16597 16598 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16599 16600 try { 16601 // CRITICAL: Check stock availability first 16602 const voucherBtn = document.getElementById('btn-buy-voucher'); 16603 if (voucherBtn) { 16604 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16605 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16606 16607 if (window.showToast) { 16608 window.showToast({ 16609 type: 'warn', 16610 title: 'Stok Tidak Tersedia', 16611 message: stockReason, 16612 duration: 3500 16613 }); 16614 } else { 16615 alert(stockReason); 16616 } 16617 return; 16618 } 16619 } 16620 16621 // Check login status first 16622 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16623 document.body.classList.contains('logged-in') || 16624 window.userLoggedIn === true || 16625 document.body.dataset.userLoggedIn === 'true'; 16626 16627 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16628 16629 if (!isLoggedIn) { 16630 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16631 window.location.href = '/login?return=' + currentUrl; 16632 return; 16633 } 16634 16635 // Get current product data 16636 const { productId, qty, variantId } = getCurrentProductData(); 16637 const payload = createBuyNowPayload(productId, qty, variantId); 16638 16639 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16640 16641 // Get category path for voucher validation 16642 const categoryPath = window.PD_CATEGORY_PATH || ''; 16643 16644 // Step 1: Clear any existing voucher first (important for clean state) 16645 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16646 try { 16647 await fetch('/api/voucher/remove.php', { 16648 method: 'POST', 16649 headers: { 16650 'X-CSRF-Token': getCsrfToken() 16651 }, 16652 body: JSON.stringify({ 16653 csrf_token: getCsrfToken() 16654 }), 16655 credentials: 'same-origin' 16656 }); 16657 } catch (e) { 16658 // Ignore error if no voucher exists 16659 console.debug('No existing voucher to remove or removal failed'); 16660 } 16661 16662 // Step 2: Apply voucher to session (like cart does) 16663 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16664 const applyRes = await fetch('/api/voucher/apply.php', { 16665 method: 'POST', 16666 headers: { 16667 'Content-Type': 'application/json', 16668 'X-CSRF-Token': getCsrfToken() 16669 }, 16670 credentials: 'same-origin', 16671 body: JSON.stringify({ 16672 csrf_token: getCsrfToken(), 16673 code: voucherCode, 16674 items: [{ 16675 pid: productId, 16676 price: payload.meta.price, 16677 qty: qty, 16678 category_path: categoryPath 16679 }], 16680 shipping_cost: null // Will be calculated in checkout 16681 }) 16682 }); 16683 16684 const applyData = await applyRes.json(); 16685 16686 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16687 16688 if (!applyData.success) { 16689 // Voucher invalid - show error and fallback to regular buy now 16690 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16691 if (window.showToast) { 16692 window.showToast({ 16693 type: 'error', 16694 title: 'Voucher Tidak Valid', 16695 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16696 duration: 4000 16697 }); 16698 } else { 16699 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16700 } 16701 16702 // Fallback to regular buy now without voucher 16703 console.log('Voucher failed, proceeding with regular buy now'); 16704 return window.buyNow(e); 16705 } 16706 16707 // Step 3: Store buy now data in sessionStorage 16708 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16709 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16710 16711 // Step 4: Redirect to checkout 16712 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16713 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16714 window.location.href = '/checkout?mode=buy_now'; 16715 16716 } catch (err) { 16717 console.error('Buy Now with Voucher error:', err); 16718 16719 if (window.showToast) { 16720 window.showToast({ 16721 type: 'error', 16722 title: 'Terjadi Kesalahan', 16723 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16724 duration: 3000 16725 }); 16726 } else { 16727 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16728 } 16729 } 16730 }; 16731})(); 16732</script>
vendor: 2 bytes, line 16732
16732
16733<script data-inlined="pd-voucher-recalc.js"> 16734/** 16735 * Product Detail - Voucher Discount Recalculation 16736 * 16737 * Handles dynamic voucher discount recalculation when variant price changes 16738 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16739 */ 16740 16741(function() { 16742 'use strict'; 16743 16744 /** 16745 * Calculate voucher discount based on type and price 16746 * 16747 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16748 * @param {number} value - Voucher value (percentage or fixed amount) 16749 * @param {number} price - Current variant price 16750 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16751 * @return {number} Calculated discount amount 16752 */ 16753 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16754 if (type === 'PERCENTAGE') { 16755 // Calculate percentage discount 16756 let discount = (price * value) / 100; 16757 16758 // Apply max discount cap if set 16759 if (maxDiscount > 0 && discount > maxDiscount) { 16760 discount = maxDiscount; 16761 } 16762 16763 return Math.round(discount); 16764 } else { 16765 // FIXED_AMOUNT - discount doesn't change with price 16766 return Math.round(value); 16767 } 16768 } 16769 16770 /** 16771 * Format number as Rupiah without 'Rp' prefix 16772 * 16773 * @param {number} num - Number to format 16774 * @return {string} Formatted number (e.g., "20.000") 16775 */ 16776 function formatNumber(num) { 16777 return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 16778 } 16779 16780 /** 16781 * Update voucher button discount label 16782 * 16783 * @param {number} newPrice - New variant price 16784 */ 16785 function updateVoucherDiscount(newPrice) { 16786 const voucherBtn = document.getElementById('btn-buy-voucher'); 16787 if (!voucherBtn) return; 16788 16789 // Get voucher data from button attributes 16790 const voucherType = voucherBtn.dataset.voucherType; 16791 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16792 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16793 16794 if (!voucherType || voucherValue === 0) return; 16795 16796 // Calculate new discount 16797 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16798 16799 // Update the discount label 16800 const saveLabel = voucherBtn.querySelector('.bv-save'); 16801 if (saveLabel) { 16802 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16803 } 16804 16805 // Also update mobile FAB voucher display if exists 16806 updateMobileFabVoucher(newDiscount); 16807 } 16808 16809 /** 16810 * Update mobile FAB voucher value 16811 * 16812 * @param {number} discountAmount - New discount amount 16813 */ 16814 function updateMobileFabVoucher(discountAmount) { 16815 const fab = document.getElementById('pd-mobile-fab'); 16816 if (fab) { 16817 fab.dataset.voucherValue = discountAmount.toString(); 16818 } 16819 } 16820 16821 /** 16822 * Listen to variant change events and recalculate discount 16823 */ 16824 function initVoucherRecalculation() { 16825 window.addEventListener('pd-variant-changed', function(e) { 16826 const detail = e.detail || {}; 16827 const newPrice = detail.price || 0; 16828 16829 if (newPrice > 0) { 16830 updateVoucherDiscount(newPrice); 16831 } 16832 }); 16833 } 16834 16835 // Initialize when DOM is ready 16836 if (document.readyState === 'loading') { 16837 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16838 } else { 16839 initVoucherRecalculation(); 16840 } 16841 16842})(); 16843 16844</script>
vendor: 2 bytes, line 16844
16844
16845<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>
16845 16846
16847<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16848(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'}})}} 16849 16850function _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()}})(); 16851 16852(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()}})})(); 16853</script>
16853<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16854 <div class="ft-top ft-collapsible" id="ftTop"> 16855 <div class="container ft-grid"> 16856 16857 <div class="ft-col ft-help"> 16858 <h4 class="ft-title">Butuh Bantuan?</h4> 16859 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16860 <a class="ft-wa-btn" href="https://wa.me/+6287725361707" target="_blank" rel="nofollow noopener"> 16861 <span class="ico" aria-hidden="true"> 16862 <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> 16863 </span> 16864 Kirim Pesan 16865 </a> 16866 16867 <p class="ft-desc sm-gap">Ikuti kami:</p> 16868 <div class="ft-social"> 16869 <a aria-label="Facebook" href="https://facebook.com/judimpoofficial"><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> 16870 <a aria-label="Instagram" href="https://instagram.com/judimpoofficial/"><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
16870V7.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> 16871 <a aria-label="TikTok" href="https://www.tiktok.com/@mpojudy"><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> 16872 </div> 16873 </div> 16874 16875 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16876 <div class="ft-collapsible-content" id="ftCollapsible"> 16877 16878 <div class="ft-col"> 16879 <h4 class="ft-title">Tentang GAME MPO</h4> 16880 <ul class="ft-links"> 16881 <li><a href="https://hop.exchange/">Tentang GAME MPO</a></li> 16882 <li><a href="https://hop.exchange/">Kontak GAME MPO</a></li> 16883 <li><a href="https://hop.exchange/">Lokasi Toko</a></li> 16884 <li><a href="https://hop.exchange/">FAQ</a></li> 16885 </ul> 16886 <div class="ft-address"> 16887 <p class="company">GAME MPO</p> 16888 <p>JL. Cengkareng Barat No. 56, Jakarta Timur</p> 16889 <p>Kec. Jakarta, Kota Jakarta, DKI JAKARTA, 11752</p> 16890 </div> 16891 </div> 16892 16893 <div class="ft-col"> 16894 <h4 class="ft-title">Informasi</h4> 16895 <ul class="ft-links"> 16896 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16897 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16898 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16899 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16900 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16901 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16902 </ul> 16903 16904 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16905 <div class="pay-row"> 16906 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-mandiri.png" alt="Mandiri" loading="lazy"> 16907 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bni.gif" alt="BNI" loading="lazy"> 16908 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bri.gif" alt="BRI" loading="lazy"> 16909 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-qris.gif" alt="QRIS" loading="lazy"> 16910 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bca.gif" alt="BCA" loading="lazy"> 16911 </div> 16912 </div> 16913 </div> 16914 16915 </div> 16916 <!-- End Collapsible Content --> 16917 16918 <!-- Expand Button (Mobile Only) --> 16919 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16920 <span class="expand-text">Selengkapnya</span> 16921 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16922 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16923 </svg> 16924 </button> 16925 16926 </div> 16927 </div> 16928 <div class="ft-bottom"> 16929 <div class="container ft-bottom-inner"> 16930 <div class="ft-copy"> 16931 <span>GAME MPO © <span id="yearNow">2026</span></span> 16932 <span>GAME MPO OFFICIAL. All rights reserved.</span> 16933 </div> 16934 </div> 16935 </div> 16936 16937 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16938 /* Chat Widget Styles - Lightweight & Minimal */ 16939 .chat-widget { 16940 position: fixed; 16941 bottom: 20px; 16942 right: 20px; 16943 z-index: 9999; 16944 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16945 } 16946 16947 /* Hide floating button on mobile - use bottom nav instead */ 16948 @media (max-width: 640px) { 16949 .chat-toggle-btn { 16950 display: none !important; 16951 } 16952 16953 /* Ensure popup is still visible on mobile even when button is hidden */ 16954 .chat-widget { 16955 display: block !important; 16956 } 16957 16958 .chat-popup { 16959 display: none; /* Hidden by default, shown via JS */ 16960 } 16961 16962 .chat-popup[style*="display: block"], 16963 .chat-popup[style*="display:block"] { 16964 display: block !important; 16965 } 16966 } 16967 16968 .chat-toggle-btn { 16969 width: 60px; 16970 height: 60px; 16971 border-radius: 50%; 16972 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 16973 border: none; 16974 color: white; 16975 cursor: pointer; 16976 box-shadow: 0 4px 12px rgba(245,202,86,0.3); 16977 position: relative; 16978 transition: transform 0.2s, box-shadow 0.2s; 16979 } 16980 16981 .chat-toggle-btn:hover { 16982 transform: scale(1.05); 16983 box-shadow: 0 6px 20px rgba(245,202,86,0.4); 16984 } 16985 16986 #chat-toggle-btn .chat-icon { 16987 position: absolute; 16988 top: 50%; 16989 left: 50%; 16990 transform: translate(-50%, -50%); 16991 width: 28px !important; 16992 height: 28px !important; 16993 } 16994 16995 .chat-unread-badge { 16996 position: absolute; 16997 top: -5px; 16998 right: -5px; 16999 background: #C77DFF; 17000 color: white; 17001 border-radius: 12px; 17002 padding: 2px 6px; 17003 font-size: 11px; 17004 font-weight: 600;
17005 min-width: 20px; 17006 text-align: center; 17007 } 17008 17009 .chat-popup { 17010 position: absolute; 17011 bottom: 80px; 17012 right: 0; 17013 width: 380px; 17014 max-width: calc(100vw - 40px); 17015 height: 500px; 17016 max-height: calc(100vh - 120px); 17017 background: white; 17018 border-radius: 12px; 17019 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 17020 display: flex; 17021 flex-direction: column; 17022 overflow: hidden; 17023 } 17024 17025 .chat-popup-header { 17026 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 17027 color: white; 17028 padding: 16px 20px; 17029 display: flex; 17030 justify-content: space-between; 17031 align-items: center; 17032 flex-shrink: 0; 17033 } 17034 17035 .chat-header-title { 17036 display: flex; 17037 flex-direction: column; 17038 gap: 2px; 17039 min-height: 0; 17040 } 17041 17042 .chat-popup-header h3 { 17043 margin: 0; 17044 font-size: 18px; 17045 font-weight: 600; 17046 line-height: 1.2; 17047 } 17048 17049 .chat-operating-hours { 17050 font-size: 11px; 17051 opacity: 0.9; 17052 font-weight: 400; 17053 display: flex; 17054 align-items: center; 17055 gap: 4px; 17056 line-height: 1.2; 17057 } 17058 17059 .chat-operating-hours svg { 17060 width: 12px; 17061 height: 12px; 17062 opacity: 0.8; 17063 flex-shrink: 0; 17064 } 17065 17066 .chat-header-actions { 17067 display: flex; 17068 align-items: center; 17069 gap: 8px; 17070 } 17071 17072 /* Notification Toggle Button */ 17073 .btn-notif-toggle { 17074 background: none; 17075 border: none; 17076 color: white; 17077 font-size: 18px; 17078 cursor: pointer; 17079 padding: 4px 8px; 17080 transition: all 0.2s; 17081 opacity: 0.8; 17082 } 17083 17084 .btn-notif-toggle:hover { 17085 opacity: 1; 17086 transform: scale(1.1); 17087 } 17088 17089 /* Icon states */ 17090 .btn-notif-toggle[data-status="not-subscribed"] svg { 17091 opacity: 0.7; 17092 } 17093 17094 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17095 opacity: 1; 17096 } 17097 17098 .btn-notif-toggle[data-status="subscribed"] svg { 17099 opacity: 1; 17100 animation: bellRing 2s ease-in-out; 17101 } 17102 17103 .btn-notif-toggle[data-status="blocked"] svg { 17104 color: #C77DFF; 17105 } 17106 17107 .btn-notif-toggle[data-status="not-supported"] { 17108 display: none !important; 17109 } 17110 17111 @keyframes bellRing { 17112 0%, 100% { transform: rotate(0deg); } 17113 10%, 30% { transform: rotate(-10deg); } 17114 20%, 40% { transform: rotate(10deg); } 17115 } 17116 17117 /* Notification Info Bar */ 17118 .chat-notif-info-bar { 17119 position: absolute; 17120 top: 64px; 17121 left: 0; 17122 right: 0; 17123 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17124 border-bottom: 1px solid #bbdefb; 17125 padding: 8px 12px; 17126 display: flex; 17127 align-items: center; 17128 gap: 8px; 17129 font-size: 12px; 17130 color: #616161; 17131 z-index: 10; 17132 animation: slideDown 0.3s ease; 17133 } 17134 17135 .chat-notif-info-bar svg { 17136 flex-shrink: 0; 17137 } 17138 17139 .chat-notif-info-bar .small { 17140 flex: 1; 17141 line-height: 1.4; 17142 } 17143 17144 .chat-notif-info-bar .btn-dismiss { 17145 background: none; 17146 border: none; 17147 color: #9e9e9e; 17148 cursor: pointer; 17149 padding: 0 4px; 17150 font-size: 16px; 17151 transition: color 0.2s; 17152 } 17153 17154 .chat-notif-info-bar .btn-dismiss:hover { 17155 color: #616161; 17156 } 17157 17158 @keyframes slideDown { 17159 from { 17160 max-height: 0; 17161 opacity: 0; 17162 } 17163 to { 17164 max-height: 50px; 17165 opacity: 1;
17166 } 17167 } 17168 17169 .chat-close-btn { 17170 background: none; 17171 border: none; 17172 color: white; 17173 font-size: 28px; 17174 line-height: 1; 17175 cursor: pointer; 17176 padding: 0; 17177 width: 30px; 17178 height: 30px; 17179 } 17180 17181 .chat-popup-body { 17182 flex: 1 1 auto; 17183 display: flex; 17184 flex-direction: column; 17185 min-height: 0; 17186 overflow: hidden; 17187 height: calc(100% - 64px); 17188 position: relative; 17189 transition: padding-top 0.3s ease; 17190 } 17191 17192 /* Add padding when info bar is visible */ 17193 .chat-popup-body.has-info-bar { 17194 padding-top: 40px; 17195 } 17196 17197 .chat-view { 17198 flex: 1 1 auto; 17199 display: none; 17200 flex-direction: column; 17201 min-height: 0; 17202 overflow: hidden; 17203 } 17204 17205 #chat-list-view { 17206 display: flex; 17207 } 17208 17209 #chat-messages-view { 17210 display: none; 17211 } 17212 17213 #chat-messages-view.active { 17214 display: flex; 17215 flex-direction: column; 17216 } 17217 17218 #chat-new-view { 17219 display: none; 17220 } 17221 17222 #chat-new-view.active { 17223 display: flex; 17224 } 17225 17226 .chat-conversations-list { 17227 flex: 1; 17228 overflow-y: auto; 17229 padding: 0; 17230 background: white; 17231 } 17232 17233 /* Custom Scrollbar for Conversations List */ 17234 .chat-conversations-list::-webkit-scrollbar { 17235 width: 6px; 17236 } 17237 17238 .chat-conversations-list::-webkit-scrollbar-track { 17239 background: #f1f1f1; 17240 } 17241 17242 .chat-conversations-list::-webkit-scrollbar-thumb { 17243 background: #C77DFF; 17244 border-radius: 3px; 17245 } 17246 17247 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17248 background: #C77DFF; 17249 } 17250 17251 /* Firefox */ 17252 .chat-conversations-list { 17253 scrollbar-width: thin; 17254 scrollbar-color: #C77DFF #f1f1f1; 17255 } 17256 17257 .chat-conversation-item { 17258 padding: 12px 16px; 17259 cursor: pointer; 17260 display: flex; 17261 gap: 12px; 17262 align-items: flex-start; 17263 transition: background 0.15s ease; 17264 color: #3a270a; 17265 border-bottom: 1px solid #f0f0f0; 17266 } 17267 17268 .chat-conversation-item:hover { 17269 background: #f5f5f5; 17270 } 17271 17272 .chat-conversation-item.active { 17273 background: #FFF5EB; 17274 } 17275 17276 /* WhatsApp-like Avatar */ 17277 .conversation-avatar { 17278 width: 48px; 17279 height: 48px; 17280 border-radius: 50%; 17281 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 17282 flex-shrink: 0; 17283 display: flex; 17284 align-items: center; 17285 justify-content: center; 17286 color: white; 17287 font-weight: 600; 17288 font-size: 18px; 17289 } 17290 17291 .conversation-avatar svg { 17292 width: 24px; 17293 height: 24px; 17294 } 17295 17296 /* WhatsApp-like Content */ 17297 .conversation-content { 17298 flex: 1; 17299 min-width: 0; 17300 display: flex; 17301 flex-direction: column; 17302 gap: 4px; 17303 } 17304 17305 .conversation-header { 17306 display: flex; 17307 justify-content: space-between; 17308 align-items: center; 17309 gap: 8px; 17310 } 17311 17312 .conversation-name { 17313 font-weight: 600; 17314 font-size: 15px; 17315 color: #111; 17316 white-space: nowrap; 17317 overflow: hidden; 17318 text-overflow: ellipsis; 17319 } 17320 17321 .conversation-time { 17322 font-size: 12px; 17323 color: #7d8166; 17324 flex-shrink: 0; 17325 } 17326 17327 .conversation-preview { 17328 display: flex; 17329 justify-content: space-between; 17330 align-items: center; 17331 gap: 8px; 17332 } 17333 17334 .conversation-message { 17335 font-size: 14px; 17336 color: #667781; 17337 white-space: nowrap; 17338 overflow: hidden; 17339 text-overflow: ellipsis; 17340 margin: 0; 17341 flex: 1; 17342 } 17343 17344 .conversation-unread { 17345 background: #25D366; 17346 color: white; 17347 border-radius: 12px; 17348 padding: 2px 8px; 17349 font-size: 12px; 17350 font-weight: 600;
17351 min-width: 20px; 17352 text-align: center; 17353 flex-shrink: 0; 17354 } 17355 17356 /* Active item styling */ 17357 .chat-conversation-item.active .conversation-name { 17358 color: #C77DFF; 17359 } 17360 17361 .chat-conversation-item.active .conversation-time { 17362 color: #C77DFF; 17363 } 17364 17365 .chat-new-btn { 17366 margin: 12px; 17367 padding: 12px; 17368 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 17369 color: white; 17370 border: none; 17371 border-radius: 8px; 17372 cursor: pointer; 17373 font-weight: 500; 17374 font-size: 14px; 17375 display: flex; 17376 align-items: center; 17377 justify-content: center; 17378 gap: 8px; 17379 transition: all 0.2s; 17380 flex-shrink: 0; 17381 } 17382 17383 .chat-new-btn:hover { 17384 transform: translateY(-1px); 17385 box-shadow: 0 4px 8px rgba(245,202,86,0.3); 17386 } 17387 17388 .chat-conversation-title { 17389 flex: 1; 17390 font-weight: 600; 17391 font-size: 16px; 17392 color: #1f2937; 17393 } 17394 17395 .chat-messages-container { 17396 flex: 1; 17397 overflow-y: auto; 17398 padding: 16px; 17399 display: flex; 17400 flex-direction: column; 17401 gap: 8px; 17402 min-height: 0; 17403 background: #E5DDD5; 17404 } 17405 17406 /* Custom Scrollbar for Messages Container */ 17407 .chat-messages-container::-webkit-scrollbar { 17408 width: 6px; 17409 } 17410 17411 .chat-messages-container::-webkit-scrollbar-track { 17412 background: rgba(0, 0, 0, 0.05); 17413 } 17414 17415 .chat-messages-container::-webkit-scrollbar-thumb { 17416 background: rgba(245,202,86,0.6); 17417 border-radius: 3px; 17418 } 17419 17420 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17421 background: #C77DFF; 17422 } 17423 17424 /* Firefox */ 17425 .chat-messages-container { 17426 scrollbar-width: thin; 17427 scrollbar-color: rgba(245,202,86,0.6) rgba(0, 0, 0, 0.05); 17428 } 17429 17430 /* WhatsApp Group-like Messages */ 17431 .chat-message { 17432 display: flex; 17433 margin-bottom: 4px; 17434 animation: fadeIn 0.2s ease; 17435 } 17436 17437 @keyframes fadeIn { 17438 from { opacity: 0; transform: translateY(10px); } 17439 to { opacity: 1; transform: translateY(0); } 17440 } 17441 17442 .chat-message.mine { 17443 justify-content: flex-end; 17444 } 17445 17446 .chat-message-bubble { 17447 max-width: 80%; 17448 padding: 6px 8px 8px 10px; 17449 border-radius: 8px; 17450 position: relative; 17451 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17452 } 17453 17454 /* Bubble from others (Admin/CS) */ 17455 .chat-message:not(.mine) .chat-message-bubble { 17456 background: #FFFFFF; 17457 border-radius: 0 8px 8px 8px; 17458 } 17459 17460 /* Bubble tail for others */ 17461 .chat-message:not(.mine) .chat-message-bubble::before { 17462 content: ''; 17463 position: absolute; 17464 top: 0; 17465 left: -8px; 17466 width: 0; 17467 height: 0; 17468 border-style: solid; 17469 border-width: 0 8px 13px 0; 17470 border-color: transparent #FFFFFF transparent transparent; 17471 } 17472 17473 /* Bubble from user (mine) */ 17474 .chat-message.mine .chat-message-bubble { 17475 background: #e4f5d0; /* Light blue for user messages */ 17476 border-radius: 8px 0 8px 8px; 17477 } 17478 17479 /* Bubble tail for mine */ 17480 .chat-message.mine .chat-message-bubble::before { 17481 content: ''; 17482 position: absolute; 17483 top: 0; 17484 right: -8px; 17485 width: 0; 17486 height: 0; 17487 border-style: solid; 17488 border-width: 0 0 13px 8px; 17489 border-color: transparent transparent transparent #eaf5d0; 17490 } 17491 17492 .message-sender { 17493 font-size: 12.5px; 17494 font-weight: 600; 17495 color: #C77DFF !important; 17496 margin-bottom: 4px;
17497 } 17498 17499 .chat-message.mine .message-sender { 17500 display: none; 17501 } 17502 17503 /* Message text */ 17504 .message-text { 17505 font-size: 14px; 17506 line-height: 19px; 17507 color: #ffffff !important; 17508 word-wrap: break-word; 17509 white-space: pre-wrap; 17510 margin-bottom: 4px; 17511 } 17512 17513 /* Message metadata (time + status) */ 17514 .message-meta { 17515 display: flex; 17516 align-items: center; 17517 justify-content: flex-end; 17518 gap: 4px; 17519 margin-top: 2px; 17520 } 17521 17522 .message-time { 17523 font-size: 11px; 17524 color: #728166 !important; 17525 line-height: 15px; 17526 } 17527 17528 /* Use multiple selectors to override global styles */ 17529 .chat-message-bubble .message-status, 17530 .message-meta .message-status, 17531 .chat-message .message-status { 17532 width: 16px !important; 17533 height: 16px !important; 17534 min-width: 16px !important; 17535 min-height: 16px !important; 17536 max-width: 16px !important; 17537 max-height: 16px !important; 17538 flex-shrink: 0; 17539 color: #728166 !important; 17540 fill: currentColor !important; 17541 } 17542 17543 .chat-message:not(.mine) .message-status { 17544 display: none; 17545 } 17546 17547 /* Loading dots animation */ 17548 @keyframes loadingDots { 17549 0%, 20% { content: '.'; } 17550 40% { content: '..'; } 17551 60%, 100% { content: '...'; } 17552 } 17553 17554 .message-status.loading::after { 17555 content: '...'; 17556 animation: loadingDots 1.5s infinite; 17557 font-size: 16px; 17558 line-height: 16px; 17559 } 17560 17561 .message-status.loading path { 17562 display: none; 17563 } 17564 17565 .message-status.failed { 17566 color: #C77DFF !important; 17567 cursor: pointer; 17568 } 17569 17570 .message-status.sent { 17571 color: #667781 !important; 17572 } 17573 17574 .chat-new-form { 17575 flex: 1; 17576 overflow-y: auto; 17577 padding: 16px; 17578 display: flex; 17579 flex-direction: column; 17580 } 17581 17582 .chat-new-form form { 17583 display: flex; 17584 flex-direction: column; 17585 gap: 16px; 17586 flex: 1; 17587 } 17588 17589 .chat-new-form .form-group { 17590 display: flex; 17591 flex-direction: column; 17592 gap: 6px; 17593 } 17594 17595 .chat-new-form label { 17596 font-size: 14px; 17597 font-weight: 500; 17598 color: #374151; 17599 } 17600 17601 .chat-new-form .form-control { 17602 width: 100%; 17603 padding: 10px 12px; 17604 border: 1px solid #d1d5db; 17605 border-radius: 8px; 17606 font-size: 14px; 17607 font-family: inherit; 17608 transition: border-color 0.2s; 17609 box-sizing: border-box; 17610 } 17611 17612 .chat-new-form .form-control:focus { 17613 outline: none; 17614 border-color: #C77DFF; 17615 box-shadow: 0 0 0 3px rgba(245,202,86,0.1); 17616 } 17617 17618 .chat-new-form textarea.form-control { 17619 resize: vertical; 17620 min-height: 100px; 17621 } 17622 17623 .chat-new-form .btn { 17624 padding: 12px 20px; 17625 border: none; 17626 border-radius: 8px; 17627 font-size: 15px; 17628 font-weight: 500; 17629 cursor: pointer; 17630 transition: all 0.2s; 17631 text-align: center; 17632 } 17633 17634 .chat-new-form .btn-primary { 17635 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 17636 color: white; 17637 } 17638 17639 .chat-new-form .btn-primary:hover { 17640 transform: translateY(-1px); 17641 box-shadow: 0 4px 8px rgba(245,202,86,0.3); 17642 } 17643 17644 .chat-new-form .btn-primary:active { 17645 transform: scale(0.98); 17646 } 17647 17648 .chat-new-form .btn-block { 17649 width: 100%; 17650 margin-top: auto; 17651 } 17652 17653 .chat-message-form { 17654 padding: 12px; 17655 border-top: 1px solid #e5e7eb; 17656 display: flex; 17657 flex-wrap: wrap; 17658 gap: 8px; 17659 align-items: flex-end; 17660 flex-shrink: 0; 17661 background: white; 17662 } 17663 17664 .chat-product-card { 17665 flex: 1 1 100%; 17666 max-width: 100%; 17667 display: flex; 17668 gap: 12px; 17669 padding: 12px; 17670 background: #f3f4f6; 17671 border-radius: 8px; 17672 margin-bottom: 4px;
17673 border: 1px solid #e5e7eb; 17674 } 17675 17676 .chat-product-card img { 17677 width: 60px; 17678 height: 60px; 17679 object-fit: cover; 17680 border-radius: 6px; 17681 flex-shrink: 0; 17682 } 17683 17684 .chat-product-card-content { 17685 flex: 1; 17686 min-width: 0; 17687 } 17688 17689 .chat-product-card-name { 17690 font-weight: 500; 17691 font-size: 14px; 17692 color: #1f2937; 17693 margin-bottom: 4px; 17694 white-space: nowrap; 17695 overflow: hidden; 17696 text-overflow: ellipsis; 17697 } 17698 17699 .chat-product-card-variant { 17700 font-size: 12px; 17701 color: #74806b; 17702 margin-bottom: 4px; 17703 } 17704 17705 .chat-product-card-price { 17706 font-size: 13px; 17707 color: #8bea66; 17708 font-weight: 600; 17709 } 17710 17711 .chat-product-card-remove { 17712 align-self: flex-start; 17713 background: none; 17714 border: none; 17715 color: #9ca3af; 17716 cursor: pointer; 17717 padding: 4px; 17718 font-size: 18px; 17719 line-height: 1; 17720 transition: color 0.2s; 17721 } 17722 17723 .chat-product-card-remove:hover { 17724 color: #C77DFF; 17725 } 17726 17727 /* Product card in messages (read-only display) */ 17728 .message-product-card { 17729 display: flex; 17730 gap: 10px; 17731 padding: 10px; 17732 background: rgba(255, 255, 255, 0.5); 17733 border-radius: 8px; 17734 margin-bottom: 6px; 17735 border: 1px solid rgba(0, 0, 0, 0.08); 17736 align-items: center; 17737 } 17738 17739 .chat-message.mine .message-product-card { 17740 background: rgba(255, 255, 255, 0.7); 17741 } 17742 17743 .message-product-card img { 17744 width: 50px; 17745 height: 50px; 17746 object-fit: cover; 17747 border-radius: 6px; 17748 flex-shrink: 0; 17749 } 17750 17751 .message-product-info { 17752 flex: 1; 17753 min-width: 0; 17754 } 17755 17756 .message-product-name { 17757 font-weight: 500; 17758 font-size: 13px; 17759 color: #3a270a; 17760 margin-bottom: 2px; 17761 overflow: hidden; 17762 text-overflow: ellipsis; 17763 display: -webkit-box; 17764 -webkit-line-clamp: 2; 17765 -webkit-box-orient: vertical; 17766 line-height: 1.3; 17767 } 17768 17769 .message-product-variant { 17770 font-size: 11px; 17771 color: #78806b; 17772 margin-bottom: 2px; 17773 } 17774 17775 .message-product-price { 17776 font-size: 12px; 17777 color: #c7ea66; 17778 font-weight: 600; 17779 } 17780 17781 .message-product-link { 17782 flex-shrink: 0; 17783 width: 32px; 17784 height: 32px; 17785 display: flex; 17786 align-items: center; 17787 justify-content: center; 17788 background: rgba(102, 126, 234, 0.1); 17789 border-radius: 6px; 17790 color: #c7ea66; 17791 text-decoration: none; 17792 transition: all 0.2s; 17793 } 17794 17795 .message-product-link:hover { 17796 background: #b7ea66; 17797 color: white; 17798 } 17799 17800 .message-product-link svg { 17801 width: 16px; 17802 height: 16px; 17803 } 17804 17805 .chat-message-input { 17806 flex: 1; 17807 border: 1px solid #d1d5db; 17808 border-radius: 20px; 17809 padding: 10px 16px; 17810 resize: none; 17811 max-height: 100px; 17812 font-family: inherit; 17813 font-size: 14px; 17814 line-height: 1.5; 17815 box-sizing: border-box; 17816 overflow-y: hidden; 17817 } 17818 17819 .chat-message-input:focus { 17820 outline: none; 17821 border-color: #C77DFF; 17822 box-shadow: 0 0 0 3px rgba(245,202,86,0.1); 17823 } 17824 17825 .chat-send-btn { 17826 width: 40px; 17827 height: 40px; 17828 border-radius: 50%; 17829 background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%); 17830 border: none; 17831 color: white; 17832 cursor: pointer; 17833 flex-shrink: 0; 17834 display: flex; 17835 align-items: center; 17836 justify-content: center; 17837 transition: all 0.2s; 17838 } 17839 17840 .chat-send-btn:hover { 17841 transform: scale(1.05); 17842 box-shadow: 0 2px 8px rgba(245,202,86,0.3); 17843 } 17844 17845 .chat-send-btn:active { 17846 transform: scale(0.95); 17847 } 17848 17849 .chat-loading { 17850 text-align: center; 17851 padding: 20px; 17852 color: #6b7280; 17853 } 17854 17855 /* Date Separator (WhatsApp-style) */ 17856 .date-separator { 17857 display: flex; 17858 align-items: center; 17859 gap: 12px; 17860 margin: 24px 0 16px; 17861 opacity: 0; 17862 animation: fadeInSeparator 0.3s ease forwards; 17863 } 17864 17865 @keyframes fadeInSeparator { 17866 from { 17867 opacity: 0; 17868 transform: translateY(-10px); 17869 } 17870 to { 17871 opacity: 1;
17872 transform: translateY(0); 17873 } 17874 } 17875 17876 .date-separator-line { 17877 flex: 1; 17878 height: 1px; 17879 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17880 } 17881 17882 .date-separator-text { 17883 flex-shrink: 0; 17884 padding: 4px 12px; 17885 background: #e9ecef; 17886 color: #6c757d; 17887 font-size: 11px; 17888 font-weight: 600; 17889 text-transform: uppercase; 17890 letter-spacing: 0.5px; 17891 border-radius: 12px; 17892 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17893 } 17894 17895 /* Mobile responsive */ 17896 @media (max-width: 640px) { 17897 .chat-popup { 17898 position: fixed !important; /* Override absolute positioning */ 17899 bottom: 45px !important; /* Above bottom nav */ 17900 right: 0 !important; 17901 left: 0 !important; 17902 width: 100% !important; 17903 max-width: 100% !important; 17904 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17905 height: calc(100dvh - 45px) !important; 17906 max-height: calc(100dvh - 45px) !important; 17907 /* Fallback for browsers that don't support dvh */ 17908 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17909 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17910 border-radius: 0 !important; 17911 z-index: 9999 !important; 17912 } 17913 17914 .chat-popup-header { 17915 border-radius: 0; 17916 } 17917 17918 .chat-messages-container, 17919 .chat-conversations-list { 17920 /* Adjust for mobile keyboard using dynamic viewport */ 17921 max-height: calc(100dvh - 190px); 17922 /* Fallback for browsers that don't support dvh */ 17923 max-height: calc(var(--viewport-height, 100vh) - 190px); 17924 } 17925 } 17926 </style> 17927 <!-- Chat Widget Button --> 17928 <div id="chat-widget" class="chat-widget" 17929 data-user-id="" 17930 data-is-logged-in="false" 17931 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17932 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17933 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17934 <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"/> 17935 </svg> 17936 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17937 </button> 17938 17939 <div id="chat-popup" class="chat-popup" style="display: none;"> 17940 <div class="chat-popup-header"> 17941 <div class="chat-header-title"> 17942 <h3>Bantuan</h3> 17943 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17944 </div> 17945 <div class="chat-header-actions"> 17946 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17947 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17948 <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"/> 17949 </svg> 17950 </button> 17951 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">
17951Ã</button> 17952 </div> 17953 </div> 17954 17955 <!-- Notification Info Bar (conditional) --> 17956 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17957 <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffc107"> 17958 <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"/> 17959 </svg> 17960 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17961 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17962 </div> 17963 17964 <div class="chat-popup-body"> 17965 <!-- Conversations List View --> 17966 <div id="chat-list-view" class="chat-view"> 17967 <div id="chat-conversations-list" class="chat-conversations-list"> 17968 <div class="chat-loading">Memuat...</div> 17969 <!-- Conversation Item Structure (rendered by JS): 17970 <div class="chat-conversation-item"> 17971 <div class="conversation-avatar"> 17972 <svg>...</svg> 17973 </div> 17974 <div class="conversation-content"> 17975 <div class="conversation-header"> 17976 <span class="conversation-name">Admin Support</span> 17977 <span class="conversation-time">10:30</span> 17978 </div> 17979 <div class="conversation-preview"> 17980 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17981 <span class="conversation-unread">2</span> 17982 </div> 17983 </div> 17984 </div> 17985 --> 17986 </div> 17987 <button id="chat-new-btn" class="chat-new-btn"> 17988 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17989 <line x1="12" y1="5" x2="12" y2="19"></line> 17990 <line x1="5" y1="12" x2="19" y2="12"></line> 17991 </svg> 17992 Percakapan Baru 17993 </button> 17994 </div> 17995 17996 <!-- Chat Messages View --> 17997 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17998 <div id="chat-messages-container" class="chat-messages-container"> 17999 <div class="chat-loading">Memuat pesan...</div> 18000 <!-- Message Structure (rendered by JS): 18001 <div class="chat-message"> (or .chat-message.mine) 18002 <div class="chat-message-bubble"> 18003 <div class="message-sender">Admin Support</div> (only for non-mine) 18004 <div class="message-text">Halo, ada yang bisa dibantu?</div> 18005 <div class="message-meta"> 18006 <span class="message-time">10:30</span> 18007 <svg class="message-status">...</svg> (only for mine) 18008 </div> 18009 </div> 18010 </div> 18011 --> 18012 </div> 18013 18014 <form id="chat-message-form" class="chat-message-form"> 18015 <input type="hidden" id="chat-current-conversation-id" value=""> 18016 <textarea 18017 id="chat-message-input" 18018 class="chat-message-input" 18019 placeholder="Ketik pesan..." 18020 rows="1" 18021 maxlength="5000" 18022 ></textarea> 18023 <button type="submit" class="chat-send-btn" aria-label="Send message"> 18024 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18025 <line x1="22" y1="2" x2="11" y2="
1802513"></line> 18026 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 18027 </svg> 18028 </button> 18029 </form> 18030 </div> 18031 18032 <!-- New Chat View --> 18033 <div id="chat-new-view" class="chat-view" style="display: none;"> 18034 <div class="chat-messages-header"> 18035 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 18036 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18037 <path d="M19 12H5M12 19l-7-7 7-7"/> 18038 </svg> 18039 </button> 18040 <div class="chat-conversation-title">Percakapan Baru</div> 18041 </div> 18042 18043 <div class="chat-new-form"> 18044 <form id="chat-start-form"> 18045 <div class="form-group"> 18046 <label for="chat-new-subject">Subjek</label> 18047 <input 18048 type="text" 18049 id="chat-new-subject" 18050 class="form-control" 18051 placeholder="Bantuan Pelanggan" 18052 maxlength="255" 18053 > 18054 </div> 18055 <div class="form-group"> 18056 <label for="chat-new-message">Pesan</label> 18057 <textarea 18058 id="chat-new-message" 18059 class="form-control" 18060 rows="4" 18061 placeholder="Tulis pertanyaan Anda..." 18062 required 18063 maxlength="5000" 18064 ></textarea> 18065 </div> 18066 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18067 </form> 18068 </div> 18069 </div> 18070 </div> 18071 </div> 18072 </div> 18073 18074 <!-- Login Required Modal --> 18075 <div class="modal-overlay" id="chatLoginModal"> 18076 <div class="modal-dialog"> 18077 <div class="modal-header"> 18078 <h3 class="modal-title"> 18079 <div class="modal-icon info"> 18080 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18081 <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"/> 18082 </svg> 18083 </div> 18084 Login Diperlukan 18085 </h3> 18086 </div> 18087 <div class="modal-body"> 18088 <p class="modal-message">Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18089 <div class="modal-actions"> 18090 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18091 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18092 </div> 18093 </div> 18094 </div> 18095 </div> 18096
18096<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18096<script data-inlined="chat-notification.js"> 18097/** 18098 * Customer Chat Push Notification Manager 18099 * Handles browser push notification subscription for customers 18100 * Version: 1.0.0 18101 * Date: 2026-01-19 18102 */ 18103 18104const ChatNotification = { 18105 // State 18106 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18107 subscription: null, 18108 vapidPublicKey: null, // Will be loaded from server 18109 18110 /** 18111 * Initialize notification manager 18112 */ 18113 async init() { 18114 // Check if user is logged in (only for logged-in users) 18115 if (!this.isUserLoggedIn()) { 18116 this.updateUI('not-supported'); 18117 return; 18118 } 18119 18120 // Check browser support 18121 if (!this.isSupported()) { 18122 this.updateUI('not-supported'); 18123 return; 18124 } 18125 18126 // Load VAPID key from server 18127 await this.loadVapidKey(); 18128 18129 if (!this.vapidPublicKey) { 18130 this.updateUI('not-supported'); 18131 return; 18132 } 18133 18134 // Register service worker if needed 18135 await this.registerServiceWorker(); 18136 18137 // Check current permission 18138 const permission = Notification.permission; 18139 18140 if (permission === 'denied') { 18141 this.updateUI('blocked'); 18142 } else if (permission === 'granted') { 18143 // Check if already subscribed 18144 const isSubscribed = await this.checkSubscriptionStatus(); 18145 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18146 } else { 18147 // Default: not subscribed yet 18148 this.updateUI('not-subscribed'); 18149 this.showInfoBar(); 18150 } 18151 }, 18152 18153 /** 18154 * Check if user is logged in 18155 */ 18156 isUserLoggedIn() { 18157 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18158 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18159 }, 18160 18161 /** 18162 * Check if browser supports push notifications 18163 */ 18164 isSupported() { 18165 return ( 18166 'serviceWorker' in navigator && 18167 'PushManager' in window && 18168 'Notification' in window 18169 ); 18170 }, 18171 18172 /** 18173 * Load VAPID public key from server 18174 */ 18175 async loadVapidKey() { 18176 try { 18177 const response = await fetch('/api/chat/notifications/vapid_key', { 18178 method: 'GET', 18179 headers: { 18180 'Accept': 'application/json' 18181 } 18182 }); 18183 18184 if (!response.ok) { 18185 return false; 18186 } 18187 18188 const data = await response.json(); 18189 if (data.success && data.data?.public_key) { 18190 this.vapidPublicKey = data.data.public_key; 18191 return true; 18192 } 18193 18194 return false; 18195 18196 } catch (error) { 18197 console.error('[ChatNotification] Error loading VAPID key:', error); 18198 return false; 18199 } 18200 }, 18201 18202 /** 18203 * Register service worker 18204 */ 18205 async registerServiceWorker() { 18206 if (!('serviceWorker' in navigator)) return null; 18207 18208 try { 18209 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18210 scope: '/' 18211 }); 18212 return registration; 18213 } catch (error) { 18214 return null; 18215 } 18216 }, 18217 18218 /** 18219 * Check subscription status from server 18220 */ 18221 async checkSubscriptionStatus() { 18222 try { 18223 const response = await fetch('/api/chat/notifications/status', { 18224 method: 'GET', 18225 credentials: 'include', 18226 headers: { 18227 'Accept': 'application/json' 18228 } 18229 }); 18230 18231 if (!response.ok) { 18232 return false; 18233 } 18234 18235 const data = await response.json(); 18236 return data.data?.subscribed || false; 18237 18238 } catch (error) { 18239 console.error('[ChatNotification] Error checking subscription:', error); 18240 return false; 18241 } 18242 }, 18243 18244 /** 18245 * Update UI based on status 18246 */ 18247 updateUI(status) { 18248 this.status = status; 18249 const btn = document.getElementById('chatNotifToggle'); 18250 18251 if (!btn) { 18252 // Retry after a short delay (button might not be in DOM yet) 18253 setTimeout(() => this.updateUI(status), 100); 18254 return; 18255 } 18256 18257 btn.setAttribute('data-status', status); 18258 18259 // Show/hide button based on support 18260 if (status === 'not-supported') { 18261 btn.style.display = 'none'; 18262 } else { 18263 btn.style.display = 'block'; 18264 } 18265 18266 // Update SVG icon (bell vs bell-slash) 18267 const svg = btn.querySelector('svg'); 18268 if (svg) { 18269 if (status === 'subscribed') { 18270 // Bell icon (active) 18271 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"/>'; 18272 } else { 18273 // Bell slash icon (inactive/blocked) 18274 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"/>'; 18275 } 18276 } 18277 18278 // Update tooltip 18279 const tooltips = { 18280 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18281 'subscribed': 'Notifikasi aktif', 18282 'blocked': 'Notifikasi diblokir'
18283 }; 18284 18285 if (tooltips[status]) { 18286 btn.title = tooltips[status]; 18287 } 18288 18289 // Hide info bar if subscribed or blocked 18290 if (status === 'subscribed' || status === 'blocked') { 18291 this.hideInfoBar(); 18292 } 18293 }, 18294 18295 /** 18296 * Show info bar (if not dismissed) 18297 */ 18298 showInfoBar() { 18299 if (localStorage.getItem('chat_notif_info_dismissed')) { 18300 return; 18301 } 18302 18303 const infoBar = document.getElementById('chatNotifInfoBar'); 18304 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18305 18306 if (infoBar) { 18307 infoBar.style.display = 'flex'; 18308 18309 // Bind dismiss button here so it's always attached when bar is visible 18310 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18311 if (btn && !btn._dismissBound) { 18312 btn._dismissBound = true; 18313 btn.addEventListener('click', () => { 18314 infoBar.style.display = 'none'; 18315 localStorage.setItem('chat_notif_info_dismissed', '1'); 18316 if (popupBody) popupBody.classList.remove('has-info-bar'); 18317 }); 18318 } 18319 } 18320 18321 if (popupBody) { 18322 popupBody.classList.add('has-info-bar'); 18323 } 18324 }, 18325 18326 /** 18327 * Hide info bar 18328 */ 18329 hideInfoBar() { 18330 const infoBar = document.getElementById('chatNotifInfoBar'); 18331 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18332 18333 if (infoBar) { 18334 infoBar.style.display = 'none'; 18335 } 18336 18337 if (popupBody) { 18338 popupBody.classList.remove('has-info-bar'); 18339 } 18340 }, 18341 18342 /** 18343 * Dismiss info bar permanently 18344 */ 18345 dismissInfo() { 18346 this.hideInfoBar(); 18347 localStorage.setItem('chat_notif_info_dismissed', '1'); 18348 }, 18349 18350 /** 18351 * Handle bell icon click 18352 */ 18353 async handleToggleClick() { 18354 if (this.status === 'not-subscribed') { 18355 await this.subscribe(); 18356 } else if (this.status === 'subscribed') { 18357 await this.unsubscribe(); 18358 } else if (this.status === 'blocked') { 18359 this.showUnblockHelp(); 18360 } 18361 }, 18362 18363 /** 18364 * Subscribe to push notifications 18365 */ 18366 async subscribe() { 18367 try { 18368 // Request permission 18369 const permission = await Notification.requestPermission(); 18370 18371 if (permission === 'denied') { 18372 this.updateUI('blocked'); 18373 this.showToast('Notifikasi diblokir', 'error'); 18374 return; 18375 } 18376 18377 if (permission !== 'granted') { 18378 return; 18379 } 18380 18381 // Get service worker registration 18382 const registration = await navigator.serviceWorker.ready; 18383 18384 // Subscribe to push 18385 const subscription = await registration.pushManager.subscribe({ 18386 userVisibleOnly: true, 18387 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18388 }); 18389 18390 // Send subscription to server 18391 const response = await fetch('/api/chat/notifications/subscribe', { 18392 method: 'POST', 18393 credentials: 'include', 18394 headers: { 18395 'Content-Type': 'application/json', 18396 'Accept': 'application/json' 18397 }, 18398 body: JSON.stringify(subscription.toJSON()) 18399 }); 18400 18401 if (!response.ok) { 18402 throw new Error('Failed to save subscription'); 18403 } 18404 18405 const result = await response.json(); 18406 18407 this.subscription = subscription; 18408 this.updateUI('subscribed'); 18409 this.hideInfoBar(); 18410 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18411 18412 // Send test notification 18413 setTimeout(() => this.sendTestNotification(), 1000); 18414 18415 } catch (error) { 18416 console.error('[ChatNotification] Subscribe error:', error); 18417 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18418 } 18419 }, 18420 18421 /** 18422 * Unsubscribe from push notifications 18423 */ 18424 async unsubscribe() { 18425 try { 18426 // Confirm first 18427 if (!confirm('Nonaktifkan notifikasi chat?')) { 18428 return; 18429 } 18430 18431 // Get current subscription 18432 const registration = await navigator.serviceWorker.ready; 18433 const subscription = await registration.pushManager.getSubscription(); 18434 18435 if (subscription) { 18436 // Unsubscribe from browser 18437 await subscription.unsubscribe(); 18438 18439 // Tell server 18440 await fetch('/api/chat/notifications/unsubscribe', { 18441 method: 'POST', 18442 credentials: 'include', 18443 headers: { 18444 'Content-Type': 'application/json', 18445 'Accept': 'application/json' 18446 }, 18447 body: JSON.stringify(subscription.toJSON()) 18448 }); 18449 } 18450 18451 this.subscription = null; 18452 this.updateUI('not-subscribed'); 18453 this.showInfoBar(); 18454 this.showToast('Notifikasi dinonaktifkan', 'info'); 18455 18456 } catch (error) { 18457 console.error('[ChatNotification] Unsubscribe error:', error); 18458 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18459 } 18460 }, 18461 18462 /** 18463 * Send test notification 18464 */ 18465 async sendTestNotification() { 18466 try { 18467 const response = await fetch('/api/chat/notifications/send_test', { 18468 method: 'POST', 18469 credentials: 'include', 18470 headers: { 18471 'Accept': 'application/json' 18472 } 18473 }); 18474 18475 if (response.ok) { 18476 // Test notification sent successfully 18477 } 18478 } catch (error) { 18479 // Test notification failed silently 18480 } 18481 }, 18482 18483 /** 18484 * Show unblock help modal/message 18485 */ 18486 showUnblockHelp() { 18487 const message = ` 18488 Notifikasi diblokir oleh browser.\n\n 18489 Untuk mengaktifkan:\n 18490 1. Klik ikon gembok/info di address bar\n 18491 2. Cari "Notifications" atau "Notifikasi"\n 18492 3. Ubah menjadi "Allow" atau "Izinkan"\n 18493 4. Refresh halaman ini 18494 `.trim().replace(/\s+/g, ' '); 18495 18496 alert(message); 18497 }, 18498 18499 /** 18500 * Show toast notification 18501 */ 18502 showToast(message, type = 'info') { 18503 // Simple toast implementation 18504 const toast = document.createElement('div'); 18505 toast.className = `chat-toast chat-toast-${type}`; 18506 toast.textContent = message; 18507 toast.style.cssText = ` 18508 position: fixed; 18509 bottom: 80px; 18510 right: 20px; 18511 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#C77DFF' : '#2196f3'}; 18512 color: white; 18513 padding: 12px 20px; 18514 border-radius: 4px; 18515 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18516 z-index: 10000; 18517 animation: slideInUp 0.3s ease; 18518 `; 18519 18520 document.body.appendChild(toast); 18521 18522 setTimeout(() => { 18523 toast.style.animation = 'slideOutDown 0.3s ease'; 18524 setTimeout(() => toast.remove(), 300); 18525 }, 3000); 18526 }, 18527 18528 /** 18529 * Convert VAPID key to Uint8Array 18530 */ 18531 urlBase64ToUint8Array(base64String) { 18532 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18533 const base64 = (base64String + padding) 18534 .replace(/\-/g, '+') 18535 .replace(/_/g, '/'); 18536 18537 const rawData = window.atob(base64); 18538 const outputArray = new Uint8Array(rawData.length); 18539 18540 for (let i = 0; i < rawData.length; ++i) { 18541 outputArray[i] = rawData.charCodeAt(i); 18542 } 18543 return outputArray; 18544 }, 18545 18546 /** 18547 * Check if push notification is currently active 18548 * @returns {Promise<boolean>} 18549 */ 18550 async isPushActive() { 18551 try { 18552 // Check browser subscription 18553 if (!('serviceWorker' in navigator)) return false; 18554 18555 const registration = await navigator.serviceWorker.getRegistration(); 18556 if (!registration) return false; 18557 18558 const subscription = await registration.pushManager.getSubscription(); 18559 if (!subscription) return false; 18560 18561 // Also check server status 18562 const serverStatus = await this.checkSubscriptionStatus();
18563 18564 return serverStatus; 18565 } catch (error) { 18566 return false; 18567 } 18568 }, 18569 18570 /** 18571 * Show logout confirmation if push notification is active 18572 * Call this before logout action 18573 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18574 */ 18575 async confirmLogoutWithPushNotification() { 18576 const isActive = await this.isPushActive(); 18577 18578 if (!isActive) { 18579 // No active push, proceed with logout 18580 return true; 18581 } 18582 18583 // Show modal confirmation using modal.css 18584 return new Promise((resolve) => { 18585 // Create modal HTML 18586 const modalHTML = ` 18587 <div class="modal-overlay" id="logoutConfirmModal"> 18588 <div class="modal-dialog"> 18589 <div class="modal-header"> 18590 <h3 class="modal-title"> 18591 <span class="modal-icon warning">â ï¸</span> 18592 Konfirmasi Logout 18593 </h3> 18594 </div> 18595 <div class="modal-body"> 18596 <p class="modal-message"> 18597 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18598 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18599 Apakah Anda yakin ingin melanjutkan logout? 18600 </p> 18601 </div> 18602 <div class="modal-actions" style="padding: 0 20px 20px;"> 18603 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18604 Batal 18605 </button> 18606 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18607 Lanjutkan Logout 18608 </button> 18609 </div> 18610 </div> 18611 </div> 18612 `; 18613 18614 // Append to body 18615 const modalContainer = document.createElement('div'); 18616 modalContainer.innerHTML = modalHTML; 18617 document.body.appendChild(modalContainer.firstElementChild); 18618 18619 const modal = document.getElementById('logoutConfirmModal'); 18620 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18621 const cancelBtn = document.getElementById('logoutCancelBtn'); 18622 18623 // Show modal with animation 18624 requestAnimationFrame(() => { 18625 modal.classList.add('show'); 18626 }); 18627 18628 // Handle confirm 18629 const handleConfirm = () => { 18630 cleanup(); 18631 resolve(true); 18632 }; 18633 18634 // Handle cancel 18635 const handleCancel = () => { 18636 cleanup(); 18637 resolve(false); 18638 }; 18639 18640 // Cleanup function 18641 const cleanup = () => { 18642 modal.classList.remove('show'); 18643 setTimeout(() => { 18644 modal.remove(); 18645 }, 200); 18646 confirmBtn.removeEventListener('click', handleConfirm); 18647 cancelBtn.removeEventListener('click', handleCancel); 18648 modal.removeEventListener('click', handleBackdropClick); 18649 }; 18650 18651 // Handle backdrop click 18652 const handleBackdropClick = (e) => { 18653 if (e.target === modal) { 18654 handleCancel(); 18655 } 18656 }; 18657 18658 // Add event listeners 18659 confirmBtn.addEventListener('click', handleConfirm); 18660 cancelBtn.addEventListener('click', handleCancel); 18661 modal.addEventListener('click', handleBackdropClick); 18662 }); 18663 } 18664}; 18665 18666// Add CSS animations 18667const chatNotifStyle = document.createElement('style'); 18668chatNotifStyle.textContent = ` 18669 @keyframes slideInUp { 18670 from { 18671 transform: translateY(100%); 18672 opacity: 0; 18673 } 18674 to { 18675 transform: translateY(0); 18676 opacity: 1;
18677 } 18678 } 18679 18680 @keyframes slideOutDown { 18681 from { 18682 transform: translateY(0); 18683 opacity: 1; 18684 } 18685 to { 18686 transform: translateY(100%); 18687 opacity: 0; 18688 } 18689 } 18690`; 18691document.head.appendChild(chatNotifStyle); 18692 18693// Auto-initialize when DOM is ready 18694if (document.readyState === 'loading') { 18695 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18696} else { 18697 ChatNotification.init(); 18698} 18699 18700// Expose to window for access from other scripts 18701window.ChatNotification = ChatNotification; 18702 18703</script>
18703 18704 <!-- Footer Collapse Toggle Script --> 18705
18705<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18706 document.addEventListener('DOMContentLoaded', function() { 18707 const ftTop = document.getElementById('ftTop'); 18708 const expandBtn = document.getElementById('ftExpandBtn'); 18709 18710 if (ftTop && expandBtn) { 18711 const expandText = expandBtn.querySelector('.expand-text'); 18712 18713 expandBtn.addEventListener('click', function() { 18714 ftTop.classList.toggle('expanded'); 18715 const isExpanded = ftTop.classList.contains('expanded'); 18716 if (expandText) { 18717 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18718 } 18719 }); 18720 } 18721 }); 18722 </script>
18722 18723 </footer> 18724 18725<!-- Compare Sidebar Overlay --> 18726<div class="compare-overlay" id="compareOverlay"></div> 18727 18728<!-- Floating Compare Badge (Sticky) --> 18729<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18730 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18731 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18732 </svg> 18733 <span class="cfb-text">Bandingkan</span> 18734 <span class="cfb-count" id="cfbCount">0</span> 18735</button> 18736 18737<!-- AI Comparison Modal --> 18738<div class="ai-compare-modal" id="aiCompareModal"> 18739 <div class="modal-backdrop"></div> 18740 <div class="modal-container"> 18741 <div class="modal-header"> 18742 <h3 class="modal-title"> 18743 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18744 <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"/> 18745 </svg> 18746 Analisis AI: Perbandingan Produk 18747 </h3> 18748 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18749 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18750 <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"/> 18751 </svg> 18752 </button> 18753 </div> 18754 18755 <div class="modal-content"> 18756 <!-- Loading State --> 18757 <div class="ai-loading" id="aiLoading"> 18758 <div class="loading-animation"> 18759 <div class="typing-indicator"> 18760 <span></span> 18761 <span></span> 18762 <span></span> 18763 </div> 18764 <p>AI sedang menganalisis produk...</p> 18765 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18766 </div> 18767 </div> 18768 18769 <!-- Error State --> 18770 <div class="ai-error" id="aiError" hidden> 18771 <div class="error-icon"> 18772 <svg width="48" height="48" viewBox="0 0 24 24" fill="#C77DFF"> 18773 <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"/> 18774 </svg> 18775 </div> 18776 <h4>Analisis AI Gagal</h4> 18777 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18778 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18779 </div> 18780 18781 <!-- Analysis Results --> 18782 <div class="ai-results" id="aiResults" hidden> 18783 <!-- Summary Section --> 18784 <div class="ai-summary-card"> 18785 <h4> 18786 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18787 <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"/> 18788 </svg> 18789 Ringkasan Analisis 18790 </h4> 18791 <p id="aiSummary" class="summary-text"></p> 18792 <div class="winner-badge" id="winnerBadge"> 18793 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18794 <small id="winnerReason"></small> 18795 </div> 18796 </div> 18797 18798 <!-- Comparison Table --> 18799 <div class="ai-comparison-table"> 18800 <h4> 18801 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18802 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18803 </svg> 18804 Perbandingan Detail 18805 </h4> 18806 <div id="comparisonTable" class="comparison-grid"></div> 18807 </div> 18808 18809 <!-- Detailed Analysis --> 18810 <div class="ai-detailed-analysis"> 18811 <h4> 18812 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18813 <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"/> 18814 </svg> 18815 Analisis Mendalam 18816 </h4> 18817 <div id="detailedAnalysis" class="products-analysis"></div> 18818 </div> 18819 18820 <!-- Recommendations --> 18821 <div class="ai-recommendations"> 18822 <h4> 18823 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18824 <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"/> 18825 </svg> 18826 Rekomendasi Berdasarkan Kebutuhan 18827 </h4> 18828 <div class="recommendations-grid"> 18829 <div class="recommendation-card"> 18830 <h5>ð° Budget Conscious</h5> 18831 <p id="budgetRecommendation"></p> 18832 </div> 18833 <div class="recommendation-card"> 18834 <h5>â¡ Performance Focused</h5> 18835 <p id="performanceRecommendation"></p> 18836 </div> 18837 <div class="recommendation-card"> 18838 <h5>âï¸ Balanced Choice</h5> 18839 <p id="balancedRecommendation"></p> 18840 </div> 18841 </div> 18842 </div> 18843 18844 <!-- Buying Tips --> 18845 <div class="ai-tips"> 18846 <h4> 18847 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18848 <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
18848M5,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"/> 18849 </svg> 18850 Tips Pembelian 18851 </h4> 18852 <ul id="buyingTips" class="tips-list"></ul> 18853 </div> 18854 </div> 18855 </div> 18856 </div> 18857</div> 18858 18859<!-- Cart Login Modal --> 18860<div class="modal-overlay" id="cartLoginModal"> 18861 <div class="modal-dialog"> 18862 <div class="modal-header"> 18863 <h3 class="modal-title"> 18864 <div class="modal-icon info"> 18865 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18866 <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"/> 18867 </svg> 18868 </div> 18869 Login untuk Melanjutkan 18870 </h3> 18871 </div> 18872 <div class="modal-body"> 18873 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk mengakses keranjang belanja dan menyelesaikan pesanan Anda.</p> 18874 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18875 <p style="font-size: 14px; color: #765212; margin: 0; line-height: 1.6;"> 18876 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18877 â Simpan keranjang belanja Anda<br> 18878 â Lacak status pesanan secara real-time<br> 18879 â Checkout lebih cepat dengan data tersimpan<br> 18880 â Dapatkan notifikasi promo & diskon eksklusif 18881 </p> 18882 </div> 18883 <div class="modal-actions"> 18884 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18885 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #C77DFF; border-color: #C77DFF;">Login</button> 18886 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #C77DFF; border-color: #C77DFF;">Daftar</button> 18887 </div> 18888 </div> 18889 </div> 18890</div> 18891 18892
18893<script data-inlined="app.js"> 18894(function initLiveSearch(){ 18895 function setup() { 18896 const searchInput = document.getElementById('searchInput'); 18897 const suggest = document.getElementById('searchSuggest'); 18898 const searchBtn = document.getElementById('searchBtn'); 18899 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18900 18901 if (!searchInput || !suggest || !aiToggleBtn) { 18902 console.log('[LiveSearch] Elemen belum ada, retry...'); 18903 return false; 18904 } 18905 18906 let debounceTimer = null; 18907 let lastQuery = ''; 18908 let currentIndex = -1; 18909 let currentAbortController = null; 18910 let isAIModeEnabled = false; // AI mode OFF by default 18911 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18912 18913 // Trending cache 18914 let trendingCache = null; 18915 let trendingLoading = false; 18916 18917 // Trending products cache 18918 let trendingProductsCache = null; 18919 let trendingProductsLoading = false; 18920 18921 const MIN_LEN = 2; 18922 const DEBOUNCE = 300; 18923 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18924 18925 // Load AI mode preference from localStorage 18926 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18927 if (savedAIMode === 'enabled') { 18928 isAIModeEnabled = false; 18929 aiToggleBtn.classList.remove('active'); 18930 } 18931 18932 // Update placeholder based on AI mode 18933 function updatePlaceholder() { 18934 if (isAIModeEnabled) { 18935 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18936 } else { 18937 // Static placeholder for typing animation 18938 searchInput.placeholder = ''; 18939 } 18940 } 18941 18942 // Typing animation for placeholder 18943 let typingTimeout = null; 18944 let currentProductIndex = 0; 18945 let currentCharIndex = 0; 18946 let isTyping = false; 18947 let isDeleting = false; 18948 18949 const popularProducts = [ 18950 'Oppo A6', 18951 'Asus Ryzen 3', 18952 'Starlink V4', 18953 'HP Victus', 18954 'Lenovo RTX 5060', 18955 'ROG Strix', 18956 'iPhone 13', 18957 'Xpad 20', 18958 'Xiaomi 14', 18959 'Epson L3210', 18960 'Realme C85', 18961 'Brother T720', 18962 'Mouse Logitech' 18963 ]; 18964 18965 function typeWriter() { 18966 if (isAIModeEnabled || document.activeElement === searchInput) { 18967 return; // Skip typing if AI mode is on or input is focused 18968 } 18969 18970 const currentProduct = popularProducts[currentProductIndex]; 18971 18972 if (!isDeleting) { 18973 // Typing forward 18974 if (currentCharIndex < currentProduct.length) { 18975 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18976 currentCharIndex++; 18977 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18978 } else { 18979 // Finished typing, wait before deleting 18980 typingTimeout = setTimeout(() => { 18981 isDeleting = true; 18982 typeWriter(); 18983 }, 2000); // Pause before deleting 18984 } 18985 } else { 18986 // Deleting backward 18987 if (currentCharIndex > 0) { 18988 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18989 currentCharIndex--; 18990 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18991 } else { 18992 // Finished deleting, move to next product 18993 isDeleting = false; 18994 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18995 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18996 } 18997 } 18998 } 18999 19000 function startTypingAnimation() { 19001 if (!isAIModeEnabled && !typingTimeout) { 19002 currentCharIndex = 0; 19003 isDeleting = false; 19004 typeWriter(); 19005 } 19006 } 19007 19008 function stopTypingAnimation() { 19009 if (typingTimeout) { 19010 clearTimeout(typingTimeout); 19011 typingTimeout = null; 19012 } 19013 } 19014 19015 // Set initial placeholder 19016 updatePlaceholder(); 19017 19018 // Start typing animation for normal mode 19019 if (!isAIModeEnabled) { 19020 startTypingAnimation(); 19021 } 19022 19023 function esc(str){ 19024 if (str === null || str === undefined) return ''; 19025 return String(str) 19026 .replace(/&/g,'&') 19027 .replace(/</g,'<') 19028 .replace(/>/g,'>') 19029 .replace(/"/g,'"') 19030 .replace(/'/g,'''); 19031 } 19032 19033 function clearSuggest(){ 19034 suggest.innerHTML = ''; 19035 suggest.hidden = true; 19036 suggest.classList.remove('open', 'trending-mode'); 19037 currentIndex = -1; 19038 } 19039 19040 // ==================== TRENDING FUNCTIONS ==================== 19041 19042 async function fetchTrending() { 19043 if (trendingCache) return trendingCache; 19044 if (trendingLoading) return null; 19045 19046 trendingLoading = true; 19047
19048 try { 19049 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 19050 const data = await res.json(); 19051 19052 if (data.ok && Array.isArray(data.data)) { 19053 trendingCache = data.data; 19054 console.log('[Trending] Loaded', trendingCache.length, 'items'); 19055 return trendingCache; 19056 } 19057 } catch (err) { 19058 console.error('[Trending] Fetch error:', err); 19059 } finally { 19060 trendingLoading = false; 19061 } 19062 19063 return null; 19064 } 19065 19066 async function fetchTrendingProducts() { 19067 if (trendingProductsCache) return trendingProductsCache; 19068 if (trendingProductsLoading) return null; 19069 19070 trendingProductsLoading = true; 19071 19072 try { 19073 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19074 const data = await res.json(); 19075 19076 if (data.ok && Array.isArray(data.data)) { 19077 trendingProductsCache = data.data; 19078 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19079 return trendingProductsCache; 19080 } 19081 } catch (err) { 19082 console.error('[Trending Products] Fetch error:', err); 19083 } finally { 19084 trendingProductsLoading = false; 19085 } 19086 19087 return null; 19088 } 19089 19090 function renderTrendingSuggestions(trends, products) { 19091 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19092 clearSuggest(); 19093 return; 19094 } 19095 19096 let html = `<div class="trending-container">`; 19097 19098 // Left column: Trending searches 19099 if (trends && trends.length > 0) { 19100 html += ` 19101 <div class="trending-column trending-searches"> 19102 <div class="trending-header"> 19103 <span class="trending-icon">ð¥</span> 19104 <span class="trending-title">Pencarian Trending</span> 19105 </div> 19106 `; 19107 19108 html += trends.map((t, idx) => { 19109 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19110 return ` 19111 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19112 <span class="trending-rank"> 19113 <img src="/assets/img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19114 </span> 19115 <span class="trending-query">${esc(t.query)}</span> 19116 ${categoryHint} 19117 </div> 19118 `; 19119 }).join(''); 19120 19121 html += `</div>`; 19122 } 19123 19124 // Right column: Trending products 19125 if (products && products.length > 0) { 19126 html += ` 19127 <div class="trending-column trending-products"> 19128 <div class="trending-header"> 19129 <span class="trending-icon">â</span> 19130 <span class="trending-title">Produk Populer</span> 19131 </div> 19132 `; 19133 19134 html += products.map((p, idx) => { 19135 return ` 19136 <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}"> 19137 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19138 <div class="trending-product-info"> 19139 <div class="trending-product-name">${esc(p.name)}</div> 19140 <div class="trending-product-price">${esc(p.price_label)}</div> 19141 </div> 19142 </div> 19143 `; 19144 }).join(''); 19145 19146 html += `</div>`; 19147 } 19148 19149 html += `</div>`; 19150 19151 suggest.innerHTML = html; 19152 suggest.hidden = false; 19153 suggest.classList.add('open', 'trending-mode'); 19154 suggest.style.display = 'block'; 19155 19156 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19157 } 19158 19159 function trackSearchClick(query, productId) { 19160 fetch('/api/track_search_click.php', { 19161 method: 'POST', 19162 headers: { 'Content-Type': 'application/json' }, 19163 body: JSON.stringify({ 19164 query: query, 19165 product_id: productId,
19166 click_type: 'suggestion' 19167 }) 19168 }).catch(() => {}); // silent fail 19169 } 19170 19171 function trackTrendingClick(query) { 19172 fetch('/api/track_search_click.php', { 19173 method: 'POST', 19174 headers: { 'Content-Type': 'application/json' }, 19175 body: JSON.stringify({ 19176 query: query, 19177 click_type: 'trending' 19178 }) 19179 }).catch(() => {}); 19180 } 19181 19182 function trackTrendingProductClick(productId, query) { 19183 fetch('/api/track_search_click.php', { 19184 method: 'POST', 19185 headers: { 'Content-Type': 'application/json' }, 19186 body: JSON.stringify({ 19187 query: query || 'trending_product', 19188 product_id: productId, 19189 click_type: 'trending_product' 19190 }) 19191 }).catch(() => {}); 19192 } 19193 19194 function trackSearchSubmit(query) { 19195 fetch('/api/track_search_click.php', { 19196 method: 'POST', 19197 headers: { 'Content-Type': 'application/json' }, 19198 body: JSON.stringify({ 19199 query: query, 19200 click_type: 'submit' 19201 }) 19202 }).catch(() => {}); 19203 } 19204 19205 function logFinalSearch(query) { 19206 // Log the final search to analytics (not during typing) 19207 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19208 headers: { 'Accept': 'application/json' } 19209 }).catch(() => {}); 19210 } 19211 19212 // ==================== END TRENDING FUNCTIONS ==================== 19213 19214 function clearSuggest(){ 19215 suggest.innerHTML = ''; 19216 suggest.hidden = true; 19217 suggest.classList.remove('open', 'trending-mode'); 19218 currentIndex = -1; 19219 } 19220 19221 // AI Toggle Handler 19222 aiToggleBtn.addEventListener('click', (e) => { 19223 e.preventDefault(); 19224 e.stopPropagation(); 19225 19226 isAIModeEnabled = !isAIModeEnabled; 19227 19228 if (isAIModeEnabled) { 19229 aiToggleBtn.classList.add('active'); 19230 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19231 updatePlaceholder(); // Update placeholder text 19232 stopTypingAnimation(); // Stop typing animation in AI mode 19233 19234 // Show AI prompt if there's a query 19235 const query = searchInput.value.trim(); 19236 if (query.length >= MIN_LEN) { 19237 showAISearchPrompt(query); 19238 } else { 19239 showAIActivationMessage(); 19240 } 19241 } else { 19242 aiToggleBtn.classList.remove('active'); 19243 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19244 updatePlaceholder(); // Update placeholder text 19245 startTypingAnimation(); // Start typing animation in normal mode 19246 clearSuggest(); 19247 19248 // Switch to normal search if there's a query 19249 const query = searchInput.value.trim(); 19250 if (query.length >= MIN_LEN) { 19251 performSearch(query); 19252 } 19253 } 19254 }); 19255 19256 function showAIActivationMessage() { 19257 suggest.innerHTML = ` 19258 <div class="ai-progress-container"> 19259 <div class="ai-progress-header"> 19260 <div class="ai-progress-icon">â¨</div> 19261 <div class="ai-progress-text"> 19262 <div class="ai-progress-title">AI Search Mode Activated!</div> 19263 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19264 </div> 19265 </div> 19266 </div> 19267 `; 19268 suggest.hidden = false; 19269 suggest.classList.add('open'); 19270 suggest.style.display = 'block'; 19271 } 19272 19273 function renderAIInsights(insights){ 19274 if (!insights) return ''; 19275 19276 // Generate AI summary via backend endpoint 19277 const summary = insights.ai_summary || generateAISummary(insights); 19278 19279 if (!summary) return ''; 19280 19281 return ` 19282 <div class="ai-result-banner"> 19283 <div class="ai-result-icon">â¨</div> 19284 <div class="ai-result-text"> 19285 <div class="ai-result-title">AI Assistant</div> 19286 <div class="ai-result-description">${summary}</div> 19287 </div> 19288 </div> 19289 `; 19290 } 19291 19292 function generateAISummary(insights) { 19293 // Fallback client-side summary generation if backend doesn't provide ai_summary 19294 const productName = insights.product_name || ''; 19295 const categoryPath = insights.category_path || ''; 19296 const priceRange = insights.price_range || {}; 19297 const intent = insights.intent || ''; 19298 19299 let summary = ''; 19300 19301 // Parse category for natural display 19302 let categoryDisplay = ''; 19303 if (categoryPath) { 19304 const categoryParts = categoryPath.split('/');
19305 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19306 } 19307 19308 // Intent-based opening 19309 if (intent === 'cek_stok') { 19310 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19311 } else if (intent === 'rekomendasi_produk') { 19312 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19313 } else { 19314 // Default: cari_harga or general 19315 if (productName && categoryDisplay) { 19316 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19317 } else if (productName) { 19318 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19319 } else if (categoryDisplay) { 19320 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19321 } 19322 19323 // Add price context 19324 if (priceRange.min > 0 && priceRange.max > 0) { 19325 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19326 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19327 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19328 } else if (priceRange.min > 0) { 19329 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19330 summary += ` dengan harga mulai ${minPrice}`; 19331 } else if (priceRange.max > 0) { 19332 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19333 summary += ` dengan harga maksimal ${maxPrice}`; 19334 } 19335 19336 summary += ' yang sesuai dengan kebutuhan Anda.'; 19337 } 19338 19339 return summary || esc(insights.explanation || ''); 19340 } 19341 19342 function showAIProgressSteps() { 19343 return ` 19344 <div class="ai-progress-container"> 19345 <div class="ai-progress-header"> 19346 <div class="ai-progress-icon">ð¤</div> 19347 <div class="ai-progress-text"> 19348 <div class="ai-progress-title">AI sedang menganalisis...</div> 19349 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19350 </div> 19351 </div> 19352 <div class="ai-progress-steps"> 19353 <div class="ai-progress-step active"> 19354 <div class="ai-step-icon">1</div> 19355 <div class="ai-step-text">Memahami maksud pencarian...</div> 19356 </div> 19357 <div class="ai-progress-step pending"> 19358 <div class="ai-step-icon">2</div> 19359 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19360 </div> 19361 <div class="ai-progress-step pending"> 19362 <div class="ai-step-icon">3</div> 19363 <div class="ai-step-text">Mencari produk yang relevan...</div> 19364 </div> 19365 </div> 19366 </div> 19367 `; 19368 } 19369 19370 function updateAIProgressStep(step) { 19371 const steps = suggest.querySelectorAll('.ai-progress-step'); 19372 19373 steps.forEach((stepEl, idx) => { 19374 const icon = stepEl.querySelector('.ai-step-icon'); 19375 19376 if (idx < step) { 19377 stepEl.classList.remove('active', 'pending'); 19378 stepEl.classList.add('completed'); 19379 icon.textContent = 'â'; 19380 } else if (idx === step) { 19381 stepEl.classList.remove('pending', 'completed'); 19382 stepEl.classList.add('active'); 19383 } 19384 }); 19385 } 19386
19387 function renderSuggest(items, aiInsights){ 19388 if (!items.length){ 19389 clearSuggest(); 19390 return; 19391 } 19392 19393 let html = ''; 19394 19395 // Show AI insights if available 19396 if (aiInsights && aiInsights.explanation) { 19397 html += renderAIInsights(aiInsights); 19398 } 19399 19400 // Section header for products with hint 19401 html += ` 19402 <div class="suggest-section-header"> 19403 <div class="suggest-header-main"> 19404 <span class="suggest-header-icon">ð¦</span> 19405 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19406 </div> 19407 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19408 </div> 19409 `; 19410 19411 // Render product items 19412 html += items.map((it, idx)=>{ 19413 // Normalisasi & fallback 19414 const id = it.id != null ? it.id : ''; 19415 const name = esc(it.name); 19416 const slug = esc(it.slug != null ? it.slug : it.id); 19417 const img = esc(it.image || '/assets/img/no-image.png'); 19418 const price = esc(it.price_label || ''); 19419 19420 const url = '/product/' + encodeURIComponent(slug); 19421 19422 return ` 19423 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19424 <img src="${img}" alt="${name}" class="suggest-thumb"> 19425 <div class="suggest-meta"> 19426 <div class="suggest-name">${name}</div> 19427 <div class="suggest-price">${price}</div> 19428 </div> 19429 </div> 19430 `; 19431 }).join(''); 19432 19433 // Footer hint for more results - only show if 10 items (means there might be more) 19434 if (items.length >= 10) { 19435 html += ` 19436 <div class="suggest-footer-hint"> 19437 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19438 <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"/> 19439 </svg> 19440 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19441 </div> 19442 `; 19443 } 19444 19445 suggest.innerHTML = html; 19446 suggest.hidden = false; 19447 suggest.classList.add('open'); 19448 suggest.style.display = 'block'; 19449 suggest.style.visibility = 'visible'; 19450 suggest.style.opacity = '1'; 19451 } 19452 19453 function fetchRemote(q, shouldLog = false){ 19454 // Cancel previous request if any 19455 if (currentAbortController) { 19456 currentAbortController.abort(); 19457 } 19458 currentAbortController = new AbortController(); 19459 19460 // Always use standard search for normal mode 19461 // Only add log=1 parameter when shouldLog is true (intentional search) 19462 const logParam = shouldLog ? '&log=1' : ''; 19463 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19464 const fetchOptions = { 19465 headers: {'Accept':'application/json'}, 19466 signal: currentAbortController.signal 19467 }; 19468 19469 return fetch(url, fetchOptions) 19470 .then(r=>r.json()) 19471 .then(j=>{ 19472 if (j.ok && Array.isArray(j.data)) { 19473 return j; 19474 } 19475 return { ok: false, data: [] }; 19476 }) 19477 .catch(err=>{ 19478 if (err.name === 'AbortError') { 19479 return { ok: false, data: [] }; 19480 } 19481 return { ok:false, data:[] }; 19482 }); 19483 } 19484 19485 function showNoResults(q, aiInsights) { 19486 suggest.innerHTML = ` 19487 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19488 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19489 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19490 Tidak ada hasil untuk "${esc(q)}" 19491 </div> 19492 ${aiInsights && aiInsights.explanation ? ` 19493 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19494 <strong style="color: #667eea;">ð¤ AI memahami:</strong><br> 19495 ${esc(aiInsights.explanation)} 19496 </div> 19497 ` : ''} 19498 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19499 Coba kata kunci lain atau hubungi kami untuk bantuan 19500 </div> 19501 </div> 19502 `; 19503 suggest.hidden = false; 19504 suggest.classList.add('open'); 19505 suggest.style.display = 'block'; 19506 } 19507 19508 function showErrorMessage() { 19509 suggest.innerHTML = ` 19510 <div style="padding: 32px 24px; text-align: center; color: #C77DFF;"> 19511 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19512 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19513 Pencarian tidak tersedia 19514 </div> 19515 <div style="font-size: 12px; opacity: 0.8;">
19516 Silakan coba lagi dalam beberapa saat 19517 </div> 19518 </div> 19519 `; 19520 suggest.hidden = false; 19521 suggest.classList.add('open'); 19522 suggest.style.display = 'block'; 19523 } 19524 19525 function showAIErrorState(q) { 19526 suggest.innerHTML = ` 19527 <div class="ai-progress-container"> 19528 <div class="ai-progress-header"> 19529 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #C77DFF 0%, #C77DFF 100%);">â¡</div> 19530 <div class="ai-progress-text"> 19531 <div class="ai-progress-title">AI sedang memproses...</div> 19532 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19533 </div> 19534 </div> 19535 <div style="padding: 16px; margin-top: 12px; background: #fef3c7; border-radius: 8px; border-left: 3px solid #C77DFF;"> 19536 <div style="font-size: 13px; color: #ae7e21; line-height: 1.6;"> 19537 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19538 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19539 Mohon tunggu sebentar... 19540 </div> 19541 </div> 19542 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19543 <button onclick="location.reload()" style="padding: 8px 16px; background: #C77DFF; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;"> 19544 ð Refresh & Coba Lagi 19545 </button> 19546 </div> 19547 </div> 19548 `; 19549 suggest.hidden = false; 19550 suggest.classList.add('open'); 19551 suggest.style.display = 'block'; 19552 } 19553 19554 function triggerAISearch(q) { 19555 lastQuery = q; // Update lastQuery to prevent re-triggering 19556 isAIProcessing = true; // Set flag to prevent suggest box from closing 19557 19558 // Show AI progress UI - keep suggest box visible 19559 suggest.innerHTML = showAIProgressSteps(); 19560 suggest.hidden = false; 19561 suggest.classList.add('open'); 19562 suggest.style.display = 'block'; 19563 19564 setTimeout(() => updateAIProgressStep(1), 1000); 19565 setTimeout(() => updateAIProgressStep(2), 2500); 19566 19567 // Cancel previous request if any 19568 if (currentAbortController) { 19569 currentAbortController.abort(); 19570 } 19571 currentAbortController = new AbortController(); 19572 19573 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19574 const fetchOptions = { 19575 headers: {'Accept':'application/json'}, 19576 signal: currentAbortController.signal 19577 }; 19578 19579 // Add timeout 19580 const timeoutId = setTimeout(() => { 19581 currentAbortController.abort(); 19582 19583 // Show fallback loading state 19584 suggest.innerHTML = ` 19585 <div class="ai-progress-container"> 19586 <div class="ai-progress-header"> 19587 <div class="ai-progress-icon">â³</div> 19588 <div class="ai-progress-text"> 19589 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19590 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19591 </div> 19592 </div> 19593 </div> 19594 `; 19595 19596 // Fallback to traditional search 19597 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19598 headers: { 'Accept': 'application/json' } 19599 }) 19600 .then(r => r.json()) 19601 .then(j => { 19602 isAIProcessing = false; // Reset flag 19603 if (j.ok && Array.isArray(j.data) && j.data.length) { 19604 renderSuggest(j.data, null); 19605 } else { 19606 showNoResults(q, null); 19607 } 19608 }) 19609 .catch(() => { 19610 isAIProcessing = false; // Reset flag 19611 showAIErrorState(q); 19612 }); 19613 }, AI_TIMEOUT); 19614 19615 fetch(url, fetchOptions) 19616 .then(r => r.json()) 19617 .then(j => { 19618 clearTimeout(timeoutId); 19619 isAIProcessing = false; // Reset flag when done 19620 19621 if (j.ok && Array.isArray(j.data)) { 19622 if (j.data.length > 0) { 19623 renderSuggest(j.data, j.ai_insights || null); 19624 } else { 19625 showNoResults(q, j.ai_insights || null); 19626 } 19627 } else { 19628 showAIErrorState(q); 19629 } 19630 }) 19631 .catch(err => { 19632 clearTimeout(timeoutId); 19633 isAIProcessing = false; // Reset flag on error 19634 if (err.name !== 'AbortError') { 19635 showAIErrorState(q); 19636 } 19637 }); 19638 } 19639 19640 function performSearch(q){ 19641 if (q.length < MIN_LEN){ 19642 clearSuggest(); 19643 return; 19644 } 19645 if (q === lastQuery) return; 19646 lastQuery = q; 19647 19648 // Normal mode only - AI mode uses triggerAISearch() 19649 fetchRemote(q).then(j=>{ 19650 if (j.ok && Array.isArray(j.data)){ 19651 if (j.data.length > 0) { 19652 renderSuggest(j.data, null); 19653 } else { 19654 showNoResults(q, null); 19655 } 19656 } else { 19657 clearSuggest(); 19658 } 19659 }).catch(() => { 19660 clearSuggest(); 19661 }); 19662 } 19663 19664 function debounce(){ 19665 const q = searchInput.value.trim(); 19666 clearTimeout(debounceTimer); 19667 19668 // If empty, show trending suggestions and products 19669 if (q.length === 0) { 19670 debounceTimer = setTimeout(async () => { 19671 const trends = trendingCache || await fetchTrending(); 19672 const products = trendingProductsCache || await fetchTrendingProducts(); 19673 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19674 renderTrendingSuggestions(trends, products); 19675 } else { 19676 clearSuggest(); 19677 } 19678 }, 100); 19679 return; 19680 } 19681 19682 // For AI mode, just show search prompt (no auto search) 19683 if (isAIModeEnabled && q.length >= MIN_LEN) { 19684 showAISearchPrompt(q); 19685 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19686 // Normal mode: auto search with debounce 19687 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19688 } else { 19689 clearSuggest(); 19690 } 19691 } 19692 19693 function showAISearchPrompt(q) { 19694 suggest.innerHTML = ` 19695 <div class="ai-search-prompt"> 19696 <div class="ai-prompt-header"> 19697 <div class="ai-prompt-icon">ð¤</div> 19698 <div class="ai-prompt-text"> 19699 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19700 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19701 </div> 19702 </div> 19703 <div class="ai-prompt-query"> 19704 <div class="ai-query-label">
19704Query Anda:</div> 19705 <div class="ai-query-text">"${esc(q)}"</div> 19706 </div> 19707 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19708 <span class="ai-btn-icon">â¨</span> 19709 <span class="ai-btn-text">Cari dengan AI</span> 19710 </button> 19711 <div class="ai-prompt-footer"> 19712 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19713 </div> 19714 </div> 19715 `; 19716 suggest.hidden = false; 19717 suggest.classList.add('open'); 19718 suggest.style.display = 'block'; 19719 } 19720 19721 function moveHighlight(dir){ 19722 if (suggest.hidden) return; 19723 const items = suggest.querySelectorAll('.suggest-item'); 19724 if (!items.length) return; 19725 currentIndex += dir; 19726 if (currentIndex < 0) currentIndex = items.length - 1; 19727 if (currentIndex >= items.length) currentIndex = 0; 19728 items.forEach(it=> it.classList.remove('active')); 19729 const active = items[currentIndex]; 19730 if (active){ 19731 active.classList.add('active'); 19732 active.scrollIntoView({block:'nearest'}); 19733 } 19734 } 19735 19736 function activate(){ 19737 const active = suggest.querySelector('.suggest-item.active'); 19738 if (!active) return; 19739 window.location.href = active.getAttribute('data-url'); 19740 } 19741 19742 searchInput.addEventListener('input', debounce); 19743 19744 // Stop typing animation when focused & load trending if empty 19745 searchInput.addEventListener('focus', async () => { 19746 stopTypingAnimation(); 19747 19748 const q = searchInput.value.trim(); 19749 19750 // If empty, show trending suggestions and products 19751 if (q.length === 0) { 19752 const trends = trendingCache || await fetchTrending(); 19753 const products = trendingProductsCache || await fetchTrendingProducts(); 19754 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19755 renderTrendingSuggestions(trends, products); 19756 } 19757 return; 19758 } 19759 19760 // If AI mode and has query, show AI prompt 19761 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19762 showAISearchPrompt(q); 19763 } 19764 }); 19765 19766 // Resume typing animation when blurred (if suggest is closed) 19767 searchInput.addEventListener('blur', () => { 19768 // Add delay to allow click events to fire first 19769 setTimeout(() => { 19770 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19771 startTypingAnimation(); 19772 } 19773 }, 200); 19774 }); 19775 19776 searchInput.addEventListener('keydown', e=>{ 19777 switch(e.key){ 19778 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19779 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19780 case 'Enter': 19781 if (!suggest.hidden){ 19782 activate(); 19783 e.preventDefault(); 19784 } else { 19785 // Direct enter without suggestion - track search submit and log final search 19786 const q = searchInput.value.trim(); 19787 if (q.length >= MIN_LEN) { 19788 trackSearchSubmit(q); 19789 logFinalSearch(q); 19790 } 19791 } 19792 break; 19793 case 'Escape': clearSuggest(); break; 19794 } 19795 }); 19796 19797 suggest.addEventListener('click', e=>{ 19798 // Check if footer link clicked 19799 const footerLink = e.target.closest('#footerSearchLink'); 19800 if (footerLink) { 19801 e.preventDefault(); 19802 const q = searchInput.value.trim(); 19803 if (q.length >= MIN_LEN) { 19804 trackSearchSubmit(q); 19805 logFinalSearch(q); 19806 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19807 } 19808 return; 19809 } 19810 19811 // Check if AI search button clicked 19812 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19813 if (aiBtn) { 19814 const query = aiBtn.getAttribute('data-query'); 19815 if (query) { 19816 triggerAISearch(query); 19817 } 19818 return; 19819 } 19820 19821 // Handle trending item click 19822 const trendingItem = e.target.closest('.trending-item'); 19823 if (trendingItem) { 19824 const query = trendingItem.getAttribute('data-query'); 19825 if (query) { 19826 searchInput.value = query; 19827 trackTrendingClick(query); 19828 logFinalSearch(query); // Log final search when user clicks trending 19829 performSearch(query); 19830 } 19831 return; 19832 } 19833 19834 // Handle trending product click 19835 const trendingProduct = e.target.closest('.trending-product-item'); 19836 if (trendingProduct) { 19837 const productId = trendingProduct.getAttribute('data-product-id'); 19838 const slug = trendingProduct.getAttribute('data-slug'); 19839 const query = trendingProduct.getAttribute('data-query'); 19840 19841 if (productId && slug) {
19842 trackTrendingProductClick(productId, query); 19843 window.location.href = '/product/' + encodeURIComponent(slug); 19844 } 19845 return; 19846 } 19847 19848 // Handle product item click 19849 const it = e.target.closest('.suggest-item'); 19850 if (!it) return; 19851 19852 const productId = it.getAttribute('data-id'); 19853 trackSearchClick(lastQuery, productId); 19854 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19855 19856 window.location.href = it.getAttribute('data-url'); 19857 }); 19858 19859 document.addEventListener('click', e=>{ 19860 // Don't close suggest box if AI is processing 19861 if (isAIProcessing) return; 19862 19863 if (e.target === searchInput || e.target === aiToggleBtn || 19864 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19865 clearSuggest(); 19866 }); 19867 19868 if (searchBtn){ 19869 searchBtn.addEventListener('click', ()=>{ 19870 const q = searchInput.value.trim(); 19871 if (q.length >= MIN_LEN){ 19872 // Track search submit and log final search 19873 trackSearchSubmit(q); 19874 logFinalSearch(q); 19875 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19876 } 19877 }); 19878 } 19879 19880 // Pastikan style dasar muncul 19881 if (!suggest.classList.contains('wired')) { 19882 suggest.classList.add('wired'); 19883 suggest.style.position = 'absolute'; 19884 suggest.style.zIndex = '9999'; 19885 suggest.style.background = '#fff'; 19886 suggest.style.border = '1px solid #ccc'; 19887 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19888 suggest.style.maxHeight = '360px'; 19889 suggest.style.overflowY = 'auto'; 19890 } 19891 19892 return true; 19893 } 19894 19895 if (!setup()) { 19896 if (document.readyState === 'loading') { 19897 document.addEventListener('DOMContentLoaded', setup); 19898 } else { 19899 setTimeout(setup, 80); 19900 } 19901 } 19902})(); 19903</script>
vendor: 2 bytes, line 19903
19903
19904<script data-inlined="site-notice.js"> 19905/** 19906 * Site Notice Banner - Interactive Behavior 19907 * Handles dismiss functionality with localStorage persistence 19908 * Supports dynamic notice IDs for different campaigns 19909 */ 19910 19911(function() { 19912 'use strict'; 19913 19914 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19915 19916 const banner = document.getElementById('holidayNotice'); 19917 const closeBtn = document.getElementById('holidayNoticeClose'); 19918 19919 if (!banner || !closeBtn) return; 19920 19921 // Get notice ID from data attribute 19922 const noticeId = banner.dataset.noticeId; 19923 if (!noticeId) { 19924 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19925 } 19926 19927 /** 19928 * Check if banner was previously dismissed 19929 */ 19930 function wasDismissed() { 19931 if (!noticeId) return false; 19932 19933 try { 19934 const dismissed = localStorage.getItem(STORAGE_KEY); 19935 return dismissed === noticeId; 19936 } catch (e) { 19937 console.warn('localStorage not available:', e); 19938 return false; 19939 } 19940 } 19941 19942 /** 19943 * Mark banner as dismissed 19944 */ 19945 function markDismissed() { 19946 if (!noticeId) return; 19947 19948 try { 19949 localStorage.setItem(STORAGE_KEY, noticeId); 19950 } catch (e) { 19951 console.warn('Could not save to localStorage:', e); 19952 } 19953 } 19954 19955 /** 19956 * Hide banner with animation 19957 */ 19958 function hideBanner() { 19959 banner.classList.add('hidden'); 19960 document.body.classList.remove('has-holiday-notice'); 19961 19962 // Remove from DOM after animation 19963 setTimeout(() => { 19964 banner.remove(); 19965 }, 300); 19966 19967 markDismissed(); 19968 19969 // Analytics tracking (optional) 19970 if (window.gtag) { 19971 window.gtag('event', 'banner_dismiss', { 19972 'event_category': 'Site Notice', 19973 'event_label': noticeId || 'unknown', 19974 }); 19975 } 19976 } 19977 19978 /** 19979 * Initialize banner 19980 */ 19981 function init() { 19982 // Check if already dismissed 19983 if (wasDismissed()) { 19984 banner.remove(); 19985 return; 19986 } 19987 19988 // Add body class for layout adjustment 19989 document.body.classList.add('has-holiday-notice'); 19990 19991 // Close button click 19992 closeBtn.addEventListener('click', hideBanner); 19993 19994 // Keyboard support for close button 19995 closeBtn.addEventListener('keydown', (e) => { 19996 if (e.key === 'Enter' || e.key === ' ') { 19997 e.preventDefault(); 19998 hideBanner(); 19999 } 20000 }); 20001 20002 // Optional: Auto-hide after certain time (disabled by default) 20003 // setTimeout(hideBanner, 30000); // 30 seconds 20004 } 20005 20006 // Initialize when DOM is ready 20007 if (document.readyState === 'loading') { 20008 document.addEventListener('DOMContentLoaded', init); 20009 } else { 20010 init(); 20011 } 20012 20013})(); 20014 20015</script>
vendor: 2 bytes, line 20015
20015
20016<script data-inlined="guest-cart.js"> 20017/** 20018 * Guest Cart Manager - localStorage based cart for guest users 20019 * 20020 * Manages shopping cart items in browser localStorage for users who are not logged in. 20021 * Provides seamless transition to database when user logs in. 20022 */ 20023 20024class GuestCart { 20025 constructor() { 20026 this.storageKey = 'plazait_guest_cart'; 20027 this.apiBase = '/api/cart'; 20028 20029 // Initialize cart structure 20030 this.cart = this.loadFromStorage(); 20031 20032 // Bind methods 20033 this.add = this.add.bind(this); 20034 this.remove = this.remove.bind(this); 20035 this.update = this.update.bind(this); 20036 this.clear = this.clear.bind(this); 20037 this.getItems = this.getItems.bind(this); 20038 this.mergeToDatabase = this.mergeToDatabase.bind(this); 20039 } 20040 20041 /** 20042 * Load cart from localStorage 20043 */ 20044 loadFromStorage() { 20045 try { 20046 const stored = localStorage.getItem(this.storageKey); 20047 if (stored) { 20048 const parsed = JSON.parse(stored); 20049 if (parsed && typeof parsed === 'object' && parsed.items) { 20050 return parsed; 20051 } 20052 } 20053 } catch (error) { 20054 // Silent error handling 20055 } 20056 20057 return { 20058 items: {}, 20059 updated_at: Date.now(), 20060 version: '1.0' 20061 }; 20062 } 20063 20064 /** 20065 * Save cart to localStorage 20066 */ 20067 saveToStorage() { 20068 try { 20069 this.cart.updated_at = Date.now(); 20070 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20071 20072 // Dispatch custom event for cart updates 20073 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20074 detail: { 20075 itemsCount: this.getItemsCount(), 20076 linesCount: this.getLinesCount(), 20077 items: this.cart.items 20078 } 20079 })); 20080 20081 return true; 20082 } catch (error) { 20083 return false; 20084 } 20085 } 20086 20087 /** 20088 * Generate cart item key 20089 */ 20090 generateKey(productId, variantId = 0) { 20091 return `p${productId}v${variantId}`; 20092 } 20093 20094 /** 20095 * Add item to guest cart 20096 */ 20097 add(productId, qty = 1, variantId = 0, meta = {}) { 20098 const key = this.generateKey(productId, variantId); 20099 20100 // Validate inputs 20101 if (!productId || productId <= 0 || !qty || qty <= 0) { 20102 throw new Error('Invalid product ID or quantity'); 20103 } 20104 20105 qty = Math.max(1, Math.min(999, parseInt(qty))); 20106 20107 // Initialize item if not exists 20108 if (!this.cart.items[key]) { 20109 this.cart.items[key] = { 20110 pid: parseInt(productId), 20111 vid: parseInt(variantId) || 0, 20112 qty: 0, 20113 name: meta.name || '', 20114 sku: meta.sku || '', 20115 price: parseInt(meta.price) || 0, 20116 image_url: meta.image_url || '', 20117 product_url: meta.product_url || '/product/' + parseInt(productId), 20118 added_at: Date.now() 20119 }; 20120 } 20121 20122 // Update quantity 20123 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20124 20125 // Update metadata if provided 20126 if (meta.name) this.cart.items[key].name = meta.name; 20127 if (meta.sku) this.cart.items[key].sku = meta.sku; 20128 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20129 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20130 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20131 20132 this.saveToStorage(); 20133 20134 return { 20135 key, 20136 item: this.cart.items[key], 20137 itemsCount: this.getItemsCount(), 20138 linesCount: this.getLinesCount() 20139 }; 20140 } 20141 20142 /** 20143 * Remove item from guest cart 20144 */ 20145 remove(productId, variantId = 0) {
20146 const key = this.generateKey(productId, variantId); 20147 20148 if (this.cart.items[key]) { 20149 delete this.cart.items[key]; 20150 this.saveToStorage(); 20151 20152 return true; 20153 } 20154 20155 return false; 20156 } 20157 20158 /** 20159 * Update item quantity 20160 */ 20161 update(productId, qty, variantId = 0) { 20162 const key = this.generateKey(productId, variantId); 20163 20164 if (!this.cart.items[key]) { 20165 return false; 20166 } 20167 20168 qty = parseInt(qty); 20169 if (qty <= 0) { 20170 return this.remove(productId, variantId); 20171 } 20172 20173 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20174 this.saveToStorage(); 20175 20176 return true; 20177 } 20178 20179 /** 20180 * Clear entire cart 20181 */ 20182 clear() { 20183 this.cart = { 20184 items: {}, 20185 updated_at: Date.now(), 20186 version: '1.0' 20187 }; 20188 20189 this.saveToStorage(); 20190 20191 return true; 20192 } 20193 20194 /** 20195 * Get all cart items 20196 */ 20197 getItems() { 20198 return this.cart.items; 20199 } 20200 20201 /** 20202 * Get cart items as array 20203 */ 20204 getItemsArray() { 20205 return Object.values(this.cart.items); 20206 } 20207 20208 /** 20209 * Get total items count (sum of quantities) 20210 */ 20211 getItemsCount() { 20212 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20213 } 20214 20215 /** 20216 * Get lines count (number of different items) 20217 */ 20218 getLinesCount() { 20219 return Object.keys(this.cart.items).length; 20220 } 20221 20222 /** 20223 * Get cart summary 20224 */ 20225 getSummary() { 20226 const itemsCount = this.getItemsCount(); 20227 const linesCount = this.getLinesCount(); 20228 const totalValue = Object.values(this.cart.items).reduce( 20229 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20230 ); 20231 20232 return { 20233 itemsCount, 20234 linesCount, 20235 totalValue, 20236 updatedAt: this.cart.updated_at 20237 }; 20238 } 20239 20240 /** 20241 * Check if cart has items 20242 */ 20243 isEmpty() { 20244 return this.getLinesCount() === 0; 20245 } 20246 20247 /** 20248 * Merge guest cart to database after user login 20249 */ 20250 async mergeToDatabase() { 20251 const items = this.cart.items; 20252 20253 if (Object.keys(items).length === 0) { 20254 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20255 } 20256 20257 try { 20258 // Use new endpoint that handles localStorage format 20259 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20260 method: 'POST', 20261 headers: { 20262 'Content-Type': 'application/json', 20263 }, 20264 credentials: 'same-origin', 20265 body: JSON.stringify({ 20266 items: items // Send localStorage format directly 20267 }) 20268 }); 20269 20270 const result = await response.json(); 20271 20272 if (result.ok) { 20273 // Clear localStorage cart after successful merge 20274 this.clear(); 20275 20276 // Update cart counter 20277 if (window.updateCartCounter) { 20278 try { 20279 await window.updateCartCounter(); 20280 } catch (e) { 20281 // Silent error handling 20282 } 20283 } 20284 20285 // Dispatch merge completion event 20286 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20287 detail: result 20288 })); 20289 20290 return result; 20291 } else { 20292 throw new Error(result.error || 'Failed to merge cart'); 20293 } 20294 20295 } catch (error) { 20296 throw error; 20297 } 20298 } 20299 20300 /** 20301 * Export cart data (for debugging or backup) 20302 */ 20303 export() { 20304 return { 20305 ...this.cart,
20306 summary: this.getSummary() 20307 }; 20308 } 20309 20310 /** 20311 * Import cart data (for restore or testing) 20312 */ 20313 import(cartData) { 20314 if (cartData && typeof cartData === 'object' && cartData.items) { 20315 this.cart = { 20316 items: cartData.items, 20317 updated_at: cartData.updated_at || Date.now(), 20318 version: cartData.version || '1.0' 20319 }; 20320 20321 this.saveToStorage(); 20322 return true; 20323 } 20324 20325 return false; 20326 } 20327} 20328 20329// Create global instance 20330window.guestCart = new GuestCart(); 20331 20332// Auto-merge on page load if user is logged in 20333document.addEventListener('DOMContentLoaded', async function() { 20334 // Multiple ways to check if user is logged in 20335 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20336 document.body.classList.contains('logged-in') || 20337 window.userLoggedIn === true || 20338 document.body.dataset.userLoggedIn === 'true'; 20339 20340 if (isLoggedIn && !window.guestCart.isEmpty()) { 20341 // Small delay to ensure page is fully loaded 20342 setTimeout(async () => { 20343 try { 20344 const result = await window.guestCart.mergeToDatabase(); 20345 } catch (error) { 20346 // Silent error handling 20347 } 20348 }, 500); 20349 } 20350}); 20351 20352// Also listen for login events (if login happens via AJAX) 20353window.addEventListener('userLoggedIn', async function() { 20354 if (!window.guestCart.isEmpty()) { 20355 try { 20356 await window.guestCart.mergeToDatabase(); 20357 } catch (error) { 20358 // Silent error handling 20359 } 20360 } 20361}); 20362</script>
vendor: 2 bytes, line 20362
20362
20363<script data-inlined="cart.js"> 20364(function(){ 20365 function $(s,ctx=document){ return ctx.querySelector(s); } 20366 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20367 20368 // CSRF token helper 20369 function getCsrfToken() { 20370 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20371 return metaTag ? metaTag.getAttribute('content') : ''; 20372 } 20373 20374 function guessPdMeta(){ 20375 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20376 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20377 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20378 const imgEl = $('#pd-main-img'); 20379 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20380 return { name, sku, image_url }; // price removed for security 20381 } 20382 20383 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20384 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20385 const payload = { 20386 product_id: parseInt(productId||0,10) || 0, 20387 qty: parseInt(qty||1,10) || 1, 20388 variant_id: parseInt(variantId||0,10) || 0, 20389 meta 20390 }; 20391 20392 // Check if user is logged in 20393 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20394 document.body.classList.contains('logged-in') || 20395 window.userLoggedIn === true || 20396 document.body.dataset.userLoggedIn === 'true'; 20397 20398 // Skip localStorage untuk logged user - mereka pakai database 20399 if (!isLoggedIn && window.guestCart) { 20400 try { 20401 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20402 } catch (e) { 20403 console.warn('Failed to add to localStorage cart:', e); 20404 } 20405 } 20406 20407 const res = await fetch('/api/cart/add.php', { 20408 method: 'POST', 20409 headers: { 20410 'Content-Type':'application/json', 20411 'X-CSRF-Token': getCsrfToken() 20412 }, 20413 credentials: 'same-origin', 20414 body: JSON.stringify({ 20415 ...payload, 20416 csrf_token: getCsrfToken() 20417 }), 20418 }); 20419 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20420 if (!res.ok || !data.ok) { 20421 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20422 throw new Error(msg); 20423 } 20424 20425 // Update badge di navbar dengan data dari response 20426 try { 20427 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20428 if (el && data.cart && typeof data.cart.items_count === 'number') { 20429 el.textContent = String(data.cart.items_count); 20430 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20431 } 20432 } catch(_){} 20433 20434 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20435 try { 20436 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20437 } catch(_){} 20438 20439 // Update CartBadge dengan data dari response (lebih reliable) 20440 try { 20441 if (window.CartBadge && window.CartBadge.set) { 20442 window.CartBadge.set(data.cart.items_count||0); 20443 } 20444 } catch(_){} 20445 20446 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20447 setTimeout(() => { 20448 if (window.updateCartCounter) { 20449 window.updateCartCounter(); 20450 } 20451 }, 100); 20452 20453 return data; 20454 } 20455 20456 // Function to update cart counter from server 20457 async function updateCartCounter(){ 20458 try { 20459 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20460 const data = await res.json().catch(()=>({ok:false})); 20461 if (data.ok && typeof data.items_count === 'number') { 20462 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20463 if (el) { 20464 el.textContent = String(data.items_count); 20465 el.classList.toggle('is-empty', data.items_count <= 0); 20466 } 20467 // Dispatch event 20468 try { 20469 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20470 } catch(_){} 20471 } 20472 } catch (err) { 20473 console.log('Could not update cart counter:', err); 20474 } 20475 } 20476 20477 // Function to manually merge guest cart (if needed) 20478 async function mergeGuestCart(){ 20479 try { 20480 const res = await fetch('/api/cart/merge-guest.php', { 20481 method: 'POST', 20482 headers: { 20483 'Content-Type': 'application/json', 20484 'X-CSRF-Token': getCsrfToken() 20485 }, 20486 credentials: 'same-origin', 20487 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20488 }); 20489 const data = await res.json().catch(()=>({ok:false})); 20490 20491 if (data.ok && data.merged_items > 0) { 20492 console.log(`Guest cart merged: ${data.merged_items} items`); 20493 // Update cart counter after merge 20494 await updateCartCounter(); 20495 return data; 20496 } 20497 return data; 20498 } catch (err) { 20499 console.log('Could not merge guest cart:', err); 20500 return { ok: false, error: err.message }; 20501 } 20502 } 20503 20504 window.addToCart = addToCart; 20505 window.updateCartCounter = updateCartCounter; 20506 window.mergeGuestCart = mergeGuestCart; 20507 20508 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20509 document.addEventListener('click', async (e)=>{ 20510 const btn = e.target.closest('[data-addtocart]'); 20511 if (!btn) return; 20512 e.preventDefault(); 20513 if (btn.disabled) return; 20514 20515 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20516 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20517 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20518 20519 try { 20520 const data = await addToCart(pid, qty, vid); 20521 if (window.showToast) { 20522 const meta = guessPdMeta(); 20523 window.showToast({ 20524 type:'success', 20525 title:'Berhasil ditambahkan', 20526 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20527 duration: 3800, 20528 actions: [ 20529 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20530 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20531 ] 20532 }); 20533 } 20534 } catch (err) { 20535 if (window.showToast) { 20536 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20537 } else { 20538 console.error(err); 20539 } 20540 } 20541 }); 20542})(); 20543</script>
vendor: 2 bytes, line 20543
20543
20544<script data-inlined="mega-menu-tiles.js"> 20545// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20546// Includes safe-area hover handling (no backdrop flicker) 20547 20548(function(){ 20549 const trigger = document.querySelector('[data-mm2="trigger"]'); 20550 const panel = document.querySelector('[data-mm2="panel"]'); 20551 const topView = document.querySelector('[data-mm2="top"]'); 20552 const detailView= document.querySelector('[data-mm2="detail"]'); 20553 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20554 if (!trigger || !panel || !topView || !detailView) return; 20555 20556 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20557 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20558 20559 let data = []; 20560 let hoverTimer, closeTimer; 20561 let isLoading = false; 20562 20563 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20564 20565 // Mobile close button HTML 20566 const mobileCloseBtn = () => ` 20567 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20568 <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"> 20569 <line x1="18" y1="6" x2="6" y2="18"></line> 20570 <line x1="6" y1="6" x2="18" y2="18"></line> 20571 </svg> 20572 </button> 20573 `; 20574 20575 // Category-specific gradient colors (16 unique combinations) 20576 const categoryGradients = [ 20577 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#4f46e5' }, // Indigo 20578 { from: '#dbeafe', to: '#fff', border: '#bfdbfe', color: '#1e40af' }, // Blue 20579 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20580 { from: '#fef3c7', to: '#fff', border: '#fde68a', color: '#d97706' }, // Amber 20581 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20582 { from: '#e9d5ff', to: '#fff', border: '#d8b4fe', color: '#9333ea' }, // Purple 20583 { from: '#ffedd5', to: '#fff', border: '#fed7aa', color: '#C77DFF' }, // Orange 20584 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20585 { from: '#fef9c3', to: '#fff', border: '#fef08a', color: '#ca8a04' }, // Yellow 20586 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#C77DFF' }, // Rose 20587 { from: '#e0f2fe', to: '#fff', border: '#bae6fd', color: '#0284c7' }, // Sky 20588 { from: '#f3e8ff', to: '#fff', border: '#e9d5ff', color: '#a855f7' }, // Violet 20589 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20590 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#C77DFF' }, // Red 20591 { from: '#eff6ff', to: '#fff', border: '#dbeafe', color: '#2563eb' }, // Blue-600 20592 { from: '#f5f3ff', to: '#fff', border: '#ede9fe', color: '#7c3aed' }, // Violet-600 20593 ]; 20594 20595 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>`; 20596 20597 // Recent (optional future use) 20598 const REC_KEY='mm2_recent'; 20599 const saveRecent=(path,label)=>{ try{ 20600 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20601 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20602 }catch{} }; 20603 20604 function isToSafeArea(e){
20605 const t = e && e.relatedTarget ? e.relatedTarget : null; 20606 if (!t) return false; 20607 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20608 } 20609 20610 function open(){ 20611 document.body.classList.add('mm2-open'); 20612 showTop(); 20613 // Add entrance animation class 20614 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20615 } 20616 function close(){ 20617 document.body.classList.remove('mm2-open'); 20618 panel.classList.remove('mm2-animated'); 20619 } 20620 function showTop(){ 20621 topView.classList.add('is-active'); 20622 detailView.classList.remove('is-active'); 20623 } 20624 20625 // Loading skeleton 20626 function showLoading() { 20627 const skeletons = Array.from({length: 12}, (_, i) => ` 20628 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s"> 20629 <span class="mm2-ico skeleton-box"></span> 20630 <span style="flex: 1;"> 20631 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20632 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20633 </span> 20634 </div> 20635 `).join(''); 20636 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20637 } 20638 20639 // Count total products in a category tree 20640 function countProducts(node) { 20641 let count = node.product_count || 0; 20642 if (Array.isArray(node.children)) { 20643 node.children.forEach(child => { 20644 count += countProducts(child); 20645 }); 20646 } 20647 return count; 20648 } 20649 20650 // Cache for fetched thumbnails to avoid duplicate requests 20651 const thumbnailCache = new Map(); 20652 20653 // Fetch product thumbnails for a category 20654 function fetchCategoryThumbnails(categoryPath) { 20655 // Check cache first 20656 if (thumbnailCache.has(categoryPath)) { 20657 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20658 return; 20659 } 20660 20661 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20662 20663 fetch(url) 20664 .then(r => { 20665 if (!r.ok) throw new Error('Network error'); 20666 return r.json(); 20667 }) 20668 .then(data => { 20669 if (data.success && data.products && data.products.length > 0) { 20670 thumbnailCache.set(categoryPath, data.products); 20671 injectThumbnails(categoryPath, data.products); 20672 } else { 20673 // No products, remove skeleton 20674 removeThumbSkeletons(categoryPath); 20675 } 20676 }) 20677 .catch(err => { 20678 console.warn('Failed to load thumbnails for', categoryPath, err); 20679 removeThumbSkeletons(categoryPath); 20680 }); 20681 } 20682 20683 // Inject thumbnails into the DOM 20684 function injectThumbnails(categoryPath, products) { 20685 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20686 links.forEach(link => { 20687 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20688 if (!thumbsContainer) return; 20689 20690 const thumbsHtml = products.slice(0, 2).map(p => 20691 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20692 ).join(''); 20693 20694 thumbsContainer.innerHTML = thumbsHtml; 20695 thumbsContainer.removeAttribute('data-loading'); 20696 }); 20697 } 20698 20699 // Remove skeleton loaders when no products 20700 function removeThumbSkeletons(categoryPath) { 20701 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20702 links.forEach(link => { 20703 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20704 if (thumbsContainer) { 20705 thumbsContainer.remove(); 20706 } 20707 }); 20708 } 20709 20710 function showDetail(node){ 20711 const title = node.label.split('/')[0]; 20712 const children = Array.isArray(node.children) ? node.children : []; 20713 const hasChildren = children.length > 0; 20714 20715 const subnav = hasChildren ? ` 20716 <nav class="mm2-subnav" role="tablist"> 20717 ${children.map((c2,i)=>{ 20718 const t = c2.label.split('/').slice(-1)[0]; 20719 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20720 }).join('')} 20721 </nav>` : ''; 20722 20723 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20724
20725 detailView.innerHTML = ` 20726 <div class="mm2-detail"> 20727 <div class="mm2-detail-header"> 20728 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20729 <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> 20730 </button> 20731 <div class="mm2-detail-title">${esc(title)}</div> 20732 </div> 20733 20734 ${hasChildren ? ` 20735 <div class="mm2-detail-layout"> 20736 ${subnav} 20737 ${subcontent} 20738 </div>` : ` 20739 <div class="mm2-links"> 20740 <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)}"> 20741 <strong>Lihat semua ${esc(title)}</strong> 20742 </a> 20743 </div> 20744 `} 20745 </div> 20746 `; 20747 20748 topView.classList.remove('is-active'); 20749 detailView.classList.add('is-active'); 20750 20751 if (hasChildren) { 20752 const subEl = detailView.querySelector('.mm2-subnav'); 20753 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20754 let activeIdx = 0; 20755 20756 const renderSubgrid = (idx) => { 20757 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20758 const title2 = c2.label.split('/').slice(-1)[0]; 20759 const l3 = Array.isArray(c2.children) ? c2.children : []; 20760 const l3Html = l3.map(c3 => ` 20761 <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)}"> 20762 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20763 <span class="mm2-thumbs" data-loading="true"> 20764 <span class="mm2-thumb-skeleton"></span> 20765 <span class="mm2-thumb-skeleton"></span> 20766 </span> 20767 </a>`).join(''); 20768 20769 contEl.innerHTML = ` 20770 <div class="mm2-links"> 20771 <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)}"> 20772 <strong>Lihat semua ${esc(title2)}</strong> 20773 </a> 20774 ${l3Html} 20775 </div> 20776 `; 20777 20778 // Fetch product thumbnails for each level-3 category (desktop only) 20779 if (isDesktop()) { 20780 setTimeout(() => { 20781 l3.forEach(c3 => { 20782 fetchCategoryThumbnails(c3.path); 20783 }); 20784 }, 100); 20785 } 20786 }; 20787 20788 renderSubgrid(activeIdx); 20789 20790 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20791 subEl.addEventListener('mousemove', (e)=>{ 20792 if (!isDesktop()) return; 20793 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20794 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20795 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20796 it.classList.add('active'); 20797 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20798 activeIdx = idx; 20799 renderSubgrid(activeIdx); 20800 }); 20801 20802 subEl.addEventListener('click', (e)=>{ 20803 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20804 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20805 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20806 it.classList.add('active'); 20807 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20808 activeIdx = idx; 20809 renderSubgrid(activeIdx); 20810 }); 20811 20812 // Delegate link clicks to add recent 20813 detailView.addEventListener('click', (e)=>{ 20814 const nav = e.target.closest('[data-mm2="nav"]'); 20815 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20816 }); 20817 } 20818 } 20819 20820 function buildTop(){ 20821 const html = data.map((n, idx) => { 20822 const title = n.label.split('/')[0]; 20823 const childCount = (n.children||[]).length;
20824 const productCount = countProducts(n); 20825 const gradient = categoryGradients[idx % categoryGradients.length]; 20826 20827 // Dynamic subtitle based on content - shorter for mobile 20828 let sub = ''; 20829 if (productCount > 0) { 20830 sub = `${productCount} produk`; 20831 if (childCount > 0) sub += ` · ${childCount} kat`; 20832 } else if (childCount > 0) { 20833 sub = `${childCount} kategori`; 20834 } else { 20835 sub = 'Lihat semua'; 20836 } 20837 20838 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20839 20840 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)}"> 20841 <span class="mm2-ico" style="${iconStyle}">${icon()}</span> 20842 <span style="flex: 1;"> 20843 <div class="mm2-title">${esc(title)}</div> 20844 <div class="mm2-sub">${esc(sub)}</div> 20845 </span> 20846 </a>`; 20847 }).join(''); 20848 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20849 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20850 } 20851 20852 // Open/close with safe-area 20853 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20854 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20855 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20856 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20857 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20858 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20859 backdrop?.addEventListener('click', close); 20860 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20861 20862 // Card click â open detail (unless cmd/ctrl to open link) 20863 panel.addEventListener('click', (e)=>{ 20864 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20865 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20866 const card = e.target.closest('[data-mm2="card"]'); 20867 if (card && !e.metaKey && !e.ctrlKey) { 20868 e.preventDefault(); 20869 const n = data.find(d => d.path === card.dataset.path); 20870 if (n) showDetail(n); 20871 } 20872 const nav = e.target.closest('[data-mm2="nav"]'); 20873 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20874 }); 20875 20876 // Swipe-down gesture for mobile (bottom sheet close) 20877 if (!isDesktop()) { 20878 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20879 const SWIPE_THRESHOLD = 80; 20880 const VELOCITY_THRESHOLD = 0.3; 20881 20882 panel.addEventListener('touchstart', (e) => { 20883 if (!document.body.classList.contains('mm2-open')) return; 20884 const touch = e.touches[0]; 20885 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20886 20887 // Prevent drag if content is scrolled 20888 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20889 20890 // Only start drag from top area or handle 20891 const rect = panel.getBoundingClientRect(); 20892 if (touch.clientY - rect.top > 60) return; 20893 20894 startY = touch.clientY; 20895 currentY = startY; 20896 startTime = Date.now(); 20897 isDragging = true; 20898 }, { passive: true }); 20899 20900 panel.addEventListener('touchmove', (e) => { 20901 if (!isDragging) return; 20902 const touch = e.touches[0]; 20903 currentY = touch.clientY; 20904 const deltaY = currentY - startY; 20905 20906 // Only allow downward swipe 20907 if (deltaY > 0) { 20908 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20909 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20910 panel.style.opacity = opacity; 20911 } 20912 }, { passive: true });
20913 20914 const endDrag = () => { 20915 if (!isDragging) return; 20916 const deltaY = currentY - startY; 20917 const deltaTime = Date.now() - startTime; 20918 const velocity = deltaY / deltaTime; 20919 20920 // Close if swiped down enough or fast swipe 20921 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20922 close(); 20923 } 20924 20925 // Reset position 20926 panel.style.transform = ''; 20927 panel.style.opacity = ''; 20928 isDragging = false; 20929 }; 20930 20931 panel.addEventListener('touchend', endDrag, { passive: true }); 20932 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20933 } 20934 20935 // Fetch data with loading state 20936 if (!isLoading) { 20937 isLoading = true; 20938 showLoading(); 20939 20940 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20941 .then(r => { 20942 if (!r.ok) throw new Error('Network response was not ok'); 20943 return r.json(); 20944 }) 20945 .then(j => { 20946 data = Array.isArray(j.data) ? j.data : []; 20947 if (!data.length) { 20948 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20949 <p>Tidak ada kategori tersedia</p> 20950 </div></div>`; 20951 return; 20952 } 20953 buildTop(); 20954 isLoading = false; 20955 }) 20956 .catch(err => { 20957 console.error('Failed to load categories:', err); 20958 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20959 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20960 </div></div>`; 20961 isLoading = false; 20962 }); 20963 } 20964})(); 20965 20966// Add skeleton animation styles 20967const style = document.createElement('style'); 20968style.textContent = ` 20969.mm2-skeleton { 20970 pointer-events: none; 20971 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20972} 20973.skeleton-box, .skeleton-line { 20974 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20975 background-size: 200% 100%; 20976 animation: shimmer 1.5s infinite; 20977 border-radius: 8px; 20978} 20979@keyframes pulse { 20980 0%, 100% { opacity: 1; } 20981 50% { opacity: 0.6; } 20982} 20983@keyframes shimmer { 20984 0% { background-position: 200% 0; } 20985 100% { background-position: -200% 0; } 20986} 20987`; 20988document.head.appendChild(style); 20989</script>
20989 20990
20990<script data-inlined="benchmark.js"> 20991/** 20992 * Benchmark Data Loader 20993 * Handles AJAX fetch and display of benchmark data 20994 */ 20995 20996(function() { 20997 'use strict'; 20998 20999 // Wait for DOM ready 21000 if (document.readyState === 'loading') { 21001 document.addEventListener('DOMContentLoaded', initBenchmark); 21002 } else { 21003 initBenchmark(); 21004 } 21005 21006 function initBenchmark() { 21007 const section = document.querySelector('.pd-benchmark-section'); 21008 if (!section) return; 21009 21010 const button = document.getElementById('btn-load-benchmark'); 21011 const content = document.getElementById('benchmark-content'); 21012 const productId = section.dataset.productId; 21013 21014 if (!button || !content || !productId) return; 21015 21016 let isLoading = false; 21017 let dataLoaded = false; 21018 21019 button.addEventListener('click', function() { 21020 if (isLoading || dataLoaded) return; 21021 21022 loadBenchmarkData(productId, button, content); 21023 }); 21024 } 21025 21026 function loadBenchmarkData(productId, button, container) { 21027 // Update button state 21028 button.disabled = true; 21029 button.classList.add('loading'); 21030 const originalText = button.querySelector('.btn-text').textContent; 21031 button.querySelector('.btn-text').textContent = 'Memuat data...'; 21032 21033 // Show loading state 21034 container.style.display = 'block'; 21035 container.innerHTML = ` 21036 <div class="benchmark-loading"> 21037 <div class="benchmark-loading-spinner"></div> 21038 <p>Mengambil data benchmark...</p> 21039 </div> 21040 `; 21041 21042 // Fetch data 21043 fetch('/api/benchmark/fetch.php?product_id=' + productId) 21044 .then(function(response) { 21045 if (!response.ok) { 21046 throw new Error('HTTP error ' + response.status); 21047 } 21048 return response.json(); 21049 }) 21050 .then(function(result) { 21051 if (result.success && result.data) { 21052 renderBenchmarkData(result.data, container); 21053 button.style.display = 'none'; // Hide button after successful load 21054 } else { 21055 throw new Error(result.error || 'Data tidak tersedia'); 21056 } 21057 }) 21058 .catch(function(error) { 21059 console.error('Benchmark fetch error:', error); 21060 container.innerHTML = ` 21061 <div class="benchmark-error"> 21062 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21063 <p style="font-size: 14px; margin-top: 8px;"> 21064 ${error.message || 'Terjadi kesalahan saat memuat data'} 21065 </p> 21066 <button type="button" onclick="location.reload()" 21067 style="margin-top: 16px; padding: 8px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21068 Coba Lagi 21069 </button> 21070 </div> 21071 `; 21072 21073 // Reset button 21074 button.disabled = false; 21075 button.classList.remove('loading'); 21076 button.querySelector('.btn-text').textContent = originalText; 21077 }); 21078 } 21079 21080 function renderBenchmarkData(data, container) { 21081 let html = '<div class="benchmark-data-wrapper">'; 21082 21083 // Chipset info if available 21084 if (data.chipset) { 21085 html += ` 21086 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21087 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21088 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21089 </div> 21090 `; 21091 } 21092 21093 // Main benchmark scores 21094 if (data.antutu || data.geekbench_multi || data.overall_score) { 21095 html += '<div class="benchmark-data-grid">'; 21096 21097 if (data.antutu) { 21098 html += ` 21099 <div class="benchmark-card"> 21100 <div class="benchmark-card-label">ANTUTU</div> 21101 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21102 ${getScoreBadge(data.antutu, 'antutu')} 21103 </div> 21104 `; 21105 } 21106 21107 if (data.geekbench_multi) { 21108 html += ` 21109 <div class="benchmark-card"> 21110 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21111 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21112 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21113 </div> 21114 `; 21115 } 21116 21117 if (data.gaming_score) { 21118 html += ` 21119 <div class="benchmark-card"> 21120 <div class="benchmark-card-label">GAMING</div> 21121 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21122 </div> 21123 `; 21124 } 21125 21126 if (data.battery_score) { 21127 html += ` 21128 <div class="benchmark-card"> 21129 <div class="benchmark-card-label">BATTERY</div> 21130 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21131 </div> 21132 `; 21133 } 21134 21135 if (data.overall_score) { 21136 html += ` 21137 <div class="benchmark-card"> 21138 <div class="benchmark-card-label">OVERALL</div> 21139 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21140 </div> 21141 `; 21142 } 21143 21144 html += '</div>'; 21145 } 21146 21147 // AI Performance Description 21148 if (data.notes) { 21149 html += ` 21150 <div style="background: #f8f9fa; border-left: 3px solid #667eea; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21151 <div style="font-size: 11px; font-weight: 600; color: #667eea; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21152 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21153 </div> 21154 `; 21155 } 21156 21157 // Attribution 21158 html += ` 21159 <div class="benchmark-attribution"> 21160 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21161 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21162 </div> 21163 `; 21164 21165 html += '</div>'; 21166 21167 container.innerHTML = html; 21168 } 21169 21170 function formatNumber(num) { 21171 if (!num) return '-'; 21172 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 21173 } 21174 21175 function escapeHtml(text) { 21176 if (!text) return ''; 21177 const div = document.createElement('div'); 21178 div.textContent = text; 21179 return div.innerHTML; 21180 } 21181 21182 function formatDate(dateStr) { 21183 try { 21184 const date = new Date(dateStr); 21185 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21186 return date.toLocaleDateString('id-ID', options); 21187 } catch (e) { 21188 return dateStr; 21189 } 21190 } 21191 21192 function getScoreBadge(score, type) { 21193 let category = ''; 21194 let label = ''; 21195 21196 if (type === 'antutu') { 21197 if (score >= 500000) { 21198 category = 'excellent'; 21199 label = 'Flagship'; 21200 } else if (score >= 350000) { 21201 category = 'good'; 21202 label = 'High-End'; 21203 } else if (score >= 200000) { 21204 category = 'average'; 21205 label = 'Mid-Range'; 21206 } else { 21207 category = 'average'; 21208 label = 'Entry-Level'; 21209 } 21210 } 21211 21212 if (label) { 21213 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21214 } 21215 21216 return ''; 21217 } 21218})(); 21219 21220</script>
21220 21221
21221<script data-inlined="product-compare.js"> 21222(function() { 21223 'use strict'; 21224 21225 // State management 21226 let compareList = []; 21227 let isAnalyzing = false; 21228 let currentAnalysis = null; 21229 21230 // DOM elements 21231 const compareBtn = document.getElementById('compareBtn'); 21232 const compareSidebar = document.getElementById('compareSidebar'); 21233 const compareOverlay = document.getElementById('compareOverlay'); 21234 const compareClose = document.getElementById('compareClose'); 21235 const compareModal = document.getElementById('aiCompareModal'); 21236 const compareModalClose = document.getElementById('aiCompareModalClose'); 21237 21238 // Initialize 21239 document.addEventListener('DOMContentLoaded', async function() { 21240 await initializeCompare(); 21241 setupEventListeners(); 21242 }); 21243 21244 /** 21245 * Initialize compare functionality 21246 */ 21247 async function initializeCompare() { 21248 // Load compare list first before setting up buttons 21249 await loadCompareList(); 21250 21251 // Setup product detail compare button if exists 21252 const productCompareBtn = document.getElementById('pd-compare-btn'); 21253 21254 if (productCompareBtn) { 21255 setupProductCompareButton(productCompareBtn); 21256 } 21257 21258 // Check for existing compare list and update UI 21259 if (compareList.length > 0) { 21260 // Only update badge and buttons, no sticky bar 21261 } 21262 } 21263 21264 /** 21265 * Setup event listeners 21266 */ 21267 function setupEventListeners() { 21268 // Navigation compare button 21269 if (compareBtn) { 21270 compareBtn.addEventListener('click', toggleCompareSidebar); 21271 } 21272 21273 // Sidebar controls 21274 if (compareClose) { 21275 compareClose.addEventListener('click', closeCompareSidebar); 21276 } 21277 21278 if (compareOverlay) { 21279 compareOverlay.addEventListener('click', closeCompareSidebar); 21280 } 21281 21282 // Sidebar actions 21283 const clearAllBtn = document.getElementById('compareClearAll'); 21284 if (clearAllBtn) { 21285 clearAllBtn.addEventListener('click', clearAllProducts); 21286 } 21287 21288 const analyzeBtn = document.getElementById('compareAnalyze'); 21289 if (analyzeBtn) { 21290 analyzeBtn.addEventListener('click', startAIAnalysis); 21291 } 21292 21293 // Modal controls 21294 if (compareModalClose) { 21295 compareModalClose.addEventListener('click', closeModal); 21296 } 21297 21298 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21299 if (modalBackdrop) { 21300 modalBackdrop.addEventListener('click', closeModal); 21301 } 21302 21303 // Modal actions 21304 const retryBtn = document.getElementById('aiRetry'); 21305 if (retryBtn) { 21306 retryBtn.addEventListener('click', startAIAnalysis); 21307 } 21308 21309 // Removed share and add to cart buttons - replaced with AI disclaimer 21310 } 21311 21312 /** 21313 * Setup product detail compare button 21314 */ 21315 function setupProductCompareButton(button) { 21316 const productId = parseInt(button.dataset.productId); 21317 if (!productId) return; 21318 21319 // Check if product is already in compare list 21320 updateProductCompareButtonState(button, productId); 21321 21322 button.addEventListener('click', async function() { 21323 // Get current variant ID (if any) 21324 const variantIdElement = document.getElementById('pd-variant-id'); 21325 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21326 21327 // Update button's variant data for current selection 21328 button.dataset.variantId = variantId.toString(); 21329 21330 await toggleProductInCompare(productId, button, variantId); 21331 }); 21332 } 21333 21334 /** 21335 * Toggle product in compare list 21336 */ 21337 async function toggleProductInCompare(productId, button, variantId = 0) { 21338 // Check by product ID (variants of same product treated as same item) 21339 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21340 21341 try { 21342 if (isInCompare) { 21343 await removeFromCompare(productId); 21344 showToast('Produk dihapus dari perbandingan', 'success'); 21345 } else { 21346 await addToCompare(productId, variantId); 21347 showToast('Produk ditambahkan ke perbandingan', 'success'); 21348 21349 // Trigger fly-to-compare animation 21350 triggerFlyToCompareAnimation(button); 21351 } 21352 21353 updateProductCompareButtonState(button, productId); 21354 updateCompareBadge(); 21355 updateAllCompareButtons(); 21356 21357 } catch (error) { 21358 showToast(error.message || 'Terjadi kesalahan', 'error'); 21359 } 21360 } 21361 21362 /** 21363 * Update product compare button state 21364 */ 21365 function updateProductCompareButtonState(button, productId) { 21366 // Ensure both IDs are compared as integers 21367 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21368 button.classList.toggle('added', isInCompare); 21369 21370 // Handle visibility of text elements 21371 const compareText = button.querySelector('.compare-text'); 21372 const compareRemove = button.querySelector('.compare-remove'); 21373 const compareNote = document.getElementById('compare-note'); 21374 21375 if (compareText) { 21376 compareText.style.display = isInCompare ? 'none' : 'inline'; 21377 } 21378 21379 if (compareRemove) { 21380 if (isInCompare) { 21381 compareRemove.removeAttribute('hidden'); 21382 compareRemove.style.display = 'flex'; 21383 } else { 21384 compareRemove.setAttribute('hidden', ''); 21385 compareRemove.style.display = 'none'; 21386 } 21387 } 21388 21389 // Update info note 21390 if (compareNote) { 21391 if (isInCompare) { 21392 const currentCount = compareList.length; 21393 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21394 } else { 21395 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 21396 } 21397 } 21398 21399 // Inline compare preview removed - keeping it simple 21400 } 21401 21402 /** 21403 * Add product to compare list 21404 */ 21405 async function addToCompare(productId, variantId = 0) { 21406 const requestData = { product_id: productId }; 21407 if (variantId > 0) { 21408 requestData.variant_id = variantId; 21409 } 21410 21411 const response = await fetch('/api/compare/add.php', { 21412 method: 'POST', 21413 headers: { 21414 'Content-Type': 'application/json', 21415 }, 21416 body: JSON.stringify(requestData), 21417 credentials: 'same-origin' 21418 });
21419 21420 const result = await response.json(); 21421 21422 if (!result.success) { 21423 throw new Error(result.error || 'Gagal menambahkan produk'); 21424 } 21425 21426 // Send Google Analytics event for add to compare 21427 if (typeof gtag === 'function') { 21428 gtag('event', 'add_to_compare', { 21429 event_category: 'Product Compare', 21430 event_label: 'Product Added to Compare', 21431 product_id: productId, 21432 variant_id: variantId || 0, 21433 custom_map: { 21434 custom_parameter_1: 'compare_add_product' 21435 } 21436 }); 21437 } 21438 21439 await loadCompareList(); 21440 return result; 21441 } 21442 21443 /** 21444 * Remove product from compare list 21445 */ 21446 async function removeFromCompare(productId) { 21447 const response = await fetch('/api/compare/remove.php', { 21448 method: 'POST', 21449 headers: { 21450 'Content-Type': 'application/json', 21451 }, 21452 body: JSON.stringify({ product_id: productId }), 21453 credentials: 'same-origin' 21454 }); 21455 21456 const result = await response.json(); 21457 21458 if (!result.success) { 21459 throw new Error(result.error || 'Gagal menghapus produk'); 21460 } 21461 21462 // Send Google Analytics event for remove from compare 21463 if (typeof gtag === 'function') { 21464 gtag('event', 'remove_from_compare', { 21465 event_category: 'Product Compare', 21466 event_label: 'Product Removed from Compare', 21467 product_id: productId, 21468 custom_map: { 21469 custom_parameter_1: 'compare_remove_product' 21470 } 21471 }); 21472 } 21473 21474 await loadCompareList(); 21475 return result; 21476 } 21477 21478 /** 21479 * Clear all products from compare list 21480 */ 21481 async function clearAllProducts() { 21482 if (compareList.length === 0) return; 21483 21484 // Show confirmation modal 21485 const confirmed = await showConfirmModal({ 21486 title: 'Hapus Semua Produk', 21487 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21488 confirmText: 'Hapus Semua', 21489 cancelText: 'Batal', 21490 type: 'danger' 21491 }); 21492 21493 if (!confirmed) return; 21494 21495 try { 21496 const response = await fetch('/api/compare/clear.php', { 21497 method: 'POST', 21498 credentials: 'same-origin' 21499 }); 21500 21501 const result = await response.json(); 21502 21503 if (!result.success) { 21504 throw new Error(result.error || 'Gagal menghapus produk'); 21505 } 21506 21507 // Send Google Analytics event for clear all products 21508 if (typeof gtag === 'function') { 21509 gtag('event', 'compare_clear_all', { 21510 event_category: 'Product Compare', 21511 event_label: 'Clear All Products', 21512 products_count: compareList.length, 21513 custom_map: { 21514 custom_parameter_1: 'compare_clear_action' 21515 } 21516 }); 21517 } 21518 21519 await loadCompareList(); 21520 showToast('Semua produk dihapus dari perbandingan', 'success'); 21521 21522 } catch (error) { 21523 showToast(error.message || 'Terjadi kesalahan', 'error'); 21524 } 21525 } 21526 21527 /** 21528 * Load compare list from server 21529 */ 21530 async function loadCompareList() { 21531 try { 21532 const response = await fetch('/api/compare/get.php', { 21533 credentials: 'same-origin' 21534 }); 21535 21536 const result = await response.json(); 21537 21538 if (result.success) { 21539 compareList = result.products || []; 21540 updateCompareSidebar(); 21541 updateCompareBadge(); 21542 updateAllCompareButtons(); 21543 } 21544 21545 } catch (error) { 21546 // Silently handle error - compare functionality will work with empty list 21547 } 21548 } 21549 21550 /** 21551 * Update compare sidebar content 21552 */ 21553 function updateCompareSidebar() { 21554 const emptyState = document.getElementById('compareEmpty'); 21555 const listContainer = document.getElementById('compareList'); 21556 const actionsContainer = document.getElementById('compareActions'); 21557 const countElement = document.getElementById('compareCountSidebar'); 21558 21559 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21560 21561 // Update count 21562 countElement.textContent = `(${compareList.length})`; 21563 21564 if (compareList.length === 0) { 21565 emptyState.style.display = 'block'; 21566 listContainer.style.display = 'none'; 21567 actionsContainer.style.display = 'none'; 21568 } else { 21569 emptyState.style.display = 'none'; 21570 listContainer.style.display = 'block'; 21571 actionsContainer.style.display = 'flex'; 21572 21573 // Render product list 21574 listContainer.innerHTML = compareList.map(product => { 21575 // Display variant name if available 21576 let displayName = product.name; 21577 if (product.combination_name) { 21578 displayName += ` (${product.combination_name})`; 21579 } 21580 21581 // Use variant price if available 21582 let displayPrice = product.price; 21583 if (product.variant_price !== undefined && product.variant_price !== null) { 21584 displayPrice = product.variant_price; 21585 } 21586 21587 return ` 21588 <div class="compare-item" data-product-id="${product.id}"> 21589 <div class="compare-item-image"> 21590 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21591 </div> 21592 <div class="compare-item-info"> 21593 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21594 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21595 </div> 21596 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21597 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21598 <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"/> 21599 </svg> 21600 </button> 21601 </div> 21602 `; 21603 }).join(''); 21604 21605 // Setup event listeners for remove buttons 21606 setupRemoveButtons(); 21607 21608 // Update analyze button state 21609 const analyzeBtn = document.getElementById('compareAnalyze'); 21610 if (analyzeBtn) { 21611 analyzeBtn.disabled = compareList.length < 2; 21612 } 21613 } 21614 } 21615 21616 /** 21617 * Setup remove buttons event listeners 21618 */ 21619 function setupRemoveButtons() { 21620 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21621 removeButtons.forEach(button => { 21622 button.addEventListener('click', async (e) => { 21623 e.preventDefault(); 21624 const productId = parseInt(button.dataset.productId); 21625 if (productId) { 21626 await removeProduct(productId); 21627 } 21628 }); 21629 }); 21630 } 21631 21632 /** 21633 * Update all compare buttons on the page 21634 */ 21635 function updateAllCompareButtons() { 21636 // Update product detail compare button 21637 const productBtn = document.getElementById('pd-compare-btn'); 21638 if (productBtn) { 21639 const productId = parseInt(productBtn.dataset.productId); 21640 if (productId) { 21641 updateProductCompareButtonState(productBtn, productId); 21642 } 21643 } 21644 21645 // Update any other compare buttons on the page 21646 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21647 allCompareButtons.forEach(button => { 21648 const productId = parseInt(button.dataset.productId); 21649 if (productId && button.id !== 'pd-compare-btn') { 21650 updateProductCompareButtonState(button, productId); 21651 } 21652 }); 21653 } 21654 21655 /** 21656 * Update compare badge 21657 */ 21658 function updateCompareBadge() { 21659 const badge = document.getElementById('compareCount'); 21660 if (!badge) return; 21661 21662 const oldCount = parseInt(badge.textContent) || 0; 21663 const newCount = compareList.length; 21664 21665 badge.textContent = newCount; 21666 badge.classList.toggle('active', newCount > 0); 21667 21668 // Animate badge if count increased 21669 if (newCount > oldCount) { 21670 animateCompareBadge(); 21671 highlightCompareButton(); 21672 } 21673 21674 // Sticky bar removed - keeping only fly-to-navbar animation 21675 } 21676 21677 /** 21678 * Animate compare badge when new product added 21679 */ 21680 function animateCompareBadge() { 21681 const badge = document.getElementById('compareCount'); 21682 if (!badge) return; 21683 21684 badge.style.transform = 'scale(1.3)'; 21685 badge.style.transition = 'transform 0.3s ease'; 21686 21687 setTimeout(() => { 21688 badge.style.transform = 'scale(1)'; 21689 }, 300); 21690 } 21691 21692 /** 21693 * Highlight compare button temporarily 21694 */ 21695 function highlightCompareButton() { 21696 const compareBtn = document.getElementById('compareBtn'); 21697 if (!compareBtn) return; 21698 21699 compareBtn.classList.add('pulse-highlight'); 21700 21701 setTimeout(() => { 21702 compareBtn.classList.remove('pulse-highlight'); 21703 }, 3000); 21704 } 21705 21706 /** 21707 * Trigger fly-to-compare animation 21708 */ 21709 function triggerFlyToCompareAnimation(sourceButton) { 21710 if (!sourceButton) { 21711 return; 21712 } 21713 21714 // Check if mobile view (bottom nav) or desktop (top nav) 21715 const isMobile = window.innerWidth <= 768; 21716 21717 // Get source position 21718 const sourceRect = sourceButton.getBoundingClientRect(); 21719 21720 // Calculate target position 21721 let targetTop, targetLeft; 21722 21723 if (isMobile) { 21724 // Mobile: fly to fixed bottom position (bottom nav area) 21725 // Use fixed coordinates regardless of bottom nav visibility 21726 targetTop = window.innerHeight - 80; // Just above bottom nav area 21727 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21728 } else { 21729 // Desktop: fly to top navbar compare button 21730 const targetElement = document.getElementById('compareBtn'); 21731 if (!targetElement) { 21732 return; 21733 } 21734 const targetRect = targetElement.getBoundingClientRect(); 21735 targetTop = targetRect.top + 10; 21736 targetLeft = targetRect.left + 10; 21737 } 21738 21739 // Get product image for animation - try multiple selectors 21740 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"]'); 21741 21742 // Create animated element 21743 const flyingElement = document.createElement('div'); 21744 flyingElement.className = 'flying-product'; 21745 21746 if (productImage) { 21747 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21748 flyingElement.querySelector('img').style.cssText = ` 21749 width: 100%; 21750 height: 100%; 21751 object-fit: cover; 21752 border-radius: 8px; 21753 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21754 `; 21755 } else { 21756 // Fallback: colored div with icon 21757 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21758 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21759 width: 100%; 21760 height: 100%; 21761 background: #3b82f6; 21762 border-radius: 8px; 21763 display: flex; 21764 align-items: center; 21765 justify-content: center; 21766 font-size: 24px; 21767 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21768 `; 21769 } 21770 21771 flyingElement.style.cssText = ` 21772 position: fixed; 21773 top: ${sourceRect.top}px; 21774 left: ${sourceRect.left}px; 21775 width: 60px; 21776 height: 60px; 21777 z-index: 9999; 21778 pointer-events: none; 21779 opacity: 1;
21780 transform: scale(1); 21781 `; 21782 21783 document.body.appendChild(flyingElement); 21784 21785 // Use Web Animations API for more reliable animation 21786 flyingElement.animate([ 21787 // Keyframe 1: Initial state 21788 { 21789 top: `${sourceRect.top}px`, 21790 left: `${sourceRect.left}px`, 21791 transform: 'scale(1)', 21792 opacity: 1 21793 }, 21794 // Keyframe 2: Final state 21795 { 21796 top: `${targetTop}px`, 21797 left: `${targetLeft}px`, 21798 transform: 'scale(0.3)', 21799 opacity: 0 21800 } 21801 ], { 21802 duration: 800, 21803 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21804 fill: 'forwards' 21805 }); 21806 21807 // Clean up after animation completes 21808 setTimeout(() => { 21809 if (flyingElement.parentNode) { 21810 flyingElement.parentNode.removeChild(flyingElement); 21811 } 21812 }, 850); 21813 } 21814 21815 // Sticky compare bar functions removed - using only fly-to-navbar animation 21816 21817 /** 21818 * Toggle compare sidebar 21819 */ 21820 function toggleCompareSidebar() { 21821 if (!compareSidebar) return; 21822 21823 const isOpen = compareSidebar.classList.contains('open'); 21824 21825 if (isOpen) { 21826 closeCompareSidebar(); 21827 } else { 21828 openCompareSidebar(); 21829 } 21830 } 21831 21832 /** 21833 * Open compare sidebar 21834 */ 21835 function openCompareSidebar() { 21836 if (!compareSidebar || !compareOverlay) return; 21837 21838 compareSidebar.classList.add('open'); 21839 compareOverlay.classList.add('active'); 21840 document.body.style.overflow = 'hidden'; 21841 21842 // Send Google Analytics event for compare sidebar open 21843 if (typeof gtag === 'function') { 21844 gtag('event', 'compare_sidebar_open', { 21845 event_category: 'Product Compare', 21846 event_label: 'Compare Sidebar Opened', 21847 products_count: compareList.length, 21848 custom_map: { 21849 custom_parameter_1: 'compare_sidebar_view' 21850 } 21851 }); 21852 } 21853 } 21854 21855 /** 21856 * Close compare sidebar 21857 */ 21858 function closeCompareSidebar() { 21859 if (!compareSidebar || !compareOverlay) return; 21860 21861 compareSidebar.classList.remove('open'); 21862 compareOverlay.classList.remove('active'); 21863 document.body.style.overflow = ''; 21864 } 21865 21866 /** 21867 * Start AI analysis 21868 */ 21869 async function startAIAnalysis() { 21870 if (compareList.length < 2) { 21871 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21872 return; 21873 } 21874 21875 if (isAnalyzing) return; 21876 21877 // Send Google Analytics event for AI Analysis start 21878 if (typeof gtag === 'function') { 21879 gtag('event', 'ai_analysis_start', { 21880 event_category: 'Product Compare', 21881 event_label: 'AI Analysis Button Click', 21882 products_count: compareList.length, 21883 product_ids: compareList.map(p => p.id).join(','), 21884 custom_map: { 21885 custom_parameter_1: 'compare_ai_analysis' 21886 } 21887 }); 21888 } 21889 21890 // Check if products are in similar categories 21891 const mismatchInfo = await checkProductCategoryMismatch(); 21892 if (mismatchInfo.shouldWarn) { 21893 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21894 const confirmed = await showConfirmModal({ 21895 title: 'Produk Berbeda Kategori', 21896 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21897 confirmText: 'Ya, Analisis Tetap', 21898 cancelText: 'Batal', 21899 type: 'warning' 21900 }); 21901 21902 if (!confirmed) return; 21903 } 21904 21905 openModal(); 21906 showLoadingState(); 21907 isAnalyzing = true; 21908 21909 try { 21910 const response = await fetch('/api/compare/ai-analysis.php', { 21911 method: 'POST', 21912 credentials: 'same-origin' 21913 }); 21914 21915 const result = await response.json(); 21916 21917 if (!result.success) { 21918 throw new Error(result.error || 'Analisis AI gagal'); 21919 } 21920 21921 currentAnalysis = result; 21922 showAnalysisResults(result); 21923 21924 // Send Google Analytics event for successful AI Analysis 21925 if (typeof gtag === 'function') { 21926 gtag('event', 'ai_analysis_success', { 21927 event_category: 'Product Compare', 21928 event_label: 'AI Analysis Completed', 21929 products_count: compareList.length, 21930 analysis_type: 'product_comparison', 21931 custom_map: { 21932 custom_parameter_1: 'compare_ai_success' 21933 } 21934 }); 21935 } 21936 21937 } catch (error) { 21938 showErrorState(error.message); 21939 21940 // Send Google Analytics event for failed AI Analysis 21941 if (typeof gtag === 'function') { 21942 gtag('event', 'ai_analysis_error', { 21943 event_category: 'Product Compare', 21944 event_label: 'AI Analysis Failed', 21945 products_count: compareList.length, 21946 error_message: error.message, 21947 custom_map: { 21948 custom_parameter_1: 'compare_ai_error' 21949 } 21950 }); 21951 } 21952 } finally { 21953 isAnalyzing = false; 21954 } 21955 } 21956 21957 /** 21958 * Show modal 21959 */ 21960 function openModal() { 21961 if (!compareModal) return; 21962 compareModal.classList.add('open'); 21963 document.body.style.overflow = 'hidden'; 21964 } 21965 21966 /** 21967 * Close modal 21968 */ 21969 function closeModal() { 21970 if (!compareModal) return; 21971 compareModal.classList.remove('open'); 21972 document.body.style.overflow = ''; 21973 } 21974 21975 /** 21976 * Show loading state 21977 */ 21978 function showLoadingState() { 21979 const loading = document.getElementById('aiLoading'); 21980 const error = document.getElementById('aiError'); 21981 const results = document.getElementById('aiResults'); 21982 21983 if (loading) loading.style.display = 'block'; 21984 if (error) error.hidden = true; 21985 if (results) results.hidden = true; 21986 } 21987 21988 /** 21989 * Show error state 21990 */ 21991 function showErrorState(message) { 21992 const loading = document.getElementById('aiLoading'); 21993 const error = document.getElementById('aiError'); 21994 const results = document.getElementById('aiResults'); 21995 const errorMessage = document.getElementById('aiErrorMessage'); 21996 21997 if (loading) loading.style.display = 'none'; 21998 if (error) error.hidden = false; 21999 if (results) results.hidden = true; 22000 if (errorMessage) errorMessage.textContent = message; 22001 } 22002 22003 /** 22004 * Show analysis results 22005 */ 22006 function showAnalysisResults(analysis) { 22007 const loading = document.getElementById('aiLoading'); 22008 const error = document.getElementById('aiError'); 22009 const results = document.getElementById('aiResults'); 22010 22011 if (loading) loading.style.display = 'none'; 22012 if (error) error.hidden = true; 22013 if (results) results.hidden = false; 22014 22015 const data = analysis.data; 22016 22017 // Update summary 22018 const summaryElement = document.getElementById('aiSummary'); 22019 if (summaryElement) summaryElement.textContent = data.summary || ''; 22020 22021 const winnerProduct = document.getElementById('winnerProduct'); 22022 const winnerReason = document.getElementById('winnerReason'); 22023 if (winnerProduct && winnerReason && data.winner_overall) { 22024 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 22025 winnerProduct.textContent = winner ? winner.name : 'Tidak diketa
22025hui'; 22026 winnerReason.textContent = data.winner_reason || ''; 22027 } 22028 22029 // Update comparison table 22030 updateComparisonTable(data.comparison_table); 22031 22032 // Update detailed analysis 22033 updateDetailedAnalysis(data.detailed_analysis); 22034 22035 // Update recommendations 22036 updateRecommendations(data.recommendations); 22037 22038 // Update buying tips 22039 updateBuyingTips(data.buying_tips); 22040 } 22041 22042 /** 22043 * Update comparison table 22044 */ 22045 function updateComparisonTable(comparisonData) { 22046 const container = document.getElementById('comparisonTable'); 22047 if (!container || !comparisonData?.categories) return; 22048 22049 // First, create product header row with names 22050 const productHeaderHtml = ` 22051 <div class="comparison-category"> 22052 <div class="category-header">Produk</div> 22053 <div class="category-products"> 22054 ${compareList.map(product => ` 22055 <div class="product-score"> 22056 <div class="product-name-header"> 22057 <strong>${escapeHtml(product.name)}</strong> 22058 </div> 22059 <div class="product-price"> 22060 ${formatPrice(product.price)} 22061 </div> 22062 </div> 22063 `).join('')} 22064 </div> 22065 </div> 22066 `; 22067 22068 // Then create comparison categories 22069 const categoriesHtml = comparisonData.categories.map(category => ` 22070 <div class="comparison-category"> 22071 <div class="category-header">${escapeHtml(category.name)}</div> 22072 <div class="category-products"> 22073 ${compareList.map(product => { 22074 const productData = category.products?.[product.id]; 22075 return ` 22076 <div class="product-score"> 22077 <div class="score-value"> 22078 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22079 <span>${productData?.score || 0}/5</span> 22080 </div> 22081 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22082 </div> 22083 `; 22084 }).join('')} 22085 </div> 22086 </div> 22087 `).join(''); 22088 22089 container.innerHTML = productHeaderHtml + categoriesHtml; 22090 } 22091 22092 /** 22093 * Update detailed analysis 22094 */ 22095 function updateDetailedAnalysis(analysisData) { 22096 const container = document.getElementById('detailedAnalysis'); 22097 if (!container || !analysisData) return; 22098 22099 container.innerHTML = compareList.map(product => { 22100 const productAnalysis = analysisData[product.id]; 22101 if (!productAnalysis) return ''; 22102 22103 // Use product name from API response if available, fallback to local name 22104 const productName = productAnalysis.product_name || product.name; 22105 22106 return ` 22107 <div class="product-analysis"> 22108 <h5>${escapeHtml(productName)}</h5> 22109 22110 ${productAnalysis.strengths ? ` 22111 <div class="analysis-section"> 22112 <h6 class="strengths">â Kelebihan</h6> 22113 <ul> 22114 ${productAnalysis.strengths.map(strength => 22115 `<li>${escapeHtml(strength)}</li>` 22116 ).join('')} 22117 </ul> 22118 </div> 22119 ` : ''} 22120 22121 ${productAnalysis.weaknesses ? ` 22122 <div class="analysis-section"> 22123 <h6 class="weaknesses">â Kekurangan</h6> 22124 <ul> 22125 ${productAnalysis.weaknesses.map(weakness => 22126 `<li>${escapeHtml(weakness)}</li>` 22127 ).join('')} 22128 </ul> 22129 </div> 22130 ` : ''} 22131 22132 ${productAnalysis.best_for ? ` 22133 <div class="analysis-section"> 22134 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22135 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22136 </div> 22137 ` : ''} 22138 </div> 22139 `; 22140 }).filter(Boolean).join(''); 22141 } 22142 22143 /** 22144 * Update recommendations 22145 */ 22146 function updateRecommendations(recommendations) { 22147 if (!recommendations) return; 22148 22149 const budgetEl = document.getElementById('budgetRecommendation'); 22150 const performanceEl = document.getElementById('performanceRecommendation'); 22151 const balancedEl = document.getElementById('balancedRecommendation'); 22152 22153 if (budgetEl && recommendations.budget_conscious) { 22154 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22155 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22156 } 22157 22158 if (performanceEl && recommendations.performance_focused) { 22159 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22160 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22161 } 22162 22163 if (balancedEl && recommendations.balanced) { 22164 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22165 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22166 } 22167 } 22168 22169 /** 22170 * Check if products are from different categories 22171 */ 22172 async function checkProductCategoryMismatch() { 22173 if (compareList.length < 2) { 22174 return { shouldWarn: false, categories: new Set() }; 22175 } 22176 22177 // Get the root categories from each product 22178 const rootCategories = new Set(); 22179 const productCategories = []; 22180 22181 for (const product of compareList) { 22182 // Extract root category from the product's category_path if available 22183 const rootCategory = await getProductRootCategory(product); 22184 if (rootCategory) { 22185 rootCategories.add(rootCategory); 22186 productCategories.push({ 22187 product: product.name, 22188 category: rootCategory 22189 }); 22190 } 22191 } 22192 22193 // If we have more than 1 different root categories, show warning 22194 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22195 22196 return { 22197 shouldWarn: shouldWarn, 22198 categories: rootCategories, 22199 productCategories: productCategories 22200 }; 22201 } 22202 22203 /** 22204 * Get product root category from category_path or fallback to name detection 22205 */ 22206 async function getProductRootCategory(product) { 22207 // First try to get from category_path if available 22208 if (product.category_path) { 22209 const rootCategory = product.category_path.split('/')[0]; 22210 return rootCategory; 22211 } 22212 22213 // If no category_path, try to fetch from product detail or use name detection 22214 const detectedCategory = detectProductCategory(product.name); 22215 return detectedCategory; 22216 } 22217 22218 /** 22219 * Detect product category from name (fallback method) 22220 */ 22221 function detectProductCategory(productName) { 22222 const name = productName.toLowerCase(); 22223 22224 // Map keywords to actual root categories from the API structure 22225 const categoryMappings = { 22226 // Komputer & Laptop 22227 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22228 22229 // Printer & Scanner 22230 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22231 22232 // Gadget 22233 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22234 22235 // Periferal
22236 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22237 22238 // Audio 22239 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22240 22241 // Jaringan 22242 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22243 22244 // Penyimpanan 22245 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22246 22247 // Komponen PC 22248 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22249 22250 // UPS & Power 22251 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22252 22253 // Proyektor 22254 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22255 22256 // Security System 22257 'Security System': ['cctv', 'security camera', 'surveillance'], 22258 22259 // Aksesoris 22260 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22261 22262 // Software 22263 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22264 22265 // Perlengkapan Kantor 22266 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22267 }; 22268 22269 // Check which root category matches 22270 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22271 for (const keyword of keywords) { 22272 if (name.includes(keyword)) { 22273 return rootCategory; 22274 } 22275 } 22276 } 22277 22278 return null; 22279 } 22280 22281 /** 22282 * Update buying tips 22283 */ 22284 function updateBuyingTips(tips) { 22285 const container = document.getElementById('buyingTips'); 22286 if (!container || !Array.isArray(tips)) return; 22287 22288 container.innerHTML = tips.map(tip => 22289 `<li>${escapeHtml(tip)}</li>` 22290 ).join(''); 22291 } 22292 22293 // Removed share and cart functions - replaced with AI disclaimer in UI 22294 22295 /** 22296 * Remove product from compare (public method) 22297 */ 22298 async function removeProduct(productId) { 22299 try { 22300 await removeFromCompare(productId); 22301 showToast('Produk dihapus dari perbandingan', 'success'); 22302 22303 // Update all compare buttons 22304 updateAllCompareButtons(); 22305 22306 } catch (error) { 22307 showToast(error.message || 'Terjadi kesalahan', 'error'); 22308 } 22309 } 22310 22311 /** 22312 * Utility functions 22313 */ 22314 function escapeHtml(text) { 22315 const div = document.createElement('div'); 22316 div.textContent = text; 22317 return div.innerHTML; 22318 } 22319 22320 function formatPrice(price) { 22321 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22322 } 22323 22324 /** 22325 * Show confirmation modal using the existing modal.css design 22326 */ 22327 function showConfirmModal(options = {}) { 22328 return new Promise((resolve) => { 22329 const { 22330 title = 'Konfirmasi', 22331 message = 'Apakah Anda yakin?', 22332 confirmText = 'Ya', 22333 cancelText = 'Tidak', 22334 type = 'warning' 22335 } = options; 22336 22337 // Create modal HTML 22338 const modalHTML = ` 22339 <div class="modal-overlay" id="confirmModal"> 22340 <div class="modal-dialog"> 22341 <div class="modal-header"> 22342 <h3 class="modal-title"> 22343 <div class="modal-icon ${type}"> 22344 ${getModalIcon(type)} 22345 </div> 22346 ${escapeHtml(title)} 22347 </h3> 22348 </div> 22349 <div class="modal-body"> 22350 <p class="modal-message">${escapeHtml(message)}</p> 22351 <div class="modal-actions"> 22352 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22353 ${escapeHtml(cancelText)} 22354 </button> 22355 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22356 ${escapeHtml(confirmText)} 22357 </button> 22358 </div> 22359 </div> 22360 </div> 22361 </div> 22362 `; 22363 22364 // Add to DOM 22365 document.body.insertAdjacentHTML('beforeend', modalHTML); 22366 const modal = document.getElementById('confirmModal'); 22367 const cancelBtn = document.getElementById('confirmCancel'); 22368 const okBtn = document.getElementById('confirmOk'); 22369 22370 // Show modal 22371 requestAnimationFrame(() => { 22372 modal.classList.add('show'); 22373 }); 22374 22375 // Handle events 22376 function cleanup() { 22377 modal.remove(); 22378 } 22379 22380 function handleCancel() { 22381 modal.classList.remove('show'); 22382 setTimeout(() => { 22383 cleanup(); 22384 resolve(false); 22385 }, 200); 22386 } 22387 22388 function handleConfirm() { 22389 modal.classList.remove('show'); 22390 setTimeout(() => { 22391 cleanup(); 22392 resolve(true); 22393 }, 200); 22394 } 22395 22396 cancelBtn.addEventListener('click', handleCancel); 22397 okBtn.addEventListener('click', handleConfirm); 22398 22399 // Close on overlay click 22400 modal.addEventListener('click', (e) => { 22401 if (e.target === modal) { 22402 handleCancel(); 22403 } 22404 }); 22405 22406 // Close on escape 22407 document.addEventListener('keydown', function escapeHandler(e) { 22408 if (e.key === 'Escape') { 22409 document.removeEventListener('keydown', escapeHandler); 22410 handleCancel(); 22411 } 22412 }); 22413 22414 // Focus on confirm button 22415 setTimeout(() => okBtn.focus(), 100); 22416 });
22417 } 22418 22419 /** 22420 * Get icon for modal type 22421 */ 22422 function getModalIcon(type) { 22423 const icons = { 22424 danger: 'ð¨ ', 22425 warning: 'â ï¸', 22426 success: 'â ', 22427 info: 'â¹ï¸' 22428 }; 22429 return icons[type] || icons.info; 22430 } 22431 22432 22433 function showToast(message, type = 'info') { 22434 // Integration with existing toast system or create simple one 22435 22436 // Simple alert fallback - replace with proper toast system 22437 if (type === 'error') { 22438 alert('Error: ' + message); 22439 } 22440 } 22441 22442 // Inline compare preview functions removed - keeping it simple and clean 22443 22444 /** 22445 * Get current product ID from page 22446 */ 22447 function getCurrentProductId() { 22448 const button = document.getElementById('pd-compare-btn'); 22449 return button ? parseInt(button.dataset.productId) : null; 22450 } 22451 22452 // hideStickyCompareBar function removed - no longer using sticky bar 22453 22454 // Public API 22455 window.productCompare = { 22456 removeProduct: removeProduct, 22457 addToCompare: addToCompare, 22458 loadCompareList: loadCompareList, 22459 getCompareList: function() { return compareList; }, 22460 openSidebar: openCompareSidebar, 22461 startAnalysis: startAIAnalysis, 22462 // Helper function to add from external sources with variant support 22463 addProductWithVariant: async function(productId, variantId = 0) { 22464 return await addToCompare(productId, variantId); 22465 } 22466 }; 22467 22468})(); 22469 22470 22471 22472</script>
vendor: 2 bytes, line 22472
22472
22473<script data-inlined="product-card-actions.js"> 22474/** 22475 * Product Card Quick Actions 22476 * Bottom sheet menu for quick add to cart & compare 22477 * Integrates with existing product-compare.js and cart.js 22478 */ 22479 22480(function() { 22481 'use strict'; 22482 22483 let activeSheet = null; 22484 let activeProductData = null; 22485 22486 // DOM elements (will be created dynamically) 22487 let overlay = null; 22488 let bottomSheet = null; 22489 22490 /** 22491 * Initialize on DOM ready 22492 */ 22493 document.addEventListener('DOMContentLoaded', function() { 22494 createBottomSheet(); 22495 attachEventListeners(); 22496 }); 22497 22498 /** 22499 * Create bottom sheet HTML structure 22500 */ 22501 function createBottomSheet() { 22502 // Create overlay 22503 overlay = document.createElement('div'); 22504 overlay.className = 'pca-overlay'; 22505 overlay.addEventListener('click', closeBottomSheet); 22506 22507 // Create bottom sheet 22508 bottomSheet = document.createElement('div'); 22509 bottomSheet.className = 'pca-bottom-sheet'; 22510 bottomSheet.innerHTML = ` 22511 <div class="pca-sheet-handle"></div> 22512 <div class="pca-sheet-product" id="pca-product-preview"> 22513 <!-- Product info will be inserted here --> 22514 </div> 22515 <div class="pca-sheet-actions"> 22516 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22517 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22518 <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"/> 22519 </svg> 22520 <div class="pca-action-content"> 22521 <div class="pca-action-label">Tambah ke Keranjang</div> 22522 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22523 </div> 22524 </button> 22525 22526 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22527 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22528 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22529 </svg> 22530 <div class="pca-action-content"> 22531 <div class="pca-action-label">Bandingkan Produk</div> 22532 <div class="pca-action-desc">Analisis dengan AI</div> 22533 </div> 22534 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22535 </button> 22536 </div> 22537 `; 22538 22539 // Append to body 22540 document.body.appendChild(overlay); 22541 document.body.appendChild(bottomSheet); 22542 22543 // Setup action buttons 22544 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22545 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22546 22547 // Prevent clicks inside bottom sheet from closing it 22548 bottomSheet.addEventListener('click', function(e) { 22549 e.stopPropagation(); 22550 }); 22551 } 22552 22553 /** 22554 * Attach event listeners to all 3-dot buttons 22555 */ 22556 function attachEventListeners() { 22557 // Use event delegation for dynamically loaded product cards 22558 document.addEventListener('click', function(e) { 22559 const btn = e.target.closest('.prod-card-actions-btn'); 22560 if (btn) { 22561 e.preventDefault(); 22562 e.stopPropagation(); 22563 handleActionsButtonClick(btn); 22564 } 22565 }); 22566 22567 // Handle ESC key 22568 document.addEventListener('keydown', function(e) { 22569 if (e.key === 'Escape' && activeSheet) { 22570 closeBottomSheet(); 22571 } 22572 }); 22573 } 22574 22575 /** 22576 * Handle 3-dot button click 22577 */ 22578 function handleActionsButtonClick(button) { 22579 // Get product ID directly from button 22580 const productId = parseInt(button.dataset.productId) || 0; 22581 if (!productId) { 22582 console.error('Product ID not found on button:', button); 22583 showToast('Data produk tidak ditemukan', 'error'); 22584 return; 22585 } 22586 22587 // Find parent card for additional data 22588 const card = button.closest('.prod-card, .pa-card'); 22589 if (!card) { 22590 console.error('Product card not found'); 22591 showToast('Data produk tidak ditemukan', 'error'); 22592 return; 22593 } 22594 22595 // Extract product data from card 22596 const productData = extractProductData(card, productId); 22597 if (!productData) { 22598 showToast('Data produk tidak ditemukan', 'error'); 22599 return; 22600 } 22601 22602 activeProductData = productData; 22603 openBottomSheet(productData); 22604 } 22605 22606 /** 22607 * Extract product data from card element 22608 */ 22609 function extractProductData(card, productId) { 22610 try { 22611 // Product ID already passed from button 22612 if (!productId) { 22613 console.error('Product ID not provided'); 22614 return null; 22615 } 22616 // Product ID already passed from button 22617 if (!productId) { 22618 console.error('Product ID not provided'); 22619 return null; 22620 } 22621 22622 const id = productId; 22623 22624 // Get product name 22625 const nameEl = card.querySelector('.prod-name, .title'); 22626 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22627 22628 // Get product image 22629 const imgEl = card.querySelector('.thumb img, img'); 22630 const image = imgEl ? imgEl.src : '/assets/img/placeholder.png'; 22631 22632 // Get product price and detect variants 22633 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22634 let price = 0; 22635 let hasVariants = false; 22636 let priceDisplay = ''; 22637 22638 if (priceEl) { 22639 // Get text content and clean it 22640 let priceText = priceEl.textContent.trim(); 22641 priceDisplay = priceText; 22642 22643 // Check if it's a range (indicates product has variants) 22644 if (priceText.includes('-')) { 22645 hasVariants = true; 22646 // Take the first price for calculation 22647 priceText = priceText.split('-')[0].trim(); 22648 } 22649 22650 // Remove all non-numeric characters 22651 priceText = priceText.replace(/[^0-9]/g, ''); 22652 price = parseInt(priceText) || 0; 22653 } 22654 22655 // Get stock status 22656 const stockDot = card.querySelector('.stock-dot'); 22657 let stockStatus = 'unknown'; 22658 let stockLabel = 'Cek stok'; 22659 22660 if (stockDot) { 22661 if (stockDot.classList.contains('ready')) { 22662 stockStatus = 'in'; 22663 stockLabel = 'Stok tersedia'; 22664 } else if (stockDot.classList.contains('low')) { 22665 stockStatus = 'low'; 22666 stockLabel = 'Stok terbatas'; 22667 } else if (stockDot.classList.contains('out')) { 22668 stockStatus = 'out'; 22669 stockLabel = 'Stok habis'; 22670 } 22671 } 22672 22673 // Get category path (if available) 22674 const categoryPath = card.dataset.category || ''; 22675 22676 // Get URL 22677 let url = '#'; 22678 if (card.tagName === 'A') { 22679 url = card.href; 22680 } else { 22681 const link = card.querySelector('a[href*="/product/"]'); 22682 if (link) url = link.href; 22683 } 22684 22685 return { 22686 id: id, 22687 name: name, 22688 image: image, 22689 price: price, 22690 priceDisplay: priceDisplay, 22691 hasVariants: hasVariants, 22692 stockStatus: stockStatus, 22693 stockLabel: stockLabel, 22694 categoryPath: categoryPath, 22695 url: url 22696 }; 22697 } catch (error) { 22698 console.error('Error extracting product data:', error); 22699 return null; 22700 } 22701 } 22702 22703 /** 22704 * Open bottom sheet 22705 */ 22706 function openBottomSheet(productData) { 22707 if (!overlay || !bottomSheet) return; 22708 22709 // Update product preview 22710 updateProductPreview(productData); 22711 22712 // Update cart button based on variant status
22713 updateCartButtonForVariant(productData); 22714 22715 // Update compare button state 22716 updateCompareButtonState(productData.id); 22717 22718 // Show overlay and sheet 22719 overlay.classList.add('active'); 22720 bottomSheet.classList.add('active'); 22721 document.body.style.overflow = 'hidden'; 22722 22723 activeSheet = true; 22724 22725 // Google Analytics 22726 if (typeof gtag === 'function') { 22727 gtag('event', 'quick_actions_open', { 22728 event_category: 'Product Card', 22729 event_label: 'Quick Actions Menu Opened', 22730 product_id: productData.id, 22731 product_name: productData.name 22732 }); 22733 } 22734 } 22735 22736 /** 22737 * Close bottom sheet 22738 */ 22739 function closeBottomSheet() { 22740 if (!overlay || !bottomSheet) return; 22741 22742 overlay.classList.remove('active'); 22743 bottomSheet.classList.remove('active'); 22744 document.body.style.overflow = ''; 22745 22746 activeSheet = false; 22747 activeProductData = null; 22748 } 22749 22750 /** 22751 * Update product preview in bottom sheet 22752 */ 22753 function updateProductPreview(productData) { 22754 const preview = document.getElementById('pca-product-preview'); 22755 if (!preview) return; 22756 22757 // Use full price display (shows range for variants) 22758 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22759 22760 preview.innerHTML = ` 22761 <img src="${escapeHtml(productData.image)}" 22762 alt="${escapeHtml(productData.name)}" 22763 class="pca-product-image" 22764 loading="lazy"> 22765 <div class="pca-product-info"> 22766 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22767 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22768 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22769 </div> 22770 `; 22771 } 22772 22773 /** 22774 * Update cart button for variant products 22775 */ 22776 function updateCartButtonForVariant(productData) { 22777 const cartBtn = document.getElementById('pca-add-cart'); 22778 if (!cartBtn) return; 22779 22780 if (productData.hasVariants) { 22781 // Product has variants - change to "View Detail" 22782 cartBtn.dataset.hasVariants = 'true'; 22783 cartBtn.dataset.productUrl = productData.url; 22784 22785 // Update icon to eye/external 22786 const iconSvg = cartBtn.querySelector('svg'); 22787 if (iconSvg) { 22788 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"/>'; 22789 } 22790 22791 // Update text 22792 const label = cartBtn.querySelector('.pca-action-label'); 22793 const desc = cartBtn.querySelector('.pca-action-desc'); 22794 if (label) label.textContent = 'Lihat Detail Produk'; 22795 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22796 22797 } else { 22798 // Single product - normal cart button 22799 cartBtn.dataset.hasVariants = 'false'; 22800 22801 // Restore cart icon 22802 const iconSvg = cartBtn.querySelector('svg'); 22803 if (iconSvg) { 22804 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"/>'; 22805 } 22806 22807 // Restore text 22808 const label = cartBtn.querySelector('.pca-action-label'); 22809 const desc = cartBtn.querySelector('.pca-action-desc'); 22810 if (label) label.textContent = 'Tambah ke Keranjang'; 22811 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22812 } 22813 } 22814 22815 /** 22816 * Update compare button state 22817 */ 22818 function updateCompareButtonState(productId) { 22819 const compareBtn = document.getElementById('pca-add-compare'); 22820 const compareBadge = document.getElementById('pca-compare-count'); 22821 22822 if (!compareBtn) return; 22823 22824 // Check if product is in compare list (using global compareList if available) 22825 let isInCompare = false;
22826 let compareCount = 0; 22827 22828 // Try to get compare list from product-compare.js via window.productCompare 22829 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22830 const compareList = window.productCompare.getCompareList(); 22831 compareCount = compareList.length; 22832 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22833 } else { 22834 // Fallback: check badge count 22835 const mainBadge = document.getElementById('compareCount'); 22836 if (mainBadge) { 22837 compareCount = parseInt(mainBadge.textContent) || 0; 22838 } 22839 } 22840 22841 // Update button state 22842 if (isInCompare) { 22843 compareBtn.classList.add('active'); 22844 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22845 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22846 } else { 22847 compareBtn.classList.remove('active'); 22848 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk'; 22849 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22850 } 22851 22852 // Update badge 22853 if (compareBadge && compareCount > 0) { 22854 compareBadge.textContent = compareCount; 22855 compareBadge.style.display = 'inline-flex'; 22856 } else if (compareBadge) { 22857 compareBadge.style.display = 'none'; 22858 } 22859 } 22860 22861 /** 22862 * Handle add to cart 22863 */ 22864 async function handleAddToCart() { 22865 if (!activeProductData) return; 22866 22867 const button = document.getElementById('pca-add-cart'); 22868 if (!button) return; 22869 22870 // If product has variants, redirect to detail page 22871 if (activeProductData.hasVariants) { 22872 window.location.href = activeProductData.url; 22873 return; 22874 } 22875 22876 // Check stock 22877 if (activeProductData.stockStatus === 'out') { 22878 showToast('Produk sedang habis', 'warning'); 22879 return; 22880 } 22881 22882 // Show loading state 22883 button.classList.add('loading'); 22884 button.disabled = true; 22885 22886 // CSRF Token Helper 22887 function getCsrfToken() { 22888 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22889 } 22890 22891 try { 22892 // Call cart API with proper format 22893 const response = await fetch('/api/cart/add.php', { 22894 method: 'POST', 22895 headers: { 22896 'Content-Type': 'application/json', 22897 'X-CSRF-Token': getCsrfToken() 22898 }, 22899 body: JSON.stringify({ 22900 csrf_token: getCsrfToken(), 22901 product_id: activeProductData.id, 22902 qty: 1, // Use 'qty' not 'quantity' 22903 variant_id: 0, // Default variant 22904 meta: { 22905 name: activeProductData.name, 22906 image_url: activeProductData.image 22907 } 22908 }), 22909 credentials: 'same-origin' 22910 }); 22911 22912 const result = await response.json(); 22913 22914 // Check both response status and result.ok 22915 if (!response.ok || !result.ok) { 22916 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22917 } 22918 22919 // Success animation 22920 button.classList.remove('loading'); 22921 button.classList.add('success'); 22922 22923 // Update cart badge 22924 updateCartBadge(); 22925 22926 // PlazaAnalytics tracking 22927 if (window.PlazaAnalytics) { 22928 const _slug = (activeProductData.url || '').replace(/.*\/product\//, ''); 22929 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22930 } 22931 22932 // Show success toast 22933 showToast('Produk ditambahkan ke keranjang', 'success'); 22934 22935 // Google Analytics 22936 if (typeof gtag === 'function') { 22937 gtag('event', 'add_to_cart', { 22938 event_category: 'Ecommerce', 22939 event_label: 'Quick Add to Cart', 22940 items: [{ 22941 item_id: activeProductData.id, 22942 item_name: activeProductData.name, 22943 price: activeProductData.price, 22944 quantity: 1 22945 }] 22946 }); 22947 } 22948 22949 // Close sheet after short delay 22950 setTimeout(() => { 22951 closeBottomSheet(); 22952 button.classList.remove('success'); 22953 button.disabled = false; 22954 }, 1000); 22955 22956 } catch (error) { 22957 console.error('Add to cart error:', error); 22958 button.classList.remove('loading'); 22959 button.disabled = false; 22960 showToast(error.message || 'Terjadi kesalahan', 'error'); 22961 } 22962 } 22963 22964 /** 22965 * Handle add to compare 22966 */ 22967 async function handleAddToCompare() { 22968 if (!activeProductData) return; 22969 22970 const button = document.getElementById('pca-add-compare'); 22971 if (!button) return; 22972 22973 // Check if already in compare 22974 const isActive = button.classList.contains('active'); 22975 22976 // Check 5-product limit before adding 22977 if (!isActive) { 22978 const mainBadge = document.getElementById('compareCount'); 22979 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22980 22981 if (currentCount >= 5) { 22982 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22983 return; 22984 } 22985 } 22986 22987 // Show loading state 22988 button.classList.add('loading'); 22989 button.disabled = true; 22990 22991 try { 22992 if (isActive) { 22993 // Remove from compare 22994 await removeFromCompare(activeProductData.id); 22995 showToast('Produk dihapus dari perbandingan', 'success'); 22996 } else { 22997 // Add to compare 22998 await addToCompare(activeProductData.id); 22999 showToast('Produk ditambahkan ke perbandingan', 'success'); 23000 23001 // Trigger fly animation 23002 triggerFlyToCompareAnimation(); 23003 } 23004 23005 // Success animation 23006 button.classList.remove('loading'); 23007 button.classList.add('success'); 23008 23009 // Update compare button state 23010 updateCompareButtonState(activeProductData.id); 23011 23012 // Close sheet after short delay 23013 setTimeout(() => { 23014 closeBottomSheet(); 23015 button.classList.remove('success'); 23016 button.disabled = false; 23017 }, 800); 23018 23019 } catch (error) { 23020 console.error('Compare error:', error); 23021 button.classList.remove('loading'); 23022 button.disabled = false; 23023 showToast(error.message || 'Terjadi kesalahan', 'error'); 23024 } 23025 } 23026 23027 /** 23028 * Add to compare (calls existing API) 23029 */ 23030 async function addToCompare(productId) { 23031 const response = await fetch('/api/compare/add.php', { 23032 method: 'POST', 23033 headers: { 23034 'Content-Type': 'application/json', 23035 }, 23036 body: JSON.stringify({ 23037 product_id: productId, 23038 variant_id: 0 23039 }), 23040 credentials: 'same-origin' 23041 });
23042 23043 const result = await response.json(); 23044 23045 if (!result.success) { 23046 throw new Error(result.error || 'Gagal menambahkan produk'); 23047 } 23048 23049 // Trigger compare list reload if function exists 23050 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23051 await window.productCompare.loadCompareList(); 23052 } 23053 23054 return result; 23055 } 23056 23057 /** 23058 * Remove from compare (calls existing API) 23059 */ 23060 async function removeFromCompare(productId) { 23061 const response = await fetch('/api/compare/remove.php', { 23062 method: 'POST', 23063 headers: { 23064 'Content-Type': 'application/json', 23065 }, 23066 body: JSON.stringify({ 23067 product_id: productId 23068 }), 23069 credentials: 'same-origin' 23070 }); 23071 23072 const result = await response.json(); 23073 23074 if (!result.success) { 23075 throw new Error(result.error || 'Gagal menghapus produk'); 23076 } 23077 23078 // Trigger compare list reload if function exists 23079 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23080 await window.productCompare.loadCompareList(); 23081 } 23082 23083 return result; 23084 } 23085 23086 /** 23087 * Update cart badge 23088 */ 23089 function updateCartBadge() { 23090 // Try to call global cart update function if exists 23091 if (typeof window.updateCartBadge === 'function') { 23092 window.updateCartBadge(); 23093 } else { 23094 // Fallback: increment badge manually 23095 const badge = document.getElementById('cartCount'); 23096 if (badge) { 23097 const currentCount = parseInt(badge.textContent) || 0; 23098 badge.textContent = currentCount + 1; 23099 badge.classList.add('active'); 23100 } 23101 } 23102 } 23103 23104 /** 23105 * Trigger fly to compare animation 23106 */ 23107 function triggerFlyToCompareAnimation() { 23108 const compareBtn = document.getElementById('compareBtn'); 23109 if (!compareBtn || !activeProductData) return; 23110 23111 // Create flying element 23112 const flyingElement = document.createElement('div'); 23113 flyingElement.className = 'flying-product'; 23114 flyingElement.style.cssText = ` 23115 position: fixed; 23116 width: 60px; 23117 height: 60px; 23118 z-index: 99999; 23119 pointer-events: none; 23120 `; 23121 23122 flyingElement.innerHTML = ` 23123 <img src="${activeProductData.image}" 23124 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23125 `; 23126 23127 // Position at bottom sheet 23128 const sheetRect = bottomSheet.getBoundingClientRect(); 23129 const targetRect = compareBtn.getBoundingClientRect(); 23130 23131 flyingElement.style.top = `${sheetRect.top + 20}px`; 23132 flyingElement.style.left = `${sheetRect.left + 20}px`; 23133 23134 document.body.appendChild(flyingElement); 23135 23136 // Animate to target 23137 requestAnimationFrame(() => { 23138 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23139 flyingElement.style.top = `${targetRect.top + 10}px`; 23140 flyingElement.style.left = `${targetRect.left + 10}px`; 23141 flyingElement.style.transform = 'scale(0.3)'; 23142 flyingElement.style.opacity = '0'; 23143 }); 23144 23145 // Clean up 23146 setTimeout(() => { 23147 if (flyingElement.parentNode) { 23148 flyingElement.parentNode.removeChild(flyingElement); 23149 } 23150 }, 800); 23151 } 23152 23153 /** 23154 * Format price as Rupiah 23155 */ 23156 function formatPrice(price) { 23157 if (!price || price === 0) return 'Rp 0'; 23158 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23159 } 23160 23161 /** 23162 * Escape HTML 23163 */ 23164 function escapeHtml(text) { 23165 const div = document.createElement('div'); 23166 div.textContent = text; 23167 return div.innerHTML; 23168 } 23169 23170 /** 23171 * Show toast notification (uses global showToast if available) 23172 */ 23173 function showToast(message, type = 'info') { 23174 if (typeof window.showToast === 'function') { 23175 window.showToast({ 23176 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23177 message: message, 23178 type: type, 23179 duration: 3000 23180 }); 23181 } else { 23182 // Fallback: console log 23183 console.log(`[${type.toUpperCase()}] ${message}`); 23184 } 23185 } 23186 23187 /** 23188 * Expose functions for external access 23189 */ 23190 window.ProductCardActions = {
23191 openSheet: openBottomSheet, 23192 closeSheet: closeBottomSheet, 23193 updateCompareState: updateCompareButtonState 23194 }; 23195 23196})(); 23197 23198</script>
23198 23199
23199<script data-inlined="product-reviews.js"> 23200/** 23201 * Product Reviews - Frontend JavaScript 23202 * Handles review display, submission, and helpful voting 23203 */ 23204 23205const ProductReviews = (function() { 23206 'use strict'; 23207 23208 let currentPage = 1; 23209 let currentSort = 'helpful'; 23210 let productId = null; 23211 let isLoading = false; 23212 23213 /** 23214 * Public API: Initialize reviews for a product 23215 */ 23216 function init(pid) { 23217 productId = pid; 23218 23219 if (!productId) { 23220 console.warn('Product ID not provided to ProductReviews.init()'); 23221 return; 23222 } 23223 23224 initializeReviewTab(); 23225 loadReviews(); 23226 } 23227 23228 /** 23229 * Initialize review tab functionality 23230 */ 23231 function initializeReviewTab() { 23232 // Tab switching 23233 const tabButtons = document.querySelectorAll('.tab-btn'); 23234 tabButtons.forEach(btn => { 23235 btn.addEventListener('click', function() { 23236 const tabName = this.dataset.tab; 23237 switchTab(tabName); 23238 }); 23239 }); 23240 23241 // Sort dropdown 23242 const sortSelect = document.getElementById('review-sort'); 23243 if (sortSelect) { 23244 sortSelect.addEventListener('change', function() { 23245 currentSort = this.value; 23246 currentPage = 1; 23247 loadReviews(); 23248 }); 23249 } 23250 23251 // Load more button
23252 const loadMoreBtn = document.getElementById('load-more-reviews'); 23253 if (loadMoreBtn) { 23254 loadMoreBtn.addEventListener('click', function() { 23255 currentPage++; 23256 loadReviews(true); // Append mode 23257 }); 23258 } 23259 23260 // Write review button 23261 const writeReviewBtn = document.getElementById('write-review-btn'); 23262 if (writeReviewBtn) { 23263 writeReviewBtn.addEventListener('click', showReviewModal); 23264 } 23265 } 23266 23267 /** 23268 * Switch between product tabs 23269 */ 23270 function switchTab(tabName) { 23271 // Update button states 23272 document.querySelectorAll('.tab-btn').forEach(btn => { 23273 btn.classList.toggle('active', btn.dataset.tab === tabName); 23274 }); 23275 23276 // Update content visibility 23277 document.querySelectorAll('.tab-content').forEach(content => { 23278 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23279 }); 23280 } 23281 23282 /** 23283 * Load reviews from API 23284 */ 23285 async function loadReviews(append = false) { 23286 if (isLoading) return; 23287 isLoading = true; 23288 23289 const container = document.getElementById('reviews-list');
23290 const loadMoreBtn = document.getElementById('load-more-reviews'); 23291 23292 // Check if review section exists (product might not have reviews section) 23293 if (!container) { 23294 console.log('Review section not found - product may not have reviews yet'); 23295 isLoading = false; 23296 return; 23297 } 23298 23299 try { 23300 if (!append) { 23301 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23302 } else { 23303 loadMoreBtn.disabled = true; 23304 loadMoreBtn.textContent = 'Memuat...'; 23305 } 23306 23307 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23308 const data = await response.json(); 23309 23310 if (!data.success) { 23311 throw new Error(data.message || 'Gagal memuat ulasan'); 23312 } 23313 23314 // Update statistics (only on initial load) 23315 if (!append && data.stats) { 23316 updateReviewStats(data.stats); 23317 } 23318 23319 // Render reviews 23320 if (!append) { 23321 container.innerHTML = ''; 23322 } 23323 23324 const reviews = data.reviews || []; 23325 23326 if (reviews.length === 0 && !append) { 23327 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23328 } else { 23329 reviews.forEach(review => { 23330 container.appendChild(createReviewElement(review)); 23331 }); 23332 } 23333 23334 // Update load more button 23335 if (loadMoreBtn) { 23336 if (data.pagination.has_more) { 23337 loadMoreBtn.style.display = 'block'; 23338 loadMoreBtn.disabled = false; 23339 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23340 } else { 23341 loadMoreBtn.style.display = 'none'; 23342 } 23343 } 23344 23345 } catch (error) { 23346 console.error('Load reviews error:', error); 23347 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23348 } finally { 23349 isLoading = false; 23350 } 23351 } 23352 23353 /** 23354 * Update review statistics display 23355 */ 23356 function updateReviewStats(stats) { 23357 const avgRatingEl = document.getElementById('avg-rating'); 23358 const totalReviewsEl = document.getElementById('total-reviews'); 23359 const ratingBarsContainer = document.getElementById('rating-bars'); 23360 23361 if (avgRatingEl) { 23362 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23363 } 23364 23365 if (totalReviewsEl) { 23366 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23367 } 23368 23369 // Update rating breakdown bars 23370 if (ratingBarsContainer) { 23371 ratingBarsContainer.innerHTML = ''; 23372 stats.rating_breakdown.forEach(item => { 23373 const bar = document.createElement('div'); 23374 bar.className = 'rating-bar'; 23375 bar.innerHTML = ` 23376 <span class="rating-label">${item.rating}â </span> 23377 <div class="bar-container"> 23378 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23379 </div> 23380 <span class="rating-count">${item.count}</span> 23381 `; 23382 ratingBarsContainer.appendChild(bar); 23383 }); 23384 } 23385 23386 // Update tab count 23387 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23388 if (reviewTab && stats.total_reviews > 0) { 23389 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23390 } 23391 } 23392 23393 /** 23394 * Create review element 23395 */ 23396 function createReviewElement(review) { 23397 const div = document.createElement('div'); 23398 div.className = 'review-item'; 23399 div.dataset.reviewId = review.ID; 23400 23401 // Build stars 23402 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23403 23404 // Build images if available 23405 let imagesHtml = ''; 23406 if (review.images && review.images.length > 0) { 23407 imagesHtml = '<div class="review-images">'; 23408 review.images.forEach(img => { 23409 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23410 }); 23411 imagesHtml += '</div>'; 23412 } 23413 23414 // Verified purchase badge 23415 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23416 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23417 : ''; 23418 23419 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23420 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23421 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23422 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23422an untuk privasi">ð</span>' 23423 : ''; 23424 23425 div.innerHTML = ` 23426 <div class="review-header"> 23427 <div class="review-author"> 23428 <strong>${escapeHtml(displayName)}</strong> 23429 ${anonymousIndicator} 23430 ${verifiedBadge} 23431 </div> 23432 <div class="review-rating"> 23433 <span class="stars">${stars}</span> 23434 </div> 23435 </div> 23436 <div class="review-date">${review.created_ago}</div> 23437 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div> 23438 ${imagesHtml} 23439 <div class="review-actions"> 23440 <button class="btn-helpful" data-review-id="${review.ID}"> 23441 <span class="helpful-icon">ð</span> 23442 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23443 </button> 23444 </div> 23445 `; 23446 23447 // Add helpful button listener 23448 const helpfulBtn = div.querySelector('.btn-helpful'); 23449 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23450 23451 return div; 23452 } 23453 23454 /** 23455 * Get CSRF token from meta tag 23456 */ 23457 function getCsrfToken() { 23458 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23459 return metaTag ? metaTag.getAttribute('content') : ''; 23460 } 23461 23462 /** 23463 * Mark review as helpful 23464 */ 23465 async function markAsHelpful(reviewId) { 23466 // Check if logged in 23467 const isLoggedIn = document.body.classList.contains('logged-in') || 23468 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23469 23470 if (!isLoggedIn) { 23471 showModal({ 23472 title: 'Login Diperlukan', 23473 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23474 icon: 'info', 23475 buttons: [ 23476 { text: 'Batal', className: 'modal-btn-secondary' }, 23477 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23478 ] 23479 }); 23480 return; 23481 } 23482 23483 // Get current button element 23484 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23485 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23486 23487 if (!helpfulBtn) return; 23488 23489 // Disable button during request 23490 helpfulBtn.disabled = true; 23491 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23492 23493 try { 23494 const response = await fetch('/api/reviews/helpful.php', { 23495 method: 'POST', 23496 headers: { 23497 'Content-Type': 'application/json', 23498 'X-CSRF-Token': getCsrfToken() 23499 }, 23500 credentials: 'same-origin', 23501 body: JSON.stringify({ 23502 csrf_token: getCsrfToken(), 23503 review_id: reviewId 23504 }) 23505 }); 23506 23507 const data = await response.json(); 23508 23509 if (data.success) { 23510 // Update button count immediately based on action 23511 const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0); 23512 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23513 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23514 23515 // Show success message 23516 showToast(data.message, 'success'); 23517 23518 // Re-enable button 23519 helpfulBtn.disabled = false; 23520 } else { 23521 showToast(data.message, 'error'); 23522 helpfulBtn.disabled = false; 23523 } 23524 23525 } catch (error) { 23526 console.error('Mark helpful error:', error); 23527 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23528 helpfulBtn.disabled = false; 23529 } 23530 } 23531 23532 /** 23533 * Show review submission modal (placeholder) 23534 */ 23535 function showReviewModal() { 23536 // TODO: Implement review modal 23537 // For now, redirect to account page 23538 window.location.href = '/account/orders'; 23539 } 23540 23541 /** 23542 * Show toast notification 23543 */ 23544 function showToast(message, type = 'info') { 23545 const toast = document.createElement('div'); 23546 toast.className = `review-toast toast-${type}`; 23547 toast.textContent = message; 23548 23549 document.body.appendChild(toast); 23550 23551 setTimeout(() => { 23552 toast.classList.add('show'); 23553 }, 100); 23554 23555 setTimeout(() => { 23556 toast.classList.remove('show'); 23557 setTimeout(() => toast.remove(), 300); 23558 }, 3000); 23559 } 23560 23561 /** 23562 * Show modal dialog 23563 * @param {Object} options - Modal options 23564 * @param {string} options.title - Modal title 23565 * @param {string} options.message - Modal message 23566 * @param {string} options.icon - Icon type (info, success, warning, danger) 23567 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23568 */ 23569 function showModal(options) { 23570 const {
23571 title = 'Konfirmasi', 23572 message = '', 23573 icon = 'info', 23574 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23575 } = options; 23576 23577 // Icon emoji mapping 23578 const iconEmojis = { 23579 info: 'â¹ï¸', 23580 success: 'â', 23581 warning: 'â ï¸', 23582 danger: 'â' 23583 }; 23584 23585 // Create modal HTML 23586 const modalHTML = ` 23587 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23588 <div class="modal-dialog"> 23589 <div class="modal-header"> 23590 <h3 class="modal-title"> 23591 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23592 ${escapeHtml(title)} 23593 </h3> 23594 </div> 23595 <div class="modal-body"> 23596 <p class="modal-message">${escapeHtml(message)}</p> 23597 <div class="modal-actions"> 23598 ${buttons.map((btn, index) => 23599 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23600 ).join('')} 23601 </div> 23602 </div> 23603 </div> 23604 </div> 23605 `; 23606 23607 // Append to body 23608 const tempDiv = document.createElement('div'); 23609 tempDiv.innerHTML = modalHTML; 23610 const modal = tempDiv.firstElementChild; 23611 document.body.appendChild(modal); 23612 23613 // Add event listeners to buttons 23614 buttons.forEach((btn, index) => { 23615 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23616 btnElement.addEventListener('click', () => { 23617 closeModal(modal); 23618 if (btn.callback && typeof btn.callback === 'function') { 23619 btn.callback(); 23620 } 23621 }); 23622 }); 23623 23624 // Close on overlay click 23625 modal.addEventListener('click', (e) => { 23626 if (e.target === modal) { 23627 closeModal(modal); 23628 } 23629 }); 23630 23631 // Show modal with animation 23632 setTimeout(() => modal.classList.add('show'), 10); 23633 } 23634 23635 /** 23636 * Close modal dialog 23637 */ 23638 function closeModal(modal) { 23639 modal.classList.remove('show'); 23640 setTimeout(() => modal.remove(), 200); 23641 } 23642 23643 /** 23644 * Escape HTML for XSS protection 23645 */ 23646 function escapeHtml(text) { 23647 const div = document.createElement('div'); 23648 div.textContent = text; 23649 return div.innerHTML; 23650 } 23651 23652 /** 23653 * Get product ID from URL 23654 */ 23655 function getProductIdFromUrl() { 23656 const match = window.location.pathname.match(/\/product\/(\d+)/); 23657 return match ? match[1] : null; 23658 } 23659 23660 // Public API 23661 return { 23662 init: init 23663 }; 23664 23665})(); 23666 23667</script>
23667 23668
23668<script data-inlined="review-submission.js"> 23669/** 23670 * Review Submission Modal & Handler 23671 * Handles review form display, validation, and submission 23672 */ 23673 23674(function() { 23675 'use strict'; 23676 23677 let currentProductId = null; 23678 let currentOrderId = null; 23679 let currentSoRefId = null; 23680 let selectedRating = 0; 23681 let uploadedImages = []; 23682 23683 // Get CSRF token from meta tag 23684 function getCsrfToken() { 23685 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23686 return metaTag ? metaTag.getAttribute('content') : ''; 23687 } 23688 23689 // Initialize 23690 function init() { 23691 createReviewModal(); 23692 attachEventListeners(); 23693 loadReviewableOrders(); 23694 } 23695 23696 // Create review modal HTML 23697 function createReviewModal() { 23698 const modalHTML = ` 23699 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23700 <div class="modal-content review-modal"> 23701 <div class="modal-header"> 23702 <h3 class="modal-title">Tulis Ulasan</h3> 23703 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23704 <svg width="20" height="20" viewBox="0 0 24 24"> 23705 <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"/> 23706 </svg> 23707 </button> 23708 </div> 23709 23710 <div class="modal-body"> 23711 <form id="review-form" class="review-form"> 23712 <!-- Product Info --> 23713 <div class="review-product-info" id="review-product-info"> 23714 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23715 <div class="review-product-details"> 23716 <h4 id="review-product-name"></h4> 23717 <p id="review-product-sku"></p> 23718 </div> 23719 </div> 23720 23721 <!-- Rating --> 23722 <div class="form-group"> 23723 <label class="form-label">Rating <span style="color:#C77DFF;">*</span></label> 23724 <div class="star-rating-input" id="star-rating-input"> 23725 <button type="button" class="star-btn" data-rating="1">â </button> 23726 <button type="button" class="star-btn" data-rating="2">â </button> 23727 <button type="button" class="star-btn" data-rating="3">â </button> 23728 <button type="button" class="star-btn" data-rating="4">â </button> 23729 <button type="button" class="star-btn" data-rating="5">â </button> 23730 </div> 23731 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23732 </div> 23733 23734 <!-- Review Text --> 23735 <div class="form-group"> 23736 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#C77DFF;">*</span></label> 23737 <textarea 23738 id="review-text" 23739 name="review_text" 23740 class="form-control" 23741 rows="6" 23742 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23743 minlength="20" 23744 maxlength="5000" 23745 required></textarea> 23746 <p class="form-hint"> 23747 <span id="review-char-count">0</span>/5000 karakter
23748 <span style="color:#059669;font-weight:500;"> 23749 ð° +1,000 point untuk ulasan ⥠100 karakter 23750 </span> 23751 </p> 23752 </div> 23753 23754 <!-- Image Upload --> 23755 <div class="form-group"> 23756 <label class="form-label">Upload Foto (Opsional)</label> 23757 <div class="image-upload-container"> 23758 <input type="file" 23759 id="review-images" 23760 name="images[]" 23761 accept="image/jpeg,image/jpg,image/png,image/webp" 23762 multiple 23763 style="display:none;"> 23764 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23765 <svg width="24" height="24" viewBox="0 0 24 24"> 23766 <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"/> 23767 </svg> 23768 <span>Pilih Foto</span> 23769 </button> 23770 <p class="form-hint"> 23771 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23772 <span style="color:#059669;font-weight:500;"> 23773 ð° +2,000 point untuk ulasan dengan foto 23774 </span> 23775 </p> 23776 </div> 23777 <div class="image-preview-grid" id="image-preview-grid"></div> 23778 </div> 23779 23780 <!-- Point Reward Info --> 23781 <div class="point-reward-info"> 23782 <div class="pri-header"> 23783 <span>ð</span> 23784 <span>Dapatkan Point Reward</span> 23785 </div> 23786 <ul class="pri-list"> 23787 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23788 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23789 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23790 </ul> 23791 <div class="pri-total"> 23792 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23793 </div> 23794 </div> 23795 23796 <!-- Privacy Option --> 23797 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23798 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23799 <input type="checkbox" 23800 id="review-anonymous" 23801 name="is_anonymous" 23802 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23803 <span style="font-size:14px;"> 23804 <strong>ð Sembunyikan nama saya</strong> 23805 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23806 Nama Anda akan ditampilkan de
23806ngan disamarkan untuk menjaga privasi 23807 </span> 23808 </span> 23809 </label> 23810 </div> 23811 23812 <!-- Error Alert --> 23813 <div class="form-error" id="review-form-error" style="display:none;"></div> 23814 23815 <!-- Submit Buttons --> 23816 <div class="modal-footer"> 23817 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23818 <button type="submit" class="btn btn-primary" id="review-submit"> 23819 Kirim Ulasan 23820 </button> 23821 </div> 23822 </form> 23823 </div> 23824 </div> 23825 </div> 23826 `; 23827 23828 document.body.insertAdjacentHTML('beforeend', modalHTML); 23829 } 23830 23831 // Attach event listeners 23832 function attachEventListeners() { 23833 // Modal triggers - event delegation 23834 document.addEventListener('click', function(e) { 23835 const btn = e.target.closest('.btn-write-review'); 23836 if (btn) { 23837 e.preventDefault(); 23838 openReviewModal( 23839 btn.dataset.productId, 23840 btn.dataset.orderId, 23841 btn.dataset.productName, 23842 btn.dataset.productImage, 23843 btn.dataset.productSku, 23844 btn.dataset.soRefId || null 23845 ); 23846 } 23847 }); 23848 23849 // Close modal 23850 setTimeout(() => { 23851 const closeBtn = document.getElementById('review-modal-close'); 23852 const cancelBtn = document.getElementById('review-cancel'); 23853 const overlay = document.getElementById('review-modal-overlay'); 23854 23855 if (closeBtn) { 23856 closeBtn.addEventListener('click', closeReviewModal); 23857 } 23858 if (cancelBtn) { 23859 cancelBtn.addEventListener('click', closeReviewModal); 23860 } 23861 if (overlay) { 23862 overlay.addEventListener('click', function(e) { 23863 if (e.target === overlay) closeReviewModal(); 23864 }); 23865 } 23866 }, 100); 23867 23868 // Star rating 23869 setTimeout(() => { 23870 const starButtons = document.querySelectorAll('.star-btn'); 23871 starButtons.forEach(btn => { 23872 btn.addEventListener('click', function(e) { 23873 e.preventDefault(); 23874 const rating = parseInt(this.dataset.rating); 23875 setRating(rating); 23876 }); 23877 }); 23878 }, 100); 23879 23880 // Character count 23881 setTimeout(() => { 23882 const textarea = document.getElementById('review-text'); 23883 if (textarea) { 23884 textarea.addEventListener('input', function() { 23885 updateCharCount(); 23886 calculatePotentialPoints(); 23887 }); 23888 } 23889 }, 100); 23890 23891 // Image upload 23892 setTimeout(() => { 23893 const uploadTrigger = document.getElementById('upload-trigger'); 23894 const fileInput = document.getElementById('review-images'); 23895 23896 if (uploadTrigger && fileInput) { 23897 uploadTrigger.addEventListener('click', () => fileInput.click()); 23898 fileInput.addEventListener('change', handleImageSelection); 23899 } 23900 }, 100); 23901 23902 // Form submit 23903 setTimeout(() => { 23904 const form = document.getElementById('review-form'); 23905 if (form) { 23906 form.addEventListener('submit', handleSubmit); 23907 } 23908 }, 100); 23909 } 23910 23911 // Open review modal 23912 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23913 const overlay = document.getElementById('review-modal-overlay'); 23914 if (!overlay) { 23915 console.error('Modal overlay not found!'); 23916 return; 23917 } 23918 23919 currentProductId = productId; 23920 currentOrderId = orderId; 23921 currentSoRefId = soRefId; 23922 selectedRating = 0; 23923 uploadedImages = []; 23924 23925 // Set product info 23926 const nameEl = document.getElementById('review-product-name'); 23927 const imageEl = document.getElementById('review-product-image'); 23928 const skuEl = document.getElementById('review-product-sku'); 23929 23930 if (nameEl) nameEl.textContent = productName; 23931 if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; 23932 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23933 23934 // Update privacy preview with actual user name 23935 updatePrivacyPreview(); 23936 23937 // Reset form 23938 const form = document.getElementById('review-form'); 23939 const previewGrid = document.getElementById('image-preview-grid'); 23940 const errorEl = document.getElementById('review-form-error'); 23941 23942 if (form) form.reset(); 23943 if (previewGrid) previewGrid.innerHTML = ''; 23944 if (errorEl) errorEl.style.display = 'none'; 23945 23946 setRating(0); 23947 updateCharCount(); 23948 calculatePotentialPoints();
23949 23950 // Show modal 23951 overlay.style.display = 'flex'; 23952 overlay.classList.add('show'); 23953 document.body.style.overflow = 'hidden'; 23954 } 23955 23956 // Close review modal 23957 function closeReviewModal() { 23958 const overlay = document.getElementById('review-modal-overlay'); 23959 if (overlay) { 23960 overlay.classList.remove('show'); 23961 setTimeout(() => { 23962 overlay.style.display = 'none'; 23963 }, 200); // Wait for fade animation 23964 } 23965 document.body.style.overflow = ''; 23966 } 23967 23968 // Set star rating 23969 function setRating(rating) { 23970 selectedRating = rating; 23971 const stars = document.querySelectorAll('.star-btn'); 23972 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23973 23974 stars.forEach((star, index) => { 23975 if (index < rating) { 23976 star.classList.add('active'); 23977 } else { 23978 star.classList.remove('active'); 23979 } 23980 }); 23981 23982 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23983 } 23984 23985 // Update character count 23986 function updateCharCount() { 23987 const textarea = document.getElementById('review-text'); 23988 const count = textarea.value.length; 23989 document.getElementById('review-char-count').textContent = count; 23990 } 23991 23992 // Calculate potential points 23993 function calculatePotentialPoints() { 23994 const textarea = document.getElementById('review-text'); 23995 if (!textarea) return; 23996 23997 const charCount = textarea.value.trim().length; 23998 const hasImages = uploadedImages.length > 0; 23999 24000 let totalPoints = 5000; 24001 let longTextActive = charCount >= 100; 24002 let imagesActive = hasImages; 24003 24004 if (longTextActive) totalPoints += 1000; 24005 if (imagesActive) totalPoints += 2000; 24006 24007 // Update UI 24008 const longTextEl = document.getElementById('pri-long-text'); 24009 const imagesEl = document.getElementById('pri-images'); 24010 const totalEl = document.getElementById('pri-total-points'); 24011 24012 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 24013 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 24014 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 24015 } 24016 24017 // Handle image selection 24018 function handleImageSelection(e) { 24019 const files = Array.from(e.target.files); 24020 24021 if (files.length > 5) { 24022 showError('Maksimal 5 foto yang dapat diupload'); 24023 e.target.value = ''; // Reset input 24024 return; 24025 } 24026 24027 uploadedImages = []; 24028 const previewGrid = document.getElementById('image-preview-grid'); 24029 previewGrid.innerHTML = ''; 24030 24031 files.forEach((file, index) => { 24032 // Validate file type 24033 if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { 24034 showError('Format file harus JPG, PNG, atau WebP'); 24035 return; 24036 } 24037 24038 // Validate file size (2MB) 24039 if (file.size > 2 * 1024 * 1024) { 24040 showError('Ukuran file maksimal 2MB'); 24041 return; 24042 } 24043 24044 uploadedImages.push(file); 24045 24046 // Create preview 24047 const reader = new FileReader(); 24048 reader.onload = function(e) { 24049 const previewHTML = ` 24050 <div class="image-preview-item"> 24051 <img src="${e.target.result}" alt="Preview ${index + 1}"> 24052 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 24053 <svg width="16" height="16" viewBox="0 0 24 24"> 24054 <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"/> 24055 </svg> 24056 </button> 24057 </div> 24058 `; 24059 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24060 };
24061 reader.readAsDataURL(file); 24062 }); 24063 24064 calculatePotentialPoints(); 24065 24066 // Attach remove handlers 24067 setTimeout(() => { 24068 document.querySelectorAll('.remove-image').forEach(btn => { 24069 btn.addEventListener('click', function() { 24070 const index = parseInt(this.dataset.index); 24071 removeImage(index); 24072 }); 24073 }); 24074 }, 100); 24075 } 24076 24077 // Remove image 24078 function removeImage(index) { 24079 uploadedImages.splice(index, 1); 24080 24081 const previewGrid = document.getElementById('image-preview-grid'); 24082 previewGrid.innerHTML = ''; 24083 24084 uploadedImages.forEach((file, idx) => { 24085 const reader = new FileReader(); 24086 reader.onload = function(e) { 24087 const previewHTML = ` 24088 <div class="image-preview-item"> 24089 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24090 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus"> 24091 <svg width="16" height="16" viewBox="0 0 24 24"> 24092 <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"/> 24093 </svg> 24094 </button> 24095 </div> 24096 `; 24097 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24098 }; 24099 reader.readAsDataURL(file); 24100 }); 24101 24102 calculatePotentialPoints(); 24103 24104 setTimeout(() => { 24105 document.querySelectorAll('.remove-image').forEach(btn => { 24106 btn.addEventListener('click', function() { 24107 const idx = parseInt(this.dataset.index); 24108 removeImage(idx); 24109 }); 24110 }); 24111 }, 100); 24112 } 24113 24114 // Handle form submission 24115 async function handleSubmit(e) { 24116 e.preventDefault(); 24117 24118 // Validate 24119 if (selectedRating === 0) { 24120 showError('Silakan pilih rating terlebih dahulu'); 24121 return; 24122 } 24123 24124 const reviewText = document.getElementById('review-text').value.trim(); 24125 if (reviewText.length < 20) { 24126 showError('Ulasan minimal 20 karakter'); 24127 return; 24128 } 24129 24130 const submitBtn = document.getElementById('review-submit'); 24131 submitBtn.disabled = true; 24132 24133 try { 24134 // Step 1: Upload images if any 24135 let imageUrls = []; 24136 if (uploadedImages.length > 0) { 24137 submitBtn.textContent = 'Mengupload foto...'; 24138 imageUrls = await uploadImages(); 24139 if (!imageUrls) { 24140 // Upload failed 24141 submitBtn.disabled = false; 24142 submitBtn.textContent = 'Kirim Ulasan'; 24143 return; 24144 } 24145 } 24146 24147 // Step 2: Submit review with image URLs 24148 submitBtn.textContent = 'Mengirim ulasan...'; 24149 24150 const payload = { 24151 csrf_token: getCsrfToken(), 24152 product_id: currentProductId, 24153 order_id: currentOrderId || 0, 24154 so_ref_id: currentSoRefId || undefined, 24155 rating: selectedRating, 24156 review_text: reviewText, 24157 is_anonymous: document.getElementById('review-anonymous').checked, 24158 images: imageUrls 24159 }; 24160 24161 const response = await fetch('/api/reviews/submit.php', { 24162 method: 'POST', 24163 headers: { 24164 'Content-Type': 'application/json', 24165 'X-CSRF-Token': getCsrfToken() 24166 }, 24167 body: JSON.stringify(payload) 24168 }); 24169 24170 const result = await response.json(); 24171 24172 if (result.success) { 24173 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24174 closeReviewModal(); 24175 24176 // Refresh reviewable orders if on account page 24177 if (typeof loadReviewableOrders === 'function') { 24178 setTimeout(loadReviewableOrders, 500); 24179 } 24180 } else { 24181 showError(result.message || 'Gagal mengirim ulasan'); 24182 } 24183 } catch (error) { 24184 console.error('Submit error:', error); 24185 showError('Terjadi kesalahan saat mengirim ulasan'); 24186 } finally { 24187 submitBtn.disabled = false; 24188 submitBtn.textContent = 'Kirim Ulasan'; 24189 } 24190 } 24191 24192 /** 24193 * Upload images to server 24194 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24195 */ 24196 async function uploadImages() { 24197 if (uploadedImages.length === 0) { 24198 return []; 24199 } 24200 24201 const formData = new FormData();
24202 uploadedImages.forEach(file => { 24203 formData.append('images[]', file); 24204 }); 24205 24206 try { 24207 const response = await fetch('/api/reviews/upload_images.php', { 24208 method: 'POST', 24209 headers: { 24210 'X-CSRF-Token': getCsrfToken() 24211 }, 24212 body: formData 24213 }); 24214 24215 // Check if response is JSON 24216 const contentType = response.headers.get('content-type'); 24217 if (!contentType || !contentType.includes('application/json')) { 24218 const text = await response.text(); 24219 console.error('Non-JSON response:', text); 24220 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24221 return null; 24222 } 24223 24224 const result = await response.json(); 24225 24226 if (result.success && result.images) { 24227 // Extract URLs from uploaded images 24228 return result.images.map(img => img.url); 24229 } else { 24230 showError(result.message || 'Gagal mengupload foto'); 24231 return null; 24232 } 24233 } catch (error) { 24234 console.error('Upload images error:', error); 24235 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24236 return null; 24237 } 24238 } 24239 24240 // Load reviewable orders (for account page) 24241 async function loadReviewableOrders() { 24242 // Only run if on account/profile page 24243 const container = document.getElementById('reviewable-orders-list'); 24244 const loading = document.getElementById('reviewable-orders-loading'); 24245 const empty = document.getElementById('reviewable-orders-empty'); 24246 24247 if (!container) { 24248 return; 24249 } 24250 24251 // Show loading 24252 if (loading) loading.style.display = 'block'; 24253 if (empty) empty.style.display = 'none'; 24254 container.innerHTML = ''; 24255 24256 try { 24257 const response = await fetch('/api/reviews/reviewable.php'); 24258 const result = await response.json(); 24259 24260 if (loading) loading.style.display = 'none'; 24261 24262 if (result.success && result.data.length > 0) { 24263 renderReviewableOrders(result.data); 24264 if (empty) empty.style.display = 'none'; 24265 } else { 24266 if (empty) empty.style.display = 'block'; 24267 } 24268 } catch (error) { 24269 console.error('Load reviewable orders error:', error); 24270 if (loading) loading.style.display = 'none'; 24271 if (empty) { 24272 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24273 empty.style.display = 'block'; 24274 } 24275 } 24276 } 24277 24278 // Render reviewable orders 24279 function renderReviewableOrders(orders) { 24280 const container = document.getElementById('reviewable-orders-list'); 24281 if (!container) return; 24282 24283 let html = ''; 24284 24285 orders.forEach(order => { 24286 html += ` 24287 <div class="reviewable-order-item"> 24288 <img src="${escapeHtml(order.product_image || '/assets/img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24289 <div class="roi-info"> 24290 <h4>${escapeHtml(order.product_name)}</h4> 24291 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24292 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24293 </div> 24294 <button type="button" 24295 class="btn btn-primary btn-write-review" 24296 data-product-id="${order.product_id}" 24297 data-order-id="${order.order_id || ''}" 24298 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24299 data-product-name="${escapeHtml(order.product_name)}" 24300 data-product-image="${escapeHtml(order.product_image || '')}" 24301 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24302 âï¸ Tulis Ulasan 24303 </button> 24304 </div> 24305 `; 24306 }); 24307 24308 container.innerHTML = html; 24309 } 24310 24311 // Escape HTML for XSS protection 24312 function escapeHtml(text) { 24313 const div = document.createElement('div'); 24314 div.textContent = text; 24315 return div.innerHTML; 24316 } 24317 24318 // Show error 24319 function showError(message) { 24320 const errorEl = document.getElementById('review-form-error'); 24321 errorEl.textContent = message; 24322 errorEl.style.display = 'block'; 24323 24324 setTimeout(() => { 24325 errorEl.style.display = 'none'; 24326 }, 5000); 24327 } 24328 24329 // Show success 24330 function showSuccess(message) { 24331 // Create toast notification 24332 const toast = document.createElement('div'); 24333 toast.className = 'toast-notification success'; 24334 toast.textContent = message; 24335 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;'; 24336 24337 document.body.appendChild(toast); 24338 24339 setTimeout(() => { 24340 toast.style.opacity = '0'; 24341 toast.style.transition = 'opacity 0.3s'; 24342 setTimeout(() => toast.remove(), 300); 24343 }, 3000); 24344 } 24345 24346 // Mask user name for privacy preview (like PHP maskReviewerName) 24347 function maskUserName(fullName) { 24348 if (!fullName || fullName.trim() === '') { 24349 return 'Anonymous'; 24350 } 24351 24352 const words = fullName.trim().split(/\s+/); 24353 const maskedWords = words.map(word => { 24354 const len = word.length; 24355 if (len <= 1) return word; 24356 return word.charAt(0) + '*'.repeat(len - 1); 24357 }); 24358 24359 return maskedWords.join(' '); 24360 } 24361 24362 // Update privacy preview text with user's actual masked name 24363 function updatePrivacyPreview() { 24364 const previewEl = document.getElementById('review-anonymous-preview'); 24365 if (!previewEl) return; 24366 24367 const userName = window.userFullName || ''; 24368 if (userName) { 24369 const maskedName = maskUserName(userName); 24370 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24371 } else { 24372 previewEl.textContent = 'Nama Anda akan ditampilkan de
24372ngan disamarkan untuk menjaga privasi'; 24373 } 24374 } 24375 24376 // Initialize when DOM ready 24377 if (document.readyState === 'loading') { 24378 document.addEventListener('DOMContentLoaded', init); 24379 } else { 24380 init(); 24381 } 24382 24383 // Export openReviewModal to global scope for use in other scripts 24384 window.openReviewModal = openReviewModal; 24385 24386})(); 24387 24388</script>
24388 24389 <style> 24390/* ========================================================= 24391 â¡ FIXED FOOTER â #E3D05A ELEGANT MODE 24392========================================================= */ 24393 24394.sbo-top-fixed-footer{ 24395 position: fixed; 24396 left: 0; 24397 right: 0; 24398 bottom: 0; 24399 z-index: 999; 24400 display: flex; 24401 align-items: center; 24402 justify-content: space-around; 24403 gap: 8px; 24404 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24405 overflow: hidden; 24406 background: radial-gradient(circle at 50% 0%, #000000, rgb(0 0 0 / 92%) 58%, rgb(0 0 0 / 98%)), linear-gradient(180deg, rgba(25, 0, 0, 0.92), rgb(0 0 0 / 98%)); 24407 backdrop-filter: blur(12px); 24408 border-top: 1px solid rgb(163,100,34); 24409 border-radius: 18px 18px 0 0; 24410 box-shadow: 0 -12px 30px #C77DFF, inset 0 1px 0 rgba(255, 255, 255, .05); 24411 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24412} 24413 24414/* scanline */ 24415.sbo-top-fixed-footer::before{ 24416 content:""; 24417 position:absolute; 24418 inset:0; 24419 pointer-events:none; 24420 background: 24421 repeating-linear-gradient( 24422 0deg, 24423 rgba(255,255,255,.015), 24424 rgba(255,255,255,.015) 1px, 24425 transparent 1px, 24426 transparent 6px 24427 ); 24428 opacity:.85; 24429 mix-blend-mode:overlay; 24430} 24431 24432/* shine sweep */ 24433.sbo-top-fixed-footer::after{ 24434 content:""; 24435 position:absolute; 24436 top:0; 24437 left:-160%; 24438 width:60%; 24439 height:100%; 24440 background: linear-gradient(120deg, transparent, rgba(226,166,66,0.25), transparent); 24441 transform: skewX(-25deg); 24442 animation: footerFinalShine 3.8s infinite; 24443 mix-blend-mode: screen; 24444 pointer-events:none; 24445} 24446 24447@keyframes footerFinalShine{ 24448 0%{left:-160%;} 24449 68%{left:160%;} 24450 100%{left:160%;} 24451} 24452 24453/* ========================================================= 24454 ITEM 24455========================================================= */ 24456 24457.sbo-top-fixed-footer a{ 24458 flex:1; 24459 max-width:110px; 24460 24461 display:flex; 24462 flex-direction:column; 24463 align-items:center; 24464 justify-content:center; 24465 gap:6px; 24466 24467 padding: 8px 6px; 24468 24469 text-decoration:none; 24470 text-align:center; 24471 24472 color: rgba(201,131,42,1); 24473 font-size: 12px; 24474 font-weight: 800; 24475 letter-spacing: .25px; 24476 24477 border-radius: 14px; 24478 position:relative; 24479 z-index:1; 24480 24481 transition: all .18s ease; 24482} 24483 24484.sbo-top-fixed-footer a:hover{ 24485 color:#ffffff; 24486 background: rgba(226,166,66,0.08); 24487 filter: drop-shadow(0 0 12px rgba(226,166,66,0.15)); 24488 transform: translateY(-1px); 24489} 24490 24491.sbo-top-fixed-footer a:active{ 24492 transform: translateY(0) scale(.99); 24493} 24494 24495/* ========================================================= 24496 ICON 24497========================================================= */ 24498 24499.sbo-top-fixed-footer img{ 24500 width:26px; 24501 height:26px; 24502 transition: .18s ease; 24503 filter: drop-shadow(0 0 8px rgba(226,166,66,0.35)); 24504} 24505 24506.sbo-top-fixed-footer a:hover img{ 24507 transform: scale(1.08); 24508 filter: drop-shadow(0 0 16px rgba(226,166,66,0.6)); 24509} 24510 24511/* ========================================================= 24512 HUD CORNERS 24513========================================================= */ 24514 24515.sbo-top-fixed-footer .hud-corner{ 24516 position:absolute; 24517 width:18px;height:18px; 24518 pointer-events:none; 24519 border: 1px solid rgba(226,166,66,0.5); 24520 filter: drop-shadow(0 0 10px rgba(226,166,66,0.2)); 24521 opacity:.85; 24522} 24523 24524.sbo-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24525.sbo-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24526.sbo-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24527.sbo-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24528 24529/* ========================================================= 24530 CENTER CTA 24531========================================================= */ 24532 24533.sbo-top-fixed-footer .tada{ 24534 color: #ffffff; 24535 background: radial-gradient(circle at 50% 0%, #C77DFF, rgba(0, 0, 0, 0) 70%), rgba(226,166,66,0.08); 24536 border: 1px solid rgba(226,166,66,0.35); 24537 box-shadow: 0 0 18px #C77DFF, inset 0 0 16px rgba(226,166,66,0.15); 24538} 24539 24540.sbo-top-fixed-footer .tada img{ 24541 width:30px; 24542 height:30px; 24543 filter: drop-shadow(0 0 16px rgba(226,166,66,0.65)); 24544} 24545 24546/* pulse */ 24547.sbo-top-fixed-footer .tada::before{ 24548 content:""; 24549 position:absolute; 24550 inset:-12px; 24551 border-radius:999px; 24552 background: 24553 radial-gradient(circle, #000000, transparent 70%), 24554 radial-gradient(circle, rgba(226,166,66,0.08), transparent 72%); 24555 filter: blur(12px); 24556 opacity:.55; 24557 animation: corePulse 2.9s ease-in-out infinite; 24558 z-index:-1; 24559} 24560 24561@keyframes corePulse{ 24562 0%,100%{opacity:.35; transform:scale(.99);} 24563 50%{opacity:.75; transform:scale(1.02);} 24564} 24565 24566/* ACTIVE */ 24567.sbo-top-fixed-footer a.is-active{ 24568 color:#ffffff; 24569 background: rgba(226,166,66,0.1); 24570 box-shadow: inset 0 0 0 1px rgba(226,166,66,0.25); 24571} 24572 24573.sbo-top-fixed-footer a.is-active img{ 24574 filter: drop-shadow(0 0 18px rgba(226,166,66,0.8)); 24575} 24576 24577/* RESPONSIVE */ 24578@media (max-width:500px){ 24579 .sbo-top-fixed-footer{ 24580 padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); 24581 border-radius: 16px 16px 0 0; 24582 gap:6px; 24583 } 24584 .sbo-top-fixed-footer a{ 24585 font-size:11px; 24586 padding:7px 5px; 24587 gap:5px; 24588 max-width:104px; 24589 } 24590 .sbo-top-fixed-footer img{width:22px;height:22px;} 24591 .sbo-top-fixed-footer .tada img{width:26px;height:26px;} 24592} 24593</style> 24594<div class="sbo-top-fixed-footer"> 24595 <a href="https://aristrokat.s.gy/samiri" target="_blank" rel="nofollow noopener"> 24596 <img src="https://img.tongat.cc/uploads/logo-ri.png" alt="BONUS GAME MPO"> 24597 Official 24598 </a> 24599 <a href="https://aristrokat.s.gy/samiri/" target="_blank" rel="nofollow noopener"> 24600 <img src="https://img.tongat.cc/uploads/logo-ri.png" alt="LOGIN GAME MPO"> 24601 Login 24602 </a> 24603 <a href="https://aristrokat.s.gy/samiri/" target="_blank" rel="nofollow noopener"> 24604 <img src="https://img.tongat.cc/uploads/logo-ri.png" alt="Daftar GAME MPO"> 24605 Daftar 24606 </a> 24607 <a href="https://aristrokat.s.gy/samiri/" target="_blank" rel="nofollow noopener"> 24608 <img src="https://img.tongat.cc/uploads/logo-ri.png" alt="Daftar GAME MPO"> 24609 Slot 24610 </a> 24611 <a href="https://aristrokat.s.gy/samiri/" target="_blank" rel="nofollow noopener"> 24612 <img src="https://img.tongat.cc/uploads/logo-ri.png" alt="Daftar GAME MPO"> 24613 kendaraan yang bisa di-custom total 24614 </a> 24615 24616</div> 24617 24618<style id="capzrao-theme-override"> 24619 24620/* ========================================================= 24621 GAME MPO EARTHY LUXURY THEME 24622 Espresso + Saffron + Copper + Wine + Ivory 24623 Distinct from previous black/gold/red treatment. 24624========================================================= */ 24625:root{ 24626 --brand-50:#fff7e8; 24627 --brand-100:#f8e5b8; 24628 --brand-500:#d8a62e; 24629 --brand-600:#b87a22; 24630 --brand-700:#7a481e; 24631 24632 --gray-900:#f9ecd4; 24633 --gray-700:#E3D05A; 24634 --gray-600:#bca382; 24635 --gray-500:#8f765f; 24636 --gray-300:#C77DFF; 24637 --gray-200:#4d2e14; 24638 --gray-100:#C77DFF; 24639 --border:#C77DFF; 24640
24641 --bt-espresso:#130c09; 24642 --bt-coffee:#C77DFF; 24643 --bt-panel:#C77DFF; 24644 --bt-panel2:#C77DFF; 24645 24646 --bt-wine:#6f481d; 24647 --bt-brick:#9a6324; 24648 --bt-copper:#c66f2a; 24649 --bt-saffron:#d8a62e; 24650 --bt-sand:#e8c96f; 24651 --bt-ivory:#fff1d5; 24652 --bt-cream:#f9e7c6; 24653} 24654 24655html{ 24656 background:#130c09 !important; 24657} 24658 24659body{ 24660 color:#f9e7c6 !important; 24661 background: 24662 radial-gradient(circle at 48% -8%,rgba(216,166,46,.16),transparent 30%), 24663 radial-gradient(circle at 8% 31%,rgba(198,111,42,.12),transparent 35%), 24664 radial-gradient(circle at 93% 55%,rgba(119,72,27,.10),transparent 37%), 24665 linear-gradient(180deg,#C77DFF 0%,#C77DFF 46%,#120b08 100%) !important; 24666 background-attachment:fixed !important; 24667} 24668 24669.topbar{ 24670 background:linear-gradient(90deg,#160d09,#4d2e14 50%,#160d09) !important; 24671 color:#e8c96f !important; 24672 border-bottom:1px solid rgba(216,166,46,.34) !important; 24673} 24674.topbar a{color:#f9e7c6 !important} 24675.topbar a:hover{color:#d8a62e !important} 24676 24677.navbar, 24678.navbar-logo{ 24679 background: 24680 radial-gradient(circle at 50% 0%,rgba(216,166,46,.10),transparent 43%), 24681 linear-gradient(135deg,rgba(43,27,14,.98),rgba(20,12,9,.99) 54%,rgba(43,27,14,.98)) !important; 24682 border-bottom:1px solid rgba(216,166,46,.42) !important; 24683 box-shadow: 24684 0 5px 22px rgba(0,0,0,.38), 24685 0 0 24px rgba(198,111,42,.08) !important; 24686} 24687.navbar::after{ 24688 background:linear-gradient( 24689 90deg, 24690 transparent, 24691 #C77DFF, 24692 #E3D05A, 24693 #c66f2a, 24694 #d8a62e, 24695 #e8c96f, 24696 #d8a62e, 24697 #c66f2a, 24698 #E3D05A, 24699 #C77DFF, 24700 transparent 24701 ) !important; 24702 background-size:220% 100% !important; 24703 opacity:.88 !important; 24704 animation:btEarthLine 5.6s linear infinite !important; 24705} 24706@keyframes btEarthLine{ 24707 from{background-position:185% 0} 24708 to{background-position:-85% 0} 24709} 24710 24711/* Search */ 24712.search, 24713.nav-inner .search{ 24714 background:#fff8ed !important; 24715 border-color:#dcb65a !important; 24716 box-shadow: 24717 0 4px 14px rgba(0,0,0,.17), 24718 0 0 15px rgba(198,111,42,.07) !important; 24719} 24720.search:focus-within, 24721.nav-inner .search:focus-within{ 24722 border-color:#c66f2a !important; 24723 box-shadow: 24724 0 0 0 3px rgba(216,166,46,.13), 24725 0 0 21px rgba(119,72,27,.08) !important; 24726} 24727.search input, 24728.nav-inner .search input{color:#C77DFF !important} 24729.search input::placeholder, 24730.nav-inner .search input::placeholder{color:#957866 !important} 24731.search-icon-btn, 24732.nav-inner .search-icon-btn{color:#E3D05A !important} 24733.search-icon-btn:hover{color:#c66f2a !important} 24734 24735/* Buttons / nav actions */ 24736.icon-btn, 24737.login-btn, 24738.pd-sticky-share-btn, 24739.pd-sticky-btn-cart, 24740.pd-sticky-btn-chat{ 24741 color:#e8c96f !important; 24742 border-color:rgba(216,166,46,.31) !important; 24743 background:linear-gradient(135deg,#C77DFF,#130c09) !important; 24744 box-shadow: 24745 inset 0 1px 0 rgba(255,241,213,.05), 24746 0 3px 12px rgba(0,0,0,.27) !important; 24747} 24748.icon-btn:hover, 24749.login-btn:hover, 24750.pd-sticky-share-btn:hover, 24751.pd-sticky-btn-cart:hover, 24752.pd-sticky-btn-chat:hover{ 24753 color:#fff !important; 24754 border-color:#d8a62e !important; 24755 background:linear-gradient(135deg,#4d2e14,#C77DFF) !important; 24756 box-shadow:0 0 17px rgba(198,111,42,.14) !important; 24757} 24758 24759/* Categories */ 24760.mega-cats a{color:#E3D05A !important} 24761.mega-cats a:hover{ 24762 color:#fff !important; 24763 background:linear-gradient(135deg,rgba(163,100,34,.28),rgba(216,166,46,.12)) !important; 24764} 24765.mega-cats a.active, 24766.mega-more-grid a.active, 24767.mega-cats-more.active > .mega-more-btn{ 24768 color:#fff !important; 24769 background:linear-gradient(135deg,#C77DFF,#E3D05A 48%,#c66f2a 76%,#d8a62e) !important; 24770} 24771.mega-more-btn, 24772.mm2-trigger{ 24773 color:#e8c96f !important; 24774 background:#C77DFF !important; 24775 border-color:#C77DFF !important; 24776} 24777.mega-more-dropdown, 24778.mm2-panel{ 24779 background:#C77DFF !important; 24780 color:#f9e7c6 !important; 24781 border-color:#C77DFF !important; 24782} 24783 24784/* Main */ 24785.hero{ 24786 background: 24787 radial-gradient(circle at 10% 0%,rgba(216,166,46,.085),transparent 34%), 24788 linear-gradient(145deg,#C77DFF,#E3D05A) !important; 24789} 24790.usp-section, 24791.flash, 24792.products{ 24793 background: 24794 radial-gradient(circle at 92% 0%,rgba(198,111,42,.05),transparent 32%), 24795 linear-gradient(180deg,#C77DFF,#120c09) !important; 24796 border-color:rgba(216,166,46,.15) !important; 24797} 24798 24799.product-single{ 24800 --pd-border:#C77DFF; 24801 --pd-bg:#C77DFF; 24802 --pd-muted:#c9b08d; 24803 --pd-dark:#f9e7c6; 24804 --pd-accent:#d8a62e; 24805 --pd-soft:#C77DFF; 24806 --pd-soft-border:#C77DFF; 24807 --pd-danger:#b77924; 24808 --pd-danger-bg:#493019; 24809 --pd-danger-text:#e4b767; 24810 --pd-shadow:0 8px 28px -10px rgba(0,0,0,.62);
24811 color:#f9e7c6 !important; 24812} 24813 24814.product-single--v3, 24815.pd-top-layout{background:transparent !important} 24816 24817.pd-sticky-header{ 24818 background:rgba(18,12,8,.96) !important; 24819 border-bottom-color:rgba(216,166,46,.32) !important; 24820 box-shadow: 24821 0 5px 24px rgba(0,0,0,.45), 24822 0 0 18px rgba(198,111,42,.07) !important; 24823} 24824.pd-sticky-title{color:#f9e7c6 !important} 24825.pd-sticky-price{color:#e8c96f !important} 24826.pd-sticky-thumb{ 24827 background:#C77DFF !important; 24828 border-color:#C77DFF !important; 24829} 24830 24831/* Panels / cards */ 24832.pd-media-card, 24833.pd-info-card, 24834.pd-buy-card, 24835.pd-side-card, 24836.pd-full-description, 24837.pd-reviews-section, 24838.pd-related-full, 24839.pd-installment-card, 24840.pd-installment-card2, 24841.pd-compare-section, 24842.pd-short-desc, 24843.pd-variants, 24844.prod-card, 24845.usp-item, 24846.hero-box, 24847.search-suggest, 24848.user-dropdown, 24849.mobile-user-sheet-content{ 24850 background: 24851 radial-gradient(circle at 100% 0%,rgba(216,166,46,.055),transparent 36%), 24852 radial-gradient(circle at 0% 100%,rgba(163,100,34,.038),transparent 38%), 24853 linear-gradient(155deg,#C77DFF,#E3D05A) !important; 24854 color:#f9e7c6 !important; 24855 border-color:rgba(216,166,46,.18) !important; 24856 box-shadow: 24857 0 9px 28px rgba(0,0,0,.29), 24858 inset 0 1px 0 rgba(255,241,213,.024) !important; 24859} 24860.prod-card:hover, 24861.usp-item:hover, 24862.pd-media-card:hover, 24863.pd-info-card:hover{ 24864 border-color:rgba(216,166,46,.36) !important; 24865 box-shadow: 24866 0 12px 30px rgba(0,0,0,.33), 24867 0 0 19px rgba(198,111,42,.075) !important; 24868} 24869.prod-card .img-box, 24870.pd-main-image, 24871.pd-mslide, 24872.pd-thumb-btn{ 24873 background:#C77DFF !important; 24874 border-color:#4d2e14 !important; 24875} 24876.pd-thumb-btn.active{ 24877 border-color:#d8a62e !important; 24878 box-shadow: 24879 0 0 0 2px rgba(216,166,46,.12), 24880 0 0 16px rgba(198,111,42,.07) !important; 24881} 24882 24883/* Heading / price */ 24884.pd-title, 24885.pd-full-desc-title, 24886.pd-related-title, 24887.hero-text h1, 24888.price strong, 24889.pd-price-current, 24890.text-brand, 24891.hero-text .text-brand{ 24892 color:#e8c96f !important; 24893 text-shadow:0 0 11px rgba(216,166,46,.08) !important; 24894} 24895.pd-price-original{color:#a88c73 !important} 24896 24897/* Primary CTA */ 24898.btn-primary, 24899.modal-btn-primary, 24900.review-form .btn-primary, 24901.pd-compare-btn, 24902.pd-sticky-btn-buy{ 24903 background:linear-gradient(135deg,#C77DFF 0%,#E3D05A 40%,#c66f2a 72%,#d8a62e 100%) !important; 24904 border-color:#e8c96f !important; 24905 color:#fff !important; 24906 box-shadow: 24907 0 5px 18px rgba(119,72,27,.16), 24908 0 0 15px rgba(216,166,46,.06) !important; 24909} 24910.btn-primary:hover, 24911.modal-btn-primary:hover, 24912.review-form .btn-primary:hover, 24913.pd-compare-btn:hover, 24914.pd-sticky-btn-buy:hover{ 24915 background:linear-gradient(135deg,#fff1d5,#e8c96f 48%,#c66f2a) !important; 24916 border-color:#fff1d5 !important; 24917 color:#C77DFF !important; 24918} 24919 24920/* Badges */ 24921.badge, 24922.bottom-nav-item-badge, 24923.pd-discount-badge, 24924.mini-disc, 24925.discount, 24926.disc, 24927.sale-badge, 24928.product-single .stock-badge.out{ 24929 background:linear-gradient(135deg,#C77DFF,#E3D05A 52%,#c66f2a) !important; 24930 color:#fff !important; 24931 border-color:#d8a62e !important; 24932} 24933 24934/* Forms */ 24935.product-single input, 24936.product-single select, 24937.product-single textarea, 24938.modal input, 24939.modal select, 24940.modal textarea{ 24941 background:#C77DFF !important; 24942 color:#f9e7c6 !important; 24943 border-color:#C77DFF !important; 24944} 24945.product-single input:focus, 24946.product-single select:focus, 24947.product-single textarea:focus{ 24948 border-color:#d8a62e !important; 24949 box-shadow:0 0 0 3px rgba(216,166,46,.10) !important; 24950} 24951 24952/* GAME MPO machine */ 24953.Masa-Kini-yang-Selalu-v3{ 24954 background: 24955 radial-gradient(circle at 15% 0%,rgba(198,111,42,.11),transparent 46%), 24956 radial-gradient(circle at 85% 100%,rgba(119,72,27,.08),transparent 55%), 24957 linear-gradient(135deg,#C77DFF,#130c09) !important; 24958 border-color:rgba(216,166,46,.28) !important; 24959 box-shadow: 24960 inset 0 0 20px rgba(163,100,34,.04), 24961 0 0 18px rgba(216,166,46,.055) !important; 24962} 24963.capzrao-controls-v3{ 24964 background:rgba(22,13,10,.74) !important; 24965 border-color:rgba(216,166,46,.24) !important; 24966} 24967.capzrao-controls-v3 label{color:#e8c96f !important} 24968.capzrao-controls-v3 select, 24969.capzrao-controls-v3 input{ 24970 background:#C77DFF !important; 24971 color:#f9e7c6 !important; 24972 border-color:rgba(216,166,46,.28) !important; 24973} 24974#btncapzraoRandom{ 24975 background:linear-gradient(135deg,#C77DFF,#E3D05A 48%,#c66f2a 76%,#d8a62e) !important; 24976 color:#fff !important; 24977 border-color:#e8c96f !important; 24978} 24979 24980/* Footer */ 24981.bottom-nav{ 24982 background:rgba(20,12,9,.98) !important; 24983 border-top-color:#C77DFF !important; 24984 box-shadow:0 -8px 24px rgba(0,0,0,.45) !important; 24985} 24986.bottom-nav-item{color:#cfb28f !important} 24987.bottom-nav-item:hover{color:#e8c96f !important} 24988.bottom-nav-item.active{ 24989 color:#fff !important; 24990 background:linear-gradient(135deg,#C77DFF,#E3D05A 48%,#c66f2a 76%,#d8a62e) !important; 24991} 24992 24993.sbo-top-fixed-footer{ 24994 background: 24995 radial-gradient(circle at 50% 0%,rgba(216,166,46,.085),transparent 42%), 24996 linear-gradient(180deg,rgba(43,27,14,.98),rgba(18,11,8,.99)) !important; 24997 border-top-color:#8f5a22 !important; 24998 box-shadow: 24999 0 -12px 32px rgba(0,0,0,.50), 25000 inset 0 1px 0 rgba(232,201,111,.05) !important; 25001} 25002.sbo-top-fixed-footer::after{ 25003 background:linear-gradient( 25004 120deg, 25005 transparent, 25006 rgba(216,166,46,.05), 25007 rgba(232,201,111,.20), 25008 rgba(198,111,42,.09), 25009 transparent 25010 ) !important; 25011} 25012.sbo-top-fixed-footer a{color:#e8c96f !important} 25013.sbo-top-fixed-footer a:hover, 25014.sbo-top-fixed-footer a.is-active{ 25015 color:#fff !important; 25016 background:linear-gradient(135deg,rgba(119,72,27,.15),rgba(198,111,42,.075)) !important; 25017} 25018.sbo-top-fixed-footer img{ 25019 filter: 25020 drop-shadow(0 0 7px rgba(216,166,46,.28)) 25021 drop-shadow(0 0 11px rgba(198,111,42,.10)) !important; 25022} 25023.sbo-top-fixed-footer .hud-corner{ 25024 border-color:rgba(216,166,46,.30) !important; 25025 filter:drop-shadow(0 0 9px rgba(198,111,42,.09)) !important; 25026} 25027.sbo-top-fixed-footer .tada{ 25028 color:#fff !important; 25029 background: 25030 radial-gradient(circle at 50% 0%,rgba(198,111,42,.18),transparent 70%), 25031 rgba(119,72,27,.055) !important; 25032 border-color:rgba(216,166,46,.24) !important; 25033 box-shadow: 25034 0 0 15px rgba(198,111,42,.11), 25035 inset 0 0 14px rgba(216,166,46,.06) !important; 25036} 25037 25038</style> 25039 25040 25041<style id="capzrao-balanced-adjust"> 25042 25043/* ========================================================= 25044 EARTHY FINISHING PASS 25045========================================================= */ 25046:root{ 25047 --brand-500:#d8a62e !important; 25048 --brand-600:#b87a22 !important; 25049 --brand-700:#7a481e !important; 25050} 25051 25052/* Existing animation retained, recolored and slowed slightly */ 25053.pd-media-card, 25054.pd-info-card, 25055.sbo-top-fixed-footer, 25056.navbar{ 25057 background-size:145% 145% !important; 25058 animation:btEarthShift 10s ease-in-out infinite alternate !important; 25059} 25060@keyframes btEarthShift{ 25061 from{background-position:0% 20%} 25062 to{background-position:100% 80%} 25063} 25064 25065.sbo-top-fixed-footer .tada::before{ 25066 background: 25067 radial-gradient(circle,rgba(216,166,46,.11),transparent 68%), 25068 radial-gradient(circle,rgba(119,72,27,.075),transparent 74%) !important; 25069 filter:blur(12px) !important; 25070 opacity:.48 !important; 25071 animation:btEarthPulse 4s ease-in-out infinite !important; 25072} 25073@keyframes btEarthPulse{ 25074 0%,100%{opacity:.26;transform:scale(.985)} 25075 50%{opacity:.60;transform:scale(1.035)} 25076} 25077 25078button:hover, 25079a:hover{ 25080 text-shadow:0 0 9px rgba(216,166,46,.20); 25081} 25082 25083@media(prefers-reduced-motion:reduce){ 25084 .navbar::after, 25085 .pd-media-card, 25086 .pd-info-card, 25087 .sbo-top-fixed-footer, 25088 .navbar, 25089 .sbo-top-fixed-footer .tada::before{ 25090 animation:none !important; 25091 } 25092} 25093</style> 25094<div style="display:none"> 25095<a href="https://djarumtoto.work/">DJARUMTOTO</a> 25096<a href="https://acesdetailingsd.com/">https://acesdetailingsd.com/</a> 25097<a href="https://djarumtoto.work/">Apk Djarumtoto</a> 25098<a href="https://djarumtoto.work/">Login DJarumtoto</a> 25099<a href="https://paperaudio.com/">Judi Mpo 5000</a> 25100<a href="https://jimstephensrealty.com/">Login Judi Mpo</a> 25101<a href="https://bhavnagariswetmart.shop/">Mpo Slot 105</a> 25102<a href="https://zyyhmc.com/">https://zyyhmc.com/</a> 25103<a href="https://patterntechnology.com/">Slot 10k</a> 25104<a href="https://xbjwj.com/">Judi Mpo5000</a> 25105<a href="https://timelinefrance.com/">Kaptenmpo</a> 25106<a href="https://challengen.forum/">Kapten Mpo</a> 25107<a href="https://cartoria.pro/">https://cartoria.pro/</a> 25108<a href="https://ishijie.com/">https://ishijie.com/</a> 25109<a href="https://kawaipopstudio.fr/">https://kawaipopstudio.fr/</a> 25110<a href="https://jpmultiitemstore.com/">https://jpmultiitemstore.com/</a> 25111<a href="https://kavox.xyz/">https://kavox.xyz/</a> 25112<a href="https://www.concretehermiston.com/">https://www.concretehermiston.com/</a> 25113<a href="https://www.concretehelena.com/">https://www.concretehelena.com/</a> 25114<a href="https://knockout-football.nl/">https://knockout-football.nl/</a> 25115<a href="https://knotifycu.tech/">https://knotifycu.tech/</a> 25116<a href="https://kodalabsllc.com/">https://kodalabsllc.com/</a> 25117<a href="https://kavenro.shop/">https://kavenro.shop/</a> 25118<a href="https://brandymccarron.net/">https://brandymccarron.net/</a> 25119<a href="https://baycjd.com">https://baycjd.com</a> 25120<a href="https://acesdetailingsd.com/booking">https://acesdetailingsd.com/booking</a> 25121<a href="https://acesdetailingsd.com/">https://acesdetailingsd.com/</a> 25122<a href="https://cimmia.kz/">https://cimmia.kz/</a> 25123<a href="https://baihugou.com/">https://baihugou.com/</a> 25124<a href="https://bhavnagariswetmart.shop/">https://bhavnagariswetmart.shop/</a> 25125<a href="https://zyyhmc.com/">https://zyyhmc.com/</a> 25126<a href="https://patterntechnology.com/">https://patterntechnology.com/</a> 25127<a href="https://xbjwj.com/">https://xbjwj.com/</a> 25128<a href="https://www.cntgs.com/">https://www.cntgs.com/</a> 25129<a href="https://vcinstitute.site/">https://vcinstitute.site/</a>
25130<a href="https://hop.exchange/">https://hop.exchange/</a> 25131</div>
25132<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":"298a8e62e4be405b9b11bb75714e15e3","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 25132
25132
25133<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":"3f8df6c2298949a3b63a7f39bbe80701","r":1,"spa":2}' crossorigin="anonymous"></script>
25133 25134</body> 25135</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.